diff --git a/.gitignore b/.gitignore index d813fe4..8ccb237 100644 --- a/.gitignore +++ b/.gitignore @@ -32,4 +32,6 @@ scripting/include/stats scripting/ssh.sp scripting/l4d2_witch_force_attack_cmd.sp l4d2_stats_plugin/ -!sql/* \ No newline at end of file +!sql/* +!scripting/.gitignore +!plugins/.gitignore \ No newline at end of file diff --git a/plugins/l4d2_editor.smx b/plugins/l4d2_editor.smx index 41841d6..f8c6423 100644 Binary files a/plugins/l4d2_editor.smx and b/plugins/l4d2_editor.smx differ diff --git a/scripting/include/editor/editor.sp b/scripting/include/editor/editor.sp new file mode 100644 index 0000000..992d5cd --- /dev/null +++ b/scripting/include/editor/editor.sp @@ -0,0 +1,1024 @@ +int BUILDER_COLOR[4] = { 0, 255, 0, 235 }; +int GLOW_BLUE[4] = { 3, 148, 252 }; +int GLOW_RED_ALPHA[4] = { 255, 0, 0, 235 }; +int GLOW_WHITE[4] = { 255, 255, 255, 255 }; +int GLOW_GREEN[4] = { 3, 252, 53 }; +float ORIGIN_SIZE[3] = { 2.0, 2.0, 2.0 }; + +char ON_OFF_STRING[2][] = { + "\x05OFF\x01", + "\x05ON\x01" +} +char COLOR_INDEX[4] = "RGBA"; + +enum editMode { + INACTIVE = 0, + MOVE_ORIGIN, + SCALE, + COLOR, + FREELOOK, +} +char MODE_NAME[5][] = { + "Error", + "Move & Rotate", + "Scale", + "Color", + "Freelook" +} + +enum { + Edit_None, + Edit_Copy = 1, + Edit_Preview = 2, + Edit_WallCreator = 4, + Edit_Manager = 8 +} + +enum buildType { + Build_Solid, + Build_Physics, + Build_NonSolid, + // TODO: Build_Weapon (spawn as weapon?) +} + + +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[64]; + char data[32]; + char name[32]; + + float origin[3]; + float angles[3]; + float prevOrigin[3]; // for cancelling edits + float prevAngles[3]; + + float mins[3]; + float size[3]; + + int color[4]; + int colorIndex; + int axis; + int snapAngle; + float rotateSpeed; + int moveSpeed; + float moveDistance; + int entity; + bool hasCollision; /// possibly merge into .flags + bool hasCollisionRotate; //^ + StackerDirection stackerDirection; + + editMode mode; + buildType buildType; + int flags; + + PrivateForward callback; + bool isEditCallback; + + void Reset(bool initial = false) { + // Clear preview entity + 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 = 0; + this.moveDistance = 200.0; + this.flags = Edit_None; + this.classname[0] = '\0'; + this.CalculateMins(); + this.SetMode(INACTIVE); + this.rotateSpeed = 0.1; + // Settings that don't get reset on new spawns: + if(initial) { + this.color[0] = this.color[1] = this.color[2] = this.color[3] = 255; + this.moveSpeed = 1; + this.snapAngle = 30; + this.hasCollision = true; + this.hasCollisionRotate = false; + this.buildType = Build_Solid; + } + } + + void CalculateMins() { + this.mins[0] = -this.size[0]; + this.mins[1] = -this.size[1]; + this.mins[2] = -this.size[2]; + } + + void Draw(int color[4], float lifetime, float amplitude = 0.1) { + if(this.flags & Edit_WallCreator || this.entity == INVALID_ENT_REFERENCE) { + Effect_DrawBeamBoxRotatableToAll(this.origin, this.mins, this.size, this.angles, g_iLaserIndex, 0, 0, 30, lifetime, 0.4, 0.4, 0, amplitude, color, 0); + } else { + if(this.snapAngle != 1) { + this.angles[0] = RoundToNearestInterval(this.angles[0], this.snapAngle); + this.angles[1] = RoundToNearestInterval(this.angles[1], this.snapAngle); + this.angles[2] = RoundToNearestInterval(this.angles[2], this.snapAngle); + + } + TeleportEntity(this.entity, this.origin, this.angles, NULL_VECTOR); + } + Effect_DrawAxisOfRotationToAll(this.origin, this.angles, ORIGIN_SIZE, g_iLaserIndex, 0, 0, 30, 0.2, 0.1, 0.1, 0, 0.0, 0); + } + + // Updates the entity with certain changed settings + void UpdateEntity() { + int alpha = this.color[3]; + // Keep previews transparent + SetEntityRenderColor(this.entity, this.color[0], this.color[1], this.color[2], alpha); + } + + bool CheckEntity() { + if(this.entity != INVALID_ENT_REFERENCE) { + if(this.entity == -1 && !IsValidEntity(this.entity)) { + PrintToChat(this.client, "\x04[Editor]\x01 Entity has vanished, editing cancelled."); + this.Reset(); + return false; + } + } + return true; + } + + bool IsActive() { + return this.mode != INACTIVE && this.CheckEntity(); + } + + void SetMode(editMode mode) { + this.mode = mode; + } + + 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 SetCallback(PrivateForward callback, bool isEditCallback) { + this.callback = callback; + this.isEditCallback = isEditCallback; + } + + void CycleMode() { + // Remove frozen state when cycling + int flags = GetEntityFlags(this.client) & ~FL_FROZEN; + SetEntityFlags(this.client, flags); + switch(this.mode) { + // MODES: + // - MOVE & ROTAT + // - SCALE or COLOR + // - FREELOOK + case MOVE_ORIGIN: { + if(this.flags & Edit_WallCreator) { + this.mode = SCALE; + } else if(this.flags & Edit_Preview) { + this.mode = COLOR; + } else { + this.mode = FREELOOK; + } + } + case SCALE: { + this.mode = FREELOOK; + } + case COLOR: { + this.mode = FREELOOK; + } + case FREELOOK: { + this.mode = MOVE_ORIGIN; + } + } + PrintToChat(this.client, "\x04[Editor]\x01 Mode: \x05%s\x01 (Press \x04RELOAD\x01 to change)", MODE_NAME[this.mode]); + } + + void CycleStacker() { + 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]); + } + + void ToggleCollision() { + this.hasCollision = !this.hasCollision + PrintToChat(this.client, "\x04[Editor]\x01 Collision: %s", ON_OFF_STRING[view_as(this.hasCollision)]); + } + + void ToggleCollisionRotate() { + this.hasCollisionRotate = !this.hasCollisionRotate + PrintToChat(this.client, "\x04[Editor]\x01 Rotate with Collision: %s", ON_OFF_STRING[view_as(this.hasCollisionRotate)]); + } + + void CycleAxis() { + // if(tick - cmdThrottle[this.client] <= 0.1) return; + if(this.axis == 0) { + this.axis = 1; + PrintToChat(this.client, "\x04[Editor]\x01 Rotate Axis: \x05ROLL (Z)\x01"); + } else { + this.axis = 0; + PrintToChat(this.client, "\x04[Editor]\x01 Rotate Axis: \x05PITCH AND HEADING (X, Y)\x01"); + } + // cmdThrottle[this.client] = tick; + } + + void IncrementAxis(int axis, int mouse) { + if(this.snapAngle == 1) { + this.angles[axis] += mouse * this.rotateSpeed; + } else { + if(mouse > 0) this.angles[axis] += this.snapAngle; + else if(mouse < 0) this.angles[axis] -= this.snapAngle; + } + } + + void CycleSnapAngle(float tick) { + if(tick - cmdThrottle[this.client] <= 0.1) return; + switch(this.snapAngle) { + case 1: this.snapAngle = 15; + case 15: this.snapAngle = 30; + case 30: this.snapAngle = 45; + case 45: this.snapAngle = 90; + case 90: this.snapAngle = 1; + } + + // this.angles[0] = SnapTo(this.angles[0], float(this.snapAngle)); + // this.angles[1] = SnapTo(this.angles[1], float(this.snapAngle)); + // this.angles[2] = SnapTo(this.angles[2], float(this.snapAngle)); + + if(this.snapAngle == 1) + PrintToChat(this.client, "\x04[Editor]\x01 Rotate Snap Degrees: \x04(OFF)\x01", this.snapAngle); + else + PrintToChat(this.client, "\x04[Editor]\x01 Rotate Snap Degrees: \x05%d\x01", this.snapAngle); + cmdThrottle[this.client] = tick; + } + + void CycleSpeed(float tick) { + if(tick - cmdThrottle[this.client] <= 0.25) return; + this.moveSpeed++; + if(this.moveSpeed > 10) this.moveSpeed = 1; + PrintToChat(this.client, "\x04[Editor]\x01 Scale Speed: \x05%d\x01", this.moveSpeed); + cmdThrottle[this.client] = tick; + } + + void CycleBuildType() { + // No tick needed, is handled externally + if(this.classname[0] != '\0') { + PrintToChat(this.client, "\x04[Editor]\x01 Spawn as: \x05%s\x01 (fixed)", this.classname); + } else if(this.buildType == Build_Physics) { + this.buildType = Build_Solid; + PrintToChat(this.client, "\x04[Editor]\x01 Spawn as: \x05Solid\x01"); + } else if(this.buildType == Build_Solid) { + this.buildType = Build_Physics; + PrintToChat(this.client, "\x04[Editor]\x01 Spawn as: \x05Physics\x01"); + } else { + this.buildType = Build_NonSolid; + PrintToChat(this.client, "\x04[Editor]\x01 Spawn as: \x05Non Solid\x01"); + } + } + + void CycleColorComponent(float tick) { + if(tick - cmdThrottle[this.client] <= 0.25) return; + this.colorIndex++; + if(this.colorIndex > 3) this.colorIndex = 0; + char component[16]; + for(int i = 0; i < 4; i++) { + if(this.colorIndex == i) + Format(component, sizeof(component), "%s \x05%c\x01", component, COLOR_INDEX[i]); + else + Format(component, sizeof(component), "%s %c", component, COLOR_INDEX[i]); + } + PrintToChat(this.client, "\x04[Editor]\x01 Color: %s", component); + cmdThrottle[this.client] = tick; + } + + void IncrementSize(int axis, float amount) { + this.size[axis] += amount; + if(this.size[axis] < 0.0) { + this.size[axis] = 0.0; + } + this.CalculateMins(); + } + + void IncreaseColor(int amount) { + int newValue = this.color[this.colorIndex] + amount; + if(newValue > 255) newValue = 255; + else if(newValue < 0) newValue = 0; + this.color[this.colorIndex] = newValue; + this.UpdateEntity(); + PrintCenterText(this.client, "%d %d %d %d", this.color[0], this.color[1], this.color[2], this.color[3]); + } + + // Complete the edit, wall creation, or spawning + CompleteType Done(int& entity) { + CompleteType type; + if(this.flags & Edit_WallCreator) { + type = this._FinishWall(entity) ? Complete_WallSuccess : Complete_WallError; + } else if(this.flags & Edit_Preview) { + type = this._FinishPreview(entity) ? Complete_PropSpawned : Complete_PropError; + } else { + // Is edit, do nothing, just reset + PrintHintText(this.client, "Edit Complete"); + this.Reset(); + entity = 0; + + type = Complete_EditSuccess; + } + if(this.callback) { + Call_StartForward(this.callback); + Call_PushCell(this.client); + Call_PushCell(entity); + Call_PushCell(type); + bool result; + Call_Finish(result); + // Cancel menu: + if(this.isEditCallback) delete this.callback; + if(this.isEditCallback || !result) { + // No native way to close a menu, so open a dummy menu and close it: + // Handler doesn't matter, no options are added + Menu menu = new Menu(Spawn_RootHandler); + menu.Display(this.client, 1); + } else { + delete this.callback; + } + } + return type; + } + + bool _FinishWall(int& id) { + if(~this.flags & Edit_WallCreator) { + this.Reset(); + return false; + } + // Don't need to build a new one if we editing: + int blocker = this.entity; + bool isEdit = false; + if(blocker != INVALID_ENT_REFERENCE) { + RemoveEntity(this.entity); + isEdit = true; + } + blocker = CreateEntityByName("func_brush"); + if(blocker == -1) return false; + DispatchKeyValueVector(blocker, "mins", this.mins); + DispatchKeyValueVector(blocker, "maxs", this.size); + DispatchKeyValueVector(blocker, "boxmins", this.mins); + DispatchKeyValueVector(blocker, "boxmaxs", this.size); + DispatchKeyValue(blocker, "excludednpc", "player"); + + DispatchKeyValueVector(blocker, "angles", this.angles); + DispatchKeyValue(blocker, "model", DUMMY_MODEL); + DispatchKeyValue(blocker, "intialstate", "1"); + // DispatchKeyValueVector(blocker, "angles", this.angles); + DispatchKeyValue(blocker, "BlockType", "4"); + char name[32]; + Format(name, sizeof(name), "editor_%d", createdWalls.Length); + DispatchKeyValue(blocker, "targetname", name); + // DispatchKeyValue(blocker, "excludednpc", "player"); + TeleportEntity(blocker, this.origin, this.angles, NULL_VECTOR); + if(!DispatchSpawn(blocker)) return false; + SetEntPropVector(blocker, Prop_Send, "m_vecMins", this.mins); + SetEntPropVector(blocker, Prop_Send, "m_vecMaxs", this.size); + SetEntProp(blocker, Prop_Send, "m_nSolidType", 2); + int enteffects = GetEntProp(blocker, Prop_Send, "m_fEffects"); + enteffects |= 32; //EF_NODRAW + SetEntProp(blocker, Prop_Send, "m_fEffects", enteffects); + AcceptEntityInput(blocker, "Enable"); + SDKHook(blocker, SDKHook_Use, OnWallClicked); + + this.Draw(GLOW_GREEN, 5.0, 1.0); + this.Reset(); + if(!isEdit) { + id = createdWalls.Push(EntIndexToEntRef(blocker)); + PrintToChat(this.client, "\x04[Editor]\x01 Created wall \x05#%d\x01.", id); + } + return true; + } + + bool _FinishPreview(int& entity) { + if(StrContains(this.classname, "weapon") > -1) { + entity = this._CreateWeapon(); + } else { + entity = this._CreateProp(); + } + + DispatchKeyValue(entity, "targetname", "editor_propspawner"); + 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); + } + } + PrintHintText(this.client, "%s\n%s", this.classname, this.data); + // PrintToChat(this.client, "\x04[Editor]\x01 Editing copy \x05%d\x01 of entity \x05%d\x01. End with \x05/edit done\x01 or \x04/edit cancel\x01", entity, oldEntity); + // Don't kill preview until cancel + return true; + } + + int _CreateWeapon() { + int entity = -1; + entity = CreateEntityByName(this.classname); + if(entity == -1) return -1; + if(StrEqual(this.classname, "weapon_melee_spawn")) { + DispatchKeyValue(entity, "melee_weapon", this.data); + } + DispatchKeyValue(entity, "count", "1"); + DispatchKeyValue(entity, "spawnflags", "10"); + return entity; + } + + int _CreateProp() { + int entity = -1; + if(this.classname[0] != '\0') { + entity = CreateEntityByName(this.classname); + } else if(this.buildType == Build_Physics) + entity = CreateEntityByName("prop_physics"); + else + entity = CreateEntityByName("prop_dynamic_override"); + if(entity == -1) return false; + + char model[128]; + GetEntPropString(this.entity, Prop_Data, "m_ModelName", model, sizeof(model)); + DispatchKeyValue(entity, "model", model); + if(this.buildType == Build_NonSolid) + DispatchKeyValue(entity, "solid", "0"); + else + DispatchKeyValue(entity, "solid", "6"); + return entity; + } + + // Turns current entity into a copy (not for walls) + int Copy() { + if(this.entity == INVALID_ENT_REFERENCE) return -1; + char classname[64]; + GetEntityClassname(this.entity, classname, sizeof(classname)); + + int entity = CreateEntityByName(classname); + if(entity == -1) return -1; + GetEntPropString(this.entity, Prop_Data, "m_ModelName", classname, sizeof(classname)); + DispatchKeyValue(entity, "model", classname); + + + Format(classname, sizeof(classname), "editor_%d", this.entity); + DispatchKeyValue(entity, "targetname", classname); + + 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; + return entity; + } + + // Start editing a new wall entity + void StartWall() { + this.Reset(); + this.flags |= Edit_WallCreator; + } + + bool PreviewWeapon(const char[] classname, const char[] data) { + int entity; + // Melee weapons don't have weapon_ prefix + this.Reset(); + // Rotate on it's side: + this.angles[2] = 90.0; + if(StrEqual(classname, "weapon_melee_spawn")) { + // no weapon_ prefix, its a melee + entity = CreateEntityByName(classname); + if(entity == -1) return false; + 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", "editor_preview"); + DispatchKeyValue(entity, "rendercolor", "255 128 255"); + DispatchKeyValue(entity, "renderamt", "200"); + DispatchKeyValue(entity, "rendermode", "1"); + TeleportEntity(entity, this.origin, NULL_VECTOR, NULL_VECTOR); // MUST teleport before spawn or it crashes + if(!DispatchSpawn(entity)) { + PrintToServer("Failed to spawn"); + return false; + } + this.entity = entity; + this.flags |= (Edit_Copy | Edit_Preview); + this.SetMode(MOVE_ORIGIN); + // Seems some entities fail here: + return IsValidEntity(entity); + } + + 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, "weapon_melee_spawn")) { + // Pass in melee ID as data: + return this.PreviewWeapon(classname, model); + } + if(PrecacheModel(model) == 0) { + PrintToServer("Invalid model: %s", model); + return false; + } + this.Reset(); + int entity = CreateEntityByName("prop_door_rotating"); + if(classname[0] == '\0') { + entity = CreateEntityByName("prop_dynamic_override"); + } else { + strcopy(this.classname, sizeof(this.classname), classname); + entity = CreateEntityByName(classname); + } + if(entity == -1) { + PrintToServer("Invalid classname: %s", classname); + return false; + } + DispatchKeyValue(entity, "model", model); + DispatchKeyValue(entity, "targetname", "editor_preview"); + DispatchKeyValue(entity, "solid", "0"); + DispatchKeyValue(entity, "rendercolor", "255 128 255"); + DispatchKeyValue(entity, "renderamt", "255"); + DispatchKeyValue(entity, "rendermode", "1"); + TeleportEntity(entity, this.origin, NULL_VECTOR, NULL_VECTOR); + if(!DispatchSpawn(entity)) { + PrintToServer("Failed to spawn"); + return false; + } + this.entity = entity; + this.flags |= (Edit_Copy | Edit_Preview); + this.SetMode(MOVE_ORIGIN); + // Seems some entities fail here: + return IsValidEntity(entity); + } + + /** + * Adds an existing entity to the editor, to move it. + * asWallCopy: to instead copy the wall's size and position (walls only) + * @deprecated + */ + void Import(int entity, bool asWallCopy = false, editMode mode = SCALE) { + this.Reset(); + GetEntPropVector(entity, Prop_Send, "m_vecOrigin", this.origin); + GetEntPropVector(entity, Prop_Send, "m_angRotation", this.angles); + this.prevOrigin = this.origin; + this.prevAngles = this.angles; + GetEntPropVector(entity, Prop_Send, "m_vecMins", this.mins); + GetEntPropVector(entity, Prop_Send, "m_vecMaxs", this.size); + if(!asWallCopy) { + this.entity = entity; + } + this.SetMode(mode); + } + + /** + * Imports an entity + */ + void ImportEntity(int entity, int flags = 0, editMode mode = SCALE) { + this.Reset(); + this.flags = flags; + GetEntPropVector(entity, Prop_Send, "m_vecOrigin", this.origin); + GetEntPropVector(entity, Prop_Send, "m_angRotation", this.angles); + this.prevOrigin = this.origin; + this.prevAngles = this.angles; + GetEntPropVector(entity, Prop_Send, "m_vecMins", this.mins); + GetEntPropVector(entity, Prop_Send, "m_vecMaxs", this.size); + this.entity = entity; + this.SetMode(mode); + } + + // Cancels the current placement. If the edit is a copy/preview, the entity is also deleted + // If entity is not a wall, it will be returned + void Cancel() { + // Delete any copies: + if(this.flags & Edit_Copy || this.flags & Edit_Preview) { + RemoveEntity(this.entity); + } else if(~this.flags & Edit_WallCreator) { + // Is an edit of a prop + TeleportEntity(this.entity, this.prevOrigin, this.prevAngles, NULL_VECTOR); + } + this.SetMode(INACTIVE); + PrintHintText(this.client, "Cancelled"); + if(this.callback) { + delete this.callback; + } + // CPrintToChat(this.client, "\x04[Editor]\x01 Cancelled."); + } +} + +void SendEditorMessage(int client, const char[] format, any ...) { + char message[256]; + VFormat(message, sizeof(message), format, 3); + CPrintToChat(client, "\x04[Editor]\x01 %s", message); +} + +stock float RoundToNearestInterval(float value, int interval) { + return float(RoundFloat(value / float(interval)) * interval); +} +EditorData Editor[MAXPLAYERS+1]; + +Action OnWallClicked(int entity, int activator, int caller, UseType type, float value) { + int wallId = FindWallId(entity); + if(wallId > 0) { + GlowWall(wallId, GLOW_BLUE); + AcceptEntityInput(entity, "Toggle"); + } else { + PrintHintText(activator, "Invalid wall entity (%d)", entity); + } + return Plugin_Continue; +} + + + +// TODO: Stacker, copy tool, new command? +public Action Command_MakeWall(int client, int args) { + if(Editor[client].IsActive()) { + ReplyToCommand(client, "\x04[Editor]\x01 You are currently editing an entity. Finish editing your current entity with with \x05/edit done\x01 or cancel with \x04/edit cancel\x01"); + } else { + Editor[client].StartWall(); + if(args > 0) { + // Get values for X, Y and Z axis (defaulting to 1.0): + char arg2[8]; + for(int i = 0; i < 3; i++) { + GetCmdArg(i + 1, arg2, sizeof(arg2)); + float value; + if(StringToFloatEx(arg2, value) == 0) { + value = 1.0; + } + Editor[client].size[i] = value; + } + + float rot[3]; + GetClientEyeAngles(client, rot); + // Flip X and Y depending on rotation + // TODO: Validate + if(rot[2] > 45 && rot[2] < 135 || rot[2] > -135 && rot[2] < -45) { + float temp = Editor[client].size[0]; + Editor[client].size[0] = Editor[client].size[1]; + Editor[client].size[1] = temp; + } + + Editor[client].CalculateMins(); + } + + Editor[client].SetMode(SCALE); + GetCursorLimited(client, 100.0, Editor[client].origin, Filter_IgnorePlayer); + PrintToChat(client, "\x04[Editor]\x01 New Wall Started. End with \x05/wall build\x01 or \x04/wall cancel\x01"); + PrintToChat(client, "\x04[Editor]\x01 Mode: \x05Scale\x01"); + } + return Plugin_Handled; +} + +// TODO: move wall ids to own subcommand +Action Command_Editor(int client, int args) { + if(args == 0) { + PrintToChat(client, "\x04[Editor]\x01 Created Walls: \x05%d\x01", createdWalls.Length); + for(int i = 1; i <= createdWalls.Length; i++) { + GlowWall(i, GLOW_WHITE, 20.0); + } + return Plugin_Handled; + } + char arg1[16], arg2[16]; + GetCmdArg(1, arg1, sizeof(arg1)); + GetCmdArg(2, arg2, sizeof(arg2)); + if(StrEqual(arg1, "build") || StrEqual(arg1, "done")) { + // Remove frozen flag from user, as some modes use this + int flags = GetEntityFlags(client) & ~FL_FROZEN; + SetEntityFlags(client, flags); + + int entity; + CompleteType result = Editor[client].Done(entity); + switch(result) { + case Complete_WallSuccess: { + if(entity > 0) + PrintToChat(client, "\x04[Editor]\x01 Wall Creation: \x05Wall #%d Created\x01", entity + 1); + else + PrintToChat(client, "\x04[Editor]\x01 Wall Edit: \x04Complete\x01"); + } + case Complete_PropSpawned: + PrintToChat(client, "\x04[Editor]\x01 Prop Spawned: \x04%d\x01", entity); + + case Complete_EditSuccess: + PrintToChat(client, "\x04[Editor]\x01 Entity Edited: \x04%d\x01", entity); + + default: + PrintToChat(client, "\x04[Editor]\x01 Unknown result"); + } + } else if(StrEqual(arg1, "cancel")) { + int flags = GetEntityFlags(client) & ~FL_FROZEN; + SetEntityFlags(client, flags); + Editor[client].Cancel(); + if(Editor[client].flags & Edit_Preview) + PrintToChat(client, "\x04[Editor]\x01 Prop Spawer: \x04Cancelled\x01"); + else if(Editor[client].flags & Edit_WallCreator) { + PrintToChat(client, "\x04[Editor]\x01 Wall Creation: \x04Cancelled\x01"); + } else { + PrintToChat(client, "\x04[Editor]\x01 Entity Edit: \x04Cancelled\x01"); + } + } else if(StrEqual(arg1, "export")) { + // TODO: support exp #id + float origin[3], angles[3], size[3]; + if(Editor[client].IsActive()) { + origin = Editor[client].origin; + angles = Editor[client].angles; + size = Editor[client].size; + Export(client, arg2, Editor[client].entity, origin, angles, size); + } else { + int id = GetWallId(client, arg2); + if(id == -1) return Plugin_Handled; + int entity = GetWallEntity(id); + GetEntPropVector(entity, Prop_Send, "m_vecOrigin", origin); + if(HasEntProp(entity, Prop_Send, "m_vecAngles")) + GetEntPropVector(entity, Prop_Send, "m_vecAngles", angles); + GetEntPropVector(entity, Prop_Send, "m_vecMaxs", size); + Export(client, arg2, entity, origin, angles, size); + } + } else if(StrEqual(arg1, "delete")) { + if(Editor[client].IsActive() && args == 1) { + int entity = Editor[client].entity; + if(IsValidEntity(entity)) { + PrintToChat(client, "\x04[Editor]\x01 You are not editing any existing entity, use \x05/wall cancel\x01 to stop or \x05/wall delete "); + } else if(entity > MaxClients) { + RemoveEntity(entity); + Editor[client].Reset(); + PrintToChat(client, "\x04[Editor]\x01 Deleted current entity"); + } else { + PrintToChat(client, "\x04[Editor]\x01 Cannot delete player entities."); + } + } else if(StrEqual(arg2, "all")) { + int walls = createdWalls.Length; + for(int i = 1; i <= createdWalls.Length; i++) { + DeleteWall(i); + } + PrintToChat(client, "\x04[Editor]\x01 Deleted \x05%d\x01 Walls", walls); + } else { + int id = GetWallId(client, arg2); + if(id > -1) { + DeleteWall(id); + PrintToChat(client, "\x04[Editor]\x01 Deleted Wall: \x05#%d\x01", id); + } + } + } else if(StrEqual(arg1, "create")) { + ReplyToCommand(client, "\x04[Editor]\x01 Syntax: /mkwall [size x] [size y] [size z]"); + } else if(StrEqual(arg1, "toggle")) { + if(StrEqual(arg2, "all")) { + int walls = createdWalls.Length; + for(int i = 1; i <= createdWalls.Length; i++) { + int entity = GetWallEntity(i); + AcceptEntityInput(entity, "Toggle"); + GlowWall(i, GLOW_BLUE); + } + PrintToChat(client, "\x04[Editor]\x01 Toggled \x05%d\x01 walls", walls); + } else { + int id = GetWallId(client, arg2); + if(id > -1) { + int entity = GetWallEntity(id); + AcceptEntityInput(entity, "Toggle"); + GlowWall(id, GLOW_BLUE); + PrintToChat(client, "\x04[Editor]\x01 Toggled Wall: \x05#%d\x01", id); + } + } + } else if(StrEqual(arg1, "filter")) { + if(args < 3) { + ReplyToCommand(client, "\x04[Editor]\x01 Syntax: \x05/walls filter \x04"); + ReplyToCommand(client, "\x04[Editor]\x01 Valid filters: \x05player"); + return Plugin_Handled; + } + + char arg3[32]; + GetCmdArg(3, arg3, sizeof(arg3)); + + SetVariantString(arg3); + if(StrEqual(arg2, "all")) { + int walls = createdWalls.Length; + for(int i = 1; i <= createdWalls.Length; i++) { + int entity = GetWallEntity(i); + AcceptEntityInput(entity, "SetExcluded"); + } + PrintToChat(client, "\x04[Editor]\x01 Set %d walls' filter to \x05%s\x01", walls, arg3); + } else { + int id = GetWallId(client, arg2); + if(id > -1) { + int entity = GetWallEntity(id); + AcceptEntityInput(entity, "SetExcluded"); + PrintToChat(client, "\x04[Editor]\x01 Set wall #%d filter to \x05%s\x01", id, arg3); + } + } + } else if(StrEqual(arg1, "edit")) { + int id = GetWallId(client, arg2); + if(id > -1) { + int entity = GetWallEntity(id); + Editor[client].Import(entity); + PrintToChat(client, "\x04[Editor]\x01 Editing wall \x05%d\x01. End with \x05/wall done\x01 or \x04/wall cancel\x01", id); + PrintToChat(client, "\x04[Editor]\x01 Mode: \x05Scale\x01"); + } + } else if(StrEqual(arg1, "edite") || (arg1[0] == 'c' && arg1[1] == 'u')) { + int index = GetLookingEntity(client, Filter_ValidHats); //GetClientAimTarget(client, false); + if(index > 0) { + Editor[client].Import(index, false, MOVE_ORIGIN); + char classname[64]; + char targetname[32]; + GetEntityClassname(index, classname, sizeof(classname)); + GetEntPropString(index, Prop_Data, "m_target", targetname, sizeof(targetname)); + PrintToChat(client, "\x04[Editor]\x01 Editing entity \x05%d (%s) [%s]\x01. End with \x05/wall done\x01", index, classname, targetname); + PrintToChat(client, "\x04[Editor]\x01 Mode: \x05Move & Rotate\x01"); + } else { + ReplyToCommand(client, "\x04[Editor]\x01 Invalid or non existent entity"); + } + } else if(StrEqual(arg1, "copy")) { + if(Editor[client].IsActive()) { + int oldEntity = Editor[client].entity; + if(oldEntity == INVALID_ENT_REFERENCE) { + PrintToChat(client, "\x04[Editor]\x01 Finish editing your wall first: \x05/wall done\x01 or \x04/wall cancel\x01"); + } else { + int entity = Editor[client].Copy(); + PrintToChat(client, "\x04[Editor]\x01 Editing copy \x05%d\x01 of entity \x05%d\x01. End with \x05/edit done\x01 or \x04/edit cancel\x01", entity, oldEntity); + } + } else { + int id = GetWallId(client, arg2); + if(id > -1) { + int entity = GetWallEntity(id); + Editor[client].Import(entity, true); + GetCursorLimited(client, 100.0, Editor[client].origin, Filter_IgnorePlayer); + PrintToChat(client, "\x04[Editor]\x01 Editing copy of wall \x05%d\x01. End with \x05/wall build\x01 or \x04/wall cancel\x01", id); + PrintToChat(client, "\x04[Editor]\x01 Mode: \x05Scale\x01"); + } + } + } else if(StrEqual(arg1, "list")) { + for(int i = 1; i <= createdWalls.Length; i++) { + int entity = GetWallEntity(i); + ReplyToCommand(client, "Wall #%d - EntIndex: %d", i, EntRefToEntIndex(entity)); + } + } else { + ReplyToCommand(client, "\x04[Editor]\x01 See console for list of commands"); + GetCmdArg(0, arg1, sizeof(arg1)); + PrintToConsole(client, "%s done / build - Finishes editing, creates wall if making wall", arg1); + PrintToConsole(client, "%s cancel - Cancels editing (for entity edits is same as done)", arg1); + PrintToConsole(client, "%s list - Lists all walls", arg1); + PrintToConsole(client, "%s filter - Sets classname filter for walls, doesnt really work", arg1); + PrintToConsole(client, "%s toggle - Toggles if wall is active (collides)", arg1); + PrintToConsole(client, "%s delete - Deletes the wall(s)", arg1); + PrintToConsole(client, "%s edit - Edits wall id", arg1); + PrintToConsole(client, "%s copy [id] - If editing creates a new copy of wall/entity, else copies wall id", arg1); + PrintToConsole(client, "%s cursor - Starts editing the entity you looking at", arg1); + } + return Plugin_Handled; +} + +int GetWallId(int client, const char[] arg) { + int id; + if(StringToIntEx(arg, id) > 0 && id > 0 && id <= createdWalls.Length) { + int entity = GetWallEntity(id); + if(!IsValidEntity(entity)) { + ReplyToCommand(client, "\x04[Editor]\x01 The wall with specified id no longer exists."); + createdWalls.Erase(id - 1); + return -2; + } + return id; + } else { + ReplyToCommand(client, "\x04[Editor]\x01 Invalid wall id, must be between 0 - %d", createdWalls.Length - 1 ); + return -1; + } +} + +int GetWallEntity(int id) { + if(id <= 0 || id > createdWalls.Length) { + ThrowError("Invalid wall id (%d)", id); + } + return createdWalls.Get(id - 1); +} + +/// Tries to find the id of the wall based off entity +int FindWallId(int entity) { + for(int i = 1; i <= createdWalls.Length; i++) { + int entRef = createdWalls.Get(i - 1); + int ent = EntRefToEntIndex(entRef); + if(ent == entity) { + return i; + } + } + return -1; +} + +void GlowWall(int id, int glowColor[4], float lifetime = 5.0) { + int ref = GetWallEntity(id); + if(IsValidEntity(ref)) { + float pos[3], mins[3], maxs[3], angles[3]; + GetEntPropVector(ref, Prop_Send, "m_angRotation", angles); + GetEntPropVector(ref, Prop_Send, "m_vecOrigin", pos); + GetEntPropVector(ref, Prop_Send, "m_vecMins", mins); + GetEntPropVector(ref, Prop_Send, "m_vecMaxs", maxs); + Effect_DrawBeamBoxRotatableToAll(pos, mins, maxs, angles, g_iLaserIndex, 0, 0, 30, lifetime, 0.4, 0.4, 0, 1.0, glowColor, 0); + } +} + +void DeleteWall(int id) { + GlowWall(id, GLOW_RED_ALPHA); + int ref = GetWallEntity(id); + if(IsValidEntity(ref)) { + RemoveEntity(ref); + } + createdWalls.Erase(id - 1); +} + + void Export(int client, const char[] expType, int entity, const float origin[3], const float angles[3], const float size[3]) { + char sPath[PLATFORM_MAX_PATH]; + char currentMap[64]; + GetCurrentMap(currentMap, sizeof(currentMap)); + + BuildPath(Path_SM, sPath, sizeof(sPath), "data/exports"); + CreateDirectory(sPath, 1406); + BuildPath(Path_SM, sPath, sizeof(sPath), "data/exports/%s.cfg", currentMap); + File file = OpenFile(sPath, "w"); + if(file == null) { + PrintToServer("[Editor] Export: Cannot open \"%s\", cant write", sPath); + } + + PrintWriteLine(client, file, "{"); + if(entity != INVALID_ENT_REFERENCE) { + char model[64]; + GetEntPropString(entity, Prop_Data, "m_ModelName", model, sizeof(model)); + if(StrEqual(expType, "json")) { + PrintWriteLine(client, file, "\t\"model\": \"%s\",", model); + } else{ + PrintWriteLine(client, file, "\t\"model\" \"%s\"", model); + } + } + + if(StrEqual(expType, "json")) { + PrintWriteLine(client, file, "\t\"origin\": [%.2f, %.2f, %.2f],", origin[0], origin[1], origin[2]); + PrintWriteLine(client, file, "\t\"angles\": [%.2f, %.2f, %.2f],", angles[0], angles[1], angles[2]); + PrintWriteLine(client, file, "\t\"size\": [%.2f, %.2f, %.2f]", size[0], size[1], size[2]); + } else { + PrintWriteLine(client, file, "\t\"origin\" \"%.2f %.2f %.2f\"", origin[0], origin[1], origin[2]); + PrintWriteLine(client, file, "\t\"angles\" \"%.2f %.2f %.2f\"", angles[0], angles[1], angles[2]); + PrintWriteLine(client, file, "\t\"size\" \"%.2f %.2f %.2f\"", size[0], size[1], size[2]); + } + PrintWriteLine(client, file, "}"); + delete file; +} + +void PrintWriteLine(int client, File file, const char[] format, any ...) { + char line[100]; + VFormat(line, sizeof(line), format, 4); + if(file != null) + file.WriteLine(line); + PrintToChat(client, line); +} \ No newline at end of file diff --git a/scripting/include/editor/natives.sp b/scripting/include/editor/natives.sp new file mode 100644 index 0000000..f2911ad --- /dev/null +++ b/scripting/include/editor/natives.sp @@ -0,0 +1,122 @@ + +int Native_StartEdit(Handle plugin, int numParams) { + int client = GetNativeCell(1); + int entity = GetNativeCell(2); + Editor[client].Import(entity, false); + PrivateForward fwd = new PrivateForward(ET_Ignore, Param_Cell, Param_Cell, Param_Cell); + fwd.AddFunction(INVALID_HANDLE, GetNativeFunction(3)); + Editor[client].SetCallback(fwd, true); + return 0; +} +int Native_StartSpawner(Handle plugin, int numParams) { + int client = GetNativeCell(1); + g_PropData[client].Selector.Cancel(); + PrivateForward fwd = new PrivateForward(ET_Ignore, Param_Cell, Param_Cell, Param_Cell); + fwd.AddFunction(INVALID_HANDLE, GetNativeFunction(2)); + Editor[client].SetCallback(fwd, false); + ShowCategoryList(client, ROOT_CATEGORY); + return 0; +} +int Native_CancelEdit(Handle plugin, int numParams) { + int client = GetNativeCell(1); + Editor[client].Cancel(); + return 0; +} +int Native_IsEditorActive(Handle plugin, int numParams) { + int client = GetNativeCell(1); + Editor[client].IsActive(); + return 0; +} + +int Native_StartSelector(Handle plugin, int numParams) { + int client = GetNativeCell(1); + int color[3] = { 0, 255, 0 }; + PrivateForward fwd = new PrivateForward(ET_Single, Param_Cell, Param_Cell); + fwd.AddFunction(plugin, GetNativeFunction(2)); + GetNativeArray(3, color, 3); + int limit = GetNativeCell(4); + g_PropData[client].Selector.Start(color, 0, limit); + g_PropData[client].Selector.SetOnEnd(fwd); + return 0; +} +int Native_CancelSelector(Handle plugin, int numParams) { + int client = GetNativeCell(1); + g_PropData[client].Selector.Cancel(); + return 0; +} +int Native_IsSelectorActive(Handle plugin, int numParams) { + int client = GetNativeCell(1); + g_PropData[client].Selector.IsActive(); + return 0; +} +int Native_Selector_Start(Handle plugin, int numParams) { + int client = GetNativeCell(1); + int color[3] = { 0, 255, 0 }; + GetNativeArray(2, color, 3); + int flags = GetNativeCell(3); + int limit = GetNativeCell(4); + g_PropData[client].Selector.Start(color, flags, limit); + return 0; +} +int Native_Selector_GetCount(Handle plugin, int numParams) { + int client = GetNativeCell(1); + if(!g_PropData[client].Selector.IsActive()) { + return -1; + } else { + return g_PropData[client].Selector.list.Length; + } +} +int Native_Selector_GetActive(Handle plugin, int numParams) { + int client = GetNativeCell(1); + return g_PropData[client].Selector.IsActive(); +} +int Native_Selector_SetOnEnd(Handle plugin, int numParams) { + int client = GetNativeCell(1); + PrivateForward fwd = new PrivateForward(ET_Ignore, Param_Cell, Param_Cell); + fwd.AddFunction(plugin, GetNativeFunction(2)); + g_PropData[client].Selector.SetOnEnd(fwd); + return 0; +} +int Native_Selector_SetOnPreSelect(Handle plugin, int numParams) { + int client = GetNativeCell(1); + PrivateForward fwd = new PrivateForward(ET_Single, Param_Cell, Param_Cell); + if(!fwd.AddFunction(plugin, GetNativeFunction(2))) return 0; + g_PropData[client].Selector.SetOnPreSelect(fwd); + return 1; +} +int Native_Selector_SetOnPostSelect(Handle plugin, int numParams) { + int client = GetNativeCell(1); + PrivateForward fwd = new PrivateForward(ET_Ignore, Param_Cell, Param_Cell); + if(!fwd.AddFunction(plugin, GetNativeFunction(2))) return 0; + g_PropData[client].Selector.SetOnPostSelect(fwd); + return 1; +} +int Native_Selector_SetOnUnselect(Handle plugin, int numParams) { + int client = GetNativeCell(1); + PrivateForward fwd = new PrivateForward(ET_Ignore, Param_Cell, Param_Cell); + if(!fwd.AddFunction(plugin, GetNativeFunction(2))) return 0; + g_PropData[client].Selector.SetOnUnselect(fwd); + return 1; +} +int Native_Selector_AddEntity(Handle plugin, int numParams) { + int client = GetNativeCell(1); + int entity = GetNativeCell(2); + g_PropData[client].Selector.AddEntity(entity, false); + return 0; +} +int Native_Selector_RemoveEntity(Handle plugin, int numParams) { + int client = GetNativeCell(1); + int entity = GetNativeCell(2); + g_PropData[client].Selector.RemoveEntity(entity); + return 0; +} +int Native_Selector_Cancel(Handle plugin, int numParams) { + int client = GetNativeCell(1); + g_PropData[client].Selector.Cancel(); + return 0; +} +int Native_Selector_End(Handle plugin, int numParams) { + int client = GetNativeCell(1); + g_PropData[client].Selector.End(); + return 0; +} \ No newline at end of file diff --git a/scripting/include/editor/props/base.sp b/scripting/include/editor/props/base.sp new file mode 100644 index 0000000..d45f337 --- /dev/null +++ b/scripting/include/editor/props/base.sp @@ -0,0 +1,708 @@ +int g_pendingSaveClient; +ArrayList g_previewItems; +CategoryData ROOT_CATEGORY; +ArrayList g_spawnedItems; // ArrayList(block=2) +ArrayList g_savedItems; // ArrayList +StringMap g_recentItems; // Key: model[128], value: RecentEntry + +/* Wish to preface this file: +* It's kinda messy. The main structs are: +* - ItemData +* - CategoryData + +The rest are kinda necessary, for sorting reasons (SearchData, RecentEntry). + +*/ +enum ChatPrompt { + Prompt_None, + Prompt_Search, + Prompt_SaveScene, + Prompt_SaveSchematic, + Prompt_SaveCollection +} +enum SaveType { + Save_None, + Save_Scene, + Save_Schematic +} + +int GLOW_MANAGER[3] = { 52, 174, 235 }; + +enum struct Schematic { + char name[64]; + char creatorSteamid[32]; + char creatorName[32]; + ArrayList entities; + + void Reset() { + this.name[0] = '\0'; + this.creatorSteamid[0] = '\0'; + this.creatorName[0] = '\0'; + if(this.entities != null) delete this.entities; + } + + void AddEntity(int entity, int client) { + SaveData save; + save.FromEntity(entity); + this.entities.PushArray(save); + } + + void New(int client, const char[] name) { + if(client > 0) { + GetClientName(client, this.creatorName, sizeof(this.creatorName)); + GetClientAuthId(client, AuthId_Steam2, this.creatorSteamid, sizeof(this.creatorSteamid)); + } + strcopy(this.name, sizeof(this.name), name); + this.entities = new ArrayList(sizeof(SaveData)); + } + + bool Save() { + char path[PLATFORM_MAX_PATH]; + BuildPath(Path_SM, path, sizeof(path), "data/prop_spawner/schematics/%s.schem", this.name); + CreateDirectory("data/prop_spawner/schematics", 0775); + KeyValues kv = new KeyValues(this.name); + kv.SetString("creator_steamid", this.creatorSteamid); + kv.SetString("creator_name", this.creatorName); + kv.JumpToKey("entities"); + this.entities = new ArrayList(sizeof(SaveData)); + SaveData ent; + while(kv.GotoNextKey()) { + kv.GetVector("offset", ent.origin); + kv.GetVector("angles", ent.angles); + kv.GetColor4("color", ent.color); + kv.GetString("model", ent.model, sizeof(ent.model)); + this.entities.PushArray(ent); + } + kv.ExportToFile(path); + delete kv; + return true; + } + + bool Import(const char[] name) { + char path[PLATFORM_MAX_PATH]; + BuildPath(Path_SM, path, sizeof(path), "data/prop_spawner/schematics/%s.schem", name); + KeyValues kv = new KeyValues("root"); + if(kv.ImportFromFile(path)) { + delete kv; + return false; + } + strcopy(this.name, sizeof(this.name), name); + kv.GetString("creator_steamid", this.creatorSteamid, sizeof(this.creatorSteamid)); + kv.GetString("creator_name", this.creatorName, sizeof(this.creatorName)); + kv.JumpToKey("entities"); + this.entities = new ArrayList(sizeof(SaveData)); + SaveData ent; + while(kv.GotoNextKey()) { + kv.GetVector("offset", ent.origin); + kv.GetVector("angles", ent.angles); + kv.GetColor4("color", ent.color); + kv.GetString("model", ent.model, sizeof(ent.model)); + this.entities.PushArray(ent); + } + delete kv; + return true; + } + + /// Spawns all schematics entities, returns list of entities, first being parent. + ArrayList SpawnEntities(const float origin[3], bool asPreview = true) { + if(this.entities == null) return null; + SaveData ent; + int parent = -1; + ArrayList spawnedEntities = new ArrayList(); + for(int i = 0; i < this.entities.Length; i++) { + this.entities.GetArray(i, ent, sizeof(ent)); + int entity = ent.ToEntity(origin, asPreview); + spawnedEntities.Push(EntIndexToEntRef(entity)); + if(i == 0) { + SetParent(entity, parent) + } else { + parent = entity; + } + } + return spawnedEntities; + } +} +public any Native_SpawnSchematic(Handle plugin, int numParams) { + char name[32]; + float pos[3]; + float ang[3]; + GetNativeString(0, name, sizeof(name)); + GetNativeArray(1, pos, 3); + GetNativeArray(1, ang, 3); + Schematic schem; + if(!schem.Import(name)) { + return false; + } + ArrayList list = schem.SpawnEntities(pos, false); + delete list; + return true; +} + +enum struct PropSelectorIterator { + ArrayList _list; + int _index; + int Entity; + + void _Init(ArrayList list) { + this._list = list; + this._index = -1; + } + + bool Next() { + this._index++; + return this._index + 1 < this._list.Length; + } + +} + + +enum struct PropSelector { + int selectColor[3]; + int limit; + ArrayList list; + PrivateForward endCallback; + PrivateForward selectPreCallback; + PrivateForward selectPostCallback; + PrivateForward unSelectCallback; + int _client; + + PropSelectorIterator Iter() { + PropSelectorIterator iter; + iter._Init(this.list); + return iter; + } + + void Reset() { + if(this.endCallback) delete this.endCallback; + if(this.selectPreCallback) delete this.selectPreCallback; + if(this.selectPostCallback) delete this.selectPostCallback; + if(this.unSelectCallback) delete this.unSelectCallback; + if(this.list) delete this.list; + } + + void Start(int color[3], int flags = 0, int limit = 0) { + this.selectColor = color; + this.limit = 0; + this.list = new ArrayList(); + SendEditorMessage(this._client, "\x05Left click\x01 to select, \x05right click\x01 to unselect"); + SendEditorMessage(this._client, "Press \x05WALK+USE\x01 to confirm, \x05DUCK+USE\x01 to cancel"); + if(Editor[this._client].IsActive()) { + Editor[this._client].Cancel(); + } + } + + void SetOnEnd(PrivateForward callback) { + this.endCallback = callback; + } + void SetOnPreSelect(PrivateForward callback) { + this.selectPreCallback = callback; + } + void SetOnPostSelect(PrivateForward callback) { + this.selectPostCallback = callback; + } + void SetOnUnselect(PrivateForward callback) { + this.unSelectCallback = callback; + } + + void StartDirect(int color[3], SelectDoneCallback callback, int limit = 0) { + PrivateForward fwd = new PrivateForward(ET_Ignore, Param_Cell, Param_Cell); + fwd.AddFunction(INVALID_HANDLE, callback); + this.Start(color, 0, limit); + this.SetOnEnd(fwd); + } + + bool IsActive() { + return this.list != null; + } + + ArrayList End() { + if(this.list == null) return null; + SendEditorMessage(this._client, "Selection completed"); + // Reset glows, remove selection from our spawned props + for(int i = 0; i < this.list.Length; i++) { + int ref = this.list.Get(i); + if(IsValidEntity(ref)) { + L4D2_RemoveEntityGlow(ref); + RemoveSpawnedProp(ref); + } + } + if(this.endCallback) { + if(GetForwardFunctionCount(this.endCallback) == 0) { + PrintToServer("[Editor] Warn: Selector.End(): callback has no functions assigned to it."); + } + Call_StartForward(this.endCallback); + Call_PushCell(this._client); + Call_PushCell(this.list.Clone()); + int result = Call_Finish(); + if(result != SP_ERROR_NONE) { + PrintToServer("[Editor] Warn: Selector.End() forward error: %d", result); + } + } + ArrayList copy = this.list; + this.list = null; + this.Reset(); + return copy; + } + + void Cancel() { + if(this.endCallback) { + Call_StartForward(this.endCallback); + Call_PushCell(this._client); + Call_PushCell(INVALID_HANDLE); + Call_Finish(); + } + if(this.list) { + for(int i = 0; i < this.list.Length; i++) { + int ref = this.list.Get(i); + L4D2_RemoveEntityGlow(ref); + } + } + PrintToChat(this._client, "\x04[Editor]\x01 Selection cancelled."); + this.Reset(); + } + + int GetEntityRefIndex(int ref) { + int index = this.list.FindValue(ref); + if(index > -1) { + return index; + } + return -1; + } + + void Clear() { + if(this.list) { + this.list.Clear(); + } + } + + /** Removes entity from list + * @return returns entity ref of entity removed + */ + int RemoveEntity(int entity) { + if(this.list == null) return -2; + + L4D2_RemoveEntityGlow(entity); + int ref = EntIndexToEntRef(entity); + int index = this.GetEntityRefIndex(ref); + if(index > -1) { + this.list.Erase(index); + if(this.unSelectCallback != null) { + Call_StartForward(this.unSelectCallback) + Call_PushCell(this._client); + Call_PushCell(EntRefToEntIndex(ref)); + Call_Finish(); + } + return ref; + } + return INVALID_ENT_REFERENCE; + } + + /** + * Adds entity to list + * @return index into list of entity + * @return -1 if already added + * @return -2 if callback rejected + */ + int AddEntity(int entity, bool useCallback = true) { + if(this.list == null) return -2; + + int ref = EntIndexToEntRef(entity); + if(this.GetEntityRefIndex(ref) == -1) { + if(this.selectPreCallback != null && useCallback) { + Call_StartForward(this.selectPreCallback) + Call_PushCell(this._client); + Call_PushCell(entity); + bool allowed = true; + PrintToServer("Selector.AddEntity: PRE CALLBACK pre finish"); + Call_Finish(allowed); + PrintToServer("Selector.AddEntity: PRE CALLBACK pre result %b", allowed); + if(!allowed) return -2; + } + + L4D2_SetEntityGlow(entity, L4D2Glow_Constant, 10000, 0, this.selectColor, false); + int index = this.list.Push(ref); + PrintToServer("Selector.AddEntity: post CALLBACK pre"); + if(this.selectPostCallback != null && useCallback) { + Call_StartForward(this.selectPostCallback) + Call_PushCell(this._client); + Call_PushCell(entity); + //Call_PushCell(index); + Call_Finish(); + } + PrintToServer("Selector.AddEntity: post CALLBACK post"); + return index; + } + return -1; + } +} +enum struct PlayerPropData { + 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[64]; + ChatPrompt chatPrompt; + PropSelector Selector; + SaveType pendingSaveType; + + Schematic schematic; + int highlightedEntityRef; + int managerEntityRef; + + void Init(int client) { + this.Selector._client = client; + } + // Called on PlayerDisconnect + void Reset() { + if(this.Selector.IsActive()) this.Selector.Cancel(); + this.chatPrompt = Prompt_None; + this.clearListBuffer = false; + this.lastCategoryIndex = 0; + this.lastItemIndex = 0; + this.lastActiveTime = 0; + this.classnameOverride[0] = '\0'; + this.CleanupBuffers(); + this.pendingSaveType = Save_None; + this.schematic.Reset(); + this.managerEntityRef = INVALID_ENT_REFERENCE; + this.StopHighlight(); + } + + void StartHighlight(int entity) { + this.highlightedEntityRef = EntIndexToEntRef(entity); + L4D2_SetEntityGlow(entity, L4D2Glow_Constant, 10000, 0, GLOW_MANAGER, false); + } + void StopHighlight() { + if(IsValidEntity(this.highlightedEntityRef)) { + L4D2_RemoveEntityGlow(this.highlightedEntityRef); + } + this.highlightedEntityRef = INVALID_ENT_REFERENCE; + } + + void StartSchematic(int client, const char[] name) { + this.schematic.New(client, name); + this.pendingSaveType = Save_Schematic; + PrintToChat(client, "\x04[Editor]\x01 Started new schematic: \x05%s", name); + ShowCategoryList(client, ROOT_CATEGORY); + } + + // Sets the list buffer + 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 CleanupBuffers() { + this.ClearItemBuffer(); + if(this.categoryStack != null) { + delete this.categoryStack; + } + this.clearListBuffer = false; + } +} +PlayerPropData g_PropData[MAXPLAYERS+1]; + + +enum struct CategoryData { + // The display name of category + char name[64]; + // If set, overwrites the classname it is spawned as + char classnameOverride[64]; + bool hasItems; // true: items is ArrayList, false: items is ArrayList + ArrayList items; +} +enum struct ItemData { + char model[128]; + char name[64]; + + void FromSearchData(SearchData search) { + strcopy(this.model, sizeof(this.model), search.model); + strcopy(this.name, sizeof(this.name), search.name); + } +} +enum struct SearchData { + char model[128]; + char name[64]; + int index; + + void FromItemData(ItemData item) { + strcopy(this.model, sizeof(this.model), item.model); + strcopy(this.name, sizeof(this.name), item.name); + } +} + +methodmap ColorObject < JSONObject { + /// Creates a new Color with RGB between 0-255 + public ColorObject(int r = 255, int g = 255, int b = 255, int alpha = 255) { + JSONObject obj = new JSONObject(); + obj.SetInt("r", r); + obj.SetInt("g", g); + obj.SetInt("b", b); + obj.SetInt("alpha", alpha); + return view_as(obj); + } + + property int R { + public get() { return view_as(this).GetInt("r"); } + public set(int value) { view_as(this).SetInt("r", value); } + } + property int G { + public get() { return view_as(this).GetInt("g"); } + public set(int value) { view_as(this).SetInt("g", value); } + } + property int B { + public get() { return view_as(this).GetInt("b"); } + public set(int value) { view_as(this).SetInt("b", value); } + } + property int Alpha { + public get() { return view_as(this).GetInt("alpha"); } + public set(int value) { view_as(this).SetInt("alpha", value); } + } +} +methodmap Coordinates < JSONObject { + public Coordinates(float x = 0.0, float y = 0.0, float z = 0.0) { + JSONObject obj = new JSONObject(); + obj.SetFloat("x", x); + obj.SetFloat("y", y); + obj.SetFloat("z", z); + return view_as(obj); + } + + property float X { + public get() { return view_as(this).GetFloat("x"); } + public set(float value) { view_as(this).SetFloat("x", value); } + } + property float Y { + public get() { return view_as(this).GetFloat("y"); } + public set(float value) { view_as(this).SetFloat("y", value); } + } + property float Z { + public get() { return view_as(this).GetFloat("z"); } + public set(float value) { view_as(this).SetFloat("z", value); } + } + + public static Coordinates FromVec(const float vec[3]) { + return new Coordinates(vec[0], vec[1], vec[2]); + } + + public void ToVec(float vec[3]) { + vec[0] = this.X; + vec[1] = this.Y; + vec[2] = this.Z; + } +} + +methodmap SpawnerEntity < JSONObject { + property buildType BuildType { + public get() { + return view_as(view_as(this).GetInt("buildType")); + } + public set(buildType value) { + view_as(this).SetInt("buildType", value); + } + } + + property ColorObject Color { + public get() { return view_as(view_as(this).Get("color")); } + } + + property Coordinates Origin { + public get() { return view_as(view_as(this).Get("origin")); } + public set(Coordinates newVec) { + view_as(this).Remove("origin"); + view_as(this).Set("origin", newVec); + } + } + + property Coordinates Angles { + public get() { return view_as(view_as(this).Get("angles")); } + public set(Coordinates newVec) { + view_as(this).Remove("angles"); + view_as(this).Set("angles", newVec); + } + } + + public int GetModel(char[] output, int maxlen) { + return view_as(this).GetString("model", output, maxlen); + } + + public void SetModel(const char[] input) { + view_as(this).SetString("model", input); + } + + public static SpawnerEntity FromEntity(int entity) { + char buffer[128]; + GetEntityClassname(entity, buffer, sizeof(buffer)); + JSONObject obj = new JSONObject(); + obj.SetString("type", buffer); + GetEntPropString(entity, Prop_Data, "m_ModelName", buffer, sizeof(buffer)); + obj.SetString("model", buffer); + int color[4]; + GetEntityRenderColor(entity, color[0], color[1], color[2], color[3]); + obj.Set("color", view_as(new ColorObject(color[0], color[1], color[2], color[3]))); + // this.type = Build_Solid; + // if(StrEqual(this.model, "prop_physics")) this.type = Build_Physics; + // else if(StrEqual(this.model, "prop_dynamic")) { + // if(GetEntProp(entity, Prop_Send, "m_nSolidType") == 0) { + // this.type = Build_NonSolid; + // } + // } + SpawnerEntity se = view_as(obj); + float vec[3]; + GetEntPropVector(entity, Prop_Send, "m_vecOrigin", vec); + se.Origin = Coordinates.FromVec(vec); + GetEntPropVector(entity, Prop_Send, "m_angRotation", vec); + se.Angles = Coordinates.FromVec(vec); + return se; + } + + public int CreateEntity(const float offset[3], bool asPreview = true) { + + } +} + +enum struct SaveData { + char model[128]; + buildType type; + float origin[3]; + float angles[3]; + int color[4]; + + void FromEntity(int entity) { + // Use this.model as a buffer: + GetEntityClassname(entity, this.model, sizeof(this.model)); + this.type = Build_Solid; + if(StrEqual(this.model, "prop_physics")) this.type = Build_Physics; + else if(StrEqual(this.model, "prop_dynamic")) { + if(GetEntProp(entity, Prop_Send, "m_nSolidType") == 0) { + this.type = Build_NonSolid; + } + } + + GetEntPropString(entity, Prop_Data, "m_ModelName", this.model, sizeof(this.model)); + GetEntPropVector(entity, Prop_Send, "m_vecOrigin", this.origin); + GetEntPropVector(entity, Prop_Send, "m_angRotation", this.angles); + GetEntityRenderColor(entity, this.color[0],this.color[1],this.color[2],this.color[3]); + } + + int ToEntity(const float offset[3], bool asPreview = true) { + int entity = -1; + if(this.type == Build_Physics) + entity = CreateEntityByName("prop_physics"); + else + entity = CreateEntityByName("prop_dynamic_override"); + if(entity == -1) { + return -1; + } + PrecacheModel(this.model); + DispatchKeyValue(entity, "model", this.model); + DispatchKeyValue(entity, "targetname", "saved_prop"); + if(asPreview) { + DispatchKeyValue(entity, "rendermode", "1"); + DispatchKeyValue(entity, "solid", "0"); + } else { + DispatchKeyValue(entity, "solid", this.type == Build_NonSolid ? "0" : "6"); + } + float pos[3]; + for(int i = 0; i < 3; i++) + pos[i] = this.origin[i] + offset[i]; + + TeleportEntity(entity, pos, this.angles, NULL_VECTOR); + if(!DispatchSpawn(entity)) { + return -1; + } + int alpha = asPreview ? 200 : this.color[3]; + SetEntityRenderColor(entity, this.color[0], this.color[1], this.color[2], alpha); + + if(asPreview) + g_previewItems.Push(EntIndexToEntRef(entity)); + else + AddSpawnedItem(entity); + return entity; + } + + + void Serialize(char[] output, int maxlen) { + Format( + output, maxlen, "%s,%d,%.2f,%.2f,%.2f,%.2f,%.2f,%.2f,%d,%d,%d,%d", + this.model, this.type, this.origin[0], this.origin[1], this.origin[2], + this.angles[0], this.angles[1], this.angles[2], + this.color[0], this.color[1], this.color[2], this.color[3] + ); + } + void Deserialize(const char[] input) { + char buffer[16]; + int index = SplitString(input, ",", this.model, sizeof(this.model)); + index += SplitString(input[index], ",", buffer, sizeof(buffer)); + this.type = view_as(StringToInt(buffer)); + for(int i = 0; i < 3; i++) { + index += SplitString(input[index], ",", buffer, sizeof(buffer)); + this.origin[i] = StringToFloat(buffer); + } + for(int i = 0; i < 3; i++) { + index += SplitString(input[index], ",", buffer, sizeof(buffer)); + this.angles[i] = StringToFloat(buffer); + } + for(int i = 0; i < 4; i++) { + index += SplitString(input[index], ",", buffer, sizeof(buffer)); + this.color[i] = StringToInt(buffer); + } + } +} + +enum struct RecentEntry { + char name[64]; + int count; +} + +#include +#include +#include +#include +#include \ No newline at end of file diff --git a/scripting/include/editor/props/cmd.sp b/scripting/include/editor/props/cmd.sp new file mode 100644 index 0000000..aa7d28c --- /dev/null +++ b/scripting/include/editor/props/cmd.sp @@ -0,0 +1,139 @@ +Action Command_Props(int client, int args) { + char arg[32]; + GetCmdArg(1, arg, sizeof(arg)); + if(args == 0 || StrEqual(arg, "help")) { + PrintToChat(client, "See console for available sub-commands"); + PrintToConsole(client, "help - this"); + PrintToConsole(client, "list - lists all props and their distances"); + PrintToConsole(client, "search "); + 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"); + PrintToConsole(client, "schem[atic] "); + } else if(StrEqual(arg, "schem") || StrEqual(arg, "schematic")) { + char arg2[16]; + GetCmdArg(2, arg2, sizeof(arg2)); + if(StrEqual(arg2, "new")) { + char name[32]; + GetCmdArg(3, name, sizeof(name)); + if(name[0] == '\0') { + PrintToChat(client, "\x04[Editor]\x01 Please enter a name"); + } else { + g_PropData[client].StartSchematic(client, name); + } + } else if(StrEqual(arg2, "save")) { + if(g_PropData[client].pendingSaveType == Save_Schematic) { + g_PropData[client].schematic.Save(); + } else { + PrintToChat(client, "\x04[Editor]\x01 No schematic to save."); + } + } else { + PrintToChat(client, "\x04[Editor]\x01 Unknown option: %s", arg2); + } + } else if(StrEqual(arg, "list")) { + char arg2[16]; + GetCmdArg(2, arg2, sizeof(arg2)); + bool isClassname = StrEqual(arg2, "classname"); + bool isIndex = StrEqual(arg2, "index"); + bool isOwner = StrEqual(arg2, "owner"); + if(args == 1 || isClassname || isIndex || isOwner) { + PrintToChat(client, "\x04[Editor]\x01 Please specify: \x05classname, index, owner. "); + return Plugin_Handled; + } + float pos[3], propPos[3], dist; + GetAbsOrigin(client, pos); + for(int i = 0; i < g_spawnedItems.Length; i++) { + int ref = GetSpawnedItem(i); + if(ref > -1) { + GetEntPropVector(ref, Prop_Send, "m_vecOrigin", propPos); + dist = GetVectorDistance(pos, propPos, false); + if(isIndex) { + int entity = EntRefToEntIndex(ref); + PrintToConsole(client, "%d. ent #%d - %.0fu away", i, entity, dist); + } else if(isClassname) { + char classname[32]; + GetEntityClassname(ref, classname, sizeof(classname)); + PrintToConsole(client, "%d. %s - %.0fu away", i, classname, dist); + } else if(isOwner) { + int spawner = g_spawnedItems.Get(i, 1); + int player = GetClientOfUserId(spawner); + if(player > 0) { + PrintToConsole(client, "%d. %N - %.0fu away", i, player, dist); + } else { + PrintToConsole(client, "%d. (disconnected) - %.0fu away", i, dist); + } + } + } + } + PrintToChat(client, "\x04[Editor]\x01 Check console"); + } else if(StrEqual(arg, "search")) { + if(args == 1) { + PrintToChat(client, "\x04[Editor]\x01 Enter your search query:"); + g_PropData[client].chatPrompt = Prompt_Search; + } else { + char query[32]; + GetCmdArg(2, query, sizeof(query)); + DoSearch(client, query); + } + } else if(StrEqual(arg, "edit")) { + char arg2[32]; + GetCmdArg(2, arg2, sizeof(arg2)); + int index; + if(StrEqual(arg2, "last")) { + // Get last one + index = GetSpawnedIndex(client, -1); + } else { + index = StringToInt(arg2); + } + if(index >= 0 && index < g_spawnedItems.Length) { + int entity = EntRefToEntIndex(g_spawnedItems.Get(index)); + Editor[client].Import(entity); + PrintToChat(client, "\x04[Editor]\x01 Editing entity \x05%d", entity); + } else { + PrintToChat(client, "\x04[Editor]\x01 Invalid index, out of bounds. Enter a value between [0, %d]", g_spawnedItems.Length - 1); + } + } else if(StrEqual(arg, "del")) { + char arg2[32]; + GetCmdArg(2, arg2, sizeof(arg2)); + int index; + if(StrEqual(arg2, "last")) { + // Get last one + index = GetSpawnedIndex(client, -1); + } else { + index = StringToInt(arg2); + } + + if(index >= 0 && index < g_spawnedItems.Length) { + int entity = EntRefToEntIndex(g_spawnedItems.Get(index)); + if(entity > 0 && IsValidEntity(entity)) { + RemoveEntity(entity); + } + g_spawnedItems.Erase(index); + PrintToChat(client, "\x04[Editor]\x01 Deleted entity \x05%d", entity); + } else { + PrintToChat(client, "\x04[Editor]\x01 Invalid index, out of bounds. Enter a value between [0, %d]", g_spawnedItems.Length - 1); + } + } 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"); + } + return Plugin_Handled; +} \ No newline at end of file diff --git a/scripting/include/editor/props/db.sp b/scripting/include/editor/props/db.sp new file mode 100644 index 0000000..87c3519 --- /dev/null +++ b/scripting/include/editor/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/editor/props/menu_handlers.sp b/scripting/include/editor/props/menu_handlers.sp new file mode 100644 index 0000000..b8cc691 --- /dev/null +++ b/scripting/include/editor/props/menu_handlers.sp @@ -0,0 +1,517 @@ +TopMenuObject g_propSpawnerCategory; +public void OnAdminMenuReady(Handle topMenuHandle) { + TopMenu topMenu = TopMenu.FromHandle(topMenuHandle); + if(g_topMenu != topMenuHandle) { + g_propSpawnerCategory = topMenu.AddCategory("hats_editor", Category_Handler, "sm_prop"); + if(g_propSpawnerCategory != INVALID_TOPMENUOBJECT) { + topMenu.AddItem("editor_spawn", AdminMenu_Spawn, g_propSpawnerCategory, "sm_prop"); + // topMenu.AddItem("editor_edit", AdminMenu_Edit, g_propSpawnerCategory, "sm_prop"); + topMenu.AddItem("editor_delete", AdminMenu_Delete, g_propSpawnerCategory, "sm_prop"); + topMenu.AddItem("editor_saveload", AdminMenu_SaveLoad, g_propSpawnerCategory, "sm_prop"); + topMenu.AddItem("editor_manager", AdminMenu_Manager, g_propSpawnerCategory, "sm_prop"); + topMenu.AddItem("editor_selector", AdminMenu_Selector, g_propSpawnerCategory, "sm_prop"); + } + g_topMenu = topMenu; + } +} + +///////////// +// HANDLERS +///////////// +void Category_Handler(TopMenu topmenu, TopMenuAction action, TopMenuObject topobj_id, int param, char[] buffer, int maxlength) { + if(action == TopMenuAction_DisplayTitle) { + Format(buffer, maxlength, "Select a task:"); + } else if(action == TopMenuAction_DisplayOption) { + Format(buffer, maxlength, "Spawn Props"); + } +} + +void AdminMenu_Selector(TopMenu topmenu, TopMenuAction action, TopMenuObject object_id, int param, char[] buffer, int maxlength) { + if(action == TopMenuAction_DisplayOption) { + Format(buffer, maxlength, "Selector"); + } else if(action == TopMenuAction_SelectOption) { + ShowManagerSelectorMenu(param); + } +} + + +void AdminMenu_Spawn(TopMenu topmenu, TopMenuAction action, TopMenuObject object_id, int param, char[] buffer, int maxlength) { + if(action == TopMenuAction_DisplayOption) { + Format(buffer, maxlength, "Spawn Props"); + } else if(action == TopMenuAction_SelectOption) { + ConVar cheats = FindConVar("sm_cheats"); + if(cheats != null && !cheats.BoolValue) { + CReplyToCommand(param, "\x04[Editor] \x01Set \x05sm_cheats\x01 to \x051\x01 to use the prop spawner"); + return; + } + ShowSpawnRoot(param); + } +} + +int Spawn_RootHandler(Menu menu, MenuAction action, int client, int param2) { + if (action == MenuAction_Select) { + char info[2]; + menu.GetItem(param2, info, sizeof(info)); + switch(info[0]) { + case 'f': Spawn_ShowFavorites(client); + case 'r': Spawn_ShowRecents(client); + case 's': Spawn_ShowSearch(client); + case 'n': ShowCategoryList(client, ROOT_CATEGORY); + } + // TODO: handle back (to top menu) + } else if (action == MenuAction_Cancel) { + if(param2 == MenuCancel_ExitBack) { + DisplayTopMenuCategory(g_topMenu, g_propSpawnerCategory, client); + } + } else if (action == MenuAction_End) + delete menu; + return 0; +} +// void AdminMenu_Edit(TopMenu topmenu, TopMenuAction action, TopMenuObject object_id, int param, char[] buffer, int maxlength) { +// if(action == TopMenuAction_DisplayOption) { +// Format(buffer, maxlength, "Edit Props"); +// } else if(action == TopMenuAction_SelectOption) { +// ShowEditList(param); +// } +// } +void AdminMenu_Delete(TopMenu topmenu, TopMenuAction action, TopMenuObject object_id, int param, char[] buffer, int maxlength) { + if(action == TopMenuAction_DisplayOption) { + Format(buffer, maxlength, "Delete Props"); + } else if(action == TopMenuAction_SelectOption) { + ShowDeleteList(param); + } +} + +void AdminMenu_SaveLoad(TopMenu topmenu, TopMenuAction action, TopMenuObject object_id, int param, char[] buffer, int maxlength) { + if(action == TopMenuAction_DisplayOption) { + Format(buffer, maxlength, "Save / Load"); + } else if(action == TopMenuAction_SelectOption) { + Spawn_ShowSaveLoadMainMenu(param); + } +} + +void AdminMenu_Manager(TopMenu topmenu, TopMenuAction action, TopMenuObject object_id, int param, char[] buffer, int maxlength) { + if(action == TopMenuAction_DisplayOption) { + Format(buffer, maxlength, "Manage Props"); + } else if(action == TopMenuAction_SelectOption) { + Spawn_ShowManagerMainMenu(param); + } +} + +int SaveLoadMainMenuHandler(Menu menu, MenuAction action, int client, int param2) { + if (action == MenuAction_Select) { + char info[2]; + menu.GetItem(param2, info, sizeof(info)); + SaveType type = view_as(StringToInt(info)); + ShowSaves(client, type); + } else if (action == MenuAction_Cancel) { + if(param2 == MenuCancel_ExitBack) { + DisplayTopMenuCategory(g_topMenu, g_propSpawnerCategory, client); + } + } else if (action == MenuAction_End) + delete menu; + return 0; +} + +int SaveLoadSceneHandler(Menu menu, MenuAction action, int client, int param2) { + if (action == MenuAction_Select) { + char saveName[64]; + menu.GetItem(param2, saveName, sizeof(saveName)); + if(saveName[0] == '\0') { + // Save new + FormatTime(saveName, sizeof(saveName), "%Y-%m-%d_%H-%I-%M"); + if(CreateSceneSave(saveName)) { + PrintToChat(client, "\x04[Editor]\x01 Saved as \x05%s/%s.txt", g_currentMap, saveName); + } else { + PrintToChat(client, "\x04[Editor]\x01 Unable to save. Sorry."); + } + } else if(g_pendingSaveClient != 0 && g_pendingSaveClient != client) { + PrintToChat(client, "\x04[Editor]\x01 Another user is currently loading a save."); + } else if(g_PropData[client].pendingSaveType == Save_Schematic) { + PrintToChat(client, "\x04[Editor]\x01 Please complete or cancel current schematic to continue."); + } else if(LoadScene(saveName, true)) { + ConfirmSave(client, 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"); + } else { + PrintToChat(client, "\x04[Editor]\x01 Could not load save file."); + } + } else if (action == MenuAction_Cancel) { + if(param2 == MenuCancel_ExitBack) { + Spawn_ShowSaveLoadMainMenu(client); + } + } else if (action == MenuAction_End) + delete menu; + return 0; +} + + +int SaveLoadSchematicHandler(Menu menu, MenuAction action, int client, int param2) { + if (action == MenuAction_Select) { + char saveName[64]; + menu.GetItem(param2, saveName, sizeof(saveName)); + Schematic schem; + if(saveName[0] == '\0') { + if(g_PropData[client].pendingSaveType == Save_Schematic) { + if(g_PropData[client].schematic.Save()) { + PrintToChat(client, "\x04[Editor]\x01 Saved schematic as \x05%s", g_PropData[client].schematic.name); + } else { + PrintToChat(client, "\x04[Editor]\x01 Failed to save schematic."); + } + g_PropData[client].schematic.Reset(); + g_PropData[client].pendingSaveType = Save_None; + } else { + g_PropData[client].chatPrompt = Prompt_SaveSchematic; + PrintToChat(client, "\x04[Editor]\x01 Enter in chat a name for schematic"); + } + } else if(schem.Import(saveName)) { + float pos[3]; + GetCursorLocation(client, pos); + ArrayList list = schem.SpawnEntities(pos, true); + SaveData save; + int parent = list.GetArray(0, save); + delete list; + Editor[client].Import(parent); + if(g_pendingSaveClient != 0 && g_pendingSaveClient != client) { + PrintToChat(client, "\x04[Editor]\x01 Another user is currently loading a scene."); + } else { + g_pendingSaveClient = client; + PrintToChat(client, "\x04[Editor]\x01 Previewing schematic \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."); + } + } else if (action == MenuAction_Cancel) { + if(param2 == MenuCancel_ExitBack) { + Spawn_ShowSaveLoadMainMenu(client); + } + } else if (action == MenuAction_End) + delete menu; + return 0; +} + +int SaveLoadConfirmHandler(Menu menu, MenuAction action, int client, int param2) { + if (action == MenuAction_Select) { + ClearSavePreview(); + char info[64]; + menu.GetItem(param2, info, sizeof(info)); + if(info[0] != '\0') { + PrintToChat(client, "\x04[Editor]\x01 Loaded scene \x05%s", info); + LoadScene(info, false); + } + } else if (action == MenuAction_Cancel) { + if(param2 == MenuCancel_ExitBack) { + Spawn_ShowSaveLoadMainMenu(client); + } + } else if (action == MenuAction_End) + delete menu; + return 0; +} +int ManagerHandler(Menu menu, MenuAction action, int client, int param2) { + if (action == MenuAction_Select) { + char info[8]; + menu.GetItem(param2, info, sizeof(info)); + if(info[0] != '\0') { + int index = StringToInt(info); + int ref = g_spawnedItems.Get(index); + // TODO: add delete confirm + if(!IsValidEntity(ref)) { + SendEditorMessage(client, "Entity has disappeared"); + } else { + int entity = EntRefToEntIndex(ref); + g_PropData[client].managerEntityRef = ref; + g_PropData[client].StartHighlight(entity); + ShowManagerEntityMenu(client, entity); + } + } + } else if (action == MenuAction_Cancel) { + if(param2 == MenuCancel_ExitBack) { + DisplayTopMenuCategory(g_topMenu, g_propSpawnerCategory, client); + } + } else if (action == MenuAction_End) + delete menu; + return 0; +} +int ManagerEntityHandler(Menu menu, MenuAction action, int client, int param2) { + if (action == MenuAction_Select) { + g_PropData[client].StopHighlight(); + char info[32]; + menu.GetItem(param2, info, sizeof(info)); + int ref = g_PropData[client].managerEntityRef; + if(!IsValidEntity(ref)) { + SendEditorMessage(client, "Entity disappeared"); + } else if(StrEqual(info, "edit")) { + Editor[client].ImportEntity(EntRefToEntIndex(ref), Edit_Manager); + return 0; + } else if(StrEqual(info, "delete")) { + for(int i = 0; i < g_spawnedItems.Length; i++) { + int spawnedRef = g_spawnedItems.Get(i); + if(spawnedRef == ref) { + g_spawnedItems.Erase(i); + break; + } + } + if(IsValidEntity(ref)) { + RemoveEntity(ref); + } + return 0; + } else if(StrEqual(info, "view")) { + ReplyToCommand(client, "Maybe soon."); + } else if(StrEqual(info, "select")) { + int entity = EntRefToEntIndex(ref); + g_PropData[client].Selector.AddEntity(entity); + } else { + SendEditorMessage(client, "Unknown option / not implemented"); + } + ShowManagerSelectorMenu(client); + } else if (action == MenuAction_Cancel) { + g_PropData[client].StopHighlight(); + if(param2 == MenuCancel_ExitBack) { + Spawn_ShowManagerMainMenu(client); + } + } else if (action == MenuAction_End) + delete menu; + return 0; +} +int ManagerSelectorMainMenuHandler(Menu menu, MenuAction action, int client, int param2) { + if (action == MenuAction_Select) { + EntitySelector sel = EntitySelector.FromClient(client); + if(!sel.Active) { + return 0; + } + char info[32]; + menu.GetItem(param2, info, sizeof(info)); + if(StrEqual(info, "list")) { + SendEditorMessage(client, "Not implemented"); + } else if(StrEqual(info, "actions")) { + ShowManagerSelectorActionsMenu(client); + } else if(StrEqual(info, "add-self")) { + int userid = GetClientUserId(client); + int count; + for(int i = 0; i < g_spawnedItems.Length; i++) { + int ref = g_spawnedItems.Get(i); + int spawnedBy = g_spawnedItems.Get(i, 1); + if(spawnedBy == userid) { + sel.AddEntity(EntRefToEntIndex(ref)); + count++; + } + } + ReplyToCommand(client, "Added %d entities", count); + ShowManagerSelectorMenu(client); + } else if(StrEqual(info, "add-all")) { + int count; + for(int i = 0; i < g_spawnedItems.Length; i++) { + int ref = g_spawnedItems.Get(i); + sel.AddEntity(EntRefToEntIndex(ref)); + count++; + } + ReplyToCommand(client, "Added %d entities", count); + ShowManagerSelectorMenu(client); + } else if(StrEqual(info, "cancel")) { + g_PropData[client].Selector.Cancel(); + } + } else if (action == MenuAction_Cancel) { + g_PropData[client].Selector.Cancel(); + } else if (action == MenuAction_End) + delete menu; + return 0; +} +int ManagerSelectorActionHandler(Menu menu, MenuAction action, int client, int param2) { + if (action == MenuAction_Select) { + if(!g_PropData[client].Selector.IsActive()) { + return 0; + } + char info[32]; + menu.GetItem(param2, info, sizeof(info)); + if(StrEqual(info, "delete")) { + int count; + for(int i = 0; i < g_PropData[client].Selector.list.Length; i++) { + int ref = g_PropData[client].Selector.list.Get(i); + if(IsValidEntity(ref)) { + RemoveEntity(ref); + count++; + } + } + ArrayList list = g_PropData[client].Selector.End(); + delete list; + SendEditorMessage(client, "Deleted %d entities", count); + Spawn_ShowManagerMainMenu(client); + } else if(StrEqual(info, "clear")) { + g_PropData[client].Selector.Clear(); + SendEditorMessage(client, "Cleared selection."); + Spawn_ShowManagerMainMenu(client); + } else if(StrEqual(info, "save_scene")) { + ArrayList items = g_PropData[client].Selector.End(); + g_PropData[client].SetItemBuffer(items, true); + g_PropData[client].chatPrompt = Prompt_SaveScene; + SendEditorMessage(client, "Enter name for scene:"); + } else if(StrEqual(info, "save_collection")) { + ArrayList items = g_PropData[client].Selector.End(); + g_PropData[client].SetItemBuffer(items, true); + g_PropData[client].chatPrompt = Prompt_SaveCollection; + SendEditorMessage(client, "Enter name for collection:"); + } else { + SendEditorMessage(client, "Unknown option / not implemented"); + } + } else if (action == MenuAction_Cancel) { + if(param2 == MenuCancel_ExitBack) { + ShowManagerSelectorMenu(client); + } + } else if (action == MenuAction_End) + delete menu; + return 0; +} +int COLOR_DELETE[3] = { 255, 0, 0 } + +int DeleteHandler(Menu menu, MenuAction action, int client, int param2) { + if (action == MenuAction_Select) { + char info[128]; + menu.GetItem(param2, info, sizeof(info)); + int ref = StringToInt(info[2]); + int option = StringToInt(info); + if(option == -1) { + // Delete all (everyone) + int count = DeleteAll(); + PrintToChat(client, "\x04[Editor]\x01 Deleted \x05%d\x01 items", count); + ShowDeleteList(client); + } else if(option == -2) { + // Delete all (mine only) + int count = DeleteAll(client); + PrintToChat(client, "\x04[Editor]\x01 Deleted \x05%d\x01 items", count); + ShowDeleteList(client); + } else if(option == -3) { + if(g_PropData[client].Selector.IsActive()) { + g_PropData[client].Selector.End(); + PrintToChat(client, "\x04[Editor]\x01 Delete tool cancelled"); + } else { + g_PropData[client].Selector.StartDirect(COLOR_DELETE, OnDeleteToolEnd); + 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 { + int index = g_spawnedItems.FindValue(ref); + if(IsValidEntity(ref)) { + RemoveEntity(ref); + } + if(index > -1) { + g_spawnedItems.Erase(index); + index--; + } else { index = 0; } + ShowDeleteList(client, index); + } + } else if (action == MenuAction_Cancel) { + if(param2 == MenuCancel_ExitBack) { + DisplayTopMenuCategory(g_topMenu, g_propSpawnerCategory, client); + } + } else if (action == MenuAction_End) + delete menu; + 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]; + menu.GetItem(param2, info, sizeof(info)); + char index[4]; + char model[128]; + int nameIndex = SplitString(info, "|", index, sizeof(index)); + nameIndex += SplitString(info[nameIndex], "|", model, sizeof(model)); + g_PropData[client].lastItemIndex = StringToInt(index); + if(Editor[client].PreviewModel(model, g_PropData[client].classnameOverride)) { + 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); + } + // Use same item menu again: + ShowItemMenu(client); + } else if(action == MenuAction_Cancel) { + g_PropData[client].ClearItemBuffer(); + if(param2 == MenuCancel_ExitBack) { + 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(); + } + } else if (action == MenuAction_End) { + delete menu; + } + return 0; +} + +// int EditHandler(Menu menu, MenuAction action, int client, int param2) { +// if (action == MenuAction_Select) { +// char info[8]; +// menu.GetItem(param2, info, sizeof(info)); +// int ref = StringToInt(info); +// int index = g_spawnedItems.FindValue(ref); +// int entity = EntRefToEntIndex(ref); +// if(entity > 0) { +// Editor[client].Import(entity, false); +// PrintToChat(client, "\x04[Editor]\x01 Editing entity \x05%d", entity); +// } else { +// PrintToChat(client, "\x04[Editor]\x01 Entity disappeared."); +// if(index > -1) { +// g_spawnedItems.Erase(index); +// index--; +// } else { index = 0; } +// } +// ShowEditList(client, index); +// } else if (action == MenuAction_Cancel) { +// if(param2 == MenuCancel_ExitBack) { +// DisplayTopMenuCategory(g_topMenu, g_propSpawnerCategory, client); +// } +// } else if (action == MenuAction_End) +// delete menu; +// return 0; +// } diff --git a/scripting/include/editor/props/menu_methods.sp b/scripting/include/editor/props/menu_methods.sp new file mode 100644 index 0000000..dd58f38 --- /dev/null +++ b/scripting/include/editor/props/menu_methods.sp @@ -0,0 +1,317 @@ +///////////// +// METHODS +///////////// +void ShowSpawnRoot(int client) { + Menu menu = new Menu(Spawn_RootHandler); + menu.SetTitle("Choose spawn list:"); + menu.AddItem("f", "Favorites (Broken :D)"); + menu.AddItem("r", "Recently Spawned Props"); + menu.AddItem("s", "Search for Props"); + menu.AddItem("n", "Browse Props"); + menu.ExitBackButton = true; + menu.ExitButton = true; + menu.Display(client, MENU_TIME_FOREVER); +} +void Spawn_ShowRecents(int client) { + if(g_recentItems == null) LoadRecents(); + ArrayList items = GetRecentsItemList(); + if(items.Length == 0) { + CReplyToCommand(client, "\x04[Editor] \x01No recent props spawned."); + DisplayTopMenuCategory(g_topMenu, g_propSpawnerCategory, client); + return; + } + ShowTempItemMenu(client, items, "Recents"); +} +void Spawn_ShowSearch(int client) { + g_PropData[client].chatPrompt = Prompt_Search; + CReplyToCommand(client, "\x04[Editor] \x01Please enter search query in chat:"); +} +void ShowDeleteList(int client, int index = -3) { + if(g_spawnedItems.Length == 0) { + SendEditorMessage(client, "No spawned items to delete"); + DisplayTopMenuCategory(g_topMenu, g_propSpawnerCategory, client); + return; + } + Menu menu = new Menu(DeleteHandler); + menu.SetTitle("Delete Props"); + + menu.AddItem("-1", "Delete All"); + menu.AddItem("-2", "Delete All (Mine Only)"); + menu.AddItem("-3", "Delete Tool"); + // menu.AddItem("-4", "Delete Last Save"); + char info[8]; + char buffer[128]; + for(int i = 0; i < g_spawnedItems.Length; i++) { + int ref = GetSpawnedItem(i); + if(ref == -1) continue; + Format(info, sizeof(info), "0|%d", ref); + GetEntPropString(ref, Prop_Data, "m_ModelName", buffer, sizeof(buffer)); + index = FindCharInString(buffer, '/', true); + if(index != -1) + menu.AddItem(info, buffer[index + 1]); + } + + menu.ExitBackButton = true; + menu.ExitButton = true; + // Add +3 to the index for the 3 "Delete ..." buttons + // TODO: restore the delete index issue, use /7*7 + menu.DisplayAt(client, 0, MENU_TIME_FOREVER); +} +// void ShowEditList(int client, int index = 0) { +// if(g_spawnedItems.Length == 0) { +// SendEditorMessage(client, "No spawned items to edit"); +// DisplayTopMenuCategory(g_topMenu, g_propSpawnerCategory, client); +// return; +// } +// Menu menu = new Menu(EditHandler); +// menu.SetTitle("Edit Prop"); + +// char info[8]; +// char buffer[32]; +// for(int i = 0; i < g_spawnedItems.Length; i++) { +// int ref = GetSpawnedItem(i); +// if(ref == -1) continue; +// Format(info, sizeof(info), "%d", ref); +// GetEntPropString(ref, Prop_Data, "m_ModelName", buffer, sizeof(buffer)); +// index = FindCharInString(buffer, '/', true); +// if(index != -1) +// menu.AddItem(info, buffer[index + 1]); +// } + +// menu.ExitBackButton = true; +// menu.ExitButton = true; +// // Add +2 to the index for the two "Delete ..." buttons +// menu.DisplayAt(client, index, MENU_TIME_FOREVER); +// } +void ShowCategoryList(int client, CategoryData category) { + LoadCategories(); + char info[4]; + // No category list provided, use the global one. + 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); + 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; + // Round to page instead of index (int division) + int index = g_PropData[client].lastCategoryIndex / 7 * 7; + menu.DisplayAt(client, index, MENU_TIME_FOREVER); +} +void _showItemMenu(int client, ArrayList items, const char[] title = "", bool clearArray = false, const char[] classnameOverride = "") { + if(items == null) { + // Use previous list buffer + items = g_PropData[client].itemBuffer; + if(items == null) { + LogError("Previous list does not exist and no new list was provided ShowItemMenu(%N)", client); + PrintToChat(client, "\x04[Editor]\x01 An error occurred (no list)"); + return; + } + } else { + // 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); + } + if(items.Length == 0) { + PrintToChat(client, "\x04[Editor]\x01 No items to show."); + return; + } + Menu itemMenu = new Menu(SpawnItemHandler); + if(title[0] != '\0') + itemMenu.SetTitle(title); + ItemData item; + 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, for recents to work + Format(info, sizeof(info), "%d|%s|%s", i, item.model, item.name); + itemMenu.AddItem(info, item.name); + } + itemMenu.ExitBackButton = true; + itemMenu.ExitButton = true; + // We don't want to start at the index but the page of the index + 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); +} + +void Spawn_ShowFavorites(int client) { + if(g_db == null) { + PrintToChat(client, "\x04[Editor]\x01 Cannot connect to database."); + return; + } + 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)); +} + +void Spawn_ShowSaveLoadMainMenu(int client) { + Menu menu = new Menu(SaveLoadMainMenuHandler); + menu.SetTitle("Save / Load"); + // Id is SaveType + menu.AddItem("1", "Map Scenes"); + menu.AddItem("2", "Schematics"); + menu.ExitBackButton = true; + menu.ExitButton = true; + menu.Display(client, MENU_TIME_FOREVER); +} + +void Spawn_ShowManagerMainMenu(int client, int index = 0) { + if(g_spawnedItems.Length == 0) { + SendEditorMessage(client, "No spawned items to manage"); + DisplayTopMenuCategory(g_topMenu, g_propSpawnerCategory, client); + return; + } + Menu menu = new Menu(ManagerHandler); + menu.SetTitle("Manager"); + // Id is SaveType + char info[8]; + char buffer[128]; + for(int i = 0; i < g_spawnedItems.Length; i++) { + int ref = GetSpawnedItem(i); + if(ref == -1) continue; + IntToString(i, info, sizeof(info)); + GetEntPropString(ref, Prop_Data, "m_ModelName", buffer, sizeof(buffer)); + index = FindCharInString(buffer, '/', true); + if(index != -1) + menu.AddItem(info, buffer[index + 1]); + } + + menu.ExitBackButton = true; + menu.ExitButton = true; + menu.DisplayAt(client, index, MENU_TIME_FOREVER); +} +void ShowManagerEntityMenu(int client, int entity) { + if(!IsValidEntity(entity)) { + SendEditorMessage(client, "Item has vanished"); + Spawn_ShowManagerMainMenu(client); + return; + } + Menu menu = new Menu(ManagerEntityHandler); + menu.SetTitle("Manage %d", entity); + menu.AddItem("edit", "Edit"); + menu.AddItem("delete", "Delete"); + menu.AddItem("select", "Select"); + menu.AddItem("view", "View"); + menu.ExitBackButton = true; + menu.ExitButton = true; + menu.Display(client, MENU_TIME_FOREVER); +} +void ShowManagerSelectorMenu(int client) { + EntitySelector sel = EntitySelector.FromClient(client); + if(!sel.Active) { + sel.Start(GLOW_MANAGER); + sel.SetOnEnd(OnManagerSelectorEnd); + sel.SetOnPostSelect(OnManagerSelectorSelect); + sel.SetOnUnselect(OnManagerSelectorSelect); + } + Menu menu = new Menu(ManagerSelectorMainMenuHandler); + menu.SetTitle("Selector"); + menu.AddItem("list", "> List Entities"); + menu.AddItem("actions", "> Actions"); + menu.AddItem("add-self", "Add All Self-Spawned"); + menu.AddItem("add-all", "Add All Spawned"); + menu.ExitBackButton = false; + menu.ExitButton = true; + menu.Display(client, MENU_TIME_FOREVER); +} +void ShowManagerSelectorActionsMenu(int client) { + Menu menu = new Menu(ManagerSelectorActionHandler); + menu.SetTitle("Selector: Select action"); + char display[32]; + Format(display, sizeof(display), "Entities: %d", g_PropData[client].Selector.list.Length); + menu.AddItem("", display, ITEMDRAW_DISABLED); + + // menu.AddItem("edit", "Edit"); + menu.AddItem("delete", "Delete Entities"); + menu.AddItem("clear", "Clear Selection"); + // menu.AddItem("select", "Select"); + menu.AddItem("save_scene", "Save as Scene"); + menu.AddItem("save_collection", "Save as Collection"); + menu.ExitBackButton = true; + menu.ExitButton = true; + menu.Display(client, MENU_TIME_FOREVER); +} + +void ShowSaves(int client, SaveType type) { + ArrayList saves; + Menu newMenu; + if(type == Save_Scene) { + newMenu = new Menu(SaveLoadSceneHandler); + newMenu.SetTitle("Save & Load > Map Scenes"); + newMenu.AddItem("", "[Save New Scene]"); + saves = LoadScenes(); + } else if(type == Save_Schematic) { + newMenu = new Menu(SaveLoadSchematicHandler); + newMenu.SetTitle("Save & Load > Schematics"); + if(g_PropData[client].pendingSaveType == Save_Schematic) { + newMenu.AddItem("", "[Save Schematic]"); + } else { + newMenu.AddItem("", "[Start New Schematic]"); + // Don't load saves when in middle of creating schematic + saves = LoadSchematics(); + } + } + if(saves != null) { + char name[64]; + for(int i = 0; i < saves.Length; i++) { + saves.GetString(i, name, sizeof(name)); + newMenu.AddItem(name, name); + } + delete saves; + } + newMenu.ExitBackButton = true; + newMenu.ExitButton = true; + newMenu.Display(client, MENU_TIME_FOREVER); +} \ No newline at end of file diff --git a/scripting/include/editor/props/methods.sp b/scripting/include/editor/props/methods.sp new file mode 100644 index 0000000..cef42f1 --- /dev/null +++ b/scripting/include/editor/props/methods.sp @@ -0,0 +1,565 @@ + +ArrayList LoadScenes() { + char path[PLATFORM_MAX_PATH]; + BuildPath(Path_SM, path, sizeof(path), "data/prop_spawner/saves/%s", g_currentMap); + FileType fileType; + DirectoryListing listing = OpenDirectory(path); + if(listing == null) return null; + char buffer[64]; + ArrayList saves = new ArrayList(ByteCountToCells(64)); + while(listing.GetNext(buffer, sizeof(buffer), fileType)) { + if(buffer[0] == '.') continue; + saves.PushString(buffer); + } + delete listing; + return saves; +} + +ArrayList LoadSchematics() { + char path[PLATFORM_MAX_PATH]; + BuildPath(Path_SM, path, sizeof(path), "data/prop_spawner/schematics"); + FileType fileType; + DirectoryListing listing = OpenDirectory(path); + if(listing == null) return null; + char buffer[64]; + ArrayList saves = new ArrayList(ByteCountToCells(64)); + while(listing.GetNext(buffer, sizeof(buffer), fileType) && fileType == FileType_File) { + if(buffer[0] == '.') continue; + saves.PushString(buffer); + } + delete listing; + return saves; +} + +bool LoadScene(const char[] save, bool asPreview = false) { + char path[PLATFORM_MAX_PATH]; + BuildPath(Path_SM, path, sizeof(path), "data/prop_spawner/saves/%s/%s", g_currentMap, save); + // ArrayList savedItems = new ArrayList(sizeof(SaveData)); + File file = OpenFile(path, "r"); + if(file == null) return false; + char buffer[256]; + if(asPreview) { + // Kill any previous preview + if(g_previewItems != null) ClearSavePreview(); + g_previewItems = new ArrayList(); + } + SaveData data; + while(file.ReadLine(buffer, sizeof(buffer))) { + if(buffer[0] == '#') continue; + data.Deserialize(buffer); + int entity = data.ToEntity(NULL_VECTOR, asPreview); + if(entity == -1) { + PrintToServer("[Editor] LoadScene(\"%s\", %b): failed to create %s", save, asPreview, buffer); + continue; + } + } + delete file; + return true; +} + +void ConfirmSave(int client, const char[] name) { + Menu newMenu = new Menu(SaveLoadConfirmHandler); + newMenu.AddItem(name, "Spawn"); + newMenu.AddItem("", "Cancel"); + newMenu.ExitBackButton = false; + newMenu.ExitButton = false; + newMenu.Display(client, 0); +} +void ClearSavePreview() { + if(g_previewItems != null) { + for(int i = 0; i < g_previewItems.Length; i++) { + int ref = g_previewItems.Get(i); + if(IsValidEntity(ref)) { + RemoveEntity(ref); + } + } + delete g_previewItems; + } + g_pendingSaveClient = 0; +} + +void AddSpawnedItem(int entity, int client = 0) { + if(client > 0 && g_PropData[client].pendingSaveType == Save_Schematic) { + g_PropData[client].schematic.AddEntity(entity, client); + } + // TODO: confirm if we want it to be in list, otherwise we need to clean manually + int userid = client > 0 ? GetClientUserId(client) : 0; + int index = g_spawnedItems.Push(EntIndexToEntRef(entity)); + g_spawnedItems.Set(index, userid, 1); +} + +bool CreateCollection(const char[] folder, const char[] name, ArrayList entities, int client = 0) { + char path[PLATFORM_MAX_PATH], pathTemp[PLATFORM_MAX_PATH]; + BuildPath(Path_SM, path, sizeof(path), "data/prop_spawner/collections", folder); + CreateDirectory(path, 509); + Format(path, sizeof(path), "%s/%s", path, folder); + CreateDirectory(path, 509); + Format(pathTemp, sizeof(pathTemp), "%s/%s.json.tmp", path, name); + Format(path, sizeof(path), "%s/%s.json", path, name); + char buffer[132]; + JSONObject root = new JSONObject(); + FormatTime(buffer, sizeof(buffer), "%Y-%m-%dT%H:%M:%S.%f"); + root.SetString("created", buffer); + + if(client > 0) { + GetClientAuthId(client, AuthId_Steam2, buffer, sizeof(buffer)); + root.SetString("creator_steamid", buffer); + float vec[3]; + GetClientAbsOrigin(client, vec); + JSONObject origin = view_as(Coordinates.FromVec(vec)); + root.Set("origin", origin); + + GetClientEyeAngles(client, vec); + JSONObject angles = view_as(Coordinates.FromVec(vec)); + root.Set("angles", angles); + } + JSONArray entArr = new JSONArray(); + for(int i = 0; i < entities.Length; i++) { + int ref = entities.Get(i); + if(IsValidEntity(ref)) { + SpawnerEntity ent = SpawnerEntity.FromEntity(EntRefToEntIndex(ref)); + entArr.Push(ent); + } + } + root.Set("entities", entArr); + root.ToFile(pathTemp, JSON_INDENT(4)); + RenameFile(path, pathTemp); + SetFilePermissions(path, FPERM_U_WRITE | FPERM_U_READ | FPERM_G_WRITE | FPERM_G_READ | FPERM_O_READ); + LogAction(client, -1, "created collection \"%s\" in \"%s\"", name, path); + return true; +} + +bool CreateSceneSave(const char[] name, ArrayList items = null, int client = 0) { + char path[PLATFORM_MAX_PATH]; + BuildPath(Path_SM, path, sizeof(path), "data/prop_spawner/saves/%s", g_currentMap); + CreateDirectory(path, 509); + Format(path, sizeof(path), "%s/%s.txt", path, name); + File file = OpenFile(path, "w"); + if(file == null) { + PrintToServer("[Editor] Could not save: %s", path); + return false; + } + // TODO: switch to json + char buffer[132]; + SaveData data; + if(items == null) items = g_spawnedItems; + for(int i = 0; i < items.Length; i++) { + int ref = items.Get(i); + if(IsValidEntity(ref)) { + data.FromEntity(ref); + data.Serialize(buffer, sizeof(buffer)); + file.WriteLine("%s", buffer); + } + } + file.Flush(); + delete file; + return true; +} + +void UnloadSave() { + if(g_savedItems != null) { + delete g_savedItems; + } +} + +public void LoadCategories() { + 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(ROOT_CATEGORY.items, path); + ROOT_CATEGORY.items.SortCustom(SortCategories); +} +int SortCategories(int index1, int index2, ArrayList array, Handle hndl) { + CategoryData cat1; + array.GetArray(index1, cat1); + CategoryData cat2; + array.GetArray(index2, cat2); + return strcmp(cat1.name, cat2.name); +} +public void UnloadCategories() { + if(ROOT_CATEGORY.items == null) return; + _UnloadCategories(ROOT_CATEGORY.items); + delete ROOT_CATEGORY.items; +} +void _UnloadCategories(ArrayList list) { + CategoryData cat; + for(int i = 0; i < list.Length; i++) { + list.GetArray(i, cat); + _UnloadCategory(cat); + } +} +void _UnloadCategory(CategoryData cat) { + // Is a sub-category: + if(!cat.hasItems) { + _UnloadCategories(cat.items); + } + delete cat.items; +} + +void LoadFolder(ArrayList parent, const char[] rootPath) { + char buffer[PLATFORM_MAX_PATH]; + FileType fileType; + DirectoryListing listing = OpenDirectory(rootPath); + if(listing == null) { + LogError("Cannot open \"%s\"", rootPath); + } + while(listing.GetNext(buffer, sizeof(buffer), fileType)) { + if(fileType == FileType_Directory) { + // TODO: support subcategory + if(buffer[0] == '.') continue; + CategoryData data; + Format(data.name, sizeof(data.name), "%s", buffer); + data.items = new ArrayList(sizeof(CategoryData)); + + Format(buffer, sizeof(buffer), "%s/%s", rootPath, buffer); + LoadFolder(data.items, buffer); + parent.PushArray(data); + } else if(fileType == FileType_File) { + Format(buffer, sizeof(buffer), "%s/%s", rootPath, buffer); + LoadProps(parent, buffer); + } + } + delete listing; +} + +void LoadProps(ArrayList parent, const char[] filePath) { + File file = OpenFile(filePath, "r"); + if(file == null) { + PrintToServer("[Props] Cannot open file \"%s\"", filePath); + return; + } + CategoryData category; + category.items = new ArrayList(sizeof(ItemData)); + category.hasItems = true; + char buffer[128]; + if(!file.ReadLine(buffer, sizeof(buffer))) { + delete file; + return; + } + ReplaceString(buffer, sizeof(buffer), "\n", ""); + ReplaceString(buffer, sizeof(buffer), "\r", ""); + Format(category.name, sizeof(category.name), "%s", buffer); + while(file.ReadLine(buffer, sizeof(buffer))) { + if(buffer[0] == '#') continue; + ReplaceString(buffer, sizeof(buffer), "\n", ""); + ReplaceString(buffer, sizeof(buffer), "\r", ""); + ItemData item; + int index = SplitString(buffer, ":", item.model, sizeof(item.model)); + if(index == -1) { + index = SplitString(buffer, " ", item.model, sizeof(item.model)); + if(index == -1) { + // No name provided, use the model's filename + index = FindCharInString(buffer, '/', true); + strcopy(item.name, sizeof(item.name), item.model[index + 1]); + } else { + strcopy(item.name, sizeof(item.name), buffer[index]); + } + category.items.PushArray(item); + } else if(StrEqual(item.model, "Classname")) { + strcopy(category.classnameOverride, sizeof(category.classnameOverride), buffer[index]); + } else if(StrEqual(item.model, "Type")) { + Format(category.classnameOverride, sizeof(category.classnameOverride), "_%s", buffer[index]); + } + } + parent.PushArray(category); + delete file; +} +bool recentsChanged = false; +bool SaveRecents() { + if(!recentsChanged) return true; // Nothing to do, nothing changed + if(g_recentItems == null) return false; + char path[PLATFORM_MAX_PATH]; + BuildPath(Path_SM, path, sizeof(path), "data/prop_spawner/recents_cache.csv"); + File file = OpenFile(path, "w"); + if(file == null) { + PrintToServer("[Editor] Could not write to %s", path); + return false; + } + StringMapSnapshot snapshot = g_recentItems.Snapshot(); + char model[128]; + RecentEntry entry; + for(int i = 0; i < snapshot.Length; i++) { + snapshot.GetKey(i, model, sizeof(model)); + g_recentItems.GetArray(model, entry, sizeof(entry)); + file.WriteLine("%s,%s,%d", model, entry.name, entry.count); + } + file.Flush(); + delete file; + delete snapshot; + recentsChanged = false; + return true; +} +bool LoadRecents() { + if(g_recentItems != null) delete g_recentItems; + char path[PLATFORM_MAX_PATH]; + BuildPath(Path_SM, path, sizeof(path), "data/prop_spawner/recents_cache.csv"); + File file = OpenFile(path, "r"); + if(file == null) return false; + g_recentItems = new StringMap(); + char buffer[128+64+16]; + char model[128]; + RecentEntry entry; + while(file.ReadLine(buffer, sizeof(buffer))) { + int index = SplitString(buffer, ",", model, sizeof(model)); + index += SplitString(buffer[index], ",", entry.name, sizeof(entry.name)); + entry.count = StringToInt(buffer[index]); + g_recentItems.SetArray(model, entry, sizeof(entry)); + } + delete file; + return true; +} + +// Returns an ArrayList of all the recents +ArrayList GetRecentsItemList() { + ArrayList items = new ArrayList(sizeof(ItemData)); + StringMapSnapshot snapshot = g_recentItems.Snapshot(); + char model[128]; + RecentEntry entry; + ItemData item; + for(int i = 0; i < snapshot.Length; i++) { + snapshot.GetKey(i, model, sizeof(model)); + g_recentItems.GetArray(model, entry, sizeof(entry)); + strcopy(item.model, sizeof(item.model), model); + strcopy(item.name, sizeof(item.name), entry.name); + } + // This is pretty expensive in terms of allocations but shrug + items.SortCustom(SortRecents); + delete snapshot; + return items; +} + +int SortRecents(int index1, int index2, ArrayList array, Handle handle) { + ItemData data1; + array.GetArray(index1, data1); + ItemData data2; + array.GetArray(index2, data2); + + int count1, count2; + RecentEntry entry; + if(g_recentItems.GetArray(data1.model, entry, sizeof(entry))) return 0; //skip if somehow no entry + count1 = entry.count; + if(g_recentItems.GetArray(data2.model, entry, sizeof(entry))) return 0; //skip if somehow no entry + count2 = entry.count; + return count2 - count1; // desc +} + +void AddRecent(const char[] model, const char[] name) { + if(g_recentItems == null) { + if(!LoadRecents()) return; + } + RecentEntry entry; + if(!g_recentItems.GetArray(model, entry, sizeof(entry))) { + entry.count = 0; + strcopy(entry.name, sizeof(entry.name), name); + } + entry.count++; + recentsChanged = true; + g_recentItems.SetArray(model, entry, sizeof(entry)); +} +public Action OnClientSayCommand(int client, const char[] command, const char[] sArgs) { + if(g_PropData[client].chatPrompt == Prompt_None) { + return Plugin_Continue; + } + switch(g_PropData[client].chatPrompt) { + case Prompt_Search: DoSearch(client, sArgs); + case Prompt_SaveScene: { + if(CreateSceneSave(sArgs, g_PropData[client].itemBuffer, client)) { + PrintToChat(client, "\x04[Editor]\x01 Saved as \x05%s/%s.txt", g_currentMap, sArgs); + } else { + PrintToChat(client, "\x04[Editor]\x01 Unable to save. Sorry."); + } + } + case Prompt_SaveCollection: { + if(CreateCollection("global", sArgs, g_PropData[client].itemBuffer, client)) { + SendEditorMessage(client, "Saved \x05%s/%s.json\x04 successfully.", "global", sArgs); + } else { + SendEditorMessage(client, "Failed to save collection."); + } + // TODO: figure out how to know which way to return + ShowManagerSelectorMenu(client); + } + default: + PrintToChat(client, "\x04[Editor]\x01 Not implemented."); + } + g_PropData[client].chatPrompt = Prompt_None; + return Plugin_Handled; +} +void DoSearch(int client, const char[] query) { + ArrayList results = SearchItems(query); + if(results.Length == 0) { + CPrintToChat(client, "\x04[Editor]\x01 No results found. :("); + } else { + 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 +int GetSpawnedIndex(int client, int index) { + int userid = GetClientUserId(client); + if(index >= 0) { + for(int i = index; i < g_spawnedItems.Length; i++) { + int spawnedBy = g_spawnedItems.Get(i, 1); + if(spawnedBy == userid) { + return i; + } + } + } else { + for(int i = g_spawnedItems.Length + index; i >= 0; i--) { + int spawnedBy = g_spawnedItems.Get(i, 1); + if(spawnedBy == userid) { + return i; + } + } + } + return -1; +} +#define MAX_SEARCH_RESULTS 10 +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, ROOT_CATEGORY.items, query); + results.SortCustom(SortSearch); + ArrayList items = new ArrayList(sizeof(ItemData)); + ItemData item; + SearchData data; + for(int i = 0; i < results.Length; i++) { + results.GetArray(i, data); + item.FromSearchData(data); + items.PushArray(item); + } + delete results; + return items; +} + +int SortSearch(int index1, int index2, ArrayList array, Handle handle) { + SearchData data1; + array.GetArray(index1, data1); + SearchData data2; + array.GetArray(index2, data2); + return data1.index - data2.index; +} + +void _searchCategory(ArrayList results, ArrayList categories, const char[] query) { + CategoryData cat; + if(categories == null) return; + for(int i = 0; i < categories.Length; i++) { + categories.GetArray(i, cat); + if(cat.hasItems) { + //cat.items is of CatetoryData + if(!_searchItems(results, cat.items, query)) return; + } else { + //cat.items is of ItemData + _searchCategory(results, cat.items, 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); + if(searchIndex > -1) { + search.FromItemData(item); + search.index = searchIndex; + results.PushArray(search); + if(results.Length > MAX_SEARCH_RESULTS) return false; + } + } + return true; +} + +int GetSpawnedItem(int index) { + if(index < 0 || index >= g_spawnedItems.Length) return -1; + int ref = g_spawnedItems.Get(index); + if(!IsValidEntity(ref)) { + g_spawnedItems.Erase(index); + return -1; + } + return ref; +} + +bool RemoveSpawnedProp(int ref) { + // int ref = EntIndexToEntRef(entity); + int index = g_spawnedItems.FindValue(ref); + if(index > -1) { + g_spawnedItems.Erase(index); + return true; + } + return false; +} + +void OnDeleteToolEnd(int client, ArrayList entities) { + int count; + for(int i = 0; i < entities.Length; i++) { + int ref = entities.Get(i); + if(IsValidEntity(ref)) { + count++; + RemoveSpawnedProp(ref); + RemoveEntity(ref); + } + } + delete entities; + PrintToChat(client, "\x04[Editor]\x01 \x05%d\x01 entities deleted", count); +} + +void OnManagerSelectorEnd(int client, ArrayList entities) { + // TODO: implement manager selector cb + ReplyToCommand(client, "Not Implemented"); + Spawn_ShowManagerMainMenu(client); + if(entities != null) { + delete entities; + } +} +void OnManagerSelectorSelect(int client, int entity) { + // update entity count + // ShowManagerSelectorMenu(client); +} + +int DeleteAll(int onlyPlayer = 0) { + int userid = onlyPlayer > 0 ? GetClientUserId(onlyPlayer) : 0; + int count; + for(int i = 0; i < g_spawnedItems.Length; i++) { + int ref = g_spawnedItems.Get(i); + int spawnedBy = g_spawnedItems.Get(i, 1); + // Skip if wishing to only delete certain items: + if(onlyPlayer == 0 || spawnedBy == userid) { + if(IsValidEntity(ref)) { + RemoveEntity(ref); + } + // TODO: erasing while removing + g_spawnedItems.Erase(i); + i--; // go back up one + count++; + } + } + return count; +} + +#define SHOW_HINT_MIN_DURATION 600 // 600 s (10min) +void ShowHint(int client) { + int time = GetTime(); + int lastActive = g_PropData[client].lastActiveTime; + g_PropData[client].lastActiveTime = time; + if(time - lastActive < SHOW_HINT_MIN_DURATION) return; + PrintToChat(client, "\x01Change Mode: \x05ZOOM"); + PrintToChat(client, "\x01Place: \x05USE(E) \x01Cancel: \x05WALK(SHIFT) + USE(E)"); + PrintToChat(client, "\x01Rotate: \x05Hold RELOAD(R) + MOVE MOUSE\x01 Change Axis: \x05Left Click \x01Snap Angle: \x05Right Click"); + PrintToChat(client, "\x01Type \x05/prop favorite\x01 to (un)favorite."); + PrintToChat(client, "\x01More 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_editor.sp b/scripting/l4d2_editor.sp index cfc204e..fa55c29 100644 --- a/scripting/l4d2_editor.sp +++ b/scripting/l4d2_editor.sp @@ -14,6 +14,7 @@ #include #include #include +#include int g_iLaserIndex; @@ -94,7 +95,7 @@ public void OnPluginStart() { char targetName[32]; while((entity = FindEntityByClassname(entity, "func_brush")) != INVALID_ENT_REFERENCE) { GetEntPropString(entity, Prop_Data, "m_iName", targetName, sizeof(targetName)); - if(StrContains(targetName, "l4d2_hats_") == 0) { + if(StrContains(targetName, "editor") == 0) { createdWalls.Push(EntIndexToEntRef(entity)); SDKHook(entity, SDKHook_Use, OnWallClicked); } @@ -200,7 +201,8 @@ public Action OnPlayerRunCmd(int client, int& buttons, int& impulse, float vel[3 } else if(buttons & IN_USE) { if(buttons & IN_SPEED) { //Delete - g_PropData[client].Selector.End(); + ArrayList items = g_PropData[client].Selector.End(); + delete items; } else if(buttons & IN_DUCK) { //Cancel g_PropData[client].Selector.Cancel(); @@ -406,7 +408,7 @@ public void OnPluginEnd() { if(g_spawnedItems != null) { delete g_spawnedItems; } - TriggerInput("prop_preview", "Kill"); + TriggerInput("editor_preview", "Kill"); } public bool TraceEntityFilterPlayer(int entity, int contentsMask, any data) {