├── .gitattributes ├── .github └── workflows │ └── plugin.yml ├── .gitignore ├── README.md ├── configs-bms └── player_models.cfg ├── configs-hl2mp └── player_models.cfg ├── game └── materials │ └── modelchooser │ ├── background.vmt │ └── background.vtf ├── gamedata └── modelchooser.txt └── scripting ├── include ├── modelchooser.inc └── modelchooser │ ├── anims.inc │ ├── commands.inc │ ├── config.inc │ ├── globals.inc │ ├── menu.inc │ ├── natives.inc │ ├── sounds.inc │ ├── structs.inc │ ├── utils.inc │ └── viewmodels.inc ├── modelchooser_api_example.sp └── ultimate_modelchooser.sp /.gitattributes: -------------------------------------------------------------------------------- 1 | # Auto detect text files and perform LF normalization 2 | * text=auto 3 | -------------------------------------------------------------------------------- /.github/workflows/plugin.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | 3 | on: [push, pull_request, workflow_dispatch] 4 | 5 | jobs: 6 | build: 7 | name: "Build" 8 | runs-on: ubuntu-latest 9 | strategy: 10 | fail-fast: false 11 | matrix: 12 | game: 13 | - { dir: bms } 14 | - { dir: hl2mp } 15 | 16 | steps: 17 | - name: Checkout repo 18 | uses: actions/checkout@v4 19 | 20 | - name: Checkout smartdm 21 | uses: actions/checkout@v4 22 | with: 23 | repository: Alienmario/smartdm-redux 24 | path: deps/smartdm 25 | 26 | - name: Checkout smlib 27 | uses: actions/checkout@v4 28 | with: 29 | repository: bcserv/smlib 30 | ref: transitional_syntax 31 | path: deps/smlib 32 | 33 | - name: Checkout StudioHdr 34 | uses: actions/checkout@v4 35 | with: 36 | repository: Alienmario/StudioHdr 37 | path: deps/StudioHdr 38 | 39 | - name: Merge deps 40 | run: | 41 | cp -R deps/smartdm/* . 42 | cp -R deps/smlib/scripting/include/* scripting/include 43 | cp -R deps/StudioHdr/* . 44 | 45 | - name: Setup sourcemod compiler 46 | id: setup-sp 47 | uses: rumblefrog/setup-sp@master 48 | with: 49 | version: '1.12.x' 50 | version-file: ./scripting/ultimate_modelchooser.sp 51 | define-name: PLUGIN_VERSION 52 | 53 | - name: Compile plugins 54 | run: | 55 | mkdir -p plugins 56 | for file in scripting/*.sp 57 | do 58 | plugin="$(basename "${file%.*}")" 59 | echo -e "\nCompiling $plugin\n" 60 | spcomp -v2 -i scripting/include -o plugins/"$plugin".smx "$file" 61 | done 62 | 63 | - name: Create package 64 | run: | 65 | rm plugins/modelchooser_api_example.smx 66 | OUT="/tmp/build" 67 | SM="${OUT}/addons/sourcemod" 68 | mkdir -p $SM/configs 69 | cp -R game/. $OUT 70 | cp -R plugins $SM 71 | cp -R scripting $SM 72 | cp -R gamedata $SM 73 | cp -R configs-${{ matrix.game.dir }}/. $SM/configs 74 | if [ -d "data" ] 75 | then 76 | cp -R data $SM 77 | fi 78 | 79 | - name: Upload package 80 | uses: actions/upload-artifact@v4 81 | with: 82 | name: modelchooser-${{ matrix.game.dir }}-v${{ steps.setup-sp.outputs.plugin-version }}.${{ github.run_number }} 83 | path: /tmp/build/ -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .vscode/* 2 | *.code-workspace 3 | plugins/* -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | [![CI](https://github.com/Alienmario/ModelChooser/actions/workflows/plugin.yml/badge.svg)](https://github.com/Alienmario/ModelChooser/actions/workflows/plugin.yml) 2 | 3 | # Ultimate ModelChooser 4 | A "**_better_**" player model chooser for Sourcemod. 5 | 6 | https://github.com/user-attachments/assets/be5919ba-efc7-433a-91b0-d81289a9c5a4 7 | 8 | #### Features 9 | - Third-person model browser 10 | - Custom per-model sounds (all) and animations (hl2dm only) 11 | - Supports skins and bodygroups 12 | - Persistence via cookies 13 | - Extensive configuration 14 | - Admin only, team-based and locked models 15 | - Fully automatic downloads 16 | - Scripting API 17 | 18 | #### Supported games 19 | - HL2:DM 20 | - Black Mesa 21 | 22 | > If you need other games without the custom gamedata requirement, try the **v1 legacy version**. 23 | 24 | ## Installation 25 | 1. Download latest version from the [releases page](https://github.com/Alienmario/ModelChooser/releases) 26 | 2. Unpack it in your gameroot folder (hl2mp, bms, ...) 27 | 3. Done! 28 | 29 | #### Dependencies 30 | - Sourcemod 1.12+ 31 | - (Compile+Gamedata) **Alienmario/[StudioHdr](https://github.com/Alienmario/StudioHdr)** 32 | - (Compile+Gamedata) **Alienmario/[smartdm-redux](https://github.com/Alienmario/smartdm-redux)** 33 | - (Compile) **bcserv/[smlib](https://github.com/bcserv/smlib/tree/transitional_syntax)** 34 | 35 | #### Usage 36 | Type !models to enter. Press attack / movement keys to browse. Press use or jump to exit. 37 | 38 | #### Config 39 | | Convar | Default | Description | 40 | | --- | --- | --- | 41 | | **modelchooser_immunity** | `0` | (0/1) Whether players are immune to damage when selecting models | 42 | | **modelchooser_autoreload** | `0` | (0/1) Whether to reload the model list on mapchanges | 43 | | **modelchooser_teambased** | `2` | Configures model restrictions in teamplay mode
0 = Do not enforce any team restrictions
1 = Enforce configured team restrictions, allows picking unrestricted models
2 = Strictly enforce teams, only allows models with matching teams | 44 | | **modelchooser_sound** | `ui/buttonclickrelease.wav` | Menu click sound (auto downloads supported), empty to disable | 45 | | **modelchooser_overlay** | `modelchooser/background` | Screen overlay material to show when choosing models (auto downloads supported), empty to disable | 46 | | **modelchooser_lock_model** | `models/props_wasteland/prison_padlock001a.mdl` | Model to display for locked playermodels (auto downloads supported) | 47 | | **modelchooser_lock_scale** | `5.0` | Scale of the lock model | 48 | | **modelchooser_hudtext_x** | `-1` | Hudtext 1 X coordinate, from 0 (left) to 1 (right), -1 is the center | 49 | | **modelchooser_hudtext_y** | `0.01` | Hudtext 1 Y coordinate, from 0 (top) to 1 (bottom), -1 is the center | 50 | | **modelchooser_hudtext2_x** | `-1` | Hudtext 2 X coordinate, from 0 (left) to 1 (right), -1 is the center | 51 | | **modelchooser_hudtext2_y** | `0.95` | Hudtext 2 Y coordinate, from 0 (top) to 1 (bottom), -1 is the center | 52 | | **modelchooser_forcefullupdate** | `1` | (0/1) Fixes weapon prediction glitch caused by going thirdperson, recommended to keep on unless you run into issues | 53 | 54 | #### Admin commands 55 | - **sm_unlockmodel** Unlock a locked model by name for a player 56 | - **sm_lockmodel** Lock a previously unlocked model by name for a player 57 | -------------------------------------------------------------------------------- /configs-bms/player_models.cfg: -------------------------------------------------------------------------------- 1 | "ModelSystem" 2 | { 3 | "Models" 4 | { 5 | "Example" 6 | { 7 | // If set to 0, this model is skipped. 8 | "enabled" "0" 9 | 10 | // Model path 11 | // This is the only required parameter. 12 | "path" "models/example.mdl" 13 | 14 | // Allowed skin types. If not present, all skins are selectable. 15 | // This is the same number as if calling the input "skin" on player entity. 16 | "skins" "0;1;3" 17 | 18 | // Allowed body groups. If not present, all body groups are selectable. 19 | // This is the same number as if calling the input "SetBodyGroup" on player entity. 20 | "bodygroups" "0;4;25" 21 | 22 | // Model soundpack name, configured under the "Sounds" section. 23 | "sounds" "Example" 24 | 25 | // At what HP to play the hurt sound. Supports random selection, e.g. "30;90" 26 | "hurtSoundHP" "45" 27 | 28 | // Delay between jump sounds. Supports random selection, e.g. "1.0;3.0" 29 | "jumpSoundTime" "0" 30 | 31 | // HUD color to use for the name when browsing the model. 32 | "hudColor" "10 65 85" 33 | 34 | // Models with highest priority will be selected by default for new players. 35 | // If Multiple models share the same priority, a random one is picked. 36 | "defaultPrio" "100" 37 | 38 | // Restricts the model to specific team in teamplay. Supports team name or team index. 39 | "team" "scientist" 40 | 41 | // Restricts the model to admins with these flags. 42 | "adminFlags" "c" 43 | 44 | // If 1, the model starts locked and has to be unlocked per player by a command or API. 45 | "locked" "0" 46 | 47 | // The viewmodel body group(s) to apply. 48 | // 49 | // 1) A list of bodypart - submodel pairs, as displayed in HLMV's model tab. 50 | // Format: "BodyPart1;SubmodelIndex1;BodyPart2;SubmodelIndex2;BodyPartN;SubmodelIndexN" 51 | // 52 | // 2) Alternatively a single number representing the concrete body group combination as if calling the 53 | // input "SetBodyGroup" on the viewmodel entity. This method affects the state of all body parts on the model! 54 | "vmBody" "arms;3" 55 | 56 | // Downloads are handled automatically! Only specify if you need additional files! 57 | "downloads" 58 | { 59 | "path" "models/example.ext" 60 | "path" "models/another.ext" 61 | } 62 | 63 | // Extensibility support for plugin API 64 | "custom" 65 | { 66 | "key" "value" 67 | } 68 | 69 | } 70 | 71 | // ---------------------------------------------------------------------------------------- 72 | 73 | // Note: 74 | // If you want Black Mesa to play built-in pain and death sounds, use forward slashes in model path! 75 | // If not, use backward slashes. 76 | 77 | "Hev" 78 | { 79 | "path" "models/player/mp_scientist_hev.mdl" 80 | "vmbody" "arms;0" 81 | "team" "scientist" 82 | "defaultprio" "100" 83 | } 84 | "G-man" 85 | { 86 | "path" "models/player/mp_gman.mdl" 87 | "vmbody" "arms;6" 88 | } 89 | "Assassin" 90 | { 91 | "path" "models/player/hassassin.mdl" 92 | "vmbody" "arms;0" 93 | } 94 | "Guard" 95 | { 96 | "path" "models/player/mp_guard.mdl" 97 | "vmbody" "arms;3" 98 | } 99 | "Marine" 100 | { 101 | "path" "models/player/mp_marine.mdl" 102 | "vmbody" "arms;1" 103 | "team" "hgrunt" 104 | } 105 | "Scientist" 106 | { 107 | "path" "models/player/mp_scientist.mdl" 108 | "vmbody" "arms;2" 109 | } 110 | "Female scientist" 111 | { 112 | "path" "models/player/mp_scientist_female.mdl" 113 | "vmbody" "arms;2" 114 | } 115 | "Zombie guard" 116 | { 117 | "path" "models/player/mp_zombie_guard.mdl" 118 | "vmbody" "arms;5" 119 | } 120 | "Zombie Hev" 121 | { 122 | "path" "models/player/mp_zombie_hev.mdl" 123 | "vmbody" "arms;4" 124 | } 125 | "Zombie marine" 126 | { 127 | "path" "models/player/mp_zombie_marine.mdl" 128 | "vmbody" "arms;4" 129 | } 130 | "Zombie scientist" 131 | { 132 | "path" "models/player/mp_zombie_sci.mdl" 133 | "vmbody" "arms;4" 134 | } 135 | } 136 | "Sounds" 137 | { 138 | "Example" 139 | { 140 | // Model viewed in model browser 141 | "View" 142 | { 143 | // "path" "dir/example.wav" 144 | // "path" "dir/example2.wav" 145 | } 146 | // Model selected 147 | "Select" 148 | { 149 | } 150 | // Player gets hurt 151 | "Hurt" 152 | { 153 | } 154 | // Player dies 155 | "Death" 156 | { 157 | } 158 | // Player jumps 159 | "Jump" 160 | { 161 | } 162 | } 163 | } 164 | } 165 | -------------------------------------------------------------------------------- /configs-hl2mp/player_models.cfg: -------------------------------------------------------------------------------- 1 | "ModelSystem" 2 | { 3 | "Models" 4 | { 5 | "Example" 6 | { 7 | // If set to 0, this model is skipped. 8 | "enabled" "0" 9 | 10 | // Model path 11 | // This is the only required parameter. 12 | "path" "models/example.mdl" 13 | 14 | // Allowed skin types. If not present, all skins are selectable. 15 | // This is the same number as if calling the input "skin" on player entity. 16 | "skins" "0;1;3" 17 | 18 | // Allowed body groups. If not present, all body groups are selectable. 19 | // This is the same number as if calling the input "SetBodyGroup" on player entity. 20 | "bodygroups" "0;4;25" 21 | 22 | // Model soundpack name, configured under the "Sounds" section. 23 | "sounds" "Example" 24 | 25 | // At what HP to play the hurt sound. Supports random selection, e.g. "30;90" 26 | "hurtSoundHP" "45" 27 | 28 | // Delay between jump sounds. Supports random selection, e.g. "1.0;3.0" 29 | "jumpSoundTime" "0" 30 | 31 | // HUD color to use for the name when browsing the model. 32 | "hudColor" "10 65 85" 33 | 34 | // Models with highest priority will be selected by default for new players. 35 | // If Multiple models share the same priority, a random one is picked. 36 | "defaultPrio" "100" 37 | 38 | // Restricts the model to specific team in teamplay. Supports team name or team index. 39 | "team" "rebels" 40 | 41 | // Restricts the model to admins with these flags. 42 | "adminFlags" "c" 43 | 44 | // If 1, the model starts locked and has to be unlocked per player by a command or API. 45 | "locked" "0" 46 | 47 | // The viewmodel body group(s) to apply. 48 | // 49 | // 1) A list of bodypart - submodel pairs, as displayed in HLMV's model tab. 50 | // Format: "BodyPart1;SubmodelIndex1;BodyPart2;SubmodelIndex2;BodyPartN;SubmodelIndexN" 51 | // 52 | // 2) Alternatively a single number representing the concrete body group combination as if calling the 53 | // input "SetBodyGroup" on the viewmodel entity. This method affects the state of all body parts on the model! 54 | "vmBody" "arms;3" 55 | 56 | // Overrides default animations. Any sequence or activity contained in the model can be specified. 57 | "anims" 58 | { 59 | "idle" "ACT_HL2MP_IDLE" 60 | "idle_crouch" "ACT_HL2MP_IDLE_CROUCH" 61 | "walk" "ACT_HL2MP_WALK" 62 | "walk_crouch" "ACT_HL2MP_WALK_CROUCH" 63 | "run" "ACT_HL2MP_RUN" 64 | "jump" "jump" 65 | "noclip" 66 | { 67 | "anim" "ACT_GMOD_NOCLIP_LAYER" 68 | "rate" "2.0" 69 | } 70 | } 71 | 72 | // Downloads are handled automatically! Only specify if you need additional files! 73 | "downloads" 74 | { 75 | "path" "models/example.ext" 76 | "path" "models/another.ext" 77 | } 78 | 79 | // Extensibility support for plugin API 80 | "custom" 81 | { 82 | "key" "value" 83 | } 84 | 85 | } 86 | 87 | // ---------------------------------------------------------------------------------------- 88 | 89 | "Soldier" 90 | { 91 | "path" "models/combine_soldier.mdl" 92 | "team" "combine" 93 | "sounds" "Combine" 94 | "defaultprio" "10" 95 | } 96 | "Guard" 97 | { 98 | "path" "models/combine_soldier_prisonguard.mdl" 99 | "team" "combine" 100 | "sounds" "Combine" 101 | "defaultprio" "10" 102 | } 103 | "White Soldier" 104 | { 105 | "path" "models/combine_super_soldier.mdl" 106 | "team" "combine" 107 | "sounds" "Combine" 108 | "defaultprio" "10" 109 | } 110 | "Police" 111 | { 112 | "path" "models/police.mdl" 113 | "team" "combine" 114 | "sounds" "Metro" 115 | "defaultprio" "10" 116 | } 117 | "Male 1" 118 | { 119 | "path" "models/humans/group03/male_01.mdl" 120 | "team" "rebels" 121 | "sounds" "Male" 122 | "defaultprio" "10" 123 | } 124 | "Male 2" 125 | { 126 | "path" "models/humans/group03/male_02.mdl" 127 | "team" "rebels" 128 | "sounds" "Male" 129 | "defaultprio" "10" 130 | } 131 | "Male 3" 132 | { 133 | "path" "models/humans/group03/male_03.mdl" 134 | "team" "rebels" 135 | "sounds" "Male" 136 | "defaultprio" "10" 137 | } 138 | "Male 4" 139 | { 140 | "path" "models/humans/group03/male_04.mdl" 141 | "team" "rebels" 142 | "sounds" "Male" 143 | "defaultprio" "10" 144 | } 145 | "Male 5" 146 | { 147 | "path" "models/humans/group03/male_05.mdl" 148 | "team" "rebels" 149 | "sounds" "Male" 150 | "defaultprio" "10" 151 | } 152 | "Male 6" 153 | { 154 | "path" "models/humans/group03/male_06.mdl" 155 | "team" "rebels" 156 | "sounds" "Male" 157 | "defaultprio" "10" 158 | } 159 | "Male 7" 160 | { 161 | "path" "models/humans/group03/male_07.mdl" 162 | "team" "rebels" 163 | "sounds" "Male" 164 | "defaultprio" "10" 165 | } 166 | "Male 8" 167 | { 168 | "path" "models/humans/group03/male_08.mdl" 169 | "team" "rebels" 170 | "sounds" "Male" 171 | "defaultprio" "10" 172 | } 173 | "Male 9" 174 | { 175 | "path" "models/humans/group03/male_09.mdl" 176 | "team" "rebels" 177 | "sounds" "Male" 178 | "defaultprio" "10" 179 | } 180 | "Female 1" 181 | { 182 | "path" "models/humans/group03/female_01.mdl" 183 | "team" "rebels" 184 | "sounds" "Female" 185 | "defaultprio" "10" 186 | } 187 | "Female 2" 188 | { 189 | "path" "models/humans/group03/female_02.mdl" 190 | "team" "rebels" 191 | "sounds" "Female" 192 | "defaultprio" "10" 193 | } 194 | "Female 3" 195 | { 196 | "path" "models/humans/group03/female_03.mdl" 197 | "team" "rebels" 198 | "sounds" "Female" 199 | "defaultprio" "10" 200 | } 201 | "Female 4" 202 | { 203 | "path" "models/humans/group03/female_04.mdl" 204 | "team" "rebels" 205 | "sounds" "Female" 206 | "defaultprio" "10" 207 | } 208 | "Female 6" 209 | { 210 | "path" "models/humans/group03/female_06.mdl" 211 | "team" "rebels" 212 | "sounds" "Female" 213 | "defaultprio" "10" 214 | } 215 | "G-man" 216 | { 217 | "path" "models/gman.mdl" 218 | "sounds" "Gman" 219 | } 220 | "Alyx" 221 | { 222 | "path" "models/alyx.mdl" 223 | "sounds" "Alyx" 224 | } 225 | "Barney" 226 | { 227 | "path" "models/barney.mdl" 228 | "sounds" "Barney" 229 | } 230 | "Breen" 231 | { 232 | "path" "models/breen.mdl" 233 | "sounds" "Breen" 234 | } 235 | "Eli" 236 | { 237 | "path" "models/eli.mdl" 238 | "sounds" "Eli" 239 | } 240 | "Dr.Kleiner" 241 | { 242 | "path" "models/kleiner.mdl" 243 | "sounds" "Kleiner" 244 | } 245 | "Father Grigori" 246 | { 247 | "path" "models/monk.mdl" 248 | "sounds" "Monk" 249 | } 250 | "Mossman" 251 | { 252 | "path" "models/mossman.mdl" 253 | "sounds" "Mossman" 254 | } 255 | "Odessa" 256 | { 257 | "path" "models/odessa.mdl" 258 | "sounds" "Odessea" 259 | } 260 | } 261 | "Sounds" 262 | { 263 | "Example" 264 | { 265 | // Model viewed in model browser 266 | "View" 267 | { 268 | // "path" "dir/example.wav" 269 | // "path" "dir/example2.wav" 270 | } 271 | // Model selected 272 | "Select" 273 | { 274 | } 275 | // Player gets hurt 276 | "Hurt" 277 | { 278 | } 279 | // Player dies 280 | "Death" 281 | { 282 | } 283 | // Player jumps 284 | "Jump" 285 | { 286 | } 287 | } 288 | 289 | "Male" 290 | { 291 | "Hurt" 292 | { 293 | "path" "vo/npc/male01/imhurt01.wav" 294 | "path" "vo/npc/male01/imhurt02.wav" 295 | "path" "vo/npc/male01/help01.wav" 296 | "path" "vo/npc/male01/hitingut01.wav" 297 | "path" "vo/npc/male01/hitingut02.wav" 298 | "path" "vo/npc/male01/myarm02.wav" 299 | "path" "vo/npc/male01/myarm01.wav" 300 | "path" "vo/npc/male01/mygut02.wav" 301 | "path" "vo/npc/male01/myleg01.wav" 302 | "path" "vo/npc/male01/myleg02.wav" 303 | "path" "vo/npc/male01/no01.wav" 304 | "path" "vo/npc/male01/no02.wav" 305 | "path" "vo/npc/male01/ow01.wav" 306 | "path" "vo/npc/male01/ow02.wav" 307 | "path" "vo/npc/male01/pain01.wav" 308 | "path" "vo/npc/male01/pain02.wav" 309 | "path" "vo/npc/male01/pain02.wav" 310 | "path" "vo/npc/male01/pain03.wav" 311 | "path" "vo/npc/male01/pain04.wav" 312 | "path" "vo/npc/male01/pain05.wav" 313 | "path" "vo/npc/male01/pain06.wav" 314 | } 315 | "View" 316 | { 317 | "path" "vo\npc\male01\answer35.wav" 318 | "path" "vo\npc\male01\herecomehacks02.wav" 319 | "path" "vo\npc\male01\okimready01.wav" 320 | "path" "vo\npc\male01\okimready03.wav" 321 | "path" "vo\npc\male01\question02.wav" 322 | "path" "vo\npc\male01\question29.wav" 323 | "path" "vo\npc\male01\question07.wav" 324 | "path" "vo\npc\male01\question19.wav" 325 | "path" "vo\npc\male01\readywhenyouare01.wav" 326 | "path" "vo\npc\male01\readywhenyouare02.wav" 327 | } 328 | "Select" 329 | { 330 | "path" "vo\npc\male01\fantastic01.wav" 331 | "path" "vo\npc\male01\fantastic02.wav" 332 | "path" "vo\npc\male01\letsgo01.wav" 333 | "path" "vo\npc\male01\letsgo02.wav" 334 | } 335 | } 336 | "Female" 337 | { 338 | "Hurt" 339 | { 340 | "path" "vo/npc/female01/imhurt01.wav" 341 | "path" "vo/npc/female01/imhurt02.wav" 342 | "path" "vo/npc/female01/help01.wav" 343 | "path" "vo/npc/female01/hitingut01.wav" 344 | "path" "vo/npc/female01/hitingut02.wav" 345 | "path" "vo/npc/female01/myarm02.wav" 346 | "path" "vo/npc/female01/myarm01.wav" 347 | "path" "vo/npc/female01/mygut02.wav" 348 | "path" "vo/npc/female01/myleg01.wav" 349 | "path" "vo/npc/female01/myleg02.wav" 350 | "path" "vo/npc/female01/no01.wav" 351 | "path" "vo/npc/female01/no02.wav" 352 | "path" "vo/npc/female01/ow01.wav" 353 | "path" "vo/npc/female01/ow02.wav" 354 | "path" "vo/npc/female01/pain01.wav" 355 | "path" "vo/npc/female01/pain02.wav" 356 | "path" "vo/npc/female01/pain02.wav" 357 | "path" "vo/npc/female01/pain03.wav" 358 | "path" "vo/npc/female01/pain04.wav" 359 | "path" "vo/npc/female01/pain06.wav" 360 | "path" "vo/npc/female01/pain05.wav" 361 | } 362 | "View" 363 | { 364 | "path" "vo\npc\female01\answer35.wav" 365 | "path" "vo\npc\female01\herecomehacks02.wav" 366 | "path" "vo\npc\female01\okimready01.wav" 367 | "path" "vo\npc\female01\okimready03.wav" 368 | "path" "vo\npc\female01\question02.wav" 369 | "path" "vo\npc\female01\question04.wav" 370 | "path" "vo\npc\female01\question07.wav" 371 | "path" "vo\npc\female01\question19.wav" 372 | "path" "vo\npc\female01\readywhenyouare01.wav" 373 | "path" "vo\npc\female01\readywhenyouare02.wav" 374 | } 375 | "Select" 376 | { 377 | "path" "vo\npc\female01\fantastic01.wav" 378 | "path" "vo\npc\female01\fantastic02.wav" 379 | "path" "vo\npc\female01\letsgo01.wav" 380 | "path" "vo\npc\female01\letsgo02.wav" 381 | } 382 | } 383 | "Combine" 384 | { 385 | "Hurt" 386 | { 387 | "path" "npc/combine_soldier/pain1.wav" 388 | "path" "npc/combine_soldier/pain2.wav" 389 | "path" "npc/combine_soldier/pain3.wav" 390 | "path" "npc/combine_soldier/vo/coverhurt.wav" 391 | } 392 | "View" 393 | { 394 | "path" "npc\combine_soldier\vo\boomer.wav" 395 | "path" "npc\combine_soldier\vo\echo.wav" 396 | "path" "npc\combine_soldier\vo\sightlineisclear.wav" 397 | } 398 | "Select" 399 | { 400 | "path" "npc\combine_soldier\vo\affirmative2.wav" 401 | } 402 | } 403 | "Metro" 404 | { 405 | "Hurt" 406 | { 407 | "path" "npc/metropolice/vo/help.wav" 408 | "path" "npc/metropolice/vo/shit.wav" 409 | "path" "npc/metropolice/pain1.wav" 410 | "path" "npc/metropolice/pain2.wav" 411 | "path" "npc/metropolice/pain3.wav" 412 | "path" "npc/metropolice/pain4.wav" 413 | } 414 | "View" 415 | { 416 | "path" "npc\metropolice\vo\pickupthecan1.wav" 417 | "path" "npc\metropolice\vo\pickupthecan2.wav" 418 | "path" "npc\metropolice\vo\pickupthecan3.wav" 419 | } 420 | "Select" 421 | { 422 | "path" "npc\metropolice\vo\off3.wav" 423 | } 424 | } 425 | "Gman" 426 | { 427 | "Hurt" 428 | { 429 | } 430 | "View" 431 | { 432 | "path" "vo\citadel\gman_exit01.wav" 433 | "path" "vo\citadel\gman_exit10.wav" 434 | } 435 | "Select" 436 | { 437 | } 438 | } 439 | "Barney" 440 | { 441 | "Hurt" 442 | { 443 | "path" "vo/npc/barney/ba_pain01.wav" 444 | "path" "vo/npc/barney/ba_pain02.wav" 445 | "path" "vo/npc/barney/ba_pain03.wav" 446 | "path" "vo/npc/barney/ba_pain04.wav" 447 | "path" "vo/npc/barney/ba_pain05.wav" 448 | "path" "vo/npc/barney/ba_pain06.wav" 449 | "path" "vo/npc/barney/ba_pain07.wav" 450 | "path" "vo/npc/barney/ba_pain08.wav" 451 | "path" "vo/npc/barney/ba_pain09.wav" 452 | "path" "vo/npc/barney/ba_pain10.wav" 453 | "path" "vo/npc/barney/ba_no01.wav" 454 | "path" "vo/npc/barney/ba_no02.wav" 455 | "path" "vo/npc/barney/ba_ohshit03.wav" 456 | "path" "vo/npc/barney/ba_wounded02.wav" 457 | "path" "vo/npc/barney/ba_wounded03.wav" 458 | } 459 | "View" 460 | { 461 | "path" "vo\k_lab\ba_headhumper01.wav" 462 | "path" "vo\k_lab\ba_sarcastic03.wav" 463 | "path" "vo\k_lab\ba_thingaway02.wav" 464 | "path" "vo\npc\barney\ba_laugh04.wav" 465 | "path" "vo\npc\barney\ba_oldtimes.wav" 466 | "path" "vo\trainyard\ba_rememberme.wav" 467 | "path" "vo\trainyard\ba_thatbeer02.wav" 468 | } 469 | "Select" 470 | { 471 | "path" "vo\npc\barney\ba_letsdoit.wav" 472 | "path" "vo\npc\barney\ba_letsgo.wav" 473 | "path" "vo\npc\barney\ba_ohyeah.wav" 474 | "path" "vo\npc\barney\ba_yell.wav" 475 | } 476 | } 477 | "Monk" 478 | { 479 | "Hurt" 480 | { 481 | "path" "vo/ravenholm/monk_pain01.wav" 482 | "path" "vo/ravenholm/monk_pain02.wav" 483 | "path" "vo/ravenholm/monk_pain03.wav" 484 | "path" "vo/ravenholm/monk_pain04.wav" 485 | "path" "vo/ravenholm/monk_pain05.wav" 486 | "path" "vo/ravenholm/monk_pain06.wav" 487 | "path" "vo/ravenholm/monk_pain07.wav" 488 | "path" "vo/ravenholm/monk_pain08.wav" 489 | "path" "vo/ravenholm/monk_pain09.wav" 490 | "path" "vo/ravenholm/monk_pain10.wav" 491 | "path" "vo/ravenholm/monk_pain12.wav" 492 | "path" "vo/ravenholm/monk_danger01.wav" 493 | "path" "vo/ravenholm/monk_helpme02.wav" 494 | "path" "vo/ravenholm/monk_helpme04.wav" 495 | "path" "vo/ravenholm/monk_helpme05.wav" 496 | } 497 | "View" 498 | { 499 | "path" "vo\ravenholm\monk_kill08.wav" 500 | "path" "vo\ravenholm\monk_kill05.wav" 501 | "path" "vo\ravenholm\monk_blocked03.wav" 502 | } 503 | "Select" 504 | { 505 | "path" "vo\ravenholm\monk_blocked01.wav" 506 | } 507 | } 508 | "Alyx" 509 | { 510 | "Hurt" 511 | { 512 | "path" "vo/npc/alyx/uggh01.wav" 513 | "path" "vo/npc/alyx/uggh02.wav" 514 | "path" "vo/npc/alyx/hurt04.wav" 515 | "path" "vo/npc/alyx/hurt05.wav" 516 | "path" "vo/npc/alyx/hurt06.wav" 517 | "path" "vo/npc/alyx/hurt08.wav" 518 | "path" "vo/npc/alyx/gasp03.wav" 519 | "path" "vo/npc/alyx/gasp03.wav" 520 | "path" "vo/npc/alyx/gasp02.wav" 521 | } 522 | "View" 523 | { 524 | "path" "vo/streetwar/alyx_gate/al_watchmyback.wav" 525 | "path" "vo/trainyard/al_imalyx.wav" 526 | "path" "vo/eli_lab/al_gooddoggie.wav" 527 | "path" "vo/eli_lab/al_havefun.wav" 528 | } 529 | "Select" 530 | { 531 | "path" "vo/streetwar/alyx_gate/al_letsgo01.wav" 532 | "path" "vo/streetwar/alyx_gate/al_heregoes.wav" 533 | "path" "vo/eli_lab/al_allright01.wav" 534 | "path" "voeli_lab/al_awesome.wav" 535 | "path" "vo/eli_lab/al_excellent01.wav" 536 | } 537 | } 538 | "Kleiner" 539 | { 540 | "Hurt" 541 | { 542 | "path" "vo/k_lab/kl_ahhhh.wav" 543 | "path" "vo/k_lab/kl_getoutrun03.wav" 544 | "path" "vo/k_lab/kl_hedyno03.wav" 545 | "path" "vo/k_lab/kl_ohdear.wav" 546 | "path" "vo/k_lab/kl_dearme.wav" 547 | } 548 | "View" 549 | { 550 | "path" "vo/k_lab/kl_ahhhh.wav" 551 | } 552 | "Select" 553 | { 554 | "path" "vo/k_lab/kl_fiddlesticks.wav" 555 | } 556 | } 557 | "Breen" 558 | { 559 | "Hurt" 560 | { 561 | "path" "vo/citadel/br_failing11.wav" 562 | "path" "vo/citadel/br_no.wav" 563 | "path" "vo/citadel/br_ohshit.wav" 564 | "path" "vo/citadel/br_youneedme.wav" 565 | } 566 | "View" 567 | { 568 | "path" "vo/breencast/br_instinct09.wav" 569 | "path" "vo/breencast/br_instinct10.wav" 570 | "path" "vo/breencast/br_instinct12.wav" 571 | "path" "vo/breencast/br_instinct13.wav" 572 | "path" "vo/breencast/br_instinct14.wav" 573 | "path" "vo/breencast/br_instinct15.wav" 574 | "path" "vo/breencast/br_instinct16.wav" 575 | "path" "vo/breencast/br_instinct17.wav" 576 | "path" "vo/breencast/br_instinct18.wav" 577 | } 578 | "Select" 579 | { 580 | "path" "vo/citadel/br_youneedme.wav" 581 | } 582 | } 583 | "Eli" 584 | { 585 | "Hurt" 586 | { 587 | "path" "vo/citadel/eli_notobreen.wav" 588 | "path" "vo\eli_lab\eli_handle_b.wav" 589 | } 590 | "View" 591 | { 592 | "path" "vo\eli_lab\eli_wantyou.wav" 593 | "path" "vo\k_lab\eli_shutdown.wav" 594 | } 595 | "Select" 596 | { 597 | } 598 | } 599 | "Mossman" 600 | { 601 | "Hurt" 602 | { 603 | "path" "vo/npc/female01/imhurt01.wav" 604 | "path" "vo/npc/female01/imhurt02.wav" 605 | "path" "vo/npc/female01/help01.wav" 606 | "path" "vo/npc/female01/hitingut01.wav" 607 | "path" "vo/npc/female01/hitingut02.wav" 608 | "path" "vo/npc/female01/myarm02.wav" 609 | "path" "vo/npc/female01/myarm01.wav" 610 | "path" "vo/npc/female01/mygut02.wav" 611 | "path" "vo/npc/female01/myleg01.wav" 612 | "path" "vo/npc/female01/myleg02.wav" 613 | "path" "vo/npc/female01/no01.wav" 614 | "path" "vo/npc/female01/no02.wav" 615 | "path" "vo/npc/female01/ow01.wav" 616 | "path" "vo/npc/female01/ow02.wav" 617 | "path" "vo/npc/female01/pain01.wav" 618 | "path" "vo/npc/female01/pain02.wav" 619 | "path" "vo/npc/female01/pain02.wav" 620 | "path" "vo/npc/female01/pain03.wav" 621 | "path" "vo/npc/female01/pain04.wav" 622 | "path" "vo/npc/female01/pain06.wav" 623 | "path" "vo/npc/female01/pain05.wav" 624 | } 625 | "View" 626 | { 627 | "path" "vo\eli_lab\mo_airlock03.wav" 628 | "path" "vo\eli_lab\mo_airlock10.wav" 629 | "path" "vo\eli_lab\mo_noblame.wav" 630 | } 631 | "Select" 632 | { 633 | } 634 | } 635 | "Odessea" 636 | { 637 | "Hurt" 638 | { 639 | "path" "vo/npc/male01/imhurt01.wav" 640 | "path" "vo/npc/male01/imhurt02.wav" 641 | "path" "vo/npc/male01/help01.wav" 642 | "path" "vo/npc/male01/hitingut01.wav" 643 | "path" "vo/npc/male01/hitingut02.wav" 644 | "path" "vo/npc/male01/myarm02.wav" 645 | "path" "vo/npc/male01/myarm01.wav" 646 | "path" "vo/npc/male01/mygut02.wav" 647 | "path" "vo/npc/male01/myleg01.wav" 648 | "path" "vo/npc/male01/myleg02.wav" 649 | "path" "vo/npc/male01/no01.wav" 650 | "path" "vo/npc/male01/no02.wav" 651 | "path" "vo/npc/male01/ow01.wav" 652 | "path" "vo/npc/male01/ow02.wav" 653 | "path" "vo/npc/male01/pain01.wav" 654 | "path" "vo/npc/male01/pain02.wav" 655 | "path" "vo/npc/male01/pain02.wav" 656 | "path" "vo/npc/male01/pain03.wav" 657 | "path" "vo/npc/male01/pain04.wav" 658 | "path" "vo/npc/male01/pain05.wav" 659 | "path" "vo/npc/male01/pain06.wav" 660 | } 661 | "View" 662 | { 663 | "path" "vo\coast\odessa\nlo_cub_ledtobelieve.wav" 664 | "path" "vo\coast\odessa\nlo_cub_wherewasi.wav" 665 | "path" "vo\coast\odessa\nlo_cub_class01.wav" 666 | "path" "vo\coast\odessa\nlo_cub_corkscrew.wav" 667 | "path" "vo\coast\odessa\nlo_cub_radio.wav" 668 | } 669 | "Select" 670 | { 671 | "path" "vo\coast\odessa\nlo_cub_service.wav" 672 | } 673 | } 674 | } 675 | } -------------------------------------------------------------------------------- /game/materials/modelchooser/background.vmt: -------------------------------------------------------------------------------- 1 | "UnlitGeneric" 2 | { 3 | "$basetexture" "modelchooser/background" 4 | "$translucent" "1" 5 | "$distancealpha" "1" 6 | 7 | // Bar edges 8 | $softedges 1 9 | $edgesoftnessstart .1 10 | $edgesoftnessend 0 11 | $scaleedgesoftnessbasedonscreenres 1 12 | 13 | // Bar color & alpha (only with softedges) 14 | $outline 1 15 | $outlinecolor "{0 0 0}" 16 | $outlinealpha "0.98" 17 | } -------------------------------------------------------------------------------- /game/materials/modelchooser/background.vtf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Alienmario/ModelChooser/159a7c4bfb179378cdbbfabb0c6bc37dce341812/game/materials/modelchooser/background.vtf -------------------------------------------------------------------------------- /gamedata/modelchooser.txt: -------------------------------------------------------------------------------- 1 | "Games" 2 | { 3 | "#default" 4 | { 5 | "Functions" 6 | { 7 | "CBaseEntity::SetModel_" // https://github.com/alliedmodders/sourcemod/issues/1879 8 | { 9 | "offset" "CBaseEntity::SetModel" 10 | "hooktype" "entity" 11 | "return" "void" 12 | "this" "entity" 13 | "arguments" 14 | { 15 | "model" 16 | { 17 | "type" "charptr" 18 | } 19 | } 20 | } 21 | "CBasePlayer::DeathSound" 22 | { 23 | "offset" "CBasePlayer::DeathSound" 24 | "hooktype" "entity" 25 | "return" "void" 26 | "this" "entity" 27 | "arguments" 28 | { 29 | "info" 30 | { 31 | "type" "objectptr" 32 | "flags" "byref" 33 | } 34 | } 35 | } 36 | "CBasePlayer::SetAnimation" 37 | { 38 | "offset" "CBasePlayer::SetAnimation" 39 | "hooktype" "entity" 40 | "return" "void" 41 | "this" "entity" 42 | "arguments" 43 | { 44 | "playerAnim" 45 | { 46 | "type" "int" 47 | } 48 | } 49 | } 50 | } 51 | } 52 | "hl2mp" 53 | { 54 | "Offsets" 55 | { 56 | "CBaseEntity::SetModel" // CBaseEntity::SetModel(char const*) 57 | { 58 | "windows" "26" 59 | "linux" "27" 60 | } 61 | "CBasePlayer::DeathSound" // CBasePlayer::DeathSound(CTakeDamageInfo const&) 62 | { 63 | "windows" "368" 64 | "linux" "369" 65 | } 66 | "CBasePlayer::SetAnimation" // CBasePlayer::SetAnimation(PLAYER_ANIM) 67 | { 68 | "windows" "371" 69 | "linux" "372" 70 | } 71 | 72 | // Engine 73 | 74 | "CBaseServer::GetClient" // CBaseServer::GetClient(int) 75 | { 76 | "windows" "6" 77 | "linux" "7" 78 | } 79 | "CBaseClient::UpdateAcknowledgedFramecount" // CBaseClient::UpdateAcknowledgedFramecount(int) 80 | { 81 | "windows" "4" 82 | "linux" "44" 83 | } 84 | } 85 | "Signatures" 86 | { 87 | "CBaseAnimating::ResetSequence" // CBaseAnimating::ResetSequence(int nSequence) 88 | { 89 | "windows" "\x55\x8B\xEC\xA1\x2A\x2A\x2A\x2A\x53\x56\x57\x83\x78\x30\x00\x8B\xD9" // str: "ResetSequence : %s: %s -> %s\n" 90 | "linux" "@_ZN14CBaseAnimating13ResetSequenceEi" 91 | } 92 | } 93 | } 94 | "bms" 95 | { 96 | "Offsets" 97 | { 98 | "CBaseEntity::SetModel" // CBaseEntity::SetModel(char const*) 99 | { 100 | "windows" "26" 101 | "linux" "27" 102 | } 103 | "CBasePlayer::DeathSound" // CBasePlayer::DeathSound(CTakeDamageInfo const&) 104 | { 105 | "windows" "382" 106 | "linux" "383" 107 | } 108 | "CBasePlayer::SetAnimation" // CBasePlayer::SetAnimation(PLAYER_ANIM) 109 | { 110 | "windows" "385" 111 | "linux" "386" 112 | } 113 | 114 | // Engine 115 | 116 | "CBaseServer::GetClient" // CBaseServer::GetClient(int) 117 | { 118 | "windows" "6" 119 | "linux" "7" 120 | } 121 | "CBaseClient::UpdateAcknowledgedFramecount" // CBaseClient::UpdateAcknowledgedFramecount(int) 122 | { 123 | "windows" "4" 124 | "linux" "44" 125 | } 126 | } 127 | } 128 | } 129 | -------------------------------------------------------------------------------- /scripting/include/modelchooser.inc: -------------------------------------------------------------------------------- 1 | #if defined _model_chooser_included 2 | #endinput 3 | #endif 4 | #define _model_chooser_included 5 | 6 | #define MODELCHOOSER_LIBRARY "ModelChooser" 7 | 8 | public SharedPlugin __pl_model_chooser = 9 | { 10 | name = MODELCHOOSER_LIBRARY, 11 | file = "ultimate_modelchooser.smx", 12 | #if defined REQUIRE_PLUGIN 13 | required = 1, 14 | #else 15 | required = 0, 16 | #endif 17 | }; 18 | 19 | #if !defined REQUIRE_PLUGIN 20 | public void __pl_model_chooser_SetNTVOptional() 21 | { 22 | MarkNativeAsOptional("ModelChooser_GetCurrentModelName"); 23 | MarkNativeAsOptional("ModelChooser_GetCurrentModelPath"); 24 | MarkNativeAsOptional("ModelChooser_GetCurrentModelProperty"); 25 | MarkNativeAsOptional("ModelChooser_UnlockModel"); 26 | MarkNativeAsOptional("ModelChooser_LockModel"); 27 | MarkNativeAsOptional("ModelChooser_SelectModel"); 28 | MarkNativeAsOptional("ModelChooser_IsClientChoosing"); 29 | MarkNativeAsOptional("ModelChooser_OpenChooser"); 30 | MarkNativeAsOptional("ModelChooser_PlayRandomSound"); 31 | MarkNativeAsOptional("ModelChooser_GetProperty"); 32 | MarkNativeAsOptional("ModelChooser_GetModelList"); 33 | MarkNativeAsOptional("ModelChooser_GetSoundMap"); 34 | } 35 | #endif 36 | 37 | /* Forwards */ 38 | 39 | forward void ModelChooser_OnConfigLoaded(); 40 | 41 | forward void ModelChooser_OnModelChanged(int client, const char[] modelName); 42 | 43 | /* Natives */ 44 | 45 | native bool ModelChooser_GetCurrentModelName(int client, char[] modelName, int maxLength); 46 | 47 | native bool ModelChooser_GetCurrentModelPath(int client, char[] modelPath, int maxLength); 48 | 49 | native bool ModelChooser_GetCurrentModelProperty(int client, const char[] key, char[] value, int maxLength); 50 | 51 | native bool ModelChooser_UnlockModel(int client, const char[] modelName, bool select = false); 52 | 53 | native bool ModelChooser_LockModel(int client, const char[] modelName); 54 | 55 | native bool ModelChooser_SelectModel(int client, const char[] modelName); 56 | 57 | native bool ModelChooser_IsClientChoosing(int client); 58 | 59 | native bool ModelChooser_OpenChooser(int client, bool printErrorMsg); 60 | 61 | native bool ModelChooser_PlayRandomSound(int client, const char[] soundType, bool toAll = false, bool stopLast = true, int pitch = 100, float volume = 1.0); 62 | 63 | native bool ModelChooser_GetProperty(const char[] modelName, const char[] key, char[] value, int maxLength); 64 | 65 | #if defined MODELCHOOSER_RAWDOG_API 66 | 67 | #include 68 | #include 69 | #include 70 | 71 | native ModelList ModelChooser_GetModelList(); 72 | 73 | native SoundMap ModelChooser_GetSoundMap(); 74 | 75 | #endif 76 | -------------------------------------------------------------------------------- /scripting/include/modelchooser/anims.inc: -------------------------------------------------------------------------------- 1 | #pragma semicolon 1 2 | #pragma newdecls required 3 | 4 | enum PLAYER_ANIM 5 | { 6 | PLAYER_IDLE, 7 | PLAYER_WALK, 8 | PLAYER_JUMP, 9 | PLAYER_SUPERJUMP, 10 | PLAYER_DIE, 11 | PLAYER_ATTACK1, 12 | PLAYER_IN_VEHICLE, 13 | 14 | // TF Player animations 15 | PLAYER_RELOAD, 16 | PLAYER_START_AIMING, 17 | PLAYER_LEAVE_AIMING, 18 | }; 19 | 20 | public MRESReturn Hook_SetAnimation(int client, DHookParam hParams) 21 | { 22 | PLAYER_ANIM playerAnim = hParams.Get(1); 23 | 24 | float playbackRate = 1.0; 25 | int sequence = GetCustomSequenceForAnim(client, playerAnim, playbackRate); 26 | if (sequence > -1 && callResetSequence) 27 | { 28 | SetEntPropFloat(client, Prop_Data, "m_flPlaybackRate", playbackRate); 29 | if (GetEntProp(client, Prop_Send, "m_nSequence") != sequence) 30 | { 31 | SDKCall(callResetSequence, client, sequence); 32 | SetEntPropFloat(client, Prop_Data, "m_flCycle", 0.0); 33 | } 34 | return MRES_Supercede; 35 | } 36 | return MRES_Ignored; 37 | } 38 | 39 | int GetCustomSequenceForAnim(int client, PLAYER_ANIM playerAnim, float &playbackRate) 40 | { 41 | PlayerModel model; 42 | if (GetSelectedModelAuto(client, model)) 43 | { 44 | if (GetEntityMoveType(client) == MOVETYPE_NOCLIP) 45 | { 46 | if (model.anim_noclip.seqList != null) 47 | { 48 | playbackRate = model.anim_noclip.rate; 49 | return model.anim_noclip.seqList.NextSequence(); 50 | } 51 | } 52 | 53 | PlayerAnimation selectedAnim; 54 | if (playerAnim == PLAYER_IDLE) 55 | { 56 | selectedAnim = model.anim_idle; 57 | } 58 | else if (playerAnim == PLAYER_JUMP) 59 | { 60 | selectedAnim = model.anim_jump; 61 | float time = GetGameTime(); 62 | if (time > nextJumpSound[client]) 63 | { 64 | PlayRandomSound(GetSoundPack(model).GetSoundList("jump"), client, client, SNDCHAN_STATIC, true, JUMP_PITCH_MIN, JUMP_PITCH_MAX, JUMP_VOL); 65 | nextJumpSound[client] = time + model.jumpSndParams.cooldown.Rand(); 66 | } 67 | } 68 | else if (playerAnim == PLAYER_WALK) 69 | { 70 | if (GetEntityFlags(client) & FL_DUCKING) 71 | { 72 | selectedAnim = model.anim_walk_crouch; 73 | } 74 | else 75 | { 76 | if (model.anim_run.seqList && GetEntProp(client, Prop_Send, "m_fIsSprinting")) 77 | { 78 | selectedAnim = model.anim_run; 79 | } 80 | else 81 | { 82 | selectedAnim = model.anim_walk; 83 | } 84 | } 85 | } 86 | 87 | if (selectedAnim.seqList != null) 88 | { 89 | playbackRate = selectedAnim.rate; 90 | return selectedAnim.seqList.NextSequence(); 91 | } 92 | } 93 | return -1; 94 | } 95 | -------------------------------------------------------------------------------- /scripting/include/modelchooser/commands.inc: -------------------------------------------------------------------------------- 1 | #pragma semicolon 1 2 | #pragma newdecls required 3 | 4 | public Action Command_Model(int client, int args) 5 | { 6 | if (PreEnterCheck(client)) 7 | { 8 | EnterModelChooser(client); 9 | } 10 | return Plugin_Handled; 11 | } 12 | 13 | public Action Command_UnlockModel(int client, int args) 14 | { 15 | if (args != 2) 16 | { 17 | ReplyToCommand(client, "Usage: sm_unlockmodel "); 18 | return Plugin_Handled; 19 | } 20 | 21 | char arg1[65], arg2[MODELCHOOSER_MAX_NAME]; 22 | GetCmdArg(1, arg1, sizeof(arg1)); 23 | GetCmdArg(2, arg2, sizeof(arg2)); 24 | 25 | char targetName[MAX_TARGET_LENGTH]; 26 | int targets[MAXPLAYERS], targetCount; 27 | bool tnIsMl; 28 | 29 | if ((targetCount = ProcessTargetString( 30 | arg1, 31 | client, 32 | targets, 33 | MAXPLAYERS, 34 | COMMAND_FILTER_NO_IMMUNITY, 35 | targetName, 36 | sizeof(targetName), 37 | tnIsMl)) <= 0) 38 | { 39 | ReplyToTargetError(client, targetCount); 40 | return Plugin_Handled; 41 | } 42 | 43 | String_ToUpper(arg2, arg2, sizeof(arg2)); 44 | 45 | if (modelList.FindByName(arg2) == -1) 46 | { 47 | ReplyToCommand(client, "Model named %s doesn't exist!", arg2); 48 | return Plugin_Handled; 49 | } 50 | 51 | for (int i = 0; i < targetCount; i++) 52 | { 53 | UnlockModel(targets[i], arg2); 54 | } 55 | 56 | if (targetCount == 1) 57 | { 58 | ReplyToCommand(client, "Unlocked model %s for %N.", arg2, targets[0]); 59 | } 60 | else 61 | { 62 | ReplyToCommand(client, "Unlocked model %s for %d players.", arg2, targetCount); 63 | } 64 | 65 | return Plugin_Handled; 66 | } 67 | 68 | public Action Command_LockModel(int client, int args) 69 | { 70 | if (args != 2) 71 | { 72 | ReplyToCommand(client, "Usage: sm_lockmodel "); 73 | return Plugin_Handled; 74 | } 75 | 76 | char arg1[65], arg2[MODELCHOOSER_MAX_NAME]; 77 | GetCmdArg(1, arg1, sizeof(arg1)); 78 | GetCmdArg(2, arg2, sizeof(arg2)); 79 | 80 | char targetName[MAX_TARGET_LENGTH]; 81 | int targets[MAXPLAYERS], targetCount; 82 | bool tnIsMl; 83 | 84 | if ((targetCount = ProcessTargetString( 85 | arg1, 86 | client, 87 | targets, 88 | MAXPLAYERS, 89 | COMMAND_FILTER_NO_IMMUNITY, 90 | targetName, 91 | sizeof(targetName), 92 | tnIsMl)) <= 0) 93 | { 94 | ReplyToTargetError(client, targetCount); 95 | return Plugin_Handled; 96 | } 97 | 98 | String_ToUpper(arg2, arg2, sizeof(arg2)); 99 | 100 | if (modelList.FindByName(arg2) == -1) 101 | { 102 | ReplyToCommand(client, "Model named %s doesn't exist!", arg2); 103 | return Plugin_Handled; 104 | } 105 | 106 | for (int i = 0; i < targetCount; i++) 107 | { 108 | LockModel(targets[i], arg2); 109 | } 110 | 111 | if (targetCount == 1) 112 | { 113 | ReplyToCommand(client, "Locked model %s for %N.", arg2, targets[0]); 114 | } 115 | else 116 | { 117 | ReplyToCommand(client, "Locked model %s for %d players.", arg2, targetCount); 118 | } 119 | 120 | return Plugin_Handled; 121 | } 122 | -------------------------------------------------------------------------------- /scripting/include/modelchooser/config.inc: -------------------------------------------------------------------------------- 1 | #pragma semicolon 1 2 | #pragma newdecls required 3 | 4 | void LoadConfig() 5 | { 6 | char szConfigPath[PLATFORM_MAX_PATH]; 7 | BuildPath(Path_SM, szConfigPath, sizeof(szConfigPath), "configs/player_models.cfg"); 8 | if (!FileExists(szConfigPath)) 9 | SetFailState("File %s doesn't exist", szConfigPath); 10 | 11 | KeyValues kv = new KeyValues(""); 12 | char section[32]; 13 | if (!(kv.ImportFromFile(szConfigPath) && kv.GetSectionName(section, sizeof(section)) && strcmp(section, "ModelSystem", false) == 0)) 14 | SetFailState("Couldn't import %s into KeyValues", szConfigPath); 15 | 16 | if (kv.GotoFirstSubKey()) 17 | { 18 | do 19 | { 20 | if (kv.GetSectionName(section, sizeof(section))) 21 | { 22 | if (StrEqual(section, "Models", false)) 23 | { 24 | ParseModels(kv); 25 | } 26 | else if (StrEqual(section, "Sounds", false)) 27 | { 28 | ParseSounds(kv); 29 | } 30 | } 31 | } 32 | while (kv.GotoNextKey()); 33 | } 34 | delete kv; 35 | 36 | Call_StartForward(fwdOnConfigLoaded); 37 | Call_Finish(); 38 | } 39 | 40 | void ParseModels(KeyValues kv) 41 | { 42 | StringMap duplicityChecker = new StringMap(); 43 | char buffer[128]; 44 | 45 | if (kv.GotoFirstSubKey()) 46 | { 47 | do 48 | { 49 | PlayerModel model; 50 | if (kv.GetSectionName(model.name, sizeof(model.name))) 51 | { 52 | if (!kv.GetNum("enabled", 1)) 53 | continue; 54 | 55 | String_ToUpper(model.name, model.name, sizeof(model.name)); 56 | if (!duplicityChecker.SetString(model.name, "", false)) 57 | SetFailState("Duplicate model name: %s", model.name); 58 | 59 | kv.GetString("path", model.path, sizeof(model.path)); 60 | if (model.path[0] == EOS) 61 | continue; 62 | 63 | SmartDM.AddEx(model.path, downloads, true, true); 64 | 65 | StudioHdr studio = StudioHdr(model.path); 66 | model.skins = ProcessSkins(studio, ParseDelimitedIntList(kv, "skins")); 67 | model.bodyGroups = ProcessBodyGroups(studio, ParseDelimitedIntList(kv, "bodygroups")); 68 | 69 | kv.GetString("vmbody", model.vmBodyGroups, sizeof(model.vmBodyGroups)); 70 | TrimString(model.vmBodyGroups); 71 | 72 | if (kv.GetDataType("anims") == KvData_None && kv.JumpToKey("anims")) 73 | { 74 | ParseAnims(kv, studio, model); 75 | kv.GoBack(); 76 | } 77 | 78 | kv.GetString("sounds", model.sounds, sizeof(model.sounds)); 79 | 80 | ParseInterval(kv, model.jumpSndParams.cooldown, "jumpSoundTime"); 81 | ParseInterval(kv, model.hurtSndHP, "hurtSoundHP", HURT_SOUND_HP, HURT_SOUND_HP); 82 | 83 | model.locked = !!kv.GetNum("locked", 0); 84 | model.defaultPrio = kv.GetNum("defaultprio"); 85 | 86 | if (kv.GetDataType("hudcolor") != KvData_None) 87 | { 88 | kv.GetColor4("hudcolor", model.hudColor); 89 | } 90 | else 91 | { 92 | model.hudColor = DEFAULT_HUD_COLOR; 93 | } 94 | 95 | kv.GetString("adminflags", buffer, sizeof(buffer), "-1"); 96 | model.adminBitFlags = StrEqual(buffer, "-1") ? -1 : ReadFlagString(buffer); 97 | 98 | kv.GetString("team", model.team, sizeof(model.team), "0"); 99 | 100 | if (kv.GetDataType("downloads") == KvData_None && kv.JumpToKey("downloads")) 101 | { 102 | ParseFileItems(kv, false); 103 | kv.GoBack(); 104 | } 105 | 106 | model.customProperties = new StringMap(); 107 | if (kv.GetDataType("custom") == KvData_None && kv.JumpToKey("custom")) 108 | { 109 | ParseCustomProperties(kv, model.customProperties); 110 | kv.GoBack(); 111 | } 112 | 113 | modelList.PushArray(model); 114 | } 115 | } 116 | while (kv.GotoNextKey()); 117 | kv.GoBack(); 118 | } 119 | delete duplicityChecker; 120 | } 121 | 122 | void ParseAnims(KeyValues kv, StudioHdr studiohdr, PlayerModel model) 123 | { 124 | StringMap act2seq = new StringMap(); 125 | StringMap seqNums = new StringMap(); 126 | char seqName[MAX_ANIM_NAME], actName[MAX_ANIM_NAME]; 127 | int seqCount = studiohdr.numlocalseq; 128 | for (int i = 0; i < seqCount; i++) 129 | { 130 | Sequence seq = studiohdr.GetSequence(i); 131 | seq.GetLabelName(seqName, sizeof(seqName)); 132 | seq.GetActivityName(actName, sizeof(actName)); 133 | seqNums.SetValue(seqName, i); 134 | 135 | WeightedSequenceList seqList; 136 | if (!act2seq.GetValue(actName, seqList)) 137 | { 138 | seqList = new WeightedSequenceList(); 139 | act2seq.SetValue(actName, seqList); 140 | } 141 | seqList.Add(i, seq.actweight); 142 | } 143 | 144 | ParseAnim(kv, model, model.anim_idle, "idle", act2seq, seqNums); 145 | ParseAnim(kv, model, model.anim_idle_crouch, "idle_crouch", act2seq, seqNums); 146 | ParseAnim(kv, model, model.anim_walk, "walk", act2seq, seqNums); 147 | ParseAnim(kv, model, model.anim_walk_crouch, "walk_crouch", act2seq, seqNums); 148 | ParseAnim(kv, model, model.anim_run, "run", act2seq, seqNums); 149 | ParseAnim(kv, model, model.anim_jump, "jump", act2seq, seqNums); 150 | ParseAnim(kv, model, model.anim_noclip, "noclip", act2seq, seqNums); 151 | 152 | StringMapSnapshot snapshot = act2seq.Snapshot(); 153 | for (int i = 0; i < snapshot.Length; i++) 154 | { 155 | WeightedSequenceList wsl; 156 | snapshot.GetKey(i, actName, sizeof(actName)); 157 | act2seq.GetValue(actName, wsl); 158 | wsl.Close(); 159 | } 160 | snapshot.Close(); 161 | act2seq.Close(); 162 | seqNums.Close(); 163 | } 164 | 165 | void ParseAnim(KeyValues kv, PlayerModel model, PlayerAnimation anim, const char[] key, StringMap act2seq, StringMap seqNums) 166 | { 167 | char szSearchName[MAX_ANIM_NAME]; 168 | 169 | if (kv.GetDataType(key) == KvData_None && kv.JumpToKey(key)) 170 | { 171 | kv.GetString("anim", szSearchName, sizeof(szSearchName)); 172 | anim.rate = kv.GetFloat("rate", 1.0); 173 | kv.GoBack(); 174 | } 175 | else 176 | { 177 | kv.GetString(key, szSearchName, sizeof(szSearchName)); 178 | anim.rate = 1.0; 179 | } 180 | 181 | TrimString(szSearchName); 182 | if (szSearchName[0] == '\0') 183 | return; 184 | 185 | int seq; 186 | if (seqNums.GetValue(szSearchName, seq)) 187 | { 188 | anim.seqList = new WeightedSequenceList(); 189 | anim.seqList.Add(seq, 0); 190 | return; 191 | } 192 | if (act2seq.GetValue(szSearchName, anim.seqList)) 193 | { 194 | anim.seqList = view_as(CloneHandle(anim.seqList)); 195 | } 196 | else 197 | { 198 | LogMessage("Animation activity/sequence \"%s\" not found in model \"%s\"!", szSearchName, model.path); 199 | } 200 | } 201 | 202 | void ParseSounds(KeyValues kv) 203 | { 204 | if (kv.GotoFirstSubKey()) 205 | { 206 | char soundPack[MODELCHOOSER_MAX_NAME]; 207 | do 208 | { 209 | if (kv.GetSectionName(soundPack, sizeof(soundPack))) 210 | { 211 | soundMap.AddSoundPack(soundPack, ParseSoundPack(kv)); 212 | } 213 | } 214 | while (kv.GotoNextKey()); 215 | kv.GoBack(); 216 | } 217 | } 218 | 219 | SoundPack ParseSoundPack(KeyValues kv) 220 | { 221 | SoundPack soundPack = new SoundPack(); 222 | if (kv.GotoFirstSubKey()) 223 | { 224 | char soundType[MODELCHOOSER_MAX_NAME]; 225 | do 226 | { 227 | if (kv.GetSectionName(soundType, sizeof(soundType))) 228 | { 229 | soundPack.AddSoundList(soundType, view_as(ParseFileItems(kv, true, "sound"))); 230 | } 231 | } 232 | while (kv.GotoNextKey()); 233 | kv.GoBack(); 234 | } 235 | return soundPack; 236 | } 237 | 238 | ArrayList ParseFileItems(KeyValues kv, bool precache, const char[] folderType = "") 239 | { 240 | ArrayList files = new ArrayList(PLATFORM_MAX_PATH); 241 | char path[PLATFORM_MAX_PATH]; 242 | 243 | if (kv.GotoFirstSubKey(false)) 244 | { 245 | do 246 | { 247 | kv.GetString(NULL_STRING, path, sizeof(path)); 248 | if (path[0] == EOS) 249 | continue; 250 | 251 | files.PushString(path); 252 | if (folderType[0] != EOS) 253 | { 254 | Format(path, sizeof(path), "%s/%s", folderType, path); 255 | } 256 | SmartDM.AddEx(path, downloads, precache, true); 257 | } 258 | while (kv.GotoNextKey(false)); 259 | kv.GoBack(); 260 | } 261 | return files; 262 | } 263 | 264 | ArrayList ParseDelimitedIntList(KeyValues kv, const char[] key) 265 | { 266 | if (!kv.JumpToKey(key)) 267 | return null; 268 | 269 | char value[2048], buffer[32]; 270 | kv.GetString(NULL_STRING, value, sizeof(value)); 271 | 272 | ArrayList list = new ArrayList(); 273 | 274 | for (int i, n;;) 275 | { 276 | n = SplitString(value[i], ";", buffer, sizeof(buffer)); 277 | if (n == -1) 278 | { 279 | // remainder 280 | strcopy(buffer, sizeof(buffer), value[i]); 281 | TrimString(buffer); 282 | if (buffer[0] != EOS) 283 | { 284 | list.Push(StringToInt(buffer)); 285 | } 286 | break; 287 | } 288 | i += n; 289 | TrimString(buffer); 290 | if (buffer[0] != EOS) 291 | { 292 | list.Push(StringToInt(buffer)); 293 | } 294 | } 295 | 296 | kv.GoBack(); 297 | return list; 298 | } 299 | 300 | void ParseCustomProperties(KeyValues kv, StringMap map) 301 | { 302 | if (kv.GotoFirstSubKey(false)) 303 | { 304 | char key[256], value[4096]; 305 | do 306 | { 307 | if (kv.GetSectionName(key, sizeof(key))) 308 | { 309 | String_ToUpper(key, key, sizeof(key)); 310 | kv.GetString(NULL_STRING, value, sizeof(value)); 311 | map.SetString(key, value); 312 | } 313 | } 314 | while (kv.GotoNextKey(false)); 315 | kv.GoBack(); 316 | } 317 | } 318 | 319 | void ParseInterval(KeyValues kv, Interval interval, const char[] key, float defualtMin = 0.0, float defaultMax = 0.0) 320 | { 321 | char val[32]; 322 | kv.GetString(key, val, sizeof(val), ""); 323 | 324 | if (val[0] == EOS) 325 | { 326 | interval.min = defualtMin; 327 | interval.max = defaultMax; 328 | return; 329 | } 330 | 331 | int delim = StrContains(val, ";"); 332 | if (delim != -1 && delim != sizeof(val) - 1) 333 | { 334 | interval.max = StringToFloat(val[delim + 1]); 335 | val[delim] = '\0'; 336 | interval.min = StringToFloat(val); 337 | return; 338 | } 339 | 340 | interval.min = interval.max = StringToFloat(val); 341 | } 342 | 343 | ArrayList ProcessSkins(StudioHdr studio, ArrayList list) 344 | { 345 | int numSkins = studio.numskinfamilies; 346 | 347 | if (list) 348 | { 349 | for (int i = list.Length - 1; i >= 0; i--) 350 | { 351 | int skin = list.Get(i); 352 | if (skin < 0 || skin >= numSkins) 353 | { 354 | list.Erase(i); 355 | } 356 | } 357 | } 358 | else 359 | { 360 | list = new ArrayList(); 361 | for (int i = 0; i < numSkins; i++) 362 | { 363 | list.Push(i); 364 | } 365 | } 366 | 367 | if (!list.Length) 368 | { 369 | list.Push(0); 370 | } 371 | 372 | return list; 373 | } 374 | 375 | ArrayList ProcessBodyGroups(StudioHdr studio, ArrayList list) 376 | { 377 | int numBodyGroups; 378 | for (int i = studio.numbodyparts - 1; i >= 0; i--) 379 | { 380 | BodyPart pBodyPart = studio.GetBodyPart(i); 381 | numBodyGroups += pBodyPart.nummodels; 382 | } 383 | 384 | if (list) 385 | { 386 | for (int i = list.Length - 1; i >= 0; i--) 387 | { 388 | int bodyGroup = list.Get(i); 389 | if (bodyGroup < 0 || bodyGroup >= numBodyGroups) 390 | { 391 | list.Erase(i); 392 | } 393 | } 394 | } 395 | else 396 | { 397 | list = new ArrayList(); 398 | for (int i = 0; i < numBodyGroups; i++) 399 | { 400 | list.Push(i); 401 | } 402 | } 403 | 404 | if (!list.Length) 405 | { 406 | list.Push(0); 407 | } 408 | 409 | return list; 410 | } -------------------------------------------------------------------------------- /scripting/include/modelchooser/globals.inc: -------------------------------------------------------------------------------- 1 | #pragma semicolon 1 2 | #pragma newdecls required 3 | 4 | // Complete model list containing entries of PlayerModel 5 | ModelList modelList; 6 | 7 | // Complete sounds map containing entries of SoundPack, indexed by names 8 | SoundMap soundMap; 9 | 10 | // The filtered list of selectable models. Contains indexes into modelList. Is null until client models are initialized. 11 | ArrayList selectableModels[MAXPLAYERS + 1]; 12 | 13 | // Active selection data 14 | SelectionData activeSelection[MAXPLAYERS + 1]; 15 | 16 | // Menu selection data 17 | SelectionData menuSelection[MAXPLAYERS + 1]; 18 | 19 | // Map containing names of unlocked models 20 | StringMap unlockedModels[MAXPLAYERS + 1]; 21 | 22 | // Used for stopping 23 | char lastPlayedSound[MAXPLAYERS + 1][PLATFORM_MAX_PATH]; 24 | 25 | // Flag for playing hurt sound once 26 | int playedHurtSoundAt[MAXPLAYERS + 1] = {-1, ...}; 27 | 28 | // Time to play next jump sound at 29 | float nextJumpSound[MAXPLAYERS + 1]; 30 | 31 | // Counter for # of checks to pass until client models can be initialized 32 | int clientInitChecks[MAXPLAYERS + 1]; 33 | 34 | // Hud channel toggles (bi-channel switching allows displaying proper colors) 35 | int topHudChanToggle[MAXPLAYERS + 1]; 36 | int bottomHudChanToggle[MAXPLAYERS + 1] = {2, ...}; 37 | 38 | // Delayed hud init timer 39 | Handle tMenuInit[MAXPLAYERS + 1]; 40 | 41 | // Team number cached from changeteam event hook 42 | int currentTeam[MAXPLAYERS + 1]; 43 | 44 | // Downloads fileset 45 | SmartDM_FileSet downloads; 46 | 47 | // Hooks 48 | DynamicHook hkSetModel; 49 | DynamicHook hkDeathSound; 50 | DynamicHook hkSetAnimation; 51 | 52 | // Calls 53 | Handle callResetSequence; 54 | Handle callGetClient; 55 | Handle callUpdateAcknowledgedFramecount; 56 | 57 | // Persistence 58 | PersistentPreferences persistentPreferences[MAX_TEAMS]; 59 | 60 | // Forwards 61 | GlobalForward fwdOnConfigLoaded; 62 | GlobalForward fwdOnModelChanged; 63 | 64 | // Cvars 65 | ConVar cvSelectionImmunity; 66 | ConVar cvAutoReload; 67 | ConVar cvOverlay; 68 | ConVar cvLockModel; 69 | ConVar cvLockScale; 70 | ConVar cvMenuSnd; 71 | ConVar cvTeamBased; 72 | ConVar cvHudText1x; 73 | ConVar cvHudText1y; 74 | ConVar cvHudText2x; 75 | ConVar cvHudText2y; 76 | ConVar cvForceFullUpdate; 77 | ConVar mp_forcecamera; 78 | -------------------------------------------------------------------------------- /scripting/include/modelchooser/menu.inc: -------------------------------------------------------------------------------- 1 | #pragma semicolon 1 2 | #pragma newdecls required 3 | 4 | bool IsInMenu(int client) 5 | { 6 | return (menuSelection[client].index != -1); 7 | } 8 | 9 | bool PreEnterCheck(int client, bool printError = true) 10 | { 11 | if (!client) 12 | { 13 | return false; 14 | } 15 | if (selectableModels[client] == null || !selectableModels[client].Length) 16 | { 17 | if (printError) PrintToChat(client, "[ModelChooser] No models are available."); 18 | return false; 19 | } 20 | if (!IsPlayerAlive(client)) 21 | { 22 | if (printError) PrintToChat(client, "[ModelChooser] You need to be alive to use models."); 23 | return false; 24 | } 25 | if (IsInMenu(client)) 26 | { 27 | if (printError) PrintToChat(client, "[ModelChooser] You are already changing models, dummy :]"); 28 | return false; 29 | } 30 | if (GetEntityFlags(client) & FL_ATCONTROLS || Client_IsInThirdPersonMode(client)) 31 | { 32 | if (printError) PrintToChat(client, "[ModelChooser] You cannot change models currently."); 33 | return false; 34 | } 35 | return true; 36 | } 37 | 38 | void EnterModelChooser(int client) 39 | { 40 | menuSelection[client] = activeSelection[client]; 41 | 42 | int ragdoll = GetEntPropEnt(client, Prop_Send, "m_hRagdoll"); 43 | if (ragdoll != -1) 44 | { 45 | RemoveEntity(ragdoll); 46 | } 47 | 48 | StopSound(client, SNDCHAN_STATIC, lastPlayedSound[client]); 49 | StopSound(client, SNDCHAN_BODY, lastPlayedSound[client]); 50 | StopSound(client, SNDCHAN_AUTO, lastPlayedSound[client]); 51 | 52 | // center camera pitch 53 | if (mp_forcecamera) 54 | { 55 | mp_forcecamera.ReplicateToClient(client, "0"); 56 | } 57 | float eyeAngles[3]; 58 | GetClientEyeAngles(client, eyeAngles); 59 | eyeAngles[0] = 0.0; 60 | TeleportEntity(client, .angles = eyeAngles); 61 | 62 | Client_SetObserverTarget(client, 0); 63 | Client_SetObserverMode(client, OBS_MODE_DEATHCAM, false); 64 | Client_SetDrawViewModel(client, false); 65 | FixThirdpersonWeapons(client); 66 | ToggleMenuOverlay(client, true); 67 | Client_SetHideHud(client, HIDEHUD_HEALTH|HIDEHUD_CROSSHAIR|HIDEHUD_FLASHLIGHT|HIDEHUD_WEAPONSELECTION|HIDEHUD_MISCSTATUS); 68 | Client_ScreenFade(client, 100, FFADE_PURGE|FFADE_IN, 0); 69 | SetEntityFlags(client, GetEntityFlags(client) | FL_ATCONTROLS); 70 | SetEntPropFloat(client, Prop_Data, "m_flNextAttack", float(cellmax)); 71 | 72 | if (cvSelectionImmunity.BoolValue) 73 | { 74 | SDKHook(client, SDKHook_OnTakeDamage, Hook_BlockDamage); 75 | } 76 | 77 | tMenuInit[client] = CreateTimer(0.1, Timer_MenuInit1, GetClientUserId(client), TIMER_FLAG_NO_MAPCHANGE); 78 | OnMenuModelSelection(client, true); 79 | } 80 | 81 | void ExitModelChooser(int client, bool silent = false, bool cancel = false) 82 | { 83 | Client_SetObserverTarget(client, -1); 84 | Client_SetObserverMode(client, OBS_MODE_NONE, false); 85 | Client_SetDrawViewModel(client, true); 86 | if (mp_forcecamera) 87 | { 88 | mp_forcecamera.ReplicateToClient(client, mp_forcecamera.BoolValue? "1" : "0"); 89 | } 90 | ToggleMenuOverlay(client, false); 91 | Client_SetHideHud(client, 0); 92 | Client_ScreenFade(client, 200, FFADE_PURGE|FFADE_IN, 0); 93 | SetEntityFlags(client, GetEntityFlags(client) & ~FL_ATCONTROLS); 94 | ClearMenuHud(client); 95 | SDKUnhook(client, SDKHook_OnTakeDamage, Hook_BlockDamage); 96 | StopSound(client, SNDCHAN_BODY, lastPlayedSound[client]); 97 | SetEntPropFloat(client, Prop_Data, "m_flNextAttack", GetGameTime() + 0.25); 98 | ToggleMenuLock(client, false); 99 | delete tMenuInit[client]; 100 | 101 | PlayerModel model; 102 | if (!cancel && menuSelection[client].IsValid() && Selection2Model(client, menuSelection[client], model)) 103 | { 104 | if (!silent) 105 | { 106 | PlayRandomSound(GetSoundPack(model).GetSoundList("select"), client); 107 | } 108 | 109 | activeSelection[client] = menuSelection[client]; 110 | 111 | PersistentPreferences prefs; prefs = GetPreferences(client); 112 | prefs.model.Set(client, model.name); 113 | prefs.skin.SetInt(client, model.GetSkin(menuSelection[client].skin)); 114 | prefs.body.SetInt(client, model.GetBody(menuSelection[client].body)); 115 | 116 | PrintToChat(client, "\x07d9843fModel selected: \x07f5bf42%s", model.name); 117 | 118 | CallModelChanged(client, model); 119 | } 120 | 121 | menuSelection[client].Reset(); 122 | ForceFullUpdate(client); 123 | } 124 | 125 | void OnMenuModelSelection(int client, bool initial = false, bool scrolling = false) 126 | { 127 | StopSound(client, SNDCHAN_BODY, lastPlayedSound[client]); 128 | 129 | PlayerModel model; 130 | Selection2Model(client, menuSelection[client], model); 131 | 132 | menuSelection[client].skinCount = model.skins.Length; 133 | menuSelection[client].bodyCount = model.bodyGroups.Length; 134 | menuSelection[client].locked = IsModelLocked(model, client); 135 | 136 | if (!initial) 137 | { 138 | menuSelection[client].skin = 0; 139 | menuSelection[client].body = 0; 140 | PlayMenuClickSound(client); 141 | } 142 | 143 | if (!scrolling && !menuSelection[client].locked) 144 | { 145 | PlayRandomSound(GetSoundPack(model).GetSoundList("view"), client, _, SNDCHAN_BODY); 146 | } 147 | 148 | RefreshModel(client); 149 | UpdateMenuHud(client, model); 150 | ToggleMenuLock(client, menuSelection[client].locked); 151 | } 152 | 153 | void OnMenuSkinSelection(int client) 154 | { 155 | if (menuSelection[client].locked) 156 | return; 157 | 158 | PlayerModel model; 159 | GetSelectedModel(client, model, true); 160 | UpdateMenuHud(client, model); 161 | SetEntitySkin(client, model.GetSkin(menuSelection[client].skin)); 162 | PlayMenuClickSound(client); 163 | } 164 | 165 | void OnMenuBodySelection(int client) 166 | { 167 | if (menuSelection[client].locked) 168 | return; 169 | 170 | PlayerModel model; 171 | GetSelectedModel(client, model, true); 172 | UpdateMenuHud(client, model); 173 | SetEntityBody(client, model.GetBody(menuSelection[client].body)); 174 | PlayMenuClickSound(client); 175 | } 176 | 177 | void PlayMenuClickSound(int client) 178 | { 179 | char path[PLATFORM_MAX_PATH]; 180 | cvMenuSnd.GetString(path, sizeof(path)); 181 | if (path[0] != EOS) 182 | { 183 | EmitSoundToClient(client, path, .level = 0); 184 | } 185 | } 186 | 187 | void ToggleMenuOverlay(int client, bool enable) 188 | { 189 | char path[PLATFORM_MAX_PATH]; 190 | cvOverlay.GetString(path, sizeof(path)); 191 | 192 | if (!StrEqual(path, "") && !StrEqual(path, "0")) 193 | { 194 | Client_SetScreenOverlay(client, enable ? path : ""); 195 | } 196 | } 197 | 198 | void ToggleMenuLock(int client, bool enable) 199 | { 200 | static int lockRef[MAXPLAYERS + 1] = {-1, ...}; 201 | 202 | if (enable) 203 | { 204 | char lockModel[PLATFORM_MAX_PATH]; 205 | cvLockModel.GetString(lockModel, sizeof(lockModel)); 206 | 207 | SetEntityEffects(client, GetEntityEffects(client) | EF_NODRAW | EF_NOSHADOW); 208 | int weapon = Client_GetActiveWeapon(client); 209 | if (weapon != -1) 210 | { 211 | SetEntityEffects(weapon, GetEntityEffects(weapon) | EF_NODRAW | EF_NOSHADOW); 212 | } 213 | 214 | if (lockModel[0] == EOS) 215 | return; 216 | 217 | if (IsValidEntity(lockRef[client])) 218 | return; 219 | 220 | int entity = CreateEntityByName("prop_dynamic_override"); 221 | if (entity != -1) 222 | { 223 | DispatchKeyValue(entity, "model", lockModel); 224 | DispatchKeyValueFloat(entity, "modelscale", cvLockScale.FloatValue); 225 | DispatchKeyValue(entity, "disableshadows", "1"); 226 | DispatchKeyValue(entity, "solid", "0"); 227 | 228 | float eyePos[3]; 229 | GetClientEyePosition(client, eyePos); 230 | DispatchKeyValueVector(entity, "origin", eyePos); 231 | 232 | SetEntityOwner(entity, client); 233 | SDKHook(entity, SDKHook_SetTransmit, Hook_TransmitToOwnerOnly); 234 | 235 | DispatchSpawn(entity); 236 | ActivateEntity(entity); 237 | Entity_SetParent(entity, client); 238 | 239 | lockRef[client] = EntIndexToEntRef(entity); 240 | } 241 | } 242 | else 243 | { 244 | SetEntityEffects(client, GetEntityEffects(client) & ~EF_NODRAW & ~EF_NOSHADOW); 245 | int weapon = Client_GetActiveWeapon(client); 246 | if (weapon != -1) 247 | { 248 | SetEntityEffects(weapon, GetEntityEffects(weapon) & ~EF_NODRAW & ~EF_NOSHADOW); 249 | } 250 | 251 | if (!IsValidEntity(lockRef[client])) 252 | return; 253 | 254 | RemoveEntity(lockRef[client]); 255 | lockRef[client] = -1; 256 | } 257 | } 258 | 259 | static bool g_bDrawing; 260 | 261 | void UpdateMenuHud(int client, PlayerModel model, bool initital = false) 262 | { 263 | if (tMenuInit[client]) 264 | return; 265 | 266 | ClearMenuHud(client, true, true); 267 | g_bDrawing = true; 268 | 269 | static char text[128]; 270 | 271 | bool showBody = menuSelection[client].bodyCount > 1 && !menuSelection[client].locked; 272 | bool showSkin = menuSelection[client].skinCount > 1 && !menuSelection[client].locked; 273 | text = (!showBody || !showSkin) ? "\n" : ""; 274 | 275 | int color[4]; 276 | if (menuSelection[client].locked) 277 | { 278 | StrCat(text, sizeof(text), "?"); 279 | color = DEFAULT_HUD_COLOR; 280 | } 281 | else 282 | { 283 | StrCat(text, sizeof(text), model.name); 284 | color = model.hudColor; 285 | } 286 | 287 | if (showBody) 288 | { 289 | Format(text, sizeof(text), "%s\n⇣ %3d / %-3d ⇡", text, menuSelection[client].body + 1, menuSelection[client].bodyCount); 290 | } 291 | 292 | if (showSkin) 293 | { 294 | Format(text, sizeof(text), "%s\n⇠ %3d / %-3d ⇢", text, menuSelection[client].skin + 1, menuSelection[client].skinCount); 295 | } 296 | 297 | SetHudTextParamsEx(cvHudText1x.FloatValue, cvHudText1y.FloatValue, 60.0, color, {200, 200, 200, 200}, 1, 0.1, initital? 1.2 : 0.0, 0.15); 298 | ShowHudText(client, topHudChanToggle[client], text); 299 | 300 | // Bottom 301 | Format(text, sizeof(text), "◀ L %12d / %-12d R ▶", menuSelection[client].index + 1, selectableModels[client].Length); 302 | SetHudTextParamsEx(cvHudText2x.FloatValue, cvHudText2y.FloatValue, 9999999.0, DEFAULT_HUD_COLOR, _, 1, 0.0, initital? 1.2 : 0.0, 1.0); 303 | ShowHudText(client, bottomHudChanToggle[client], text); 304 | 305 | g_bDrawing = false; 306 | } 307 | 308 | void ClearMenuHud(int client, bool top = true, bool bottom = true) 309 | { 310 | g_bDrawing = true; 311 | SetHudTextParams(-1.0, -1.0, 0.0, 0, 0, 0, 0, 0, 0.0, 0.0, 0.0); 312 | if (top) 313 | { 314 | ShowHudText(client, topHudChanToggle[client], " "); 315 | topHudChanToggle[client] = !topHudChanToggle[client]; 316 | } 317 | if (bottom) 318 | { 319 | ShowHudText(client, bottomHudChanToggle[client], " "); 320 | bottomHudChanToggle[client] = bottomHudChanToggle[client] == 2? 3 : 2; 321 | } 322 | g_bDrawing = false; 323 | } 324 | 325 | public Action Hook_HudMsg(UserMsg msg_id, BfRead msg, const int[] clients, int numClients, bool reliable, bool init) 326 | { 327 | // Block other hud messages while inside the menu 328 | if (numClients == 1 && menuSelection[clients[0]].index != -1 && !g_bDrawing) 329 | { 330 | return Plugin_Handled; 331 | } 332 | return Plugin_Continue; 333 | } 334 | 335 | public void Timer_MenuInit1(Handle timer, int userId) 336 | { 337 | int client = GetClientOfUserId(userId); 338 | tMenuInit[client] = null; 339 | 340 | if (client) 341 | { 342 | // Locking the camera has to be delayed after the view angles have been snapped 343 | if (mp_forcecamera) 344 | { 345 | mp_forcecamera.ReplicateToClient(client, "1"); 346 | } 347 | tMenuInit[client] = CreateTimer(0.4, Timer_MenuInit2, userId, TIMER_FLAG_NO_MAPCHANGE); 348 | } 349 | } 350 | 351 | public void Timer_MenuInit2(Handle timer, int userId) 352 | { 353 | int client = GetClientOfUserId(userId); 354 | tMenuInit[client] = null; 355 | 356 | if (client) 357 | { 358 | // Start drawing the hud 359 | PlayerModel model; 360 | if (GetSelectedModel(client, model, true)) 361 | { 362 | UpdateMenuHud(client, model, true); 363 | } 364 | } 365 | } 366 | 367 | #define MENU_SCROLL_DELAY_MAX 0.6 368 | #define MENU_SCROLL_DELAY_MIN 0.15 369 | 370 | public void OnPlayerRunCmdPost(int client, int buttons, int impulse, const float vel[3], const float angles[3], int weapon, int subtype, int cmdnum, int tickcount, int seed, const int mouse[2]) 371 | { 372 | if (!(0 < client <= MAXPLAYERS)) 373 | return; 374 | 375 | if (IsInMenu(client)) 376 | { 377 | static int lastButtons[MAXPLAYERS + 1]; 378 | static int lastButtonsAdjusted[MAXPLAYERS + 1]; 379 | static float lastChange[MAXPLAYERS + 1]; 380 | static float delay[MAXPLAYERS + 1]; 381 | 382 | if (!IsPlayerAlive(client)) 383 | { 384 | ExitModelChooser(client, true); 385 | } 386 | else if ((buttons & IN_USE || buttons & IN_JUMP) && !menuSelection[client].locked) 387 | { 388 | ExitModelChooser(client); 389 | } 390 | else if (tMenuInit[client] == null) 391 | { 392 | MenuSelectionThink(client, buttons, lastButtonsAdjusted[client], delay[client] != MENU_SCROLL_DELAY_MAX); 393 | } 394 | 395 | float time = GetGameTime(); 396 | if (lastButtons[client] != buttons) 397 | { 398 | lastButtons[client] = lastButtonsAdjusted[client] = buttons; 399 | lastChange[client] = time; 400 | delay[client] = MENU_SCROLL_DELAY_MAX; 401 | } 402 | else if (buttons && time - lastChange[client] > delay[client]) 403 | { 404 | lastButtonsAdjusted[client] = 0; 405 | lastChange[client] = time; 406 | delay[client] = Math_Clamp(delay[client] * 0.9, MENU_SCROLL_DELAY_MIN, MENU_SCROLL_DELAY_MAX); 407 | } 408 | else 409 | { 410 | lastButtonsAdjusted[client] = lastButtons[client]; 411 | } 412 | } 413 | } 414 | 415 | void MenuSelectionThink(int client, int buttons, int oldButtons, bool scrolling) 416 | { 417 | if (selectableModels[client].Length > 1) 418 | { 419 | if (buttons & IN_ATTACK && !(oldButtons & IN_ATTACK)) 420 | { 421 | if (--menuSelection[client].index < 0) 422 | { 423 | menuSelection[client].index = selectableModels[client].Length - 1; 424 | } 425 | OnMenuModelSelection(client, false, scrolling); 426 | } 427 | if (buttons & IN_ATTACK2 && !(oldButtons & IN_ATTACK2)) 428 | { 429 | if (++menuSelection[client].index >= selectableModels[client].Length) 430 | { 431 | menuSelection[client].index = 0; 432 | } 433 | OnMenuModelSelection(client, false, scrolling); 434 | } 435 | } 436 | 437 | if (menuSelection[client].skinCount > 1) 438 | { 439 | if (buttons & IN_MOVELEFT && !(oldButtons & IN_MOVELEFT)) 440 | { 441 | if (--menuSelection[client].skin < 0) 442 | { 443 | menuSelection[client].skin = menuSelection[client].skinCount - 1; 444 | } 445 | OnMenuSkinSelection(client); 446 | } 447 | if (buttons & IN_MOVERIGHT && !(oldButtons & IN_MOVERIGHT)) 448 | { 449 | if (++menuSelection[client].skin >= menuSelection[client].skinCount) 450 | { 451 | menuSelection[client].skin = 0; 452 | } 453 | OnMenuSkinSelection(client); 454 | } 455 | } 456 | 457 | if (menuSelection[client].bodyCount > 1) 458 | { 459 | if (buttons & IN_BACK && !(oldButtons & IN_BACK)) 460 | { 461 | if (--menuSelection[client].body < 0) 462 | { 463 | menuSelection[client].body = menuSelection[client].bodyCount - 1; 464 | } 465 | OnMenuBodySelection(client); 466 | } 467 | if (buttons & IN_FORWARD && !(oldButtons & IN_FORWARD)) 468 | { 469 | if (++menuSelection[client].body >= menuSelection[client].bodyCount) 470 | { 471 | menuSelection[client].body = 0; 472 | } 473 | OnMenuBodySelection(client); 474 | } 475 | } 476 | } -------------------------------------------------------------------------------- /scripting/include/modelchooser/natives.inc: -------------------------------------------------------------------------------- 1 | #pragma semicolon 1 2 | #pragma newdecls required 3 | 4 | public APLRes AskPluginLoad2(Handle myself, bool late, char[] error, int err_max) 5 | { 6 | CreateNative("ModelChooser_GetCurrentModelName", Native_GetCurrentModelName); 7 | CreateNative("ModelChooser_GetCurrentModelPath", Native_GetCurrentModelPath); 8 | CreateNative("ModelChooser_GetCurrentModelProperty", Native_GetCurrentModelProperty); 9 | CreateNative("ModelChooser_UnlockModel", Native_UnlockModel); 10 | CreateNative("ModelChooser_LockModel", Native_LockModel); 11 | CreateNative("ModelChooser_SelectModel", Native_SelectModel); 12 | CreateNative("ModelChooser_IsClientChoosing", Native_IsClientChoosing); 13 | CreateNative("ModelChooser_OpenChooser", Native_OpenChooser); 14 | CreateNative("ModelChooser_PlayRandomSound", Native_PlayRandomSound); 15 | CreateNative("ModelChooser_GetProperty", Native_GetProperty); 16 | CreateNative("ModelChooser_GetModelList", Native_GetModelList); 17 | CreateNative("ModelChooser_GetSoundMap", Native_GetSoundMap); 18 | RegPluginLibrary("ModelChooser"); 19 | return APLRes_Success; 20 | } 21 | 22 | public any Native_GetCurrentModelName(Handle plugin, int numParams) 23 | { 24 | PlayerModel model; 25 | 26 | if (GetSelectedModel(GetNativeCell(1), model)) 27 | { 28 | SetNativeString(2, model.name, GetNativeCell(3)); 29 | return true; 30 | } 31 | return false; 32 | } 33 | 34 | public any Native_GetCurrentModelPath(Handle plugin, int numParams) 35 | { 36 | PlayerModel model; 37 | 38 | if (GetSelectedModel(GetNativeCell(1), model)) 39 | { 40 | SetNativeString(2, model.path, GetNativeCell(3)); 41 | return true; 42 | } 43 | return false; 44 | } 45 | 46 | public any Native_GetCurrentModelProperty(Handle plugin, int numParams) 47 | { 48 | PlayerModel model; 49 | 50 | if (GetSelectedModel(GetNativeCell(1), model)) 51 | { 52 | int keySize; GetNativeStringLength(2, keySize); keySize++; 53 | int valSize = GetNativeCell(4); 54 | char[] key = new char[keySize]; 55 | char[] val = new char[valSize]; 56 | GetNativeString(2, key, keySize); 57 | String_ToUpper(key, key, keySize); 58 | if (model.customProperties.GetString(key, val, valSize)) 59 | { 60 | SetNativeString(3, val, valSize); 61 | return true; 62 | } 63 | } 64 | return false; 65 | } 66 | 67 | public any Native_UnlockModel(Handle plugin, int numParams) 68 | { 69 | char modelName[MODELCHOOSER_MAX_NAME]; 70 | GetNativeString(2, modelName, sizeof(modelName)); 71 | String_ToUpper(modelName, modelName, sizeof(modelName)); 72 | int client = GetNativeCell(1); 73 | 74 | UnlockModel(client, modelName); 75 | if (GetNativeCell(3)) 76 | { 77 | return SelectModelByName(client, modelName); 78 | } 79 | return true; 80 | } 81 | 82 | public any Native_LockModel(Handle plugin, int numParams) 83 | { 84 | char modelName[MODELCHOOSER_MAX_NAME]; 85 | GetNativeString(2, modelName, sizeof(modelName)); 86 | String_ToUpper(modelName, modelName, sizeof(modelName)); 87 | 88 | LockModel(GetNativeCell(1), modelName); 89 | return true; 90 | } 91 | 92 | public any Native_SelectModel(Handle plugin, int numParams) 93 | { 94 | char modelName[MODELCHOOSER_MAX_NAME]; 95 | GetNativeString(2, modelName, sizeof(modelName)); 96 | String_ToUpper(modelName, modelName, sizeof(modelName)); 97 | 98 | return SelectModelByName(GetNativeCell(1), modelName); 99 | } 100 | 101 | public any Native_IsClientChoosing(Handle plugin, int numParams) 102 | { 103 | int client = GetNativeCell(1); 104 | return IsInMenu(client); 105 | } 106 | 107 | public any Native_OpenChooser(Handle plugin, int numParams) 108 | { 109 | int client = GetNativeCell(1); 110 | bool printError = GetNativeCell(2); 111 | if (PreEnterCheck(client, printError)) 112 | { 113 | EnterModelChooser(client); 114 | return true; 115 | } 116 | return false; 117 | } 118 | 119 | public any Native_PlayRandomSound(Handle plugin, int numParams) 120 | { 121 | PlayerModel model; 122 | int client = GetNativeCell(1); 123 | 124 | if (GetSelectedModel(client, model)) 125 | { 126 | char soundType[MODELCHOOSER_MAX_NAME]; 127 | GetNativeString(2, soundType, sizeof(soundType)); 128 | 129 | SoundPack soundPack = GetSoundPack(model, false); 130 | if (!soundPack) 131 | return false; 132 | 133 | bool toAll = GetNativeCell(3); 134 | bool stopLast = GetNativeCell(4); 135 | int pitch = GetNativeCell(5); 136 | float volume = GetNativeCell(6); 137 | 138 | if (stopLast) 139 | { 140 | StopSound(client, SNDCHAN_BODY, lastPlayedSound[client]); 141 | StopSound(client, SNDCHAN_STATIC, lastPlayedSound[client]); 142 | } 143 | 144 | return PlayRandomSound(soundPack.GetSoundList(soundType), client, 145 | .channel = SNDCHAN_STATIC, .toAll = toAll, .pitchMin = pitch, .pitchMax = pitch, .volume = volume 146 | ); 147 | } 148 | return false; 149 | } 150 | 151 | public any Native_GetProperty(Handle plugin, int numParams) 152 | { 153 | char modelName[MODELCHOOSER_MAX_NAME]; 154 | GetNativeString(1, modelName, sizeof(modelName)); 155 | if (modelList) 156 | { 157 | int i = modelList.FindByName(modelName); 158 | if (i != -1) 159 | { 160 | StringMap customProperties = modelList.Get(i, PlayerModel::customProperties); 161 | 162 | int keySize; GetNativeStringLength(2, keySize); keySize++; 163 | int valSize = GetNativeCell(4); 164 | char[] key = new char[keySize]; 165 | char[] val = new char[valSize]; 166 | GetNativeString(2, key, keySize); 167 | String_ToUpper(key, key, keySize); 168 | if (customProperties.GetString(key, val, valSize)) 169 | { 170 | SetNativeString(3, val, valSize); 171 | return true; 172 | } 173 | } 174 | } 175 | return false; 176 | } 177 | 178 | public any Native_GetModelList(Handle plugin, int numParams) 179 | { 180 | return modelList; 181 | } 182 | 183 | public any Native_GetSoundMap(Handle plugin, int numParams) 184 | { 185 | return soundMap; 186 | } 187 | -------------------------------------------------------------------------------- /scripting/include/modelchooser/sounds.inc: -------------------------------------------------------------------------------- 1 | #pragma semicolon 1 2 | #pragma newdecls required 3 | 4 | public void Sounds_EventPlayerHurt(Event event, int client) 5 | { 6 | if (playedHurtSoundAt[client] != -1 || currentTeam[client] == 1) 7 | return; 8 | 9 | int health = event.GetInt("health"); 10 | 11 | PlayerModel model; 12 | if (GetSelectedModelAuto(client, model)) 13 | { 14 | if (0 < health <= model.hurtSndHP.Rand()) 15 | { 16 | PlayRandomSound(GetSoundPack(model).GetSoundList("hurt"), client, client, SNDCHAN_STATIC, true, HURT_PITCH_MIN, HURT_PITCH_MAX); 17 | playedHurtSoundAt[client] = health; 18 | } 19 | } 20 | } 21 | 22 | public void Sounds_EventPlayerSpawn(Event event, int client) 23 | { 24 | int m_hRagdoll = GetEntPropEnt(client, Prop_Send, "m_hRagdoll"); 25 | if (m_hRagdoll != -1) 26 | { 27 | StopSound(m_hRagdoll, SNDCHAN_STATIC, lastPlayedSound[client]); 28 | } 29 | StopSound(client, SNDCHAN_STATIC, lastPlayedSound[client]); 30 | } 31 | 32 | public Action Sounds_CheckHealthRaise(Handle timer) 33 | { 34 | for (int i = 1; i <= MaxClients; i++) 35 | { 36 | if (IsClientInGame(i)) 37 | { 38 | if (playedHurtSoundAt[i] != -1 && GetClientHealth(i) > playedHurtSoundAt[i]) 39 | { 40 | playedHurtSoundAt[i] = -1; 41 | } 42 | } 43 | } 44 | return Plugin_Continue; 45 | } 46 | 47 | public MRESReturn Hook_DeathSound(int client, DHookParam hParams) 48 | { 49 | if (IsInMenu(client) && !IsPlayerAlive(client)) 50 | { 51 | ExitModelChooser(client, true); 52 | } 53 | 54 | PlayerModel model; 55 | if (GetSelectedModel(client, model)) 56 | { 57 | StopSound(client, SNDCHAN_BODY, lastPlayedSound[client]); 58 | StopSound(client, SNDCHAN_STATIC, lastPlayedSound[client]); 59 | 60 | int target = client; 61 | int m_hRagdoll = GetEntPropEnt(client, Prop_Send, "m_hRagdoll"); 62 | if (m_hRagdoll != -1 && !(GetEntityFlags(m_hRagdoll) & FL_DISSOLVING)) 63 | { 64 | target = m_hRagdoll; 65 | } 66 | 67 | if (PlayRandomSound(GetSoundPack(model).GetSoundList("death"), client, target, SNDCHAN_STATIC, true, HURT_PITCH_MIN, HURT_PITCH_MAX)) 68 | return MRES_Supercede; 69 | } 70 | return MRES_Ignored; 71 | } 72 | 73 | bool PlayRandomSound(ArrayList soundList, int client, int entity = SOUND_FROM_PLAYER, int channel = SNDCHAN_AUTO, 74 | bool toAll = false, int pitchMin = 100, int pitchMax = 100, float volume = SNDVOL_NORMAL) 75 | { 76 | if (soundList && soundList.Length) 77 | { 78 | int pitch = pitchMin == pitchMax? pitchMax : GetRandomInt(pitchMin, pitchMax); 79 | soundList.GetString(GetRandomInt(0, soundList.Length - 1), lastPlayedSound[client], sizeof(lastPlayedSound[])); 80 | 81 | if (toAll) 82 | { 83 | EmitSoundToAll(lastPlayedSound[client], entity, channel, .volume = volume, .pitch = pitch); 84 | } 85 | else 86 | { 87 | EmitSoundToClient(client, lastPlayedSound[client], entity, channel, .volume = volume, .pitch = pitch); 88 | } 89 | return true; 90 | } 91 | return false; 92 | } 93 | 94 | -------------------------------------------------------------------------------- /scripting/include/modelchooser/structs.inc: -------------------------------------------------------------------------------- 1 | #pragma semicolon 1 2 | #pragma newdecls required 3 | 4 | #define MODELCHOOSER_MAX_NAME 64 5 | 6 | #if !defined MAX_TEAMS 7 | #define MAX_TEAMS 32 // Max number of teams in a game 8 | #endif 9 | 10 | #if !defined MAX_TEAM_NAME_LENGTH 11 | #define MAX_TEAM_NAME_LENGTH 32 // Max length of a team's name 12 | #endif 13 | 14 | enum struct Interval 15 | { 16 | float min; 17 | float max; 18 | 19 | float Rand() 20 | { 21 | return GetRandomFloat(this.min, this.max); 22 | } 23 | } 24 | 25 | enum struct SoundParams 26 | { 27 | Interval cooldown; 28 | } 29 | 30 | methodmap SoundList < ArrayList 31 | { 32 | public SoundList() 33 | { 34 | return view_as(new ArrayList()); 35 | } 36 | 37 | public void Precache() 38 | { 39 | int len = this.Length; 40 | char path[PLATFORM_MAX_PATH]; 41 | 42 | for (int i = 0; i < len; i++) 43 | { 44 | this.GetString(i, path, sizeof(path)); 45 | PrecacheSound(path, true); 46 | } 47 | } 48 | } 49 | 50 | methodmap SoundPack < StringMap 51 | { 52 | public SoundPack() 53 | { 54 | return view_as(new StringMap()); 55 | } 56 | 57 | public void AddSoundList(const char[] soundType, SoundList soundList) 58 | { 59 | char soundTypeNorm[MODELCHOOSER_MAX_NAME]; 60 | _modelchooser_normalize_name(soundType, soundTypeNorm); 61 | 62 | if (!this.SetValue(soundTypeNorm, soundList, false)) 63 | { 64 | ThrowError("Sound type \"%s\" already added", soundTypeNorm); 65 | } 66 | } 67 | 68 | public SoundList GetSoundList(const char[] soundType) 69 | { 70 | if (!this.Size) 71 | return null; 72 | 73 | char soundTypeNorm[MODELCHOOSER_MAX_NAME]; 74 | _modelchooser_normalize_name(soundType, soundTypeNorm); 75 | 76 | SoundList soundList; 77 | this.GetValue(soundTypeNorm, soundList); 78 | return soundList; 79 | } 80 | 81 | public void Clear() 82 | { 83 | StringMapSnapshot snapshot = this.Snapshot(); 84 | SoundList soundList; 85 | for (int i = 0; i < snapshot.Length; i++) 86 | { 87 | int keySize = snapshot.KeyBufferSize(i); 88 | char[] key = new char[keySize]; 89 | snapshot.GetKey(i, key, keySize); 90 | if (this.GetValue(key, soundList)) 91 | { 92 | soundList.Close(); 93 | } 94 | } 95 | snapshot.Close(); 96 | view_as(this).Clear(); 97 | } 98 | 99 | public void Close() 100 | { 101 | this.Clear(); 102 | view_as(this).Close(); 103 | } 104 | 105 | public void Precache() 106 | { 107 | StringMapSnapshot snapshot = this.Snapshot(); 108 | SoundList soundList; 109 | for (int i = 0; i < snapshot.Length; i++) 110 | { 111 | int keySize = snapshot.KeyBufferSize(i); 112 | char[] key = new char[keySize]; 113 | snapshot.GetKey(i, key, keySize); 114 | if (this.GetValue(key, soundList)) 115 | { 116 | soundList.Precache(); 117 | } 118 | } 119 | snapshot.Close(); 120 | } 121 | } 122 | 123 | methodmap SoundMap < StringMap 124 | { 125 | public SoundMap() 126 | { 127 | return view_as(new StringMap()); 128 | } 129 | 130 | public void AddSoundPack(const char[] name, SoundPack pack) 131 | { 132 | char nameNorm[MODELCHOOSER_MAX_NAME]; 133 | _modelchooser_normalize_name(name, nameNorm); 134 | 135 | if (!this.SetValue(nameNorm, pack, false)) 136 | { 137 | ThrowError("Sound pack \"%s\" already added", nameNorm); 138 | } 139 | } 140 | 141 | public SoundPack GetSoundPack(const char[] name) 142 | { 143 | if (!this.Size) 144 | return null; 145 | 146 | char nameNorm[MODELCHOOSER_MAX_NAME]; 147 | _modelchooser_normalize_name(name, nameNorm); 148 | 149 | SoundPack soundPack; 150 | this.GetValue(nameNorm, soundPack); 151 | return soundPack; 152 | } 153 | 154 | public void Clear() 155 | { 156 | StringMapSnapshot snapshot = this.Snapshot(); 157 | SoundPack soundPack; 158 | for (int i = 0; i < snapshot.Length; i++) 159 | { 160 | int keySize = snapshot.KeyBufferSize(i); 161 | char[] key = new char[keySize]; 162 | snapshot.GetKey(i, key, keySize); 163 | if (this.GetValue(key, soundPack)) 164 | { 165 | soundPack.Close(); 166 | } 167 | } 168 | snapshot.Close(); 169 | view_as(this).Clear(); 170 | } 171 | 172 | public void Close() 173 | { 174 | this.Clear(); 175 | view_as(this).Close(); 176 | } 177 | 178 | public void Precache() 179 | { 180 | StringMapSnapshot snapshot = this.Snapshot(); 181 | SoundPack soundPack; 182 | for (int i = 0; i < snapshot.Length; i++) 183 | { 184 | int keySize = snapshot.KeyBufferSize(i); 185 | char[] key = new char[keySize]; 186 | snapshot.GetKey(i, key, keySize); 187 | if (this.GetValue(key, soundPack)) 188 | { 189 | soundPack.Precache(); 190 | } 191 | } 192 | snapshot.Close(); 193 | } 194 | } 195 | 196 | enum struct WeightedSequence 197 | { 198 | int weight; 199 | int sequence; 200 | } 201 | 202 | methodmap WeightedSequenceList < ArrayList 203 | { 204 | public WeightedSequenceList() 205 | { 206 | return view_as(new ArrayList(sizeof(WeightedSequence))); 207 | } 208 | 209 | public void Add(int sequence, int weight) 210 | { 211 | WeightedSequence ws; 212 | ws.weight = weight; 213 | ws.sequence = sequence; 214 | this.PushArray(ws); 215 | } 216 | 217 | public int NextSequence() 218 | { 219 | int size = this.Length; 220 | if (size) 221 | { 222 | if (size == 1) 223 | return this.Get(0, WeightedSequence::sequence); 224 | 225 | int weightSum; 226 | for (int i = 0; i < size; i++) 227 | { 228 | weightSum += this.Get(i, WeightedSequence::weight); 229 | } 230 | float target = GetURandomFloat() * weightSum; 231 | for (int i = 0; i < size; i++) 232 | { 233 | target -= this.Get(i, WeightedSequence::weight); 234 | if (target <= 0) 235 | { 236 | return this.Get(i, WeightedSequence::sequence); 237 | } 238 | } 239 | } 240 | return -1; 241 | } 242 | } 243 | 244 | enum struct PlayerAnimation 245 | { 246 | WeightedSequenceList seqList; 247 | float rate; 248 | 249 | void Close() 250 | { 251 | delete this.seqList; 252 | } 253 | } 254 | 255 | enum struct PlayerModel 256 | { 257 | char name[MODELCHOOSER_MAX_NAME]; 258 | char path[PLATFORM_MAX_PATH]; 259 | char team[MAX_TEAM_NAME_LENGTH]; 260 | char sounds[MODELCHOOSER_MAX_NAME]; 261 | char vmBodyGroups[256]; 262 | SoundParams jumpSndParams; 263 | Interval hurtSndHP; 264 | int adminBitFlags; 265 | int defaultPrio; 266 | int hudColor[4]; 267 | bool locked; 268 | 269 | ArrayList skins; 270 | ArrayList bodyGroups; 271 | StringMap customProperties; 272 | 273 | PlayerAnimation anim_idle; 274 | PlayerAnimation anim_walk; 275 | PlayerAnimation anim_run; 276 | PlayerAnimation anim_jump; 277 | PlayerAnimation anim_idle_crouch; 278 | PlayerAnimation anim_walk_crouch; 279 | PlayerAnimation anim_noclip; 280 | 281 | void Close() 282 | { 283 | this.skins.Close(); 284 | this.bodyGroups.Close(); 285 | this.customProperties.Close(); 286 | this.anim_idle.Close(); 287 | this.anim_walk.Close(); 288 | this.anim_run.Close(); 289 | this.anim_jump.Close(); 290 | this.anim_idle_crouch.Close(); 291 | this.anim_walk_crouch.Close(); 292 | this.anim_noclip.Close(); 293 | } 294 | 295 | void Precache() 296 | { 297 | PrecacheModel(this.path, true); 298 | } 299 | 300 | int GetTeamNum() 301 | { 302 | int team; 303 | if (!StringToIntEx(this.team, team)) 304 | { 305 | team = FindTeamByName(this.team); 306 | } 307 | if (team < 0 || team >= MAX_TEAMS) 308 | { 309 | if (GetTeamCount() > 2) 310 | { 311 | LogError("Invalid team \"%s\" specified for model \"%s\"", this.team, this.name); 312 | } 313 | team = 0; 314 | } 315 | return team; 316 | } 317 | 318 | int GetSkin(int index) 319 | { 320 | return this.skins.Get(index); 321 | } 322 | 323 | int GetBody(int index) 324 | { 325 | return this.bodyGroups.Get(index); 326 | } 327 | 328 | int IndexOfSkin(int skin, int fallback = 0) 329 | { 330 | int i = this.skins.FindValue(skin); 331 | return i == -1 ? fallback : i; 332 | } 333 | 334 | int IndexOfBody(int body, int fallback = 0) 335 | { 336 | int i = this.bodyGroups.FindValue(body); 337 | return i == -1 ? fallback : i; 338 | } 339 | } 340 | 341 | methodmap ModelList < ArrayList 342 | { 343 | public ModelList() 344 | { 345 | return view_as(new ArrayList(sizeof(PlayerModel))); 346 | } 347 | 348 | public void Clear() 349 | { 350 | PlayerModel model; 351 | int len = this.Length; 352 | for (int i = 0; i < len; i++) 353 | { 354 | this.GetArray(i, model); 355 | model.Close(); 356 | } 357 | view_as(this).Clear(); 358 | } 359 | 360 | public int FindByName(const char[] modelName) 361 | { 362 | char nameNorm[MODELCHOOSER_MAX_NAME]; 363 | _modelchooser_normalize_name(modelName, nameNorm); 364 | 365 | return this.FindString(nameNorm, PlayerModel::name); 366 | } 367 | 368 | public void Precache() 369 | { 370 | int len = this.Length; 371 | PlayerModel model; 372 | for (int i = 0; i < len; i++) 373 | { 374 | this.GetArray(i, model); 375 | model.Precache(); 376 | } 377 | } 378 | } 379 | 380 | enum struct SelectionData 381 | { 382 | // Index into selectableModels, -1 = invalid 383 | int index; 384 | 385 | // Index into PlayerModel.skins 386 | int skin; 387 | 388 | // Index into PlayerModel.bodyGroups 389 | int body; 390 | 391 | // Cached by menu 392 | int skinCount; 393 | int bodyCount; 394 | bool locked; 395 | 396 | void Reset() 397 | { 398 | this.index = -1; 399 | this.skin = this.skinCount = this.body = this.bodyCount = 0; 400 | this.locked = false; 401 | } 402 | 403 | bool IsValid() 404 | { 405 | return this.index != -1 && !this.locked; 406 | } 407 | } 408 | 409 | enum struct PersistentPreferences 410 | { 411 | int team; 412 | Cookie model; 413 | Cookie skin; 414 | Cookie body; 415 | 416 | void Init(int team) 417 | { 418 | if (this.model) 419 | return; 420 | 421 | this.team = team; 422 | 423 | char name[32]; 424 | char suffix[4]; 425 | if (team > 1) 426 | { 427 | FormatEx(suffix, sizeof(suffix), "#%d", team); 428 | } 429 | 430 | FormatEx(name, sizeof(name), "playermodel%s", suffix); 431 | this.model = new Cookie(name, "Stores player model preference", CookieAccess_Protected); 432 | 433 | FormatEx(name, sizeof(name), "playermodel_skin%s", suffix); 434 | this.skin = new Cookie(name, "Stores player model skin type preference", CookieAccess_Protected); 435 | 436 | FormatEx(name, sizeof(name), "playermodel_body%s", suffix); 437 | this.body = new Cookie(name, "Stores player model body type preference", CookieAccess_Protected); 438 | } 439 | } 440 | 441 | void _modelchooser_normalize_name(const char[] name, char nameNorm[MODELCHOOSER_MAX_NAME]) 442 | { 443 | strcopy(nameNorm, sizeof(nameNorm), name); 444 | for (int i = 0; nameNorm[i] != '\0'; i++) 445 | { 446 | nameNorm[i] = CharToUpper(nameNorm[i]); 447 | } 448 | } -------------------------------------------------------------------------------- /scripting/include/modelchooser/utils.inc: -------------------------------------------------------------------------------- 1 | #pragma semicolon 1 2 | #pragma newdecls required 3 | 4 | Action Hook_BlockDamage(int victim, int &attacker, int &inflictor, float &damage, int &damagetype) 5 | { 6 | return Plugin_Handled; 7 | } 8 | 9 | Action Hook_TransmitToOwnerOnly(int entity, int client) 10 | { 11 | return (Entity_GetOwner(entity) == client) ? Plugin_Continue : Plugin_Stop; 12 | } 13 | 14 | stock void LoadDHookDetour(GameData pGameConfig, DynamicDetour& pHandle, const char[] szFuncName, DHookCallback pCallbackPre = null, DHookCallback pCallbackPost = null) 15 | { 16 | pHandle = DynamicDetour.FromConf(pGameConfig, szFuncName); 17 | if (!pHandle) 18 | SetFailState("Couldn't create hook %s", szFuncName); 19 | if (pCallbackPre && !pHandle.Enable(Hook_Pre, pCallbackPre)) 20 | SetFailState("Couldn't enable pre detour hook %s", szFuncName); 21 | if (pCallbackPost && !pHandle.Enable(Hook_Post, pCallbackPost)) 22 | SetFailState("Couldn't enable post detour hook %s", szFuncName); 23 | } 24 | 25 | stock void LoadDHookVirtual(GameData pGameConfig, DynamicHook& pHandle, const char[] szFuncName) 26 | { 27 | pHandle = DynamicHook.FromConf(pGameConfig, szFuncName); 28 | if (pHandle == null) 29 | SetFailState("Couldn't create hook %s", szFuncName); 30 | } 31 | 32 | stock int GetEntityBody(int entity) 33 | { 34 | return GetEntProp(entity, Prop_Send, "m_nBody"); 35 | } 36 | 37 | stock void SetEntityBody(int entity, int body) 38 | { 39 | SetEntProp(entity, Prop_Send, "m_nBody", body); 40 | } 41 | 42 | stock int GetEntitySkin(int entity) 43 | { 44 | return GetEntProp(entity, Prop_Data, "m_nSkin"); 45 | } 46 | 47 | stock void SetEntitySkin(int entity, int body) 48 | { 49 | SetEntProp(entity, Prop_Data, "m_nSkin", body); 50 | } 51 | 52 | stock int GetEntityEffects(int entity) 53 | { 54 | return GetEntProp(entity, Prop_Data, "m_fEffects"); 55 | } 56 | 57 | stock void SetEntityEffects(int entity, int effects) 58 | { 59 | SetEntProp(entity, Prop_Data, "m_fEffects", effects); 60 | ChangeEdictState(entity, FindDataMapInfo(entity, "m_fEffects")); 61 | } 62 | 63 | /** 64 | * HL2DM displays all carried weapons' shadows in thirdperson. 65 | * We fix this to only show active weapon. 66 | */ 67 | stock void FixThirdpersonWeapons(int client) 68 | { 69 | int activeWeapon = Client_GetActiveWeapon(client); 70 | LOOP_CLIENTWEAPONS(client, weapon, index) 71 | { 72 | if (activeWeapon != weapon) 73 | { 74 | SetEntityEffects(weapon, GetEntityEffects(weapon) | EF_NODRAW | EF_NOSHADOW); 75 | } 76 | else 77 | { 78 | SetEntityEffects(weapon, GetEntityEffects(weapon) & ~EF_NODRAW & ~EF_NOSHADOW); 79 | } 80 | } 81 | } 82 | 83 | stock int EntIndexToEntRefEx(int ent) 84 | { 85 | int index = EntRefToEntIndex(ent); 86 | return index == -1? -1 : EntIndexToEntRef(index); 87 | } 88 | 89 | // Copy pasta of "SetBodygroup" from the SDK 90 | stock void CalcBodygroup(StudioHdr pStudioHdr, int& body, int iGroup, int iValue) 91 | { 92 | if (!pStudioHdr) 93 | return; 94 | 95 | BodyPart pBodyPart = pStudioHdr.GetBodyPart(iGroup); 96 | if (!pBodyPart.valid) 97 | return; 98 | 99 | int numModels = pBodyPart.nummodels; 100 | if (iValue >= numModels) 101 | return; 102 | 103 | int base = pBodyPart.base; 104 | int iCurrent = (body / base) % numModels; 105 | 106 | body = (body - (iCurrent * base) + (iValue * base)); 107 | } 108 | 109 | stock void ApplyEntityBodyGroupsFromString(int entity, const char[] str) 110 | { 111 | if (str[0] == EOS) 112 | return; 113 | 114 | StudioHdr pStudio = StudioHdr.FromEntity(entity); 115 | if (!pStudio.valid) 116 | return; 117 | 118 | int numBodyParts = pStudio.numbodyparts; 119 | int body = GetEntityBody(entity); 120 | 121 | char buffer1[128], buffer2[128]; 122 | for (int count, strIndex, n;; count++) 123 | { 124 | n = SplitString(str[strIndex], ";", buffer1, sizeof(buffer1)); 125 | TrimString(buffer1); 126 | if (n == -1) 127 | { 128 | if (count) 129 | { 130 | LogError("Invalid bodygroup string: \"%s\"", str); 131 | return; 132 | } 133 | else 134 | { 135 | // no separator found - assume raw body index specified 136 | body = StringToInt(buffer1); 137 | break; 138 | } 139 | } 140 | strIndex += n; 141 | 142 | n = SplitString(str[strIndex], ";", buffer2, sizeof(buffer2)); 143 | if (n == -1) 144 | { 145 | // copy remainder 146 | strcopy(buffer2, sizeof(buffer2), str[strIndex]); 147 | } 148 | TrimString(buffer2); 149 | 150 | // Convert buffers to actual indexes on the model 151 | 152 | int bodyPartIndex = -1; 153 | int subModelIndex = StringToInt(buffer2); 154 | 155 | for (int i = 0; i < numBodyParts; i++) 156 | { 157 | BodyPart pBodyPart = pStudio.GetBodyPart(i); 158 | pBodyPart.GetName(buffer2, sizeof(buffer2)); 159 | if (StrEqual(buffer1, buffer2, false)) 160 | { 161 | bodyPartIndex = i; 162 | break; 163 | } 164 | } 165 | 166 | if (bodyPartIndex != -1) 167 | CalcBodygroup(pStudio, body, bodyPartIndex, subModelIndex); 168 | 169 | if (n == -1) 170 | { 171 | // end of list 172 | break; 173 | } 174 | strIndex += n; 175 | } 176 | SetEntityBody(entity, body); 177 | } 178 | -------------------------------------------------------------------------------- /scripting/include/modelchooser/viewmodels.inc: -------------------------------------------------------------------------------- 1 | #pragma semicolon 1 2 | #pragma newdecls required 3 | 4 | public MRESReturn Hook_SetViewModelModel(int vm, DHookParam hParams) 5 | { 6 | RequestFrame(UpdateViewModel, EntIndexToEntRef(vm)); 7 | return MRES_Ignored; 8 | } 9 | 10 | void UpdateViewModels(int client) 11 | { 12 | int count = GetEntPropArraySize(client, Prop_Send, "m_hViewModel"); 13 | for (int i = 0; i < count; i++) 14 | { 15 | int vm = GetEntPropEnt(client, Prop_Send, "m_hViewModel", i); 16 | if (vm != -1) 17 | { 18 | UpdateViewModel(vm); 19 | } 20 | } 21 | } 22 | 23 | void UpdateViewModel(int vm) 24 | { 25 | vm = EntRefToEntIndex(vm); 26 | if (vm != -1) 27 | { 28 | int client = GetEntPropEnt(vm, Prop_Data, "m_hOwner"); 29 | if (0 < client <= MaxClients) 30 | { 31 | PlayerModel model; 32 | if (GetSelectedModelAuto(client, model)) 33 | { 34 | ApplyEntityBodyGroupsFromString(vm, model.vmBodyGroups); 35 | } 36 | } 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /scripting/modelchooser_api_example.sp: -------------------------------------------------------------------------------- 1 | #include 2 | 3 | #define MODELCHOOSER_RAWDOG_API /* Enable deep access? */ 4 | #undef REQUIRE_PLUGIN /* Is ModelChooser plugin dependency required or optional? */ 5 | #include 6 | 7 | public void OnLibraryAdded(const char[] name) 8 | { 9 | if (StrEqual(name, MODELCHOOSER_LIBRARY)) 10 | { 11 | PrintToServer("ModelChooser plugin is running"); 12 | // useModelChooser = true; 13 | } 14 | } 15 | 16 | public void OnLibraryRemoved(const char[] name) 17 | { 18 | if (StrEqual(name, MODELCHOOSER_LIBRARY)) 19 | { 20 | PrintToServer("ModelChooser plugin has unloaded"); 21 | // useModelChooser = false; 22 | } 23 | } 24 | 25 | public void ModelChooser_OnModelChanged(int client, const char[] modelName) 26 | { 27 | PrintToServer("%N changed model to %s", client, modelName); 28 | 29 | char value[512]; 30 | if (ModelChooser_GetCurrentModelProperty(client, "test", value, sizeof(value))) 31 | { 32 | PrintToServer("Value of test property: %s", value); 33 | } 34 | else 35 | { 36 | PrintToServer("There is no custom 'test' property on this model"); 37 | } 38 | } 39 | 40 | public void ModelChooser_OnConfigLoaded() 41 | { 42 | PrintToServer("Model config loaded"); 43 | 44 | ModelList modelList = ModelChooser_GetModelList(); 45 | PlayerModel model; 46 | 47 | int size = modelList.Length; 48 | for (int i = 0; i < size; i++) 49 | { 50 | modelList.GetArray(i, model); 51 | PrintToServer("Model at index %d is named %s", i, model.name); 52 | } 53 | } -------------------------------------------------------------------------------- /scripting/ultimate_modelchooser.sp: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | #include 4 | #include 5 | #include 6 | 7 | #include 8 | #include 9 | #include 10 | 11 | #pragma semicolon 1 12 | #pragma newdecls required 13 | 14 | #define PLUGIN_VERSION "3.1" 15 | 16 | public Plugin myinfo = 17 | { 18 | name = "Ultimate modelchooser", 19 | author = "Alienmario", 20 | description = "The enhanced playermodel system", 21 | version = PLUGIN_VERSION, 22 | url = "https://github.com/Alienmario/ModelChooser" 23 | }; 24 | 25 | #define MAX_ANIM_NAME 128 26 | #define HURT_SOUND_HP 45.0 27 | 28 | #define HURT_PITCH_MIN 95 29 | #define HURT_PITCH_MAX 102 30 | #define JUMP_PITCH_MIN 90 31 | #define JUMP_PITCH_MAX 105 32 | #define JUMP_VOL 0.5 33 | 34 | #define EF_NOSHADOW 0x010 35 | #define EF_NODRAW 0x020 36 | 37 | #define FALLBACK_MODEL "models/error.mdl" 38 | int DEFAULT_HUD_COLOR[] = {150, 150, 150, 150}; 39 | 40 | #include 41 | #include 42 | #include 43 | #include 44 | #include 45 | #include 46 | #include 47 | #include 48 | #include 49 | #include 50 | 51 | public void OnPluginStart() 52 | { 53 | LoadTranslations("common.phrases"); 54 | 55 | modelList = new ModelList(); 56 | soundMap = new SoundMap(); 57 | downloads = new SmartDM_FileSet(); 58 | persistentPreferences[TEAM_UNASSIGNED].Init(TEAM_UNASSIGNED); 59 | 60 | fwdOnConfigLoaded = new GlobalForward("ModelChooser_OnConfigLoaded", ET_Ignore, Param_Cell, Param_String); 61 | fwdOnModelChanged = new GlobalForward("ModelChooser_OnModelChanged", ET_Ignore, Param_Cell, Param_String); 62 | 63 | RegConsoleCmd("sm_models", Command_Model); 64 | RegConsoleCmd("sm_model", Command_Model); 65 | RegConsoleCmd("sm_skins", Command_Model); 66 | RegConsoleCmd("sm_skin", Command_Model); 67 | RegAdminCmd("sm_unlockmodel", Command_UnlockModel, ADMFLAG_KICK, "Unlock a locked model by name for a player"); 68 | RegAdminCmd("sm_lockmodel", Command_LockModel, ADMFLAG_KICK, "Lock a previously unlocked model by name for a player"); 69 | 70 | cvSelectionImmunity = CreateConVar("modelchooser_immunity", "0", "Whether players are immune to damage when selecting models", _, true, 0.0, true, 1.0); 71 | cvAutoReload = CreateConVar("modelchooser_autoreload", "0", "Whether to reload the model list on mapchanges", _, true, 0.0, true, 1.0); 72 | cvTeamBased = CreateConVar("modelchooser_teambased", "2", "Configures model restrictions in teamplay mode\n 0 = Do not enforce any team restrictions\n 1 = Enforce configured team restrictions, allows picking unrestricted models\n 2 = Strictly enforce teams, only allows models with matching teams", _, true, 0.0, true, 2.0); 73 | cvMenuSnd = CreateConVar("modelchooser_sound", "ui/buttonclickrelease.wav", "Menu click sound (auto downloads supported), empty to disable"); 74 | cvOverlay = CreateConVar("modelchooser_overlay", "modelchooser/background", "Screen overlay material to show when choosing models (auto downloads supported), empty to disable"); 75 | cvLockModel = CreateConVar("modelchooser_lock_model", "models/props_wasteland/prison_padlock001a.mdl", "Model to display for locked playermodels (auto downloads supported)"); 76 | cvLockScale = CreateConVar("modelchooser_lock_scale", "5.0", "Scale of the lock model", _, true, 0.1); 77 | cvHudText1x = CreateConVar("modelchooser_hudtext_x", "-1", "Hudtext 1 X coordinate, from 0 (left) to 1 (right), -1 is the center"); 78 | cvHudText1y = CreateConVar("modelchooser_hudtext_y", "0.01", "Hudtext 1 Y coordinate, from 0 (top) to 1 (bottom), -1 is the center"); 79 | cvHudText2x = CreateConVar("modelchooser_hudtext2_x", "-1", "Hudtext 2 X coordinate, from 0 (left) to 1 (right), -1 is the center"); 80 | cvHudText2y = CreateConVar("modelchooser_hudtext2_y", "0.95", "Hudtext 2 Y coordinate, from 0 (top) to 1 (bottom), -1 is the center"); 81 | cvForceFullUpdate = CreateConVar("modelchooser_forcefullupdate", "1", "Fixes weapon prediction glitch caused by going thirdperson, recommended to keep on unless you run into issues"); 82 | mp_forcecamera = FindConVar("mp_forcecamera"); 83 | 84 | cvTeamBased.AddChangeHook(Hook_TeamBasedCvarChanged); 85 | 86 | UserMsg hudMsgId = GetUserMessageId("HudMsg"); 87 | if (hudMsgId != INVALID_MESSAGE_ID) 88 | { 89 | HookUserMessage(hudMsgId, Hook_HudMsg, true); 90 | } 91 | HookEvent("player_hurt", Event_PlayerHurt, EventHookMode_Post); 92 | HookEvent("player_spawn", Event_PlayerSpawn, EventHookMode_Post); 93 | HookEvent("player_team", Event_PlayerTeam, EventHookMode_Post); 94 | CreateTimer(2.0, Sounds_CheckHealthRaise, _, TIMER_REPEAT); 95 | 96 | GameData gamedata = new GameData("modelchooser"); 97 | if (!gamedata) 98 | { 99 | SetFailState("Failed to load \"modelchooser\" gamedata"); 100 | } 101 | 102 | LoadDHookVirtual(gamedata, hkSetModel, "CBaseEntity::SetModel_"); 103 | LoadDHookVirtual(gamedata, hkDeathSound, "CBasePlayer::DeathSound"); 104 | LoadDHookVirtual(gamedata, hkSetAnimation, "CBasePlayer::SetAnimation"); 105 | 106 | if (GetEngineVersion() == Engine_HL2DM) 107 | { 108 | char szResetSequence[] = "CBaseAnimating::ResetSequence"; 109 | StartPrepSDKCall(SDKCall_Entity); 110 | if (PrepSDKCall_SetFromConf(gamedata, SDKConf_Signature, szResetSequence)) 111 | { 112 | PrepSDKCall_AddParameter(SDKType_PlainOldData, SDKPass_Plain); 113 | if (!(callResetSequence = EndPrepSDKCall())) 114 | LogError("Could not prep SDK call %s", szResetSequence); 115 | } 116 | else LogError("Could not obtain gamedata signature %s", szResetSequence); 117 | 118 | if (!callResetSequence) 119 | LogError("Custom animations will not work"); 120 | } 121 | 122 | char szGetClient[] = "CBaseServer::GetClient"; 123 | StartPrepSDKCall(SDKCall_Server); 124 | if (PrepSDKCall_SetFromConf(gamedata, SDKConf_Virtual, szGetClient)) 125 | { 126 | PrepSDKCall_SetReturnInfo(SDKType_PlainOldData, SDKPass_Plain); 127 | PrepSDKCall_AddParameter(SDKType_PlainOldData, SDKPass_Plain); 128 | if (!(callGetClient = EndPrepSDKCall())) 129 | LogError("Could not prep SDK call %s", szGetClient); 130 | } 131 | else LogError("Could not obtain gamedata offset %s", szGetClient); 132 | 133 | char szUpdateAcknowledgedFramecount[] = "CBaseClient::UpdateAcknowledgedFramecount"; 134 | StartPrepSDKCall(SDKCall_Raw); 135 | if (PrepSDKCall_SetFromConf(gamedata, SDKConf_Virtual, szUpdateAcknowledgedFramecount)) 136 | { 137 | PrepSDKCall_AddParameter(SDKType_PlainOldData, SDKPass_Plain); 138 | if (!(callUpdateAcknowledgedFramecount = EndPrepSDKCall())) 139 | LogError("Could not prep SDK call %s", szUpdateAcknowledgedFramecount); 140 | } 141 | else LogError("Could not obtain gamedata offset %s", szUpdateAcknowledgedFramecount); 142 | 143 | if (!callGetClient || !callUpdateAcknowledgedFramecount) 144 | LogError("Prediction fix \"modelchooser_forcefullupdate\" will not work"); 145 | 146 | gamedata.Close(); 147 | } 148 | 149 | public void OnConfigsExecuted() 150 | { 151 | static bool init; 152 | if (!init || cvAutoReload.BoolValue) 153 | { 154 | modelList.Clear(); 155 | soundMap.Clear(); 156 | downloads.Clear(); 157 | LoadConfig(); 158 | init = true; 159 | } 160 | else 161 | { 162 | soundMap.Precache(); 163 | modelList.Precache(); 164 | } 165 | 166 | char file[PLATFORM_MAX_PATH]; 167 | 168 | cvLockModel.GetString(file, sizeof(file)); 169 | SmartDM.AddEx(file, downloads); 170 | 171 | cvOverlay.GetString(file, sizeof(file)); 172 | if (!StrEqual(file, "") && !StrEqual(file, "0")) 173 | { 174 | Format(file, sizeof(file), "materials/%s.vmt", file); 175 | SmartDM.AddEx(file, downloads); 176 | } 177 | 178 | cvMenuSnd.GetString(file, sizeof(file)); 179 | if (!StrEqual(file, "")) 180 | { 181 | Format(file, sizeof(file), "sound/%s", file); 182 | SmartDM.AddEx(file, downloads, true); 183 | } 184 | 185 | downloads.AddToDownloadsTable(); 186 | 187 | PrecacheModel(FALLBACK_MODEL, true); 188 | 189 | for (int i = 1; i <= MaxClients; i++) 190 | { 191 | if (IsClientInGame(i)) 192 | { 193 | OnClientConnected(i); 194 | OnClientPutInServer(i); 195 | if (IsClientAuthorized(i)) 196 | OnClientPostAdminCheck(i); 197 | if (AreClientCookiesCached(i)) 198 | OnClientCookiesCached(i); 199 | } 200 | } 201 | } 202 | 203 | public void OnClientConnected(int client) 204 | { 205 | ResetClientModels(client); 206 | ResetUnlockedModels(client); 207 | tMenuInit[client] = null; 208 | clientInitChecks[client] = 3; 209 | currentTeam[client] = 0; 210 | } 211 | 212 | public void OnClientPutInServer(int client) 213 | { 214 | if (!IsFakeClient(client)) 215 | { 216 | currentTeam[client] = GetClientTeam(client); 217 | DHookEntity(hkSetModel, false, client, _, Hook_SetModel); 218 | DHookEntity(hkDeathSound, false, client, _, Hook_DeathSound); 219 | DHookEntity(hkSetAnimation, false, client, _, Hook_SetAnimation); 220 | 221 | if (!--clientInitChecks[client]) 222 | InitClientModels(client); 223 | } 224 | } 225 | 226 | public void OnClientPostAdminCheck(int client) 227 | { 228 | if (!--clientInitChecks[client]) 229 | InitClientModels(client); 230 | } 231 | 232 | public void OnClientCookiesCached(int client) 233 | { 234 | if (!--clientInitChecks[client]) 235 | InitClientModels(client); 236 | } 237 | 238 | public void Event_PlayerSpawn(Event event, const char[] name, bool dontBroadcast) 239 | { 240 | int client = GetClientOfUserId(event.GetInt("userid")); 241 | if (client) 242 | { 243 | Sounds_EventPlayerSpawn(event, client); 244 | } 245 | } 246 | 247 | public void Event_PlayerTeam(Event event, const char[] name, bool dontBroadcast) 248 | { 249 | if (event.GetBool("disconnect")) 250 | return; 251 | 252 | int client = GetClientOfUserId(event.GetInt("userid")); 253 | if (client) 254 | { 255 | int oldTeam = event.GetInt("oldteam"); 256 | int team = currentTeam[client] = event.GetInt("team"); 257 | if (team != oldTeam) 258 | { 259 | if (IsInMenu(client)) 260 | { 261 | ExitModelChooser(client, true, true); 262 | } 263 | if (team == TEAM_UNASSIGNED || team > TEAM_SPECTATOR) 264 | { 265 | ReloadClientModels(client); 266 | } 267 | } 268 | } 269 | } 270 | 271 | public void Event_PlayerHurt(Event event, const char[] name, bool dontBroadcast) 272 | { 273 | int client = GetClientOfUserId(event.GetInt("userid")); 274 | if (client) 275 | { 276 | Sounds_EventPlayerHurt(event, client); 277 | } 278 | } 279 | 280 | public void OnEntityCreated(int entity, const char[] classname) 281 | { 282 | if (StrContains(classname, "viewmodel") != -1 && HasEntProp(entity, Prop_Data, "m_hWeapon")) 283 | { 284 | DHookEntity(hkSetModel, false, entity, _, Hook_SetViewModelModel); 285 | } 286 | } 287 | 288 | void Hook_TeamBasedCvarChanged(ConVar convar, const char[] oldValue, const char[] newValue) 289 | { 290 | for (int i = 1; i <= MaxClients; i++) 291 | { 292 | if (IsClientInGame(i) && !IsFakeClient(i)) 293 | ReloadClientModels(i); 294 | } 295 | } 296 | 297 | //------------------------------------------------------ 298 | // Core functions 299 | //------------------------------------------------------ 300 | 301 | bool GetSelectedModelAuto(int client, PlayerModel model) 302 | { 303 | return GetSelectedModel(client, model, menuSelection[client].IsValid()); 304 | } 305 | 306 | bool GetSelectedModel(int client, PlayerModel model, bool inMenu = false) 307 | { 308 | return Selection2Model(client, inMenu? menuSelection[client] : activeSelection[client], model); 309 | } 310 | 311 | void GetSelectionDataAuto(int client, SelectionData selectionData) 312 | { 313 | selectionData = menuSelection[client].IsValid()? menuSelection[client] : activeSelection[client]; 314 | } 315 | 316 | bool Selection2Model(int client, const SelectionData data, PlayerModel model) 317 | { 318 | if (data.index != -1 && selectableModels[client] && selectableModels[client].Length) 319 | { 320 | modelList.GetArray(selectableModels[client].Get(data.index), model); 321 | return true; 322 | } 323 | return false; 324 | } 325 | 326 | public MRESReturn Hook_SetModel(int client, DHookParam hParams) 327 | { 328 | SelectionData selection; 329 | PlayerModel model; 330 | GetSelectionDataAuto(client, selection); 331 | if (Selection2Model(client, selection, model)) 332 | { 333 | DHookSetParamString(hParams, 1, model.path); 334 | SetEntitySkin(client, model.GetSkin(selection.skin)); 335 | SetEntityBody(client, model.GetBody(selection.body)); 336 | // Delay needed for Black Mesa 337 | CreateTimer(0.1, Timer_UpdateModelAccessories, GetClientUserId(client), TIMER_FLAG_NO_MAPCHANGE); 338 | return MRES_ChangedHandled; 339 | } 340 | return MRES_Ignored; 341 | } 342 | 343 | void Timer_UpdateModelAccessories(Handle timer, int userid) 344 | { 345 | int client = GetClientOfUserId(userid); 346 | if (client) 347 | { 348 | SelectionData selection; 349 | PlayerModel model; 350 | GetSelectionDataAuto(client, selection); 351 | if (Selection2Model(client, selection, model)) 352 | { 353 | SetEntitySkin(client, model.GetSkin(selection.skin)); 354 | SetEntityBody(client, model.GetBody(selection.body)); 355 | UpdateViewModels(client); 356 | } 357 | } 358 | } 359 | 360 | void RefreshModel(int client) 361 | { 362 | if (IsClientInGame(client)) 363 | { 364 | // something went wrong if this sticks 365 | SetEntityModel(client, FALLBACK_MODEL); 366 | } 367 | } 368 | 369 | void ResetClientModels(int client) 370 | { 371 | delete selectableModels[client]; 372 | activeSelection[client].Reset(); 373 | menuSelection[client].Reset(); 374 | playedHurtSoundAt[client] = -1; 375 | nextJumpSound[client] = 0.0; 376 | } 377 | 378 | void ReloadClientModels(int client) 379 | { 380 | if (!selectableModels[client]) 381 | return; 382 | 383 | if (IsInMenu(client)) 384 | { 385 | ExitModelChooser(client, true, true); 386 | } 387 | ResetClientModels(client); 388 | InitClientModels(client); 389 | } 390 | 391 | void InitClientModels(int client) 392 | { 393 | if (selectableModels[client]) 394 | return; 395 | 396 | selectableModels[client] = BuildSelectableModels(client); 397 | if (!selectableModels[client].Length) 398 | return; 399 | 400 | PersistentPreferences prefs; 401 | prefs = GetPreferences(client); 402 | 403 | char modelName[MODELCHOOSER_MAX_NAME]; 404 | prefs.model.Get(client, modelName, sizeof(modelName)); 405 | 406 | // first select by team based preferences 407 | if (SelectModelByName(client, modelName, prefs.skin.GetInt(client), prefs.body.GetInt(client))) 408 | return; 409 | 410 | // if we failed picking by team, try the no-team preferences 411 | if (prefs.team != 0) 412 | { 413 | prefs = GetPreferencesByTeam(0); 414 | prefs.model.Get(client, modelName, sizeof(modelName)); 415 | if (SelectModelByName(client, modelName, prefs.skin.GetInt(client), prefs.body.GetInt(client))) 416 | return; 417 | } 418 | 419 | // no selectable preferences, pick a default 420 | SelectDefaultModel(client); 421 | } 422 | 423 | ArrayList BuildSelectableModels(int client) 424 | { 425 | ArrayList list = new ArrayList(); 426 | for (int i = 0; i < modelList.Length; i++) 427 | { 428 | PlayerModel model; 429 | modelList.GetArray(i, model); 430 | 431 | if (model.adminBitFlags != -1) 432 | { 433 | int clientFlags = GetUserFlagBits(client); 434 | if (!(clientFlags & ADMFLAG_ROOT || clientFlags & model.adminBitFlags)) 435 | continue; 436 | } 437 | 438 | int modelTeam = model.GetTeamNum(); 439 | if (currentTeam[client] > TEAM_SPECTATOR && currentTeam[client] != modelTeam) 440 | { 441 | if (cvTeamBased.IntValue == 2) 442 | continue; 443 | if (cvTeamBased.IntValue == 1 && modelTeam > TEAM_SPECTATOR) 444 | continue; 445 | } 446 | 447 | list.Push(i); 448 | } 449 | return list; 450 | } 451 | 452 | bool SelectModelByName(int client, const char[] modelName, int skin = 0, int body = 0) 453 | { 454 | int index = modelList.FindByName(modelName); 455 | if (index != -1) 456 | { 457 | PlayerModel model; 458 | modelList.GetArray(index, model); 459 | int clIndex = selectableModels[client].FindValue(index); 460 | if (clIndex != -1 && !IsModelLocked(model, client)) 461 | { 462 | activeSelection[client].index = clIndex; 463 | activeSelection[client].skin = model.IndexOfSkin(skin); 464 | activeSelection[client].body = model.IndexOfBody(body); 465 | RefreshModel(client); 466 | CallModelChanged(client, model); 467 | return true; 468 | } 469 | } 470 | return false; 471 | } 472 | 473 | bool SelectDefaultModel(int client) 474 | { 475 | if (!selectableModels[client].Length) 476 | return false; 477 | 478 | // find models with the highest prio 479 | // select random if there are multiple 480 | int maxPrio = cellmin; 481 | ArrayList maxPrioList = new ArrayList(); 482 | 483 | for (int i = 0; i < selectableModels[client].Length; i++) 484 | { 485 | PlayerModel model; 486 | modelList.GetArray(selectableModels[client].Get(i), model); 487 | 488 | if (IsModelLocked(model, client)) 489 | continue; 490 | 491 | if (model.defaultPrio > maxPrio) 492 | { 493 | maxPrio = model.defaultPrio; 494 | maxPrioList.Clear(); 495 | maxPrioList.Push(i); 496 | } 497 | else if (model.defaultPrio == maxPrio) 498 | { 499 | maxPrioList.Push(i); 500 | } 501 | } 502 | 503 | if (maxPrioList.Length) 504 | { 505 | activeSelection[client].index = maxPrioList.Get(Math_GetRandomInt(0, maxPrioList.Length - 1)); 506 | 507 | PlayerModel model; 508 | Selection2Model(client, activeSelection[client], model); 509 | RefreshModel(client); 510 | CallModelChanged(client, model); 511 | delete maxPrioList; 512 | return true; 513 | } 514 | 515 | delete maxPrioList; 516 | return false; 517 | } 518 | 519 | void CallModelChanged(int client, const PlayerModel model) 520 | { 521 | Call_StartForward(fwdOnModelChanged); 522 | Call_PushCell(client); 523 | Call_PushString(model.name); 524 | Call_Finish(); 525 | } 526 | 527 | bool IsModelLocked(const PlayerModel model, int client) 528 | { 529 | return (model.locked && !unlockedModels[client].GetValue(model.name, client)); 530 | } 531 | 532 | void UnlockModel(int client, char modelName[MODELCHOOSER_MAX_NAME]) 533 | { 534 | unlockedModels[client].SetValue(modelName, true); 535 | } 536 | 537 | void LockModel(int client, char modelName[MODELCHOOSER_MAX_NAME]) 538 | { 539 | unlockedModels[client].Remove(modelName); 540 | } 541 | 542 | void ResetUnlockedModels(int client) 543 | { 544 | delete unlockedModels[client]; 545 | unlockedModels[client] = new StringMap(); 546 | } 547 | 548 | PersistentPreferences GetPreferences(int client) 549 | { 550 | int team = currentTeam[client]; 551 | if (team <= TEAM_SPECTATOR || !cvTeamBased.BoolValue) 552 | { 553 | team = 0; 554 | } 555 | return GetPreferencesByTeam(team); 556 | } 557 | 558 | PersistentPreferences GetPreferencesByTeam(int team) 559 | { 560 | PersistentPreferences prefs; 561 | prefs = persistentPreferences[team]; 562 | prefs.Init(team); 563 | return prefs; 564 | } 565 | 566 | SoundPack GetSoundPack(const PlayerModel model, bool emptyDefault = true) 567 | { 568 | SoundPack soundPack = soundMap.GetSoundPack(model.sounds); 569 | if (soundPack || !emptyDefault) 570 | { 571 | return soundPack; 572 | } 573 | static SoundPack emptySoundPack; 574 | if (!emptySoundPack) 575 | { 576 | emptySoundPack = new SoundPack(); 577 | } 578 | return emptySoundPack; 579 | } 580 | 581 | void ForceFullUpdate(int client) 582 | { 583 | if (cvForceFullUpdate.BoolValue && callGetClient && callUpdateAcknowledgedFramecount) 584 | { 585 | int pClient = SDKCall(callGetClient, client - 1) - 4; 586 | SDKCall(callUpdateAcknowledgedFramecount, pClient, -1); 587 | } 588 | } --------------------------------------------------------------------------------