diff --git a/plugins/l4d2_hats.smx b/plugins/l4d2_hats.smx index fc5d48a..2d24ad6 100644 Binary files a/plugins/l4d2_hats.smx and b/plugins/l4d2_hats.smx differ diff --git a/scripting/include/hats/editor.sp b/scripting/include/hats/editor.sp index 1fdc7f0..695cca0 100644 --- a/scripting/include/hats/editor.sp +++ b/scripting/include/hats/editor.sp @@ -7,8 +7,8 @@ int GLOW_GREEN[4] = { 3, 252, 53 }; float ORIGIN_SIZE[3] = { 2.0, 2.0, 2.0 }; char ON_OFF_STRING[2][] = { - "\x05ON\x01", - "\x05OFF\x01" + "\x05OFF\x01", + "\x05ON\x01" } char COLOR_INDEX[4] = "RGBA"; @@ -48,13 +48,33 @@ enum CompleteType { Complete_EditSuccess } +enum StackerDirection { + Stack_Off, + Stack_Left, + Stack_Right, + Stack_Forward, + Stack_Backward, + Stack_Up, + Stack_Down +} + +char STACK_DIRECTION_NAME[7][] = { + "\x05OFF", + "\x04Left", + "\x04Right", + "\x04Forward", + "\x04Backward", + "\x04Up", + "\x04Down", +} ArrayList createdWalls; enum struct EditorData { int client; - char classname[32]; + char classname[64]; char data[32]; + char name[32]; float origin[3]; float angles[3]; @@ -73,6 +93,7 @@ enum struct EditorData { int entity; bool hasCollision; /// possibly merge into .flags bool hasCollisionRotate; //^ + StackerDirection stackerDirection; editMode mode; buildType buildType; @@ -83,15 +104,16 @@ enum struct EditorData { if(this.entity != INVALID_ENT_REFERENCE && this.flags & Edit_Preview && IsValidEntity(this.entity)) { RemoveEntity(this.entity); } + this.stackerDirection = Stack_Off; this.entity = INVALID_ENT_REFERENCE; this.data[0] = '\0'; + this.name[0] = '\0'; this.size[0] = this.size[1] = this.size[2] = 5.0; this.angles[0] = this.angles[1] = this.angles[2] = 0.0; this.colorIndex = 0; this.axis = 1; this.moveDistance = 200.0; this.flags = Edit_None; - this.buildType = Build_Solid; this.classname[0] = '\0'; this.CalculateMins(); this.SetMode(INACTIVE); @@ -102,6 +124,7 @@ enum struct EditorData { this.snapAngle = 30; this.hasCollision = true; this.hasCollisionRotate = false; + this.buildType = Build_Solid; } } @@ -152,6 +175,9 @@ enum struct EditorData { void SetData(const char[] data) { strcopy(this.data, sizeof(this.data), data); } + void SetName(const char[] name) { + strcopy(this.name, sizeof(this.name), name); + } void CycleMode() { // Remove frozen state when cycling @@ -184,6 +210,16 @@ enum struct EditorData { PrintToChat(this.client, "\x04[Editor]\x01 Mode: \x05%s\x01 (Press \x04RELOAD\x01 to change)", MODE_NAME[this.mode]); } + void CycleStacker(float tick) { + if(tick - cmdThrottle[this.client] <= 0.20) return; + int newDirection = view_as(this.stackerDirection) + 1; + if(newDirection == view_as(Stack_Down)) newDirection = 0; + this.stackerDirection = view_as(newDirection); + + PrintToChat(this.client, "\x04[Editor]\x01 Stacker: %s\x01", STACK_DIRECTION_NAME[this.stackerDirection]); + cmdThrottle[this.client] = tick; + } + void ToggleCollision(float tick) { if(tick - cmdThrottle[this.client] <= 0.25) return; this.hasCollision = !this.hasCollision @@ -192,14 +228,14 @@ enum struct EditorData { } void ToggleCollisionRotate(float tick) { - if(tick - cmdThrottle[this.client] <= 0.25) return; + if(tick - cmdThrottle[this.client] <= 0.20) return; this.hasCollisionRotate = !this.hasCollisionRotate PrintToChat(this.client, "\x04[Editor]\x01 Rotate with Collision: %s", ON_OFF_STRING[view_as(this.hasCollisionRotate)]); cmdThrottle[this.client] = tick; } void CycleAxis(float tick) { - if(tick - cmdThrottle[this.client] <= 0.15) return; + if(tick - cmdThrottle[this.client] <= 0.1) return; if(this.axis == 0) { this.axis = 1; PrintToChat(this.client, "\x04[Editor]\x01 Rotate Axis: \x05HEADING (Y)\x01"); @@ -214,7 +250,7 @@ enum struct EditorData { } void CycleSnapAngle(float tick) { - if(tick - cmdThrottle[this.client] <= 0.15) return; + if(tick - cmdThrottle[this.client] <= 0.1) return; switch(this.snapAngle) { case 1: this.snapAngle = 15; case 15: this.snapAngle = 30; @@ -352,15 +388,62 @@ enum struct EditorData { return true; } + bool _FinishPreview(int& entity) { + if(StrContains(this.classname, "weapon") > -1) { + entity = this._CreateWeapon(); + } else { + entity = this._CreateProp(); + } + + DispatchKeyValue(entity, "targetname", "propspawner_prop"); + TeleportEntity(entity, this.origin, this.angles, NULL_VECTOR); + if(!DispatchSpawn(entity)) { + return false; + } + SetEntityRenderColor(entity, this.color[0], this.color[1], this.color[2], this.color[3]); + SetEntityRenderColor(this.entity, 255, 128, 255, 200); // reset ghost color + GlowEntity(entity, 1.1); + + // Confusing when we resume into freelook, so reset + if(this.mode == FREELOOK) + this.SetMode(MOVE_ORIGIN); + + // Add to spawn list and add to recent list + AddSpawnedItem(entity, this.client); + char model[128]; + GetEntPropString(entity, Prop_Data, "m_ModelName", model, sizeof(model)); + AddRecent(model, this.name); + + // Get the new position for preview with regards to this.stackerDirection + if(this.stackerDirection != Stack_Off) { + float size[3]; + GetEntityDimensions(this.entity, size); + float sign = 1.0; + if(this.stackerDirection == Stack_Left || this.stackerDirection == Stack_Right) { + if(this.stackerDirection == Stack_Left) sign = -1.0; + GetSidePositionFromOrigin(this.origin, this.angles, sign * size[1] * 0.90, this.origin); + } else if(this.stackerDirection == Stack_Forward || this.stackerDirection == Stack_Backward) { + if(this.stackerDirection == Stack_Backward) sign = -1.0; + GetHorizontalPositionFromOrigin(this.origin, this.angles, sign * size[0] * 0.90, this.origin); + } else { + if(this.stackerDirection == Stack_Down) sign = -1.0; + this.origin[2] += (size[2] * sign); + } + } + + // Don't kill preview until cancel + return true; + } + int _CreateWeapon() { int entity = -1; - if(StrEqual(this.classname, "weapon_melee")) { - entity = CreateEntityByName("weapon_melee_spawn"); + entity = CreateEntityByName(this.classname); + if(entity == -1) return -1; + if(StrEqual(this.classname, "weapon_melee_spawn")) { DispatchKeyValue(entity, "melee_weapon", this.data); - } else { - entity = CreateEntityByName(this.classname); - DispatchKeyValue(entity, "spawnflags", "8"); } + DispatchKeyValue(entity, "count", "1"); + DispatchKeyValue(entity, "spawnflags", "10"); return entity; } @@ -384,34 +467,6 @@ enum struct EditorData { return entity; } - bool _FinishPreview(int& entity) { - if(StrContains(this.classname, "weapon") > -1) { - entity = this._CreateWeapon(); - } else { - entity = this._CreateProp(); - } - - DispatchKeyValue(entity, "targetname", "propspawner_prop"); - TeleportEntity(entity, this.origin, this.angles, NULL_VECTOR); - if(!DispatchSpawn(entity)) { - return false; - } - SetEntityRenderColor(entity, this.color[0], this.color[1], this.color[2], this.color[3]); - SetEntityRenderColor(this.entity, 255, 128, 255, 200); // reset ghost color - GlowEntity(entity, 1.4); - - if(this.mode == FREELOOK) - this.SetMode(MOVE_ORIGIN); - - AddSpawnedItem(entity, this.client); - char model[128]; - GetEntPropString(entity, Prop_Data, "m_ModelName", model, sizeof(model)); - AddRecent(model, this.data); - - // Don't kill preview until cancel - return true; - } - // Turns current entity into a copy (not for walls) int Copy() { if(this.entity == INVALID_ENT_REFERENCE) return -1; @@ -430,6 +485,9 @@ enum struct EditorData { DispatchKeyValue(entity, "solid", "6"); DispatchSpawn(entity); + if(StrEqual(this.classname, "prop_wall_breakable")) { + DispatchKeyValue(entity, "classname", "prop_door_rotating"); + } TeleportEntity(entity, this.origin, this.angles, NULL_VECTOR); this.entity = entity; this.flags |= Edit_Copy; @@ -442,26 +500,26 @@ enum struct EditorData { this.flags |= Edit_WallCreator; } - bool PreviewWeapon(const char[] classname) { + bool PreviewWeapon(const char[] classname, const char[] data) { int entity; // Melee weapons don't have weapon_ prefix this.Reset(); - // TODO: prevent use of preview _spawn - if(StrContains(classname, "weapon_") == -1) { + // Rotate on it's side: + this.angles[2] = 90.0; + if(StrEqual(classname, "weapon_melee_spawn")) { // no weapon_ prefix, its a melee - PrintToServer("Spawning weapon_melee: %s", classname); - entity = CreateEntityByName("weapon_melee"); - if(entity == -1) return false; - DispatchKeyValue(entity, "melee_weapon", classname); - strcopy(this.classname, sizeof(this.classname), "weapon_melee"); - this.SetData(classname); - } else { - PrintToServer("Spawning normal weapon: %s", classname); entity = CreateEntityByName(classname); if(entity == -1) return false; - DispatchKeyValue(entity, "spawnflags", "8"); - Format(this.classname, sizeof(this.classname), "%s_spawn", classname); + DispatchKeyValue(entity, "melee_weapon", data); + this.SetData(data); + strcopy(this.classname, sizeof(this.classname), classname); + } else { + entity = CreateEntityByName(data); + if(entity == -1) return false; + strcopy(this.classname, sizeof(this.classname), data); } + DispatchKeyValue(entity, "count", "1"); + DispatchKeyValue(entity, "spawnflags", "10"); DispatchKeyValue(entity, "targetname", "prop_preview"); DispatchKeyValue(entity, "rendercolor", "255 128 255"); DispatchKeyValue(entity, "renderamt", "200"); @@ -481,9 +539,11 @@ enum struct EditorData { bool PreviewModel(const char[] model, const char[] classname = "") { // Check for an invalid model // this.origin is not cleared by this.Reset(); + this.Reset(); GetClientAbsOrigin(this.client, this.origin); - if(StrEqual(classname, "_weapon") || StrEqual(classname, "_melee")) { - return this.PreviewWeapon(model); + if(StrEqual(classname, "_weapon") || StrEqual(classname, "weapon_melee_spawn")) { + // Pass in melee ID as data: + return this.PreviewWeapon(classname, model); } if(PrecacheModel(model) == 0) { PrintToServer("Invalid model: %s", model); @@ -549,7 +609,6 @@ enum struct EditorData { CPrintToChat(this.client, "\x04[Editor]\x01 Cancelled."); } } - EditorData Editor[MAXPLAYERS+1]; Action OnWallClicked(int entity, int activator, int caller, UseType type, float value) { @@ -751,7 +810,7 @@ Action Command_Editor(int client, int args) { int index = GetLookingEntity(client, Filter_ValidHats); //GetClientAimTarget(client, false); if(index > 0) { Editor[client].Import(index, false, MOVE_ORIGIN); - char classname[32]; + char classname[64]; char targetname[32]; GetEntityClassname(index, classname, sizeof(classname)); GetEntPropString(index, Prop_Data, "m_target", targetname, sizeof(targetname)); diff --git a/scripting/include/hats/hats.sp b/scripting/include/hats/hats.sp index 796aa85..a550a9d 100644 --- a/scripting/include/hats/hats.sp +++ b/scripting/include/hats/hats.sp @@ -27,15 +27,6 @@ enum struct HatInstance { bool rainbowReverse; char attachPoint[32]; } -enum struct PlayerHatData { - char activePresetName[32]; - int lastRequestTime; - - void Reset() { - this.activePresetName[0] = '\0'; - this.lastRequestTime = 0; - } -} enum hatFeatures { HatConfig_None = 0, HatConfig_PlayerHats = 1, @@ -48,8 +39,7 @@ enum hatFeatures { HatConfig_DeleteThrownHats = 128 } char ActivePreset[MAXPLAYERS+1][32]; -int lastHatrequestTime[MAXPLAYERS+1]; -PlayerHatData g_hatData[MAXPLAYERS+1]; +int lastHatRequestTime[MAXPLAYERS+1]; HatInstance hatData[MAXPLAYERS+1]; StringMap g_HatPresets; @@ -84,25 +74,13 @@ static char REVERSE_CLASSNAMES[MAX_REVERSE_CLASSNAMES][] = { "func_movelinear" }; -Action Command_Hat(int client, int args) { - static char cmdName[8]; - GetCmdArg(0, cmdName, sizeof(cmdName)); - AdminId adminId = GetUserAdmin(client); - bool isForced = adminId != INVALID_ADMIN_ID && StrEqual(cmdName, "sm_hatf"); - +Action Command_DoAHat(int client, int args) { int hatter = GetHatter(client); if(hatter > 0) { ClearHat(hatter, HasFlag(hatter, HAT_REVERSED)); PrintToChat(hatter, "[Hats] %N has unhatted themselves", client); return Plugin_Handled; } - // if(g_Hat[client].Ha) -} - -Action Command_DoAHat(int client, int args) { - if(UnhatSelf(client)) { - return Plugin_Handled; - } static char cmdName[8]; GetCmdArg(0, cmdName, sizeof(cmdName)); @@ -391,7 +369,7 @@ Action Command_DoAHat(int client, int args) { PrintToConsole(client, "[Hats] Selected a child entity, selecting parent (child %d -> parent %d)", entity, parent); entity = parent; } else if(entity <= MaxClients) { // Checks for hatting a player entity - if(IsFakeClient(entity) && L4D_GetIdlePlayerOfBot(entity) != -1) { + if(IsFakeClient(entity) && L4D_GetIdlePlayerOfBot(entity) > 0) { PrintToChat(client, "[Hats] Cannot hat idle bots"); return Plugin_Handled; } else if(GetClientTeam(entity) != 2 && ~cvar_sm_hats_flags.IntValue & view_as(HatConfig_InfectedHats)) { @@ -407,7 +385,7 @@ Action Command_DoAHat(int client, int args) { } else if(!IsPlayerAlive(entity) || GetEntProp(entity, Prop_Send, "m_isHangingFromLedge") || L4D_IsPlayerCapped(entity)) { PrintToChat(client, "[Hats] Player is either dead, hanging, or in the process of dying."); return Plugin_Handled; - } else if(EntRefToEntIndex(hatData[entity].entity) == client) { + } else if(EntRefToEntIndex(hatData[entity].entity) == entity || EntRefToEntIndex(hatData[entity].entity) == client) { PrintToChat(client, "[Hats] Woah you can't be making a black hole, jesus be careful."); return Plugin_Handled; } else if(~cvar_sm_hats_flags.IntValue & view_as(HatConfig_PlayerHats)) { @@ -539,7 +517,10 @@ int HatConsentHandler(Menu menu, MenuAction action, int target, int param2) { ReplyToCommand(target, "Player has disconnected"); return 0; } else if(hatAction == 1) { - EquipHat(activator, target, "player", 0); + if(EntRefToEntIndex(hatData[target].entity) == activator ) + PrintToChat(activator, "[Hats] Woah you can't be making a black hole, jesus be careful."); + else + EquipHat(activator, target, "player", 0); } else { ClientCommand(activator, "play player/orch_hit_csharp_short.wav"); PrintHintText(activator, "%N refused your request", target); diff --git a/scripting/include/hats/props/base.sp b/scripting/include/hats/props/base.sp index 01a23de..df769dd 100644 --- a/scripting/include/hats/props/base.sp +++ b/scripting/include/hats/props/base.sp @@ -15,40 +15,84 @@ enum ChatPrompt { Prompt_Save } enum struct PlayerPropData { - ArrayList listBuffer; + ArrayList categoryStack; + ArrayList itemBuffer; bool clearListBuffer; int lastCategoryIndex; int lastItemIndex; // When did the user last interact with prop spawner? (Shows hints after long inactivity) int lastActiveTime; - char classnameOverride[32]; + char classnameOverride[64]; ChatPrompt chatPrompt; ArrayList markedProps; // Called on PlayerDisconnect void Reset() { - if(this.listBuffer != null) delete this.listBuffer; if(this.markedProps != null) delete this.markedProps; this.chatPrompt = Prompt_None; this.clearListBuffer = false; this.lastCategoryIndex = 0; this.lastItemIndex = 0; - this.lastShowedHint = 0; + this.lastActiveTime = 0; this.classnameOverride[0] = '\0'; + this.CleanupBuffers(); } // Sets the list buffer - void SetList(ArrayList list, bool cleanupAfterUse = false) { - this.listBuffer = list; + void SetItemBuffer(ArrayList list, bool cleanupAfterUse = false) { + // Cleanup previous buffer if exist + this.itemBuffer = list; this.clearListBuffer = cleanupAfterUse; } + void ClearItemBuffer() { + if(this.itemBuffer != null && this.clearListBuffer) { + PrintToServer("ClearItemBuffer(): arraylist deleted."); + delete this.itemBuffer; + } + this.clearListBuffer = false; + } + + void PushCategory(CategoryData category) { + if(this.categoryStack == null) this.categoryStack = new ArrayList(sizeof(CategoryData)); + this.categoryStack.PushArray(category); + } + + bool PopCategory(CategoryData data) { + if(this.categoryStack == null || this.categoryStack.Length == 0) return false; + int index = this.categoryStack.Length - 1; + this.categoryStack.GetArray(index, data); + this.categoryStack.Erase(index); + return true; + } + bool PeekCategory(CategoryData data) { + if(this.categoryStack == null || this.categoryStack.Length == 0) return false; + int index = this.categoryStack.Length - 1; + this.categoryStack.GetArray(index, data); + return true; + } + + void GetCategoryTitle(char[] title, int maxlen) { + CategoryData cat; + for(int i = 0; i < this.categoryStack.Length; i++) { + this.categoryStack.GetArray(i, cat); + if(i == 0) + Format(title, maxlen, "%s", cat.name); + else + Format(title, maxlen, "%s>[%s]", title, cat.name); + } + } + + bool HasCategories() { + return this.categoryStack != null && this.categoryStack.Length > 0; + } // Is currently only called on item/category handler cancel (to clear search/recents buffer) - void CleanupBuffer() { - if(this.listBuffer != null && this.clearListBuffer) { - delete this.listBuffer; - this.clearListBuffer = false; + void CleanupBuffers() { + this.ClearItemBuffer(); + if(this.categoryStack != null) { + delete this.categoryStack; } + this.clearListBuffer = false; } } PlayerPropData g_PropData[MAXPLAYERS+1]; @@ -57,7 +101,7 @@ enum struct CategoryData { // The display name of category char name[64]; // If set, overwrites the classname it is spawned as - char classnameOverride[32]; + char classnameOverride[64]; bool hasItems; // true: items is ArrayList, false: items is ArrayList ArrayList items; } @@ -137,12 +181,12 @@ enum struct RecentEntry { char name[64]; int count; } - -ArrayList g_categories; // ArrayList +CategoryData ROOT_CATEGORY; ArrayList g_spawnedItems; // ArrayList(block=2) ArrayList g_savedItems; // ArrayList StringMap g_recentItems; // Key: model[128], value: RecentEntry +#include #include #include #include diff --git a/scripting/include/hats/props/cmd.sp b/scripting/include/hats/props/cmd.sp index d75bf58..76ae1d5 100644 --- a/scripting/include/hats/props/cmd.sp +++ b/scripting/include/hats/props/cmd.sp @@ -9,7 +9,9 @@ Action Command_Props(int client, int args) { PrintToConsole(client, "edit "); PrintToConsole(client, "del "); PrintToConsole(client, "add "); + PrintToConsole(client, "favorite - favorites active editor entity"); PrintToConsole(client, "controls - list all the controls"); + PrintToConsole(client, "reload - reload prop list"); } else if(StrEqual(arg, "list")) { char arg2[16]; GetCmdArg(2, arg2, sizeof(arg2)); @@ -20,7 +22,6 @@ Action Command_Props(int client, int args) { PrintToChat(client, "\x04[Editor]\x01 Please specify: \x05classname, index, owner. "); return Plugin_Handled; } - int userid = GetClientUserId(client); float pos[3], propPos[3], dist; GetAbsOrigin(client, pos); for(int i = 0; i < g_spawnedItems.Length; i++) { @@ -94,8 +95,22 @@ Action Command_Props(int client, int args) { } else { PrintToChat(client, "\x04[Editor]\x01 Invalid index, out of bounds. Enter a value between [0, %d]", g_spawnedItems.Length - 1); } - } else if(StrEqual("controls")) { + } else if(StrEqual(arg, "controls")) { PrintToChat(client, "View controls at https://admin.jackz.me/docs/props"); + } else if(StrEqual(arg, "favorite")) { + if(g_db == null) { + PrintToChat(client, "\x04[Editor]\x01 Cannot connect to database."); + } else if(Editor[client].IsActive()) { + char model[128]; + GetEntPropString(Editor[client].entity, Prop_Data, "m_ModelName", model, sizeof(model)); + ToggleFavorite(client, model, Editor[client].data); + } else { + PrintToChat(client, "\x04[Editor]\x01 Edit a prop to use this command."); + } + } else if(StrEqual(arg, "reload")) { + PrintHintText(client, "Reloading categories..."); + UnloadCategories(); + LoadCategories(); } else { PrintToChat(client, "\x05Not implemented"); } diff --git a/scripting/include/hats/props/db.sp b/scripting/include/hats/props/db.sp new file mode 100644 index 0000000..37af63e --- /dev/null +++ b/scripting/include/hats/props/db.sp @@ -0,0 +1,125 @@ +#define DATABASE_CONFIG_NAME "hats_editor" +Database g_db; + +bool ConnectDB() { + char error[255]; + Database db = SQL_Connect(DATABASE_CONFIG_NAME, true, error, sizeof(error)); + if (db == null) { + LogError("Database error %s", error); + return false; + } else { + PrintToServer("l4d2_hats: Connected to database %s", DATABASE_CONFIG_NAME); + db.SetCharset("utf8mb4"); + g_db = db; + return true; + } +} + +void DB_GetFavoritesCallback(Database db, DBResultSet results, const char[] error, int userid) { + if(results == null) { + PrintToServer("l4d2_hats: DB_GetFavoritesCallback returned error: \"%s\"", error); + } + int client = GetClientOfUserId(userid); + if(client > 0) { + if(results == null) { + PrintToChat(client, "\x04[Editor]\x01 Error occurred fetching favorites"); + return; + } + ArrayList list = new ArrayList(sizeof(ItemData)); + ItemData item; + while(results.FetchRow()) { + results.FetchString(0, item.model, sizeof(item.model)); + DBResult result; + results.FetchString(1, item.name, sizeof(item.name), result); + if(result == DBVal_Null) { + // No name set - use the end part of the model + int index = FindCharInString(item.model, '/', true); + strcopy(item.name, sizeof(item.name), item.model[index + 1]); + } + } + ShowTempItemMenu(client, list, "Favorites"); + } +} + +void DB_ToggleFavoriteCallback(Database db, DBResultSet results, const char[] error, DataPack pack) { + if(results == null) { + PrintToServer("l4d2_hats: DB_GetFavoriteCallback returned error: \"%s\"", error); + } + pack.Reset(); + int userid = pack.ReadCell(); + int client = GetClientOfUserId(userid); + if(client > 0) { + if(results == null) { + PrintToChat(client, "\x04[Editor]\x01 Error occurred fetching favorite data"); + delete pack; + return; + } + char query[256]; + char model[128]; + char steamid[32]; + GetClientAuthId(client, AuthId_Steam2, steamid, sizeof(steamid)); + pack.ReadString(model, sizeof(model)); + if(results.FetchRow()) { + // Model was favorited, erase it + g_db.Format(query, sizeof(query), "DELETE FROM editor_favorites WHERE steamid = '%s' AND model = '%s'", steamid, model); + g_db.Query(DB_DeleteFavoriteCallback, query, userid); + } else { + // Model is not favorited, save it. + char name[64]; + pack.ReadString(name, sizeof(name)); + // TODO: calculate next position automatically + int position = 0; + g_db.Format(query, sizeof(query), + "INSERT INTO editor_favorites (steamid, model, name, position) VALUES ('%s', '%s', '%s', %d)", + steamid, model, name, position + ); + g_db.Query(DB_InsertFavoriteCallback, query, pack); + } + } else { + // Only delete if we lost client - otherwise we will reuse it + delete pack; + } +} + +void DB_DeleteFavoriteCallback(Database db, DBResultSet results, const char[] error, DataPack pack) { + if(results == null) { + PrintToServer("l4d2_hats: DB_DeleteFavoriteCallback returned error: \"%s\"", error); + } + pack.Reset(); + char model[128]; + char name[64]; + int client = GetClientOfUserId(pack.ReadCell()); + if(client > 0) { + if(results == null) { + PrintToChat(client, "\x04[Editor]\x01 Could not delete favorite"); + delete pack; + return; + } + pack.ReadString(model, sizeof(model)); + pack.ReadString(name, sizeof(name)); + int index = FindCharInString(model, '/', true); + PrintToChat(client, "\x04[Editor]\x01 Removed favorite: \"%s\" \x05(%s)", model[index], name); + } + delete pack; +} +void DB_InsertFavoriteCallback(Database db, DBResultSet results, const char[] error, DataPack pack) { + if(results == null) { + PrintToServer("l4d2_hats: DB_InsertFavoriteCallback returned error: \"%s\"", error); + } + pack.Reset(); + char model[128]; + char name[64]; + int client = GetClientOfUserId(pack.ReadCell()); + if(client > 0) { + if(results == null) { + PrintToChat(client, "\x04[Editor]\x01 Could not add favorite"); + delete pack; + return; + } + pack.ReadString(model, sizeof(model)); + pack.ReadString(name, sizeof(name)); + int index = FindCharInString(model, '/', true); + PrintToChat(client, "\x04[Editor]\x01 Added favorite: \"%s\" \x05(%s)", model[index], name); + } + delete pack; +} \ No newline at end of file diff --git a/scripting/include/hats/props/menu_handlers.sp b/scripting/include/hats/props/menu_handlers.sp index 97cda91..032ab31 100644 --- a/scripting/include/hats/props/menu_handlers.sp +++ b/scripting/include/hats/props/menu_handlers.sp @@ -45,7 +45,7 @@ int Spawn_RootHandler(Menu menu, MenuAction action, int client, int param2) { case 'f': Spawn_ShowFavorites(client); case 'r': Spawn_ShowRecents(client); case 's': Spawn_ShowSearch(client); - case 'n': ShowCategoryList(client); + case 'n': ShowCategoryList(client, ROOT_CATEGORY); } // TODO: handle back (to top menu) } else if (action == MenuAction_Cancel) { @@ -94,22 +94,6 @@ void AdminMenu_SaveLoad(TopMenu topmenu, TopMenuAction action, TopMenuObject obj } } -int SpawnCategoryHandler(Menu menu, MenuAction action, int client, int param2) { - if (action == MenuAction_Select) { - char info[8]; - menu.GetItem(param2, info, sizeof(info)); - int index = StringToInt(info); - // Use g_categories, but if this is nested, then when a nested is selected, we need to use that list - ShowItemMenu(client, index); - } else if (action == MenuAction_Cancel) { - if(param2 == MenuCancel_ExitBack) { - ShowSpawnRoot(client); - } - } else if (action == MenuAction_End) - delete menu; - return 0; -} - int SaveLoadHandler(Menu menu, MenuAction action, int client, int param2) { if (action == MenuAction_Select) { char saveName[64]; @@ -124,9 +108,13 @@ int SaveLoadHandler(Menu menu, MenuAction action, int client, int param2) { } } else if(LoadSave(saveName, true)) { strcopy(g_pendingSaveName, sizeof(g_pendingSaveName), saveName); - g_pendingSaveClient = client; - PrintToChat(client, "\x04[Editor]\x01 Previewing save \x05%s", saveName); - PrintToChat(client, "\x04[Editor]\x01 Press \x05Shift + Middle Mouse\x01 to spawn, \x05Middle Mouse\x01 to cancel"); + if(g_pendingSaveClient != 0 && g_pendingSaveClient != client) { + PrintToChat(client, "\x04[Editor]\x01 Another user is currently loading a save."); + } else { + g_pendingSaveClient = client; + PrintToChat(client, "\x04[Editor]\x01 Previewing save \x05%s", saveName); + PrintToChat(client, "\x04[Editor]\x01 Press \x05Shift + Middle Mouse\x01 to spawn, \x05Middle Mouse\x01 to cancel"); + } } else { PrintToChat(client, "\x04[Editor]\x01 Could not load save file."); } @@ -158,7 +146,7 @@ int DeleteHandler(Menu menu, MenuAction action, int client, int param2) { EndDeleteTool(client, false); } else { g_PropData[client].markedProps = new ArrayList(); - PrintToChat(client, "\x04[Editor]\x01 Delete tool active. Press \x05E (Interact)\x01 to mark props."); + PrintToChat(client, "\x04[Editor]\x01 Delete tool active. Press \x05Left Mouse\x01 to mark props, \x05Right Mouse\x01 to undo. SHIFT+USE to spawn, CTRL+USE to cancel"); } ShowDeleteList(client); } else { @@ -182,6 +170,47 @@ int DeleteHandler(Menu menu, MenuAction action, int client, int param2) { return 0; } +int SpawnCategoryHandler(Menu menu, MenuAction action, int client, int param2) { + if (action == MenuAction_Select) { + char info[8]; + menu.GetItem(param2, info, sizeof(info)); + int index = StringToInt(info); + // Reset item index when selecting new category + if(g_PropData[client].lastCategoryIndex != index) { + g_PropData[client].lastCategoryIndex = index; + g_PropData[client].lastItemIndex = 0; + } + CategoryData category; + g_PropData[client].PeekCategory(category); // Just need to get the category.items[index], don't want to pop + category.items.GetArray(index, category); + if(category.items == null) { + LogError("Category %s has null items array (index=%d)", category.name, index); + } else if(category.hasItems) { + ShowCategoryItemMenu(client, category); + } else { + // Reset the category index for nested + g_PropData[client].lastCategoryIndex = 0; + // Make the list now be the selected category's list. + ShowCategoryList(client, category); + } + } else if (action == MenuAction_Cancel) { + if(param2 == MenuCancel_ExitBack) { + CategoryData category; + // Double pop + if(g_PropData[client].PopCategory(category) && g_PropData[client].PopCategory(category)) { + // Use the last category (go back one) + ShowCategoryList(client, category); + } else { + ShowSpawnRoot(client); + } + } else { + g_PropData[client].CleanupBuffers(); + } + } else if (action == MenuAction_End) + delete menu; + return 0; +} + int SpawnItemHandler(Menu menu, MenuAction action, int client, int param2) { if (action == MenuAction_Select) { char info[132]; @@ -192,21 +221,28 @@ int SpawnItemHandler(Menu menu, MenuAction action, int client, int param2) { nameIndex += SplitString(info[nameIndex], "|", model, sizeof(model)); g_PropData[client].lastItemIndex = StringToInt(index); if(Editor[client].PreviewModel(model, g_PropData[client].classnameOverride)) { - Editor[client].SetData(info[nameIndex]); + Editor[client].SetName(info[nameIndex]); PrintHintText(client, "%s\n%s", info[nameIndex], model); ShowHint(client); } else { PrintToChat(client, "\x04[Editor]\x01 Error spawning preview \x01(%s)", model); } - - ShowItemMenuAny(client, null); // Use last menu - // ShowItemMenu(client, g_PropData[client].lastCategoryIndex); + // Use same item menu again: + ShowItemMenu(client); } else if(action == MenuAction_Cancel) { + g_PropData[client].ClearItemBuffer(); if(param2 == MenuCancel_ExitBack) { - ShowCategoryList(client, g_PropData[client].listBuffer); + CategoryData category; + if(g_PropData[client].PopCategory(category)) { + // Use the last category (go back one) + ShowCategoryList(client, category); + } else { + // If there is no categories, it means we are in a temp menu (search / recents / favorites) + ShowSpawnRoot(client); + } + } else { + g_PropData[client].CleanupBuffers(); } - g_PropData[client].CleanupBuffer(); - } else if (action == MenuAction_End) { delete menu; } diff --git a/scripting/include/hats/props/menu_methods.sp b/scripting/include/hats/props/menu_methods.sp index c1e41ad..cdef072 100644 --- a/scripting/include/hats/props/menu_methods.sp +++ b/scripting/include/hats/props/menu_methods.sp @@ -12,33 +12,14 @@ void ShowSpawnRoot(int client) { menu.ExitButton = true; menu.Display(client, MENU_TIME_FOREVER); } - -void Spawn_ShowFavorites(int client) { - PrintToChat(client, "In development"); - return; - // Menu menu = new Menu(SpawnItemHandler); - // char model[128]; - // for(int i = 0; i <= g_spawnedItems.Length; i++) { - // int ref = g_spawnedItems.Get(i); - // if(IsValidEntity(ref)) { - // GetEntPropString(ref, Prop_Data, "m_ModelName", model, sizeof(model)); - // menu.AddItem(model, model); - // } - // } - // menu.ExitBackButton = true; - // menu.ExitButton = true; - // menu.Display(client, MENU_TIME_FOREVER); -} void Spawn_ShowRecents(int client) { - CReplyToCommand(client, "\x04[Editor] \x01Disabled due to crash issues :D"); - return; if(g_recentItems == null) LoadRecents(); ArrayList items = GetRecentsItemList(); if(items.Length == 0) { CReplyToCommand(client, "\x04[Editor] \x01No recent props spawned."); return; } - ShowItemMenuAny(client, items, "Recents", true); + ShowTempItemMenu(client, items, "Recents"); } void Spawn_ShowSearch(int client) { g_PropData[client].chatPrompt = Prompt_Search; @@ -91,23 +72,25 @@ void ShowEditList(int client, int index = 0) { // Add +2 to the index for the two "Delete ..." buttons menu.DisplayAt(client, index, MENU_TIME_FOREVER); } -void ShowCategoryList(int client, ArrayList categoryList = null) { +void ShowCategoryList(int client, CategoryData category) { LoadCategories(); - Menu menu = new Menu(SpawnCategoryHandler); - menu.SetTitle("Choose a category"); - CategoryData cat; char info[4]; // No category list provided, use the global one. - PrintToServer("ShowCategoryList (root = %b)", categoryList == null); - if(categoryList == null) { - categoryList = g_categories; - } - g_PropData[client].SetList(categoryList, false); - for(int i = 0; i < categoryList.Length; i++) { - categoryList.GetArray(i, cat); + g_PropData[client].PushCategory(category); + Menu menu = new Menu(SpawnCategoryHandler); + char title[32]; + g_PropData[client].GetCategoryTitle(title, sizeof(title)); + menu.SetTitle(title); + CategoryData cat; + for(int i = 0; i < category.items.Length; i++) { + category.items.GetArray(i, cat); Format(info, sizeof(info), "%d", i); - // TODO: maybe add > folder indicator - menu.AddItem(info, cat.name); + if(cat.hasItems) + menu.AddItem(info, cat.name); + else { + Format(title, sizeof(title), "[%s]", cat.name); + menu.AddItem(info, title); + } } menu.ExitBackButton = true; menu.ExitButton = true; @@ -115,14 +98,17 @@ void ShowCategoryList(int client, ArrayList categoryList = null) { int index = g_PropData[client].lastCategoryIndex / 7 * 7; menu.DisplayAt(client, index, MENU_TIME_FOREVER); } -void ShowItemMenuAny(int client, ArrayList items, const char[] title = "", bool clearArray = false, const char[] classnameOverride = "") { +void _showItemMenu(int client, ArrayList items, const char[] title = "", bool clearArray = false, const char[] classnameOverride = "") { if(items == null) { - items = g_PropData[client].listBuffer; + // Use previous list buffer + items = g_PropData[client].itemBuffer; if(items == null) { - LogError("Items is null and listBuffer is null as well"); + LogError("Previous list does not exist and no new list was provided ShowItemMenu(%N)", client); } } else { - g_PropData[client].SetList(items, clearArray); + // Populate the buffer with this list + g_PropData[client].SetItemBuffer(items, clearArray); + // Reset the index, so we start on the first item g_PropData[client].lastItemIndex = 0; strcopy(g_PropData[client].classnameOverride, 32, classnameOverride); } @@ -134,10 +120,10 @@ void ShowItemMenuAny(int client, ArrayList items, const char[] title = "", bool if(title[0] != '\0') itemMenu.SetTitle(title); ItemData item; - char info[128+64+8]; + char info[8+128+64]; //i[8] + item.model[128] + item.name[64] for(int i = 0; i < items.Length; i++) { items.GetArray(i, item); - // Sadly need to duplicate item.name. + // Sadly need to duplicate item.name, for recents to work Format(info, sizeof(info), "%d|%s|%s", i, item.model, item.name); itemMenu.AddItem(info, item.name); } @@ -147,28 +133,52 @@ void ShowItemMenuAny(int client, ArrayList items, const char[] title = "", bool int index = (g_PropData[client].lastItemIndex / 7) * 7; itemMenu.DisplayAt(client, index, MENU_TIME_FOREVER); } +/** + * Show a list of a category's items to spawn to the client + * + * @param client client to show menu to + * @param category the category to show items of + */ +void ShowCategoryItemMenu(int client, CategoryData category) { + char title[32]; + g_PropData[client].GetCategoryTitle(title, sizeof(title)); + Format(title, sizeof(title), "%s>%s", title, category.name); + _showItemMenu(client, category.items, title, false, category.classnameOverride); +} +/** + * Show a list of items to spawn to the client + * + * @param client client to show menu to + * @param items A list of ItemData. Optional, null to reuse last list + * @param title An optional title to show + * @param clearArray Should the items array be destroyed when menu is closed? + * @param classnameOverride Override the classname to spawn as + */ +void ShowItemMenu(int client, ArrayList items = null, const char[] title = "", const char[] classnameOverride = "") { + _showItemMenu(client, items, title, false, classnameOverride); +} +/** + * Show a list of items, deleting the arraylist on completion + * @param client client to show menu to + * @param items A list of ItemData + * @param title An optional title to show + * @param classnameOverride Override the classname to spawn as + */ +void ShowTempItemMenu(int client, ArrayList items, const char[] title = "", const char[] classnameOverride = "") { + if(items == null) { + LogError("ShowTempItemMenu: Given null item list"); + } + _showItemMenu(client, items, title, true, classnameOverride); +} -// Calls ShowItemMenuAny with the correct category automatically -bool ShowItemMenu(int client, int index) { - if(g_PropData[client].lastCategoryIndex != index) { - g_PropData[client].lastCategoryIndex = index; - g_PropData[client].lastItemIndex = 0; //Reset +void Spawn_ShowFavorites(int client) { + if(g_db == null) { + PrintToChat(client, "\x04[Editor]\x01 Cannot connect to database."); + return; } - CategoryData category; - // Use the list in the buffer - g_PropData[client].listBuffer.GetArray(index, category); - if(category.items == null) { - LogError("Category %s has null items array (index=%d)", category.name, index); - } else if(category.hasItems) { - PrintToServer("Selected category has item entries, showing item menu"); - ShowItemMenuAny(client, category.items, category.name, false, category.classnameOverride); - } else { - PrintToServer("Selected category has nested categories, showing"); - // Has nested categories - // Reset the category index for nested - g_PropData[client].lastCategoryIndex = 0; - g_PropData[client].SetList(category.items); - ShowCategoryList(client, g_PropData[client].listBuffer); - } - return true; + PrintCenterText(client, "Loading favorites...\nPlease wait"); + char query[256]; + GetClientAuthId(client, AuthId_Steam2, query, sizeof(query)); + g_db.Format(query, sizeof(query), "SELECT model, name FROM editor_favorites WHERE steamid = '%s' ORDER BY position DESC", query); + g_db.Query(DB_GetFavoritesCallback, query, GetClientUserId(client)); } \ No newline at end of file diff --git a/scripting/include/hats/props/methods.sp b/scripting/include/hats/props/methods.sp index b4e0ebf..333c161 100644 --- a/scripting/include/hats/props/methods.sp +++ b/scripting/include/hats/props/methods.sp @@ -37,8 +37,11 @@ bool LoadSave(const char[] save, bool asPreview = false) { if(data.type == Build_Physics) entity = CreateEntityByName("prop_physics"); else - entity = CreateEntityByName("prop_dynamic"); - if(entity == -1) continue; + entity = CreateEntityByName("prop_dynamic_override"); + if(entity == -1) { + PrintToServer("[Editor] LoadSave(\"%s\", %b): failed to create %s", save, asPreview, buffer); + continue; + } PrecacheModel(data.model); DispatchKeyValue(entity, "model", data.model); DispatchKeyValue(entity, "targetname", "saved_prop"); @@ -49,19 +52,19 @@ bool LoadSave(const char[] save, bool asPreview = false) { DispatchKeyValue(entity, "solid", data.type == Build_NonSolid ? "0" : "6"); } TeleportEntity(entity, data.origin, data.angles, NULL_VECTOR); - if(!DispatchSpawn(entity)) continue; + if(!DispatchSpawn(entity)) { + PrintToServer("[Editor] LoadSave(\"%s\", %b): failed to spawn %s", save, asPreview, buffer); + continue; + } int alpha = asPreview ? 200 : data.color[3]; SetEntityRenderColor(entity, data.color[0], data.color[1], data.color[2], alpha); if(asPreview) - g_savedItems.Push(EntIndexToEntRef(entity)); + g_previewItems.Push(EntIndexToEntRef(entity)); else AddSpawnedItem(entity); } delete file; - if(asPreview) { - delete g_previewItems; - } return true; } @@ -118,12 +121,12 @@ void UnloadSave() { } public void LoadCategories() { - if(g_categories != null) return; - g_categories = new ArrayList(sizeof(CategoryData)); + if(ROOT_CATEGORY.items != null) return; + ROOT_CATEGORY.items = new ArrayList(sizeof(CategoryData)); char path[PLATFORM_MAX_PATH]; BuildPath(Path_SM, path, sizeof(path), "data/prop_spawner/models"); - LoadFolder(g_categories, path); - g_categories.SortCustom(SortCategories); + LoadFolder(ROOT_CATEGORY.items, path); + ROOT_CATEGORY.items.SortCustom(SortCategories); } int SortCategories(int index1, int index2, ArrayList array, Handle hndl) { CategoryData cat1; @@ -133,9 +136,9 @@ int SortCategories(int index1, int index2, ArrayList array, Handle hndl) { return strcmp(cat1.name, cat2.name); } public void UnloadCategories() { - if(g_categories == null) return; - _UnloadCategories(g_categories); - delete g_categories; + if(ROOT_CATEGORY.items == null) return; + _UnloadCategories(ROOT_CATEGORY.items); + delete ROOT_CATEGORY.items; } void _UnloadCategories(ArrayList list) { CategoryData cat; @@ -164,7 +167,7 @@ void LoadFolder(ArrayList parent, const char[] rootPath) { // TODO: support subcategory if(buffer[0] == '.') continue; CategoryData data; - Format(data.name, sizeof(data.name), "%s>>", buffer); + Format(data.name, sizeof(data.name), "%s", buffer); data.items = new ArrayList(sizeof(CategoryData)); Format(buffer, sizeof(buffer), "%s/%s", rootPath, buffer); @@ -194,7 +197,7 @@ void LoadProps(ArrayList parent, const char[] filePath) { } ReplaceString(buffer, sizeof(buffer), "\n", ""); ReplaceString(buffer, sizeof(buffer), "\r", ""); - Format(category.name, sizeof(category.name), "%s>", buffer); + Format(category.name, sizeof(category.name), "%s", buffer); while(file.ReadLine(buffer, sizeof(buffer))) { if(buffer[0] == '#') continue; ReplaceString(buffer, sizeof(buffer), "\n", ""); @@ -246,7 +249,6 @@ bool SaveRecents() { return true; } bool LoadRecents() { - return false; if(g_recentItems != null) delete g_recentItems; char path[PLATFORM_MAX_PATH]; BuildPath(Path_SM, path, sizeof(path), "data/prop_spawner/recents_cache.csv"); @@ -335,7 +337,9 @@ void DoSearch(int client, const char[] query) { if(results.Length == 0) { CPrintToChat(client, "\x04[Editor]\x01 No results found. :("); } else { - ShowItemMenuAny(client, results, "", true); + char title[64]; + Format(title, sizeof(title), "Results for \"%s\"", query); + ShowTempItemMenu(client, results, title); } } // Gets the index of the spawned item, starting at index. negative to go from back @@ -363,7 +367,7 @@ ArrayList SearchItems(const char[] query) { // We have to put it into SearchData enum struct, then convert it back to ItemResult LoadCategories(); ArrayList results = new ArrayList(sizeof(SearchData)); - _searchCategory(results, g_categories, query); + _searchCategory(results, ROOT_CATEGORY.items, query); results.SortCustom(SortSearch); ArrayList items = new ArrayList(sizeof(ItemData)); ItemData item; @@ -404,6 +408,7 @@ void _searchCategory(ArrayList results, ArrayList categories, const char[] query bool _searchItems(ArrayList results, ArrayList items, const char[] query) { ItemData item; SearchData search; + if(items == null) return false; for(int i = 0; i < items.Length; i++) { items.GetArray(i, item); int searchIndex = StrContains(item.name, query, false); @@ -470,6 +475,7 @@ int DeleteAll(int onlyPlayer = 0) { if(IsValidEntity(ref)) { RemoveEntity(ref); } + // TODO: erasing while removing g_spawnedItems.Erase(i); count++; } @@ -485,5 +491,17 @@ void ShowHint(int client) { PrintToChat(client, "\x05R: \x01Change Mode"); PrintToChat(client, "\x05Middle Click: \x01Cancel Placement \x05Shift + Middle Click: \x01Place \x05Ctrl + Middle Click: \x01Change Type"); PrintToChat(client, "\x05E: \x01Rotate (hold, use mouse) \x05Left Click: \x01Change Axis \x05Right Click: \x01Snap Angle"); + PrintToChat(client, "Type \x05/prop favorite\x01 to (un)favorite."); PrintToChat(client, "More information & cheatsheat: \x05%s", "https://admin.jackz.me/docs/props"); } + +void ToggleFavorite(int client, const char[] model, const char[] name = "") { + char query[256]; + GetClientAuthId(client, AuthId_Steam2, query, sizeof(query)); + DataPack pack; + pack.WriteCell(GetClientUserId(client)); + pack.WriteString(model); + pack.WriteString(name); + g_db.Format(query, sizeof(query), "SELECT name FROM editor_favorites WHERE steamid = '%s' AND model = '%s'", query, model); + g_db.Query(DB_ToggleFavoriteCallback, query, pack); +} \ No newline at end of file diff --git a/scripting/l4d2_hats.sp b/scripting/l4d2_hats.sp index d859487..3330cfd 100644 --- a/scripting/l4d2_hats.sp +++ b/scripting/l4d2_hats.sp @@ -48,7 +48,7 @@ char g_currentMap[64]; #include public Plugin myinfo = { - name = "L4D2 Hats", + name = "L4D2 Hats & Editor", author = "jackzmc", description = "", version = PLUGIN_VERSION, @@ -66,6 +66,7 @@ public void OnPluginStart() { createdWalls = new ArrayList(); g_spawnedItems = new ArrayList(2); + ROOT_CATEGORY.name = "Categories"; LoadTranslations("common.phrases"); HookEvent("player_entered_checkpoint", OnEnterSaferoom); @@ -89,6 +90,13 @@ public void OnPluginStart() { cvar_sm_hats_rainbow_speed = CreateConVar("sm_hats_rainbow_speed", "1", "Speed of rainbow", FCVAR_NONE, true, 0.0); cvar_sm_hats_max_distance = CreateConVar("sm_hats_distance", "240", "The max distance away you can hat something. 0 = disable", FCVAR_NONE, true, 0.0); + if(SQL_CheckConfig(DATABASE_CONFIG_NAME)) { + if(!ConnectDB()) { + LogError("Failed to connect to database."); + } + } + + noHatVictimCookie = new Cookie("hats_no_target", "Disables other players from making you their hat", CookieAccess_Public); noHatVictimCookie.SetPrefabMenu(CookieMenu_OnOff_Int, "Disable player hats for self", OnLocalPlayerHatCookieSelect); @@ -158,7 +166,7 @@ Action Timer_PlaceHat(Handle h, int userid) { if(client > 0 && HasHat(client)) { GetClientAbsOrigin(client, hatData[client].orgPos); GetClientEyeAngles(client, hatData[client].orgAng); - GetHorizontalPositionFromOrigin(hatData[client].orgPos, hatData[client].orgAng, 40.0, hatData[client].orgPos); + // GetHorizontalPositionFromOrigin(hatData[client].orgPos, hatData[client].orgAng, 40.0, hatData[client].orgPos); hatData[client].orgAng[0] = 0.0; PrintToChat(client, "[Hats] Hat has been placed down"); ClearHat(client, true); @@ -467,6 +475,7 @@ public Action OnPlayerRunCmd(int client, int& buttons, int& impulse, float vel[3 ////////////////////////////// // OnPlayerRunCmd :: HATS ///////////////////////////// + int oldButtons = GetEntProp(client, Prop_Data, "m_nOldButtons"); if(IsHatsEnabled(client)) { int entity = GetHat(client); int visibleEntity = EntRefToEntIndex(hatData[client].visibleEntity); @@ -569,6 +578,7 @@ public Action OnPlayerRunCmd(int client, int& buttons, int& impulse, float vel[3 if(buttons & IN_ZOOM) { ClearSavePreview(); if(buttons & IN_SPEED) { + PrintToChat(client, "\x04[Editor]\x01 Loaded save \x05%s", g_pendingSaveName); LoadSave(g_pendingSaveName, false); } } @@ -612,7 +622,6 @@ public Action OnPlayerRunCmd(int client, int& buttons, int& impulse, float vel[3 bool allowMove = true; switch(Editor[client].mode) { case MOVE_ORIGIN: { - SetWeaponDelay(client, 0.5); bool isRotate; @@ -621,22 +630,27 @@ public Action OnPlayerRunCmd(int client, int& buttons, int& impulse, float vel[3 if(!g_inRotate[client]) { g_inRotate[client] = true; } - if(buttons & IN_SPEED) { + if(buttons & IN_JUMP) { + buttons = buttons & ~IN_JUMP; + Editor[client].CycleStacker(tick); + } if(buttons & IN_SPEED) { Editor[client].ToggleCollision(tick); + return Plugin_Handled; } else if(buttons & IN_DUCK) { Editor[client].ToggleCollisionRotate(tick); + return Plugin_Handled; } else { - // PrintCenterText(client, "%.1f %.1f %.1f", Editor[client].angles[0], Editor[client].angles[1], Editor[client].angles[2]); + PrintCenterText(client, "%.1f %.1f %.1f", Editor[client].angles[0], Editor[client].angles[1], Editor[client].angles[2]); isRotate = true; SetEntityFlags(client, flags |= FL_FROZEN); if(buttons & IN_ATTACK) Editor[client].CycleAxis(tick); else if(buttons & IN_ATTACK2) Editor[client].CycleSnapAngle(tick); // Rotation control: - if(tick - cmdThrottle[client] > 0.20) { - // Turn off rotate when player wants rotate - Editor[client].hasCollisionRotate = false; - if(Editor[client].axis == 3) { + // Turn off rotate when player wants rotate + Editor[client].hasCollisionRotate = false; + if(tick - cmdThrottle[client] > 0.1) { + if(Editor[client].axis == 2) { if(mouse[1] > 10) Editor[client].angles[2] += Editor[client].snapAngle; else if(mouse[1] < -10) Editor[client].angles[2] -= Editor[client].snapAngle; } else { @@ -652,8 +666,9 @@ public Action OnPlayerRunCmd(int client, int& buttons, int& impulse, float vel[3 g_inRotate[client] = false; } // Move position - if(buttons & IN_ATTACK) Editor[client].moveDistance++; - else if(buttons & IN_ATTACK2) Editor[client].moveDistance--; + float moveAmount = (buttons & IN_SPEED) ? 2.0 : 1.0; + if(buttons & IN_ATTACK) Editor[client].moveDistance += moveAmount; + else if(buttons & IN_ATTACK2) Editor[client].moveDistance -= moveAmount; } // Clear IN_FROZEN when no longer rotate @@ -661,12 +676,12 @@ public Action OnPlayerRunCmd(int client, int& buttons, int& impulse, float vel[3 flags = flags & ~FL_FROZEN; SetEntityFlags(client, flags); } - CalculateEditorPosition(client, Filter_IgnorePlayerAndWall); + if(Editor[client].stackerDirection == Stack_Off) + CalculateEditorPosition(client, Filter_IgnorePlayerAndWall); } case SCALE: { SetWeaponDelay(client, 0.5); allowMove = false; - bool sizeChanged = false; if(buttons & IN_USE) { Editor[client].CycleSpeed(tick); } else { @@ -702,10 +717,9 @@ public Action OnPlayerRunCmd(int client, int& buttons, int& impulse, float vel[3 } } } - - if(tick - cmdThrottle[client] >= 0.25) { + if(tick - cmdThrottle[client] > 0.13) { if(buttons & IN_RELOAD) - Editor[client].CycleMode(); // R: Cycle forward + Editor[client].CycleMode(); // R: Cycle forward else if(buttons & IN_ZOOM) { buttons &= ~IN_ZOOM; @@ -722,7 +736,6 @@ public Action OnPlayerRunCmd(int client, int& buttons, int& impulse, float vel[3 } Editor[client].Draw(BUILDER_COLOR, 0.1, 0.1); - return allowMove ? Plugin_Continue : Plugin_Handled; } @@ -765,22 +778,37 @@ public Action OnTakeDamageAlive(int victim, int& attacker, int& inflictor, float return Plugin_Continue; } -public void Event_PlayerSpawn(Event event, const char[] name, bool dontBroadcast) { +void Event_PlayerSpawn(Event event, const char[] name, bool dontBroadcast) { int client = GetClientOfUserId(event.GetInt("userid")); - if(client > 0 && !HasHat(client) && !IsFakeClient(client)) { - hatPresetCookie.Get(client, ActivePreset[client], 32); - if(ActivePreset[client][0] != '\0') { - RestoreActivePreset(client); - ReplyToCommand(client, "[Hats] Applied your hat preset! Clear it with /hatp"); + if(client > 0) { + if(!HasHat(client) && !IsFakeClient(client)) { + hatPresetCookie.Get(client, ActivePreset[client], 32); + if(ActivePreset[client][0] != '\0') { + RestoreActivePreset(client); + ReplyToCommand(client, "[Hats] Applied your hat preset! Clear it with /hatp"); + } + } + SDKHook(client, SDKHook_WeaponCanUse, OnWeaponUse); + } +} + +Action OnWeaponUse(int client, int weapon) { + int ref = EntIndexToEntRef(weapon); + // Prevent picking up weapons that are previews + for(int i = 1; i <= MaxClients; i++) { + if(Editor[i].entity == ref && Editor[i].flags & Edit_Preview) { + return Plugin_Handled; } } + return Plugin_Continue; } public void OnClientDisconnect(int client) { tempGod[client] = false; Editor[client].Reset(); g_PropData[client].Reset(); - hatData[client].yeetGroundTimer = null; + if(hatData[client].yeetGroundTimer != null) + delete hatData[client].yeetGroundTimer; if(g_pendingSaveClient == client) { g_pendingSaveClient = 0; ClearSavePreview(); @@ -820,7 +848,6 @@ public void OnMapEnd() { if(hatData[i].yeetGroundTimer != null) { delete hatData[i].yeetGroundTimer; } - hatData[i].yeetGroundTimer = null; DeleteWall(i); } createdWalls.Clear();