-
Notifications
You must be signed in to change notification settings - Fork 5
/
viewmodel_override.sp
392 lines (319 loc) · 11.8 KB
/
viewmodel_override.sp
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
/**
* Weapon model overrides.
*
* Provides three attributes "viewmodel override", "worldmodel override",
* and "clientmodel override". Attribute values are full paths to models (include "models/"
* prefix).
*
* - "viewmodel override" is used exclusively for the owning player's view.
* - "worldmodel override" is used for other players' views, dropped weapons, and attached
* sappers.
* - "clientmodel override" can be used in place of both if they share the same model, and will
* take priority.
*/
#pragma semicolon 1
#include <sourcemod>
#include <sdktools>
#include <sdkhooks>
#pragma newdecls required
#include <tf_custom_attributes>
#include <stocksoup/tf/entity_prop_stocks>
#include <stocksoup/tf/econ>
#include <tf_econ_data>
#include <tf2utils>
#define EF_NODRAW (1 << 5)
#define MODEL_NONE_ACTIVE 0
#define MODEL_VIEW_ACTIVE (1 << 0)
#define MODEL_ARM_ACTIVE (1 << 1)
#define MODEL_WORLD_ACTIVE (1 << 2)
#define MODEL_OFFHAND_ACTIVE (1 << 3)
#define TF_ITEM_DEFINDEX_GUNSLINGER 142
bool g_bIgnoreWeaponSwitch[MAXPLAYERS + 1];
int g_iLastViewmodelRef[MAXPLAYERS + 1] = { INVALID_ENT_REFERENCE, ... };
int g_iLastArmModelRef[MAXPLAYERS + 1] = { INVALID_ENT_REFERENCE, ... };
int g_iLastWorldModelRef[MAXPLAYERS + 1] = { INVALID_ENT_REFERENCE, ... };
int g_iLastOffHandViewmodelRef[MAXPLAYERS + 1] = { INVALID_ENT_REFERENCE, ... };
public void OnMapStart() {
for (int i = 1; i <= MaxClients; i++) {
if (IsClientInGame(i)) {
OnClientPutInServer(i);
}
}
HookEvent("player_death", OnPlayerDeath);
HookEvent("post_inventory_application", OnInventoryAppliedPre, EventHookMode_Pre);
HookEvent("player_sapped_object", OnObjectSappedPost);
}
public void OnPluginEnd() {
for (int i = 1; i <= MaxClients; i++) {
if (IsClientInGame(i)) {
DetachVMs(i);
}
}
}
public void OnClientPutInServer(int client) {
SDKHook(client, SDKHook_Spawn, OnPlayerSpawnPre);
SDKHook(client, SDKHook_SpawnPost, OnPlayerSpawnPost);
SDKHook(client, SDKHook_WeaponSwitchPost, OnWeaponSwitchPost);
}
public void OnEntityCreated(int entity, const char[] className) {
if (StrEqual(className, "tf_dropped_weapon")) {
SDKHook(entity, SDKHook_SpawnPost, OnDroppedWeaponSpawnPost);
}
}
/**
* Hotfix to ensure any attached Sniper Rifle is rendered when coming out of being in scope.
*/
public void TF2_OnConditionRemoved(int client, TFCond cond) {
if (cond == TFCond_Slowed && TF2_GetPlayerClass(client) == TFClass_Sniper
&& IsValidEntity(g_iLastViewmodelRef[client])) {
UpdateClientWeaponModel(client);
}
}
/**
* Sets the world model of a dropped weapon.
*/
void OnDroppedWeaponSpawnPost(int weapon) {
char wm[PLATFORM_MAX_PATH];
if (TF2CustAttr_GetString(weapon, "clientmodel override", wm, sizeof(wm))
|| TF2CustAttr_GetString(weapon, "worldmodel override", wm, sizeof(wm))) {
SetEntityModel(weapon, wm);
SetWeaponWorldModel(weapon, wm);
}
}
/**
* Called when the player's loadout is applied. Note that other plugins may not have finished
* applying weapons by this time; however, they should implicitly invoke WeaponSwitchPost
* (because of GiveNamedItem, etc.) so viewmodels should be correct.
*/
void OnInventoryAppliedPre(Event event, const char[] name, bool dontBroadcast) {
int client = GetClientOfUserId(event.GetInt("userid"));
if (!client) {
return;
}
UpdateClientWeaponModel(client);
/**
* start processing weapon switches, since other plugins may be equipping new weapons in
* post_inventory_application -- and that's still within the player's spawn function call
*/
g_bIgnoreWeaponSwitch[client] = false;
}
Action OnPlayerSpawnPre(int client) {
g_bIgnoreWeaponSwitch[client] = true;
return Plugin_Continue;
}
void OnPlayerSpawnPost(int client) {
g_bIgnoreWeaponSwitch[client] = false;
}
void OnWeaponSwitchPost(int client, int weapon) {
if (!g_bIgnoreWeaponSwitch[client]) {
UpdateClientWeaponModel(client);
}
}
/**
* Called on weapon switch. Detaches any old viewmodel overrides and attaches replacements.
*/
void UpdateClientWeaponModel(int client) {
DetachVMs(client);
int weapon = TF2_GetClientActiveWeapon(client);
if (!IsValidEntity(weapon)) {
return;
}
int bitsActiveModels = MODEL_NONE_ACTIVE;
char cm[PLATFORM_MAX_PATH];
TF2CustAttr_GetString(weapon, "clientmodel override", cm, sizeof(cm));
char vm[PLATFORM_MAX_PATH];
if (TF2CustAttr_GetString(weapon, "viewmodel override", vm, sizeof(vm), cm)
&& FileExists(vm, true)) {
// override viewmodel by attaching arm and weapon viewmodels
PrecacheModel(vm);
int weaponvm = TF2_SpawnWearableViewmodel();
SetEntityModel(weaponvm, vm);
TF2Util_EquipPlayerWearable(client, weaponvm);
g_iLastViewmodelRef[client] = EntIndexToEntRef(weaponvm);
bitsActiveModels |= MODEL_VIEW_ACTIVE;
}
char wm[PLATFORM_MAX_PATH];
if (TF2CustAttr_GetString(weapon, "worldmodel override", wm, sizeof(wm), cm)) {
// this allows other players to see the given weapon with the correct model
SetWeaponWorldModel(weapon, wm);
// the following shows the weapon in third-person, as m_nModelIndexOverrides is messy
int weaponwm = TF2_SpawnWearable();
SetEntityModel(weaponwm, wm);
TF2Util_EquipPlayerWearable(client, weaponwm);
g_iLastWorldModelRef[client] = EntIndexToEntRef(weaponwm);
SetEntityRenderMode(weapon, RENDER_TRANSCOLOR);
SetEntityRenderColor(weapon, 0, 0, 0, 0);
bitsActiveModels |= MODEL_WORLD_ACTIVE;
}
if (TF2_GetPlayerClass(client) == TFClass_DemoMan && TF2Util_IsEntityWeapon(weapon)
&& TF2Util_GetWeaponSlot(weapon) == TFWeaponSlot_Melee) {
// display shield if player has their melee weapon out on demoman
int shield = TF2Util_GetPlayerLoadoutEntity(client, 1);
char ohvm[PLATFORM_MAX_PATH];
if (IsValidEntity(shield) && TF2Util_IsEntityWearable(shield)
&& TF2CustAttr_GetString(shield, "clientmodel override", ohvm, sizeof(ohvm))
&& FileExists(ohvm, true)) {
PrecacheModel(ohvm);
int offhandwearable = TF2_SpawnWearableViewmodel();
SetEntityModel(offhandwearable, ohvm);
TF2Util_EquipPlayerWearable(client, offhandwearable);
g_iLastOffHandViewmodelRef[client] = EntIndexToEntRef(offhandwearable);
SetEntityModel(shield, ohvm);
bitsActiveModels |= MODEL_OFFHAND_ACTIVE;
}
}
if (bitsActiveModels & (MODEL_VIEW_ACTIVE | MODEL_OFFHAND_ACTIVE) == 0) {
// we need to attach arm viewmodels if we render a new weapon viewmodel
// or if we have something attached to our offhand
return;
}
char armvmPath[PLATFORM_MAX_PATH];
if (GetArmViewModel(client, armvmPath, sizeof(armvmPath))) {
// armvmPath might not be precached on the server
// mainly an issue with the gunslinger variation of the arm model
PrecacheModel(armvmPath);
int armvm = TF2_SpawnWearableViewmodel();
SetEntityModel(armvm, armvmPath);
TF2Util_EquipPlayerWearable(client, armvm);
g_iLastArmModelRef[client] = EntIndexToEntRef(armvm);
int clientView = GetEntPropEnt(client, Prop_Send, "m_hViewModel");
SetEntProp(clientView, Prop_Send, "m_fEffects", EF_NODRAW);
bitsActiveModels |= MODEL_ARM_ACTIVE;
if (bitsActiveModels & MODEL_VIEW_ACTIVE == 0) {
// we didn't create a custom weapon viewmodel, so we need to render the original one
// for that weapon
int itemdef = GetEntProp(weapon, Prop_Send, "m_iItemDefinitionIndex");
if (!TF2Econ_GetItemDefinitionString(itemdef, "model_player", vm, sizeof(vm))) {
return;
}
PrecacheModel(vm);
int weaponvm = TF2_SpawnWearableViewmodel();
SetEntityModel(weaponvm, vm);
TF2Util_EquipPlayerWearable(client, weaponvm);
g_iLastViewmodelRef[client] = EntIndexToEntRef(weaponvm);
bitsActiveModels |= MODEL_VIEW_ACTIVE;
}
}
}
/**
* Destroys wearable worldmodels on death so ragdolls aren't holding them.
*/
void OnPlayerDeath(Event event, const char[] name, bool dontBroadcast) {
int client = GetClientOfUserId(event.GetInt("userid"));
if (client) {
MaybeRemoveWearable(client, g_iLastWorldModelRef[client]);
}
}
/**
* Allows the use of custom models on sappers attached to buildings.
*/
void OnObjectSappedPost(Event event, const char[] name, bool dontBroadcast) {
int client = GetClientOfUserId(event.GetInt("userid"));
if (!IsValidEntity(client)) {
return;
}
int sapper = GetPlayerWeaponSlot(client, TFWeaponSlot_Secondary);
if (!IsValidEntity(sapper)) {
return;
}
char wm[PLATFORM_MAX_PATH];
if (TF2CustAttr_GetString(sapper, "clientmodel override", wm, sizeof(wm))
|| TF2CustAttr_GetString(sapper, "worldmodel override", wm, sizeof(wm))) {
int attachedSapper = event.GetInt("sapperid");
SetAttachedSapperModel(attachedSapper, wm);
}
}
bool SetWeaponWorldModel(int weapon, const char[] worldmodel) {
if (!FileExists(worldmodel, true)) {
return false;
}
int model = PrecacheModel(worldmodel);
if (HasEntProp(weapon, Prop_Send, "m_iWorldModelIndex")) {
SetEntProp(weapon, Prop_Send, "m_iWorldModelIndex", model);
}
/**
* setting m_nModelIndexOverrides causes firing animations to break, but prevents the
* weapon from showing up with the overwritten model in taunts
*
* to display the overwritten world model on dropped items see OnDroppedWeaponSpawnPost
*/
for (int i = 1; i < GetEntPropArraySize(weapon, Prop_Send, "m_nModelIndexOverrides"); i++) {
// SetEntProp(weapon, Prop_Send, "m_nModelIndexOverrides", model, .element = i);
}
return true;
}
/**
* Sets the model on the given building-attached sapper.
*/
bool SetAttachedSapperModel(int sapper, const char[] worldmodel) {
if (!FileExists(worldmodel, true)) {
return false;
}
SetEntityModel(sapper, worldmodel);
return true;
}
/**
* Detaches any custom viewmodels on the client and displays the original viewmodel.
*/
void DetachVMs(int client) {
MaybeRemoveWearable(client, g_iLastViewmodelRef[client]);
MaybeRemoveWearable(client, g_iLastArmModelRef[client]);
if (MaybeRemoveWearable(client, g_iLastWorldModelRef[client])) {
int activeWeapon = TF2_GetClientActiveWeapon(client);
if (IsValidEntity(activeWeapon)) {
SetEntityRenderMode(activeWeapon, RENDER_NORMAL);
}
}
MaybeRemoveWearable(client, g_iLastOffHandViewmodelRef[client]);
int clientView = GetEntPropEnt(client, Prop_Send, "m_hViewModel");
if (IsValidEntity(clientView)) {
SetEntProp(clientView, Prop_Send, "m_fEffects", 0);
}
}
/**
* Returns the arm viewmodel appropriate for the given player.
*/
int GetArmViewModel(int client, char[] buffer, int maxlen) {
static char armModels[TFClassType][] = {
"",
"models/weapons/c_models/c_scout_arms.mdl",
"models/weapons/c_models/c_sniper_arms.mdl",
"models/weapons/c_models/c_soldier_arms.mdl",
"models/weapons/c_models/c_demo_arms.mdl",
"models/weapons/c_models/c_medic_arms.mdl",
"models/weapons/c_models/c_heavy_arms.mdl",
"models/weapons/c_models/c_pyro_arms.mdl",
"models/weapons/c_models/c_spy_arms.mdl",
"models/weapons/c_models/c_engineer_arms.mdl"
};
TFClassType playerClass = TF2_GetPlayerClass(client);
// special case kludge: use gunslinger vm if gunslinger is active on engineer
if (playerClass == TFClass_Engineer) {
int meleeWeapon = GetPlayerWeaponSlot(client, TFWeaponSlot_Melee);
if (IsValidEntity(meleeWeapon)
&& TF2_GetItemDefinitionIndex(meleeWeapon) == TF_ITEM_DEFINDEX_GUNSLINGER) {
return strcopy(buffer, maxlen, "models/weapons/c_models/c_engineer_gunslinger.mdl");
}
}
return strcopy(buffer, maxlen, armModels[ view_as<int>(playerClass) ]);
}
bool MaybeRemoveWearable(int client, int wearable) {
if (IsValidEntity(wearable)) {
// the below function does not take entrefs.
TF2_RemoveWearable(client, EntRefToEntIndex(wearable));
return true;
}
return false;
}
/**
* Creates a wearable viewmodel.
* This sets EF_BONEMERGE | EF_BONEMERGE_FASTCULL when equipped.
*/
stock int TF2_SpawnWearableViewmodel() {
int wearable = CreateEntityByName("tf_wearable_vm");
if (IsValidEntity(wearable)) {
SetEntProp(wearable, Prop_Send, "m_iItemDefinitionIndex", DEFINDEX_UNDEFINED);
DispatchSpawn(wearable);
}
return wearable;
}