diff --git a/README.md b/README.md new file mode 100644 index 0000000..1e5101e --- /dev/null +++ b/README.md @@ -0,0 +1,12 @@ +# Toss Buildings + +Pretty simple idea: Engi can now throw his buildings while carrying them. +To throw buildings, press your Reload key. They will construct while in air, bounce off walls and finally be usable at the desitnation. +You can't throw during Waiting For Players, If a building lands in a spawn room or sticks into other things, it will blow up. + +Credits to zen for the idea :) + +## Dependencies +* VPhysics Extension - because it's the best way to accelerate physics props +* TF2Utils - because that can check if things are in spawn rooms +* SMLib Transitional-Syntax - because it makes things easier for me (*COMPILE ONLY*) diff --git a/sp.sauce b/sp.sauce new file mode 100644 index 0000000..6ed0fc8 --- /dev/null +++ b/sp.sauce @@ -0,0 +1,9 @@ +sourcemod 1.10 + +auth github ${GITHUB_TOKEN} + +dependency github nosoop/SM-TFUtils latest +dependency github bcserv/smlib transitional_syntax-SNAPSHOT +dependency limetech vphysics latest + +spcomp -O2 tossbuildings.sp diff --git a/tbobj.games.txt b/tbobj.games.txt new file mode 100644 index 0000000..cfffe37 --- /dev/null +++ b/tbobj.games.txt @@ -0,0 +1,20 @@ +"Games" { + "tf" { + "Signatures" { + // one of two methods with: "robo_sapper", starts with member lookup, first non-virtual call returns ~vtable fun 30 + "StartBuilding" { + "library" "server" + "linux" "@_ZN16CTFWeaponBuilder13StartBuildingEv" + "windows" "\x55\x8B\xEC\x51\x8B\xD1\x53\x89" + //LKASM 55 8b ec 51 8b d1 53 89 55 fc + //MASK ff ff ff ff ff ff ff ff 00 00 + } + } + //"Offsets" { + // "IsPlacementPosValid" { + // "linux" "335" + // "windows" "334" + // } + //} + } +} \ No newline at end of file diff --git a/tossbuildings.sp b/tossbuildings.sp new file mode 100644 index 0000000..24b7622 --- /dev/null +++ b/tossbuildings.sp @@ -0,0 +1,340 @@ +#include +#include +#include +#include +#include +#include +#include +#include + +#pragma newdecls required +#pragma semicolon 1 + +#define PLUGIN_VERSION "22w21a" + +public Plugin myinfo = { + name = "[TF2] Toss Buildings", + description = "Use /toss to throw picked up buildings", + author = "reBane, zen", + version = PLUGIN_VERSION, + url = "N/A", +}; + +enum { + BUILDING_INVALID_OBJECT = ((1<<8)-1), // s8_t:-1 + BUILDING_DISPENSER = 0, + BUILDING_TELEPORTER, + BUILDING_SENTRYGUN, + BUILDING_ATTACHMENT_SAPPER, +} +enum { + BS_IDLE, + BS_SELECTING, + BS_PLACING, + BS_PLACING_INVALID, +}; + +bool g_bPlayerThrow[MAXPLAYERS+1]; +Handle sdk_fnStartBuilding; +//Handle sdk_fnIsPlacementPosValid; +ArrayList g_aAirbornObjects; +float g_flClientLastBeep[MAXPLAYERS+1]; + +#define TBLOCK_WFP (1<<0) +int g_iBlockFlags; + +public void OnPluginStart() { + GameData data = new GameData("tbobj.games"); + if (data == null) + SetFailState("Could not load gamedata: File is missing"); + + StartPrepSDKCall(SDKCall_Entity); //weapon + PrepSDKCall_SetFromConf(data, SDKConf_Signature, "StartBuilding"); + if ((sdk_fnStartBuilding = EndPrepSDKCall())==null) + SetFailState("Could not load gamedata: StartBuilding Signature missing or outdated"); + +// StartPrepSDKCall(SDKCall_Entity); //building +// PrepSDKCall_SetFromConf(data, SDKConf_Virtual, "IsPlacementPosValid"); +// PrepSDKCall_SetReturnInfo(SDKType_PlainOldData, SDKPass_Plain); +// if ((sdk_fnIsPlacementPosValid = EndPrepSDKCall())==null) +// SetFailState("Could not load gamedata: IsPlacementPosValid Offset missing or outdated"); + + delete data; + + HookEvent("player_carryobject", OnPlayerCarryObject); + HookEvent("player_builtobject", OnPlayerBuiltObject); + HookEvent("player_dropobject", OnPlayerBuiltObject); + + g_aAirbornObjects = new ArrayList(3); //phys parent, object, thrown angle (yaw) +} + +public void OnMapStart() { + g_aAirbornObjects.Clear(); + CreateTimer(0.1, Timer_PlaceBuildings, _, TIMER_REPEAT|TIMER_FLAG_NO_MAPCHANGE); +} + +public void OnClientDisconnect(int client) { + g_bPlayerThrow[client] = false; + g_flClientLastBeep[client] = 0.0; +} + +public Action OnPlayerRunCmd(int client, int &buttons, int &impulse, float vel[3], float angles[3], int &weapon, int &subtype, int &cmdnum, int &tickcount, int &seed, int mouse[2]) { + if (!(1<=client<=MaxClients) || !IsClientInGame(client) || IsFakeClient(client)) return Plugin_Continue; + if ((buttons & IN_RELOAD)!=0 && !g_bPlayerThrow[client]) { + //trigger force build and throw on Reload + g_bPlayerThrow[client] = true; + if (CheckThrowPos(client)) StartBuilding(client); + g_bPlayerThrow[client] = false; + } + return Plugin_Continue; +} + +public void OnPlayerCarryObject(Event event, const char[] name, bool dontBroadcast) { + int owner = GetClientOfUserId(event.GetInt("userid")); + int objecttype = event.GetInt("object"); + int building = event.GetInt("index"); + if ((BUILDING_DISPENSER <= objecttype <= BUILDING_SENTRYGUN) && IsClientInGame(owner) && IsValidEdict(building)) { + PrintHintText(owner, "Press [RELOAD] to toss the building"); + } +} +public void OnPlayerBuiltObject(Event event, const char[] name, bool dontBroadcast) { + int owner = GetClientOfUserId(event.GetInt("userid")); + int objecttype = event.GetInt("object"); + int building = event.GetInt("index"); + + if ((BUILDING_DISPENSER <= objecttype <= BUILDING_SENTRYGUN) && IsClientInGame(owner) && IsValidEdict(building) && g_bPlayerThrow[owner]) { + g_bPlayerThrow[owner] = false; + RequestFrame(ThrowBuilding,EntIndexToEntRef(building)); + } +} + +public void TF2_OnWaitingForPlayersStart() { + g_iBlockFlags |= TBLOCK_WFP; +} +public void TF2_OnWaitingForPlayersEnd() { + g_iBlockFlags &=~ TBLOCK_WFP; +} + +public Action Timer_PlaceBuildings(Handle timer) { + ValidateThrown(); +} + +public bool TEF_HitSelfFilter(int entity, int contentsMask, any data) { + return entity > MaxClients && entity != data; +} +public void ThrowBuilding(any buildref) { + int building = EntRefToEntIndex(buildref); + if (building == INVALID_ENT_REFERENCE) return; + int owner = GetEntPropEnt(building, Prop_Send, "m_hBuilder"); + if (owner < 1 || owner > MaxClients || !IsClientInGame(owner)) return; + + float eyes[3]; + float origin[3]; + float angles[3]; + float fwd[3]; + float velocity[3]; + GetClientEyePosition(owner, origin); + eyes = origin; + //set origin in front of player + GetClientEyeAngles(owner, angles); + angles[0]=angles[2]=0.0; + GetAngleVectors(angles, fwd, NULL_VECTOR, NULL_VECTOR); + ScaleVector(fwd, 64.0); + AddVectors(origin, fwd, origin); + //get angles/velocity + GetClientEyeAngles(owner, angles); + GetAngleVectors(angles, fwd, NULL_VECTOR, NULL_VECTOR); + ScaleVector(fwd, 520.0); + fwd[2]+=160.0;//bit more archy + Entity_GetAbsVelocity(owner, velocity); + AddVectors(velocity, fwd, velocity); + + int phys = CreateEntityByName("prop_physics_multiplayer"); + if (phys == INVALID_ENT_REFERENCE) return; + + char buffer[PLATFORM_MAX_PATH]; + switch (GetEntProp(building, Prop_Send, "m_iObjectType")) { + case BUILDING_SENTRYGUN: DispatchKeyValue(phys, "model", "models/buildables/sentry1.mdl"); + case BUILDING_DISPENSER: DispatchKeyValue(phys, "model", "models/buildables/dispenser_light.mdl"); + case BUILDING_TELEPORTER: DispatchKeyValue(phys, "model", "models/buildables/teleporter_light.mdl"); + } + DispatchKeyValue(phys, "physicsmode", "1"); + DispatchKeyValueVector(phys, "origin", origin); + DispatchKeyValueVector(phys, "angles", angles); + Format(buffer, sizeof(buffer), "%i", GetEntProp(building, Prop_Send, "m_nSkin")); + DispatchKeyValue(phys, "skin", buffer); + if (GetEntProp(building, Prop_Send, "m_bDisposableBuilding")) buffer = "0.66"; + else if (GetEntProp(building, Prop_Send, "m_bMiniBuilding")) buffer = "0.75"; + else buffer = "1.0"; + DispatchKeyValue(phys, "modelscale", buffer);//mini sentries are .75 + DispatchKeyValue(phys, "solid", "6"); + if (!DispatchSpawn(phys)) { + PrintToChat(owner, "Failed to spawn sentry prop"); + return; + } + ActivateEntity(phys); + + SetEntProp(building, Prop_Send, "m_bDisabled", 1); + Entity_SetSolidFlags(building, FSOLID_NOT_SOLID); + SetEntityRenderMode(building, RENDER_NONE); + TeleportEntity(building, origin, NULL_VECTOR, NULL_VECTOR); + SetVariantString("!activator"); + AcceptEntityInput(building, "SetParent", phys); + Phys_ApplyForceCenter(phys, velocity);// works best + + any onade[3]; + onade[0]=EntIndexToEntRef(phys); + onade[1]=EntIndexToEntRef(building); + onade[2]=angles[1]; + g_aAirbornObjects.PushArray(onade); +} + +public bool TEF_HitThrownFilter(int entity, int contentsMask, any data) { + int edicts[3]; + g_aAirbornObjects.GetArray(data,edicts); + return entity > MaxClients && entity != EntRefToEntIndex(edicts[0]) && entity != EntRefToEntIndex(edicts[1]); +} + +void ValidateThrown() { + for (int i=g_aAirbornObjects.Length-1; i>=0; i--) { + any data[3]; + g_aAirbornObjects.GetArray(i,data); + int phys = EntRefToEntIndex(data[0]); + int obj = EntRefToEntIndex(data[1]); + float yaw = data[2]; + //if at least one of the entities went away, something went wrong + // -> remove and continue + if (!IsValidEdict(phys)) { + if (IsValidEdict(obj)) AcceptEntityInput(obj, "Kill"); + g_aAirbornObjects.Erase(i); + PrintToServer("Phys entity invalid"); + continue; + } else if (!IsValidEdict(obj)) { + if (IsValidEdict(phys)) AcceptEntityInput(phys, "Kill"); + g_aAirbornObjects.Erase(i); + PrintToServer("Building entity invalid"); + continue; + } + + float pos[3], vec[3]; + Entity_GetAbsOrigin(phys, pos); + vec = pos; + vec[2] -= 16.0; + pos[2] += 48.0; + Handle trace = TR_TraceRayFilterEx(pos, vec, MASK_SOLID, RayType_EndPoint, TEF_HitThrownFilter, i); + if (!TR_DidHit(trace)) { + delete trace; + continue; //no ground below (FL_ONGROUND failed?) + } else { + TR_GetEndPosition(pos, trace); + TR_GetPlaneNormal(trace, vec); //vanilla is not checking this + delete trace; + //check surface slope + float up[3]; up[2]=1.0; + float slope = ArcCosine( GetVectorDotProduct(vec, up) ) * 180.0/3.1415927; + if (slope > 35.0) { + //this slope is too steep to place a building + continue; + } + //construct angles by random direction, using standard right to get propper forward + float angles[3]; + angles[1]=yaw; + //clear parent + AcceptEntityInput(obj, "ClearParent"); + //fix building + float zeros[3]; + TeleportEntity(obj, pos, angles, zeros); //use 0-velocity to calm down bouncyness + //restore other props + SetEntProp(obj, Prop_Send, "m_bDisabled", 0); + Entity_RemoveSolidFlags(obj, FSOLID_NOT_SOLID); + SetEntityRenderMode(obj, RENDER_NORMAL); + //check valid + CreateTimer(0.1, ValidateBuilding, EntIndexToEntRef(obj), TIMER_FLAG_NO_MAPCHANGE); + } + RemoveEntity(phys); + g_aAirbornObjects.Erase(i); + } +} + +bool CheckThrowPos(int client) { + if (g_iBlockFlags != 0) return false; + float eyes[3]; + float origin[3]; + float angles[3]; + float fwd[3]; + GetClientEyePosition(client, origin); + eyes = origin; + //set origin in front of player + GetClientEyeAngles(client, angles); + angles[0]=angles[2]=0.0; + GetAngleVectors(angles, fwd, NULL_VECTOR, NULL_VECTOR); + ScaleVector(fwd, 64.0); + AddVectors(origin, fwd, origin); + //ensure we see the target + Handle trace = TR_TraceRayFilterEx(eyes, origin, MASK_SOLID, RayType_EndPoint, TEF_HitSelfFilter, client); + bool hit = TR_DidHit(trace); + delete trace; + //can't see throw point (prevent through walls)? make noise + if (hit) Beep(client); + return !hit; +} + +bool StartBuilding(int client) { + if (!IsClientInGame(client) || !IsPlayerAlive(client)) + return false; + int weapon = Client_GetActiveWeapon(client); + int item = IsValidEdict(weapon) ? GetEntProp(weapon, Prop_Send, "m_iItemDefinitionIndex") : -1; + if (item != 28) + return false; //require builder + int type = GetEntProp(weapon, Prop_Send, "m_iObjectType"); + if (!(BUILDING_DISPENSER <= type <= BUILDING_SENTRYGUN)) + return false; //supported buildings + //trying to fix a crash related to dereferencing a 0 owner? + SetEntPropEnt(weapon, Prop_Send, "m_hOwner", client); + SetEntProp(weapon, Prop_Send, "m_iBuildState", BS_PLACING); + SDKCall(sdk_fnStartBuilding, weapon); + return true; +} +//crashes, idk why +//bool IsPlacementPosValid(int building) { +// char classname[64]; +// if (!IsValidEdict(building) +// || !GetEntityClassname(building, classname, sizeof(classname)) +// || !(StrEqual(classname, "obj_sentrygun") +// || StrEqual(classname, "obj_teleporter") +// || StrEqual(classname, "obj_dispenser") +// )) +// ThrowError("Entity is not a building"); +// return SDKCall(sdk_fnIsPlacementPosValid, building); +//} + +public Action ValidateBuilding(Handle timer, any building) { + int obj = EntRefToEntIndex(building); + if (obj == INVALID_ENT_REFERENCE) return Plugin_Stop; + + float mins[3],maxs[3],origin[3]; + float four[3]; + four[0]=four[1]=four[2]=4.0; + Entity_GetAbsOrigin(obj,origin); + Entity_GetMinSize(obj,mins); + Entity_GetMaxSize(obj,maxs); + AddVectors(mins,four,mins); + SubtractVectors(maxs,four,maxs); + + Handle trace = TR_TraceHullFilterEx(origin, origin, mins, maxs, MASK_SOLID, TEF_HitSelfFilter, obj); + bool hit = TR_DidHit(trace); + delete trace; + if (hit || TF2Util_IsPointInRespawnRoom(origin, obj)) { + SetVariantInt(1000); + AcceptEntityInput(obj, "RemoveHealth"); + } + return Plugin_Stop; +} + +void Beep(int client) { + if (!(1<=client<=MaxClients) || !IsClientInGame(client) || IsFakeClient(client)) return; + if (GetClientTime(client) - g_flClientLastBeep[client] >= 1.0) { + g_flClientLastBeep[client] = GetClientTime(client); + EmitSoundToClient(client, "common/wpn_denyselect.wav");//should aready be precached by game + } +} \ No newline at end of file