Files
wnsrc/lua/includes/modules/undo.lua
lifestorm 94063e4369 Upload
2024-08-04 22:55:00 +03:00

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 } )