Skip to content

Instantly share code, notes, and snippets.

@nosoop
Last active September 6, 2021 02:40
Show Gist options
  • Save nosoop/538169d7cb9e60ef8ea9126ca16e8270 to your computer and use it in GitHub Desktop.
Save nosoop/538169d7cb9e60ef8ea9126ca16e8270 to your computer and use it in GitHub Desktop.
/**
* [TF2] Jungle Inferno Equipment
*/
#pragma semicolon 1
#include <sourcemod>
#include <tf2_stocks>
#include <clientprefs>
#include <sdkhooks>
#pragma newdecls required
#define PLUGIN_VERSION "1.0.2"
public Plugin myinfo = {
name = "[TF2] Jungle Inferno Equipment",
author = "nosoop",
description = "Allows players to equip new weapons for Pyro and Heavy. "
... "Because Valve is giving normal community servers fewer reasons to exist.",
version = PLUGIN_VERSION,
url = "https://csrd.science/"
}
enum UpdateWeapon {
Weapon_DragonsFury,
Weapon_ThermalThruster,
Weapon_GasPasser,
Weapon_HotHand,
Weapon_SecondBanana
};
char INFERNO_WEAPON_LOADOUT_NAMES[UpdateWeapon][] = {
"The Dragon's Fury (Pyro Primary)",
"The Thermal Thruster (Pyro Secondary)",
"The Gas Passer (Pyro Secondary)",
"The Hot Hand (Pyro Melee)",
"The Second Banana (Heavy Secondary)"
};
Handle g_PrefJungleWeapons;
int g_EquippedWeapons[MAXPLAYERS + 1];
bool g_bUpdateWeaponNotify[MAXPLAYERS + 1];
float g_flGasPasserCharge[MAXPLAYERS + 1];
public void OnPluginStart() {
HookEvent("post_inventory_application", OnInventoryApplied);
HookEvent("player_spawn", OnPlayerSpawn);
HookEvent("player_disconnect", OnPlayerDisconnect);
RegConsoleCmd("sm_loadout", OpenLoadoutOverrideMenu);
g_PrefJungleWeapons = RegClientCookie("loadout_jungle_inferno",
"Preferences for Jungle Inferno weapons.", CookieAccess_Private);
}
public void OnMapStart() {
for (int i = 1; i <= MaxClients; i++) {
if (IsClientConnected(i)) {
SDKHook(i, SDKHook_PostThinkPost, OnPlayerChargeMeterUpdate);
if (AreClientCookiesCached(i)) {
OnClientCookiesCached(i);
}
}
}
}
public void OnClientCookiesCached(int client) {
char weaponBits[16];
GetClientCookie(client, g_PrefJungleWeapons, weaponBits, sizeof(weaponBits));
g_EquippedWeapons[client] = StringToInt(weaponBits);
}
public void OnPlayerDisconnect(Event event, const char[] name, bool dontBroadcast) {
int client = GetClientOfUserId(event.GetInt("userid"));
g_EquippedWeapons[client] = 0;
g_bUpdateWeaponNotify[client] = false;
}
public Action OpenLoadoutOverrideMenu(int client, int argc) {
Menu loadoutMenu = new Menu(LoadoutUpdateCallback, MENU_ACTIONS_ALL);
loadoutMenu.SetTitle("Select an item to toggle.");
for (int i = 0; i < view_as<int>(UpdateWeapon); i++) {
char menuInfo[8];
IntToString(i, menuInfo, sizeof(menuInfo));
// display will be overridden in the menu callback
loadoutMenu.AddItem(menuInfo, INFERNO_WEAPON_LOADOUT_NAMES[i]);
}
loadoutMenu.Display(client, 10);
return Plugin_Handled;
}
public int LoadoutUpdateCallback(Menu menu, MenuAction action, int param1, int param2) {
switch (action) {
case MenuAction_Select: {
int client = param1, selection = param2;
char menuInfo[8];
menu.GetItem(selection, menuInfo, sizeof(menuInfo));
UpdateWeapon menuItem = view_as<UpdateWeapon>(StringToInt(menuInfo));
ToggleUpdateWeaponOverride(client, menuItem);
menu.Display(client, 10);
}
case MenuAction_DisplayItem: {
int client = param1, item = param2;
char menuInfo[8];
menu.GetItem(item, menuInfo, sizeof(menuInfo));
UpdateWeapon menuItem = view_as<UpdateWeapon>(StringToInt(menuInfo));
char menuDisplay[128];
Format(menuDisplay, sizeof(menuDisplay), "[%s] %s",
IsUpdateWeaponOverride(client, menuItem) ? "x" : " ",
INFERNO_WEAPON_LOADOUT_NAMES[menuItem]);
return RedrawMenuItem(menuDisplay);
}
case MenuAction_End: {
int endReason = param1;
if (endReason == MenuEnd_Cancelled || endReason == MenuEnd_Exit) {
delete menu;
}
}
}
return 0;
}
public void OnInventoryApplied(Event event, const char[] name, bool dontBroadcast) {
int client = GetClientOfUserId(event.GetInt("userid"));
if (!client) {
return;
}
if (!g_bUpdateWeaponNotify[client]) {
// todo notify:
// * when both conditions are met:
// * player hasn't connected in the last week, and
// * player doesn't have all of the weapons (possible to check?)
// * or player has any plugin-granted weapons (reminder to erase)
PrintToChat(client, "This server is allowing players to equip the new Jungle Inferno "
... "update weapons. Type /loadout to toggle them.");
g_bUpdateWeaponNotify[client] = true;
}
switch(TF2_GetPlayerClass(client)) {
case TFClass_Pyro: {
if (IsUpdateWeaponOverride(client, Weapon_DragonsFury)) {
TF2_RemoveWeaponSlot(client, 0);
TF2_CreateAndEquipWeapon(client, 1178, "tf_weapon_rocketlauncher_fireball");
}
if (IsUpdateWeaponOverride(client, Weapon_ThermalThruster)) {
TF2_RemoveWeaponSlot(client, 1);
TF2_CreateAndEquipWeapon(client, 1179, "tf_weapon_rocketpack");
}
if (IsUpdateWeaponOverride(client, Weapon_GasPasser)) {
TF2_RemoveWeaponSlot(client, 1);
int weapon = TF2_CreateAndEquipWeapon(client, 1180, "tf_weapon_jar_gas");
TF2_EmptyWeaponAmmo(weapon);
// Match game behavior by persisting recharge meter
// BUG: Item is unusuable if it spawns at full charge
float flItemChargeMeter = g_flGasPasserCharge[client] > 99.5?
99.5 : g_flGasPasserCharge[client];
SetEntPropFloat(client, Prop_Send, "m_flItemChargeMeter",
flItemChargeMeter, 1);
}
if (IsUpdateWeaponOverride(client, Weapon_HotHand)) {
TF2_RemoveWeaponSlot(client, 2);
TF2_CreateAndEquipWeapon(client, 1181, "tf_weapon_slap");
}
}
case TFClass_Heavy: {
if (IsUpdateWeaponOverride(client, Weapon_SecondBanana)) {
TF2_RemoveWeaponSlot(client, 1);
TF2_CreateAndEquipWeapon(client, 1190, "tf_weapon_lunchbox");
}
}
}
}
public void OnPlayerSpawn(Event event, const char[] name, bool dontBroadcast) {
int client = GetClientOfUserId(event.GetInt("userid"));
// Gas Passer starts empty on spawn
g_flGasPasserCharge[client] = 0.0;
}
public void OnPlayerChargeMeterUpdate(int client) {
if (IsUpdateWeaponOverride(client, Weapon_GasPasser)) {
int item = GetPlayerWeaponSlot(client, 1);
if (GetEntProp(item, Prop_Send, "m_iItemDefinitionIndex") == 1180) {
g_flGasPasserCharge[client] = GetEntPropFloat(client, Prop_Send,
"m_flItemChargeMeter", 1);
}
}
}
bool IsUpdateWeaponOverride(int client, UpdateWeapon weapon) {
return !!((g_EquippedWeapons[client] >> view_as<int>(weapon)) & 1);
}
void ToggleUpdateWeaponOverride(int client, UpdateWeapon weapon) {
// disable the other secondary weapon
if (weapon == Weapon_ThermalThruster) {
g_EquippedWeapons[client] &= ~(1 << view_as<int>(Weapon_GasPasser));
} else if (weapon == Weapon_GasPasser) {
g_EquippedWeapons[client] &= ~(1 << view_as<int>(Weapon_ThermalThruster));
}
g_EquippedWeapons[client] ^= (1 << view_as<int>(weapon));
char weaponBits[16];
IntToString(g_EquippedWeapons[client], weaponBits, sizeof(weaponBits));
SetClientCookie(client, g_PrefJungleWeapons, weaponBits);
}
int TF2_CreateAndEquipWeapon(int client, int defindex, const char[] weaponClass) {
int weapon = TF2_CreateWeapon(defindex, weaponClass);
TF2_EquipPlayerWeapon(client, weapon);
return weapon;
}
/**
* Creates a weapon for the specified player.
*/
stock int TF2_CreateWeapon(int defindex, const char[] weaponClass) {
int weapon = CreateEntityByName(weaponClass);
if (IsValidEntity(weapon)) {
SetEntProp(weapon, Prop_Send, "m_iItemDefinitionIndex", defindex);
SetEntProp(weapon, Prop_Send, "m_bInitialized", 1);
// Allow quality / level override by updating through the offset.
char netClass[64];
GetEntityNetClass(weapon, netClass, sizeof(netClass));
SetEntData(weapon, FindSendPropInfo(netClass, "m_iEntityQuality"), 6);
SetEntData(weapon, FindSendPropInfo(netClass, "m_iEntityLevel"), 1);
SetEntProp(weapon, Prop_Send, "m_iEntityQuality", 6);
SetEntProp(weapon, Prop_Send, "m_iEntityLevel", 1);
DispatchSpawn(weapon);
}
return weapon;
}
stock void TF2_EquipPlayerWeapon(int client, int weapon) {
char weaponClass[64];
GetEntityClassname(weapon, weaponClass, sizeof(weaponClass));
if (StrContains(weaponClass, "tf_wearable", false) == 0) {
// CTFPlayer_EquipWearable(client, weapon);
} else {
EquipPlayerWeapon(client, weapon);
/**
* Ensure that weapon spawns with the correct amount of ammo.
*/
TF2_EmptyWeaponAmmo(weapon);
TF2_GiveWeaponAmmo(weapon, 500);
}
}
/**
* Gives a weapon ammo. Rather, it gives ammo to the player holding the weapon.
* This stock uses GivePlayerAmmo, so it will not give any more ammo than what the player can
* carry. It does not check if the existing amount exceeds what the player can carry.
*
* @return Amount of ammo actually given.
*/
stock int TF2_GiveWeaponAmmo(int weapon, int amount, bool supressSound = true) {
int ammoType = GetEntProp(weapon, Prop_Send, "m_iPrimaryAmmoType");
int client = GetEntPropEnt(weapon, Prop_Send, "m_hOwner");
if (client > 0 && client <= MaxClients) {
return GivePlayerAmmo(client, amount, ammoType, supressSound);
}
return 0;
}
/**
* Directly sets the amount of ammo a client can carry for a specific weapon.
*/
stock void TF2_SetWeaponAmmo(int weapon, int amount) {
int ammoType = GetEntProp(weapon, Prop_Send, "m_iPrimaryAmmoType");
int client = GetEntPropEnt(weapon, Prop_Send, "m_hOwner");
if (ammoType != -1) {
SetEntProp(client, Prop_Send, "m_iAmmo", amount, 4, ammoType);
}
}
/**
* Reduces a client's currently carried amount of ammo for a specified weapon to zero.
*/
stock void TF2_EmptyWeaponAmmo(int weapon) {
TF2_SetWeaponAmmo(weapon, 0);
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment