├── .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 | [](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 | }
--------------------------------------------------------------------------------