mirror of
https://github.com/lifestorm/wnsrc.git
synced 2025-12-16 21:33:46 +03:00
520 lines
13 KiB
Lua
520 lines
13 KiB
Lua
--[[
|
|
| This file was obtained through the combined efforts
|
|
| of Madbluntz & Plymouth Antiquarian Society.
|
|
|
|
|
| Credits: lifestorm, Gregory Wayne Rossel JR.,
|
|
| Maloy, DrPepper10 @ RIP, Atle!
|
|
|
|
|
| Visit for more: https://plymouth.thetwilightzone.ru/
|
|
--]]
|
|
|
|
|
|
module( "undo", package.seeall )
|
|
|
|
-- undo.Create("Wheel")
|
|
-- undo.AddEntity( axis )
|
|
-- undo.AddEntity( constraint )
|
|
-- undo.SetPlayer( self.Owner )
|
|
-- undo.Finish()
|
|
|
|
if ( CLIENT ) then
|
|
|
|
local ClientUndos = {}
|
|
local bIsDirty = true
|
|
|
|
--[[---------------------------------------------------------
|
|
GetTable
|
|
Returns the undo table for whatever reason
|
|
-----------------------------------------------------------]]
|
|
function GetTable()
|
|
return ClientUndos
|
|
end
|
|
|
|
--[[---------------------------------------------------------
|
|
UpdateUI
|
|
Actually updates the UI. Removes old controls and
|
|
re-creates them using the new data.
|
|
-----------------------------------------------------------]]
|
|
local function UpdateUI()
|
|
|
|
local Panel = controlpanel.Get( "Undo" )
|
|
if ( !IsValid( Panel ) ) then return end
|
|
|
|
Panel:Clear()
|
|
Panel:AddControl( "Header", { Description = "#spawnmenu.utilities.undo.help" } )
|
|
|
|
local ComboBox = Panel:ListBox()
|
|
ComboBox:SetTall( 500 )
|
|
|
|
local Limit = 100
|
|
local Count = 0
|
|
for k, v in ipairs( ClientUndos ) do
|
|
|
|
local Item = ComboBox:AddItem( tostring( v.Name ) )
|
|
Item.DoClick = function() RunConsoleCommand( "gmod_undonum", tostring( v.Key ) ) end
|
|
|
|
Count = Count + 1
|
|
if ( Count > Limit ) then break end
|
|
|
|
end
|
|
|
|
end
|
|
|
|
--[[---------------------------------------------------------
|
|
AddUndo
|
|
Called from server. Adds a new undo to our UI
|
|
-----------------------------------------------------------]]
|
|
net.Receive( "Undo_AddUndo", function()
|
|
|
|
local k = net.ReadInt( 16 )
|
|
local v = net.ReadString()
|
|
|
|
table.insert( ClientUndos, 1, { Key = k, Name = v } )
|
|
|
|
MakeUIDirty()
|
|
|
|
end )
|
|
|
|
-- Called from server, fires GM:OnUndo
|
|
net.Receive( "Undo_FireUndo", function()
|
|
|
|
local name = net.ReadString()
|
|
local hasCustomText = net.ReadBool()
|
|
local customtext
|
|
if ( hasCustomText ) then
|
|
customtext = net.ReadString()
|
|
end
|
|
|
|
hook.Run( "OnUndo", name, customtext )
|
|
|
|
end )
|
|
|
|
|
|
--[[---------------------------------------------------------
|
|
Undone
|
|
Called from server. Notifies us that one of our undos
|
|
has been undone or made redundant. We act by updating
|
|
out data (We wait until the UI is viewed until updating)
|
|
-----------------------------------------------------------]]
|
|
local function Undone()
|
|
|
|
local key = net.ReadInt( 16 )
|
|
|
|
local NewUndo = {}
|
|
local i = 1
|
|
for k, v in ipairs( ClientUndos ) do
|
|
|
|
if ( v.Key != key ) then
|
|
NewUndo [ i ] = v
|
|
i = i + 1
|
|
end
|
|
end
|
|
|
|
ClientUndos = NewUndo
|
|
NewUndo = nil
|
|
|
|
MakeUIDirty()
|
|
|
|
end
|
|
net.Receive( "Undo_Undone", Undone )
|
|
|
|
--[[---------------------------------------------------------
|
|
MakeUIDirty
|
|
Makes the UI dirty - it will re-create the controls
|
|
the next time it is viewed.
|
|
-----------------------------------------------------------]]
|
|
function MakeUIDirty()
|
|
|
|
bIsDirty = true
|
|
|
|
end
|
|
|
|
--[[---------------------------------------------------------
|
|
CPanelPaint
|
|
Called when the inner panel of the undo CPanel is painted
|
|
We hook onto this to determine when the panel is viewed.
|
|
When it's viewed we update the UI if it's marked as dirty
|
|
-----------------------------------------------------------]]
|
|
local function CPanelUpdate( panel )
|
|
|
|
-- This is kind of a shitty way of doing it - but we only want
|
|
-- to update the UI when it's visible.
|
|
if ( bIsDirty ) then
|
|
|
|
-- Doing this in a timer so it calls it in a sensible place
|
|
-- Calling in the paint function could cause all kinds of problems
|
|
-- It's a hack but hey welcome to GMod!
|
|
timer.Simple( 0, UpdateUI )
|
|
bIsDirty = false
|
|
|
|
end
|
|
|
|
end
|
|
|
|
--[[---------------------------------------------------------
|
|
SetupUI
|
|
Adds a hook (CPanelPaint) to the control panel paint
|
|
function so we can determine when it is being drawn.
|
|
-----------------------------------------------------------]]
|
|
function SetupUI()
|
|
|
|
local UndoPanel = controlpanel.Get( "Undo" )
|
|
if ( !IsValid( UndoPanel ) ) then return end
|
|
|
|
-- Mark as dirty please
|
|
MakeUIDirty()
|
|
|
|
-- Panels only think when they're visible
|
|
UndoPanel.Think = CPanelUpdate
|
|
|
|
end
|
|
|
|
hook.Add( "PostReloadToolsMenu", "BuildUndoUI", SetupUI )
|
|
|
|
end
|
|
|
|
|
|
if ( !SERVER ) then return end
|
|
|
|
local PlayerUndo = {}
|
|
-- PlayerUndo
|
|
-- - Player UniqueID
|
|
-- - Undo Table
|
|
-- - Name
|
|
-- - Entities (table of ents)
|
|
-- - Owner (player)
|
|
|
|
local Current_Undo = nil
|
|
|
|
util.AddNetworkString( "Undo_Undone" )
|
|
util.AddNetworkString( "Undo_AddUndo" )
|
|
util.AddNetworkString( "Undo_FireUndo" )
|
|
|
|
--[[---------------------------------------------------------
|
|
GetTable
|
|
Returns the undo table for whatever reason
|
|
-----------------------------------------------------------]]
|
|
function GetTable()
|
|
return PlayerUndo
|
|
end
|
|
|
|
--[[---------------------------------------------------------
|
|
GetTable
|
|
Save/Restore the undo tables
|
|
-----------------------------------------------------------]]
|
|
local function Save( save )
|
|
|
|
saverestore.WriteTable( PlayerUndo, save )
|
|
|
|
end
|
|
|
|
local function Restore( restore )
|
|
|
|
PlayerUndo = saverestore.ReadTable( restore )
|
|
|
|
end
|
|
|
|
saverestore.AddSaveHook( "UndoTable", Save )
|
|
saverestore.AddRestoreHook( "UndoTable", Restore )
|
|
|
|
--[[---------------------------------------------------------
|
|
Start a new undo
|
|
-----------------------------------------------------------]]
|
|
function Create( text )
|
|
|
|
Current_Undo = {}
|
|
Current_Undo.Name = text
|
|
Current_Undo.Entities = {}
|
|
Current_Undo.Owner = nil
|
|
Current_Undo.Functions = {}
|
|
|
|
end
|
|
|
|
--[[---------------------------------------------------------
|
|
Adds an entity to this undo (The entity is removed on undo)
|
|
-----------------------------------------------------------]]
|
|
function SetCustomUndoText( CustomUndoText )
|
|
|
|
if ( !Current_Undo ) then return end
|
|
|
|
Current_Undo.CustomUndoText = CustomUndoText
|
|
|
|
end
|
|
|
|
--[[---------------------------------------------------------
|
|
Adds an entity to this undo (The entity is removed on undo)
|
|
-----------------------------------------------------------]]
|
|
function AddEntity( ent )
|
|
|
|
if ( !Current_Undo ) then return end
|
|
if ( !IsValid( ent ) ) then return end
|
|
|
|
table.insert( Current_Undo.Entities, ent )
|
|
|
|
end
|
|
|
|
--[[---------------------------------------------------------
|
|
Add a function to call to this undo
|
|
-----------------------------------------------------------]]
|
|
function AddFunction( func, ... )
|
|
|
|
if ( !Current_Undo ) then return end
|
|
if ( !func ) then return end
|
|
|
|
table.insert( Current_Undo.Functions, { func, {...} } )
|
|
|
|
end
|
|
|
|
--[[---------------------------------------------------------
|
|
Replace Entity
|
|
-----------------------------------------------------------]]
|
|
function ReplaceEntity( from, to )
|
|
|
|
local ActionTaken = false
|
|
|
|
for _, PlayerTable in pairs( PlayerUndo ) do
|
|
for _, UndoTable in pairs( PlayerTable ) do
|
|
if ( UndoTable.Entities ) then
|
|
|
|
for key, ent in pairs( UndoTable.Entities ) do
|
|
if ( ent == from ) then
|
|
UndoTable.Entities[ key ] = to
|
|
ActionTaken = true
|
|
end
|
|
end
|
|
|
|
end
|
|
end
|
|
end
|
|
|
|
return ActionTaken
|
|
|
|
end
|
|
|
|
|
|
--[[---------------------------------------------------------
|
|
Sets who's undo this is
|
|
-----------------------------------------------------------]]
|
|
function SetPlayer( ply )
|
|
|
|
if ( !Current_Undo ) then return end
|
|
if ( !IsValid( ply ) ) then return end
|
|
|
|
Current_Undo.Owner = ply
|
|
|
|
end
|
|
|
|
--[[---------------------------------------------------------
|
|
SendUndoneMessage
|
|
Sends a message to notify the client that one of their
|
|
undos has been removed. They can then update their GUI.
|
|
-----------------------------------------------------------]]
|
|
local function SendUndoneMessage( ent, id, ply )
|
|
|
|
if ( !IsValid( ply ) ) then return end
|
|
|
|
-- For further optimization we could queue up the ids and send them
|
|
-- in one batch every 0.5 seconds or something along those lines.
|
|
|
|
net.Start( "Undo_Undone" )
|
|
net.WriteInt( id, 16 )
|
|
net.Send( ply )
|
|
|
|
end
|
|
|
|
--[[---------------------------------------------------------
|
|
Checks whether an undo is allowed to be created
|
|
-----------------------------------------------------------]]
|
|
local function Can_CreateUndo( undo )
|
|
|
|
local call = hook.Run( "CanCreateUndo", undo.Owner, undo )
|
|
|
|
return call == true or call == nil
|
|
|
|
end
|
|
|
|
--[[---------------------------------------------------------
|
|
Finish
|
|
-----------------------------------------------------------]]
|
|
function Finish( NiceText )
|
|
|
|
if ( !Current_Undo ) then return end
|
|
|
|
-- Do not add undos that have no owner or anything to undo
|
|
if ( !IsValid( Current_Undo.Owner ) or ( table.IsEmpty( Current_Undo.Entities ) && table.IsEmpty( Current_Undo.Functions ) ) or !Can_CreateUndo( Current_Undo ) ) then
|
|
Current_Undo = nil
|
|
return false
|
|
end
|
|
|
|
local index = Current_Undo.Owner:UniqueID()
|
|
PlayerUndo[ index ] = PlayerUndo[ index ] or {}
|
|
|
|
Current_Undo.NiceText = NiceText or Current_Undo.Name
|
|
|
|
local id = table.insert( PlayerUndo[ index ], Current_Undo )
|
|
|
|
net.Start( "Undo_AddUndo" )
|
|
net.WriteInt( id, 16 )
|
|
net.WriteString( Current_Undo.NiceText )
|
|
net.Send( Current_Undo.Owner )
|
|
|
|
-- Have one of the entities in the undo tell us when it gets undone.
|
|
if ( IsValid( Current_Undo.Entities[ 1 ] ) ) then
|
|
|
|
local ent = Current_Undo.Entities[ 1 ]
|
|
ent:CallOnRemove( "undo" .. id, SendUndoneMessage, id, Current_Undo.Owner )
|
|
|
|
end
|
|
|
|
Current_Undo = nil
|
|
|
|
return true
|
|
|
|
end
|
|
|
|
--[[---------------------------------------------------------
|
|
Undos an undo
|
|
-----------------------------------------------------------]]
|
|
function Do_Undo( undo )
|
|
|
|
if ( !undo ) then return false end
|
|
|
|
if ( hook.Run( "PreUndo", undo ) == false ) then return end
|
|
|
|
local count = 0
|
|
|
|
-- Call each function
|
|
if ( undo.Functions ) then
|
|
for index, func in pairs( undo.Functions ) do
|
|
|
|
local success = func[ 1 ]( undo, unpack( func[ 2 ] ) )
|
|
if ( success != false ) then
|
|
count = count + 1
|
|
end
|
|
|
|
end
|
|
end
|
|
|
|
-- Remove each entity in this undo
|
|
if ( undo.Entities ) then
|
|
for index, entity in pairs( undo.Entities ) do
|
|
|
|
if ( IsValid( entity ) ) then
|
|
entity:Remove()
|
|
count = count + 1
|
|
end
|
|
|
|
end
|
|
end
|
|
|
|
if ( count > 0 ) then
|
|
net.Start( "Undo_FireUndo" )
|
|
net.WriteString( undo.Name )
|
|
net.WriteBool( undo.CustomUndoText != nil )
|
|
if ( undo.CustomUndoText != nil ) then
|
|
net.WriteString( undo.CustomUndoText )
|
|
end
|
|
net.Send( undo.Owner )
|
|
end
|
|
|
|
hook.Run( "PostUndo", undo, count )
|
|
|
|
return count
|
|
|
|
end
|
|
|
|
--[[---------------------------------------------------------
|
|
Checks whether a player is allowed to undo
|
|
-----------------------------------------------------------]]
|
|
local function Can_Undo( ply, undo )
|
|
|
|
local call = hook.Run( "CanUndo", ply, undo )
|
|
|
|
return call == true or call == nil
|
|
|
|
end
|
|
|
|
--[[---------------------------------------------------------
|
|
Console command
|
|
-----------------------------------------------------------]]
|
|
local function CC_UndoLast( pl, command, args )
|
|
|
|
local index = pl:UniqueID()
|
|
PlayerUndo[ index ] = PlayerUndo[ index ] or {}
|
|
|
|
local last = nil
|
|
local lastk = nil
|
|
|
|
for k, v in pairs( PlayerUndo[ index ] ) do
|
|
|
|
lastk = k
|
|
last = v
|
|
|
|
end
|
|
|
|
-- No undos
|
|
if ( !last ) then return end
|
|
|
|
-- This is quite messy, but if the player rejoined the server
|
|
-- 'Owner' might no longer be a valid entity. So replace the Owner
|
|
-- with the player that is doing the undoing
|
|
last.Owner = pl
|
|
|
|
if ( !Can_Undo( pl, last ) ) then return end
|
|
|
|
local count = Do_Undo( last )
|
|
|
|
net.Start( "Undo_Undone" )
|
|
net.WriteInt( lastk, 16 )
|
|
net.Send( pl )
|
|
|
|
PlayerUndo[ index ][ lastk ] = nil
|
|
|
|
-- If our last undo object is already deleted then compact
|
|
-- the undos until we hit one that does something
|
|
if ( count == 0 ) then
|
|
CC_UndoLast( pl )
|
|
end
|
|
|
|
end
|
|
|
|
--[[---------------------------------------------------------
|
|
Console command
|
|
-----------------------------------------------------------]]
|
|
local function CC_UndoNum( ply, command, args )
|
|
|
|
if ( !args[ 1 ] ) then return end
|
|
|
|
local index = ply:UniqueID()
|
|
PlayerUndo[ index ] = PlayerUndo[ index ] or {}
|
|
|
|
local UndoNum = tonumber( args[ 1 ] )
|
|
if ( !UndoNum ) then return end
|
|
|
|
local TheUndo = PlayerUndo[ index ][ UndoNum ]
|
|
if ( !TheUndo ) then return end
|
|
|
|
-- Do the same as above
|
|
TheUndo.Owner = ply
|
|
|
|
if ( !Can_Undo( ply, TheUndo ) ) then return end
|
|
|
|
-- Undo!
|
|
Do_Undo( TheUndo )
|
|
|
|
-- Notify the client UI that the undo happened
|
|
-- This is normally called by the deleted entity via SendUndoneMessage
|
|
-- But in cases where the undo only has functions that will not do
|
|
net.Start( "Undo_Undone" )
|
|
net.WriteInt( UndoNum, 16 )
|
|
net.Send( ply )
|
|
|
|
-- Don't delete the entry completely so nothing new takes its place and ruin CC_UndoLast's logic (expecting newest entry be at highest index)
|
|
PlayerUndo[ index ][ UndoNum ] = {}
|
|
|
|
end
|
|
|
|
concommand.Add( "undo", CC_UndoLast, nil, "", { FCVAR_DONTRECORD } )
|
|
concommand.Add( "gmod_undo", CC_UndoLast, nil, "", { FCVAR_DONTRECORD } )
|
|
concommand.Add( "gmod_undonum", CC_UndoNum, nil, "", { FCVAR_DONTRECORD } )
|