Skip to content

Instantly share code, notes, and snippets.

@aduermael
Last active April 14, 2023 18:17
Show Gist options
  • Save aduermael/f916172fbd18a3c2413f1f1d5274ae9a to your computer and use it in GitHub Desktop.
Save aduermael/f916172fbd18a3c2413f1f1d5274ae9a to your computer and use it in GitHub Desktop.
Config = {
Map = "aduermael.hills",
Items = { "jacksbertox.easel" }
}
-------------------
--== Constants ==--
-------------------
local EVENT_TYPE = {
LOAD = -3,
SAVE = -2,
SENDCLIENTDATA = -1,
SENDDATA = 0,
MOVE = 1,
CHAT = 2,
DRAW = 3,
UNDOREDO = 4,
PLACEEASEL = 5,
CANVASINTERACT = 6,
WIPLOADED = 100
}
local DATA_TYPE = {
USER = 0,
WIP = 1,
PAINTING = 2,
}
painting_keys = {
"data1",
"data2",
"data3",
"artist",
"name"
}
maxWIPs = 5
local encode_string = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ=+"
-------------------
--== Variables ==--
-------------------
default_color = 190
cameraSetPos = false
cameraTargetPos = nil
cameraTargetRot = nil
cameraPrevRot = nil
observe = false
canMove = true
candraw = false
chatting = true
saveBTN = nil
loadShown = false
loadQueue = {}
easels = {}
usrTags = {}
myWIPs = {}
myWIPsData = {}
--color costants
colorWhite = DefaultColors[229]
btnDisabled = Color(214, 214, 214, 20)
btnEnabled = Color(216, 216, 216)
btnToggleON = Color(0, 0, 0)
--a number converter
function toBase64(int)
local fin = ""
local i = 0
local n = int
while (n ~= 0) do
i = n % 64
n = math.floor(n / 64)
if i < 10 then
fin = i .. fin
else
fin = encode_string:sub(i - 9, i - 9) .. fin
end
end
return fin
end
function fromBase64(str)
local fin = 0
for i = 1, #str do
local c = str:sub(i, i)
local d, _ = string.find(encode_string, c)
if d then d = d + 9 end
local a = d or c
fin = 64 * fin + a
end
return math.floor(fin)
end
function canvasToString(c)
local fin = ""
for x = 0, c.Width - 1 do
for y = 0, c.Height - 1 do
local n = c:GetBlock(x, y, 0).PaletteIndex
local col = getIndexInArray(DefaultColors, c.Palette[n].Color)
local a = toBase64(col)
if #a < 2 then a = "0" .. a end
fin = fin .. a
end
end
return fin
end
function stringToCanvas(s, c)
local i = 0
for x = 0, c.Width - 1 do
for y = 0, c.Height - 1 do
local a = fromBase64(s:sub(i + 1, i + 2))
i = i + 2
c:GetBlock(x, y, 0):Replace(DefaultColors[a])
end
end
end
function createID()
local template = "xxxxxx"
return string.gsub(template, "[xy]", function(c)
local v = (c == "x") and math.random(0, 0xf) or math.random(8, 0xb)
return string.format("%x", v)
end)
end
function canvasData(canva)
local data = canvasToString(canva)
return { data1 = data:sub(1, 250),
data2 = data:sub(251, 500),
data3 = data:sub(501, 512),
name = canva.name or "???",
artist = canva.owner.Username
}
end
function tablelength(T)
local count = 0
for _ in pairs(T) do count = count + 1 end
return count
end
function getIndexInArray(array, item)
for i = 1, #array do
if array[i] == item then return i end
end
return nil
end
function saveData(key, args)
local e = Event()
e.action = EVENT_TYPE.SAVE
e.key = key
e.data = JSON:Encode(args)
e:SendTo(Server)
end
function loadData(key, keys, type)
local e = Event()
e.action = EVENT_TYPE.LOAD
e.key = key
e.keys = JSON:Encode(keys)
local k = e.keys
table.insert(loadQueue, { type = type, key = key })
e:SendTo(Server)
end
local Private = {
ItemEditorClearUndoRedoAndGrid = function (self)
end,
ItemEditorCreateUndoRedoAndGrid = function (self)
local ui = require "uikit"
local undo = ui:createButton("Undo", 13)
undo.Text = "Undo"
local redo = ui:createButton("Redo", 13)
redo.Text = "Undo"
local save = ui:createButton("Save", 13)
return undo, redo, save
end
}
Client.OnStart = function()
function playerSpawn(player)
player.Position = Map.Scale * { Map.Width * 0.5, Map.Height + 25, Map.Depth * 0.5 }
player.Velocity = { 0, 0, 0 }
player.Motion = { 0, 0, 0 }
end
require("multi").teleportTriggerDistance = 100
World:AddChild(Player)
playerSpawn(Player)
sun = Light()
sun:SetParent(World)
sun.Type = LightType.Directional
sun.CastsShadows = true
sun.Color = Color(99,75,0)
sun.Rotation = { 0.7, 0.5, 0.0 }
canvasShape = MutableShape()
for x = 0, 15 do
for y = 0, 15 do
canvasShape:AddBlock(colorWhite, x, y, 0)
end
end
loadData(Player.UserID, { "WIPs" }, DATA_TYPE.USER)
Camera.playerPOV = function(self)
self:SetModeFirstPerson()
Player.Head.IsHidden = true
Player.Body.IsHidden = true
Player.LeftLeg.IsHidden = true
Player.RightLeg.IsHidden = true
Player.RightArm.IsHidden = true
Player.LeftLeg.IsHidden = true
end
uiKit = require("uikit")
uiKit.margin = 8
uiKit:init()
local picker = ColorPicker()
picker.SelectedIndex = default_color
picker.OnColorSelect = function(index)
default_color = index
end
ui = {
start = {
start = Label("Place Down your easel to start drawing", Anchor.HCenter, Anchor.Top)
},
walk = {},
draw = {
picker = picker,
back = Button("Back", Anchor.Left, Anchor.Top),
load = Button("⬅ 💾"),
save = Button("➡ 💾"),
--[[backBt = back,
undo = undoBtn,
redo = redoBtn,]]
},
load = {},
}
for i = 1, maxWIPs do
local b = Button("")
b.id = i
b.OnPress = function()
local data = myWIPsData[myWIPs[b.id]]
if data then
stringToCanvas(data.data1 .. data.data2 .. data.data3, curCanvas)
curCanvas.id = myWIPs[b.id]
local e = Event()
e.action = EVENT_TYPE.SENDCLIENTDATA
e.easelDown = easelPlaced
e.easelPos = myEasel.Position
e.easelRot = myEasel.Rotation
e.paint = canvasToString(curCanvas)
e:SendTo(OtherPlayers)
end
end
ui.load["loadBTN" .. i] = b
b:Remove()
end
ui.draw.save.OnPress = function()
if #myWIPs < maxWIPs then
print("✏ painting ID: " .. curCanvas.id .. " Please name your drawing")
chatting = false
Client.OpenChatInput()
end
end
ui.draw.save.callBack = function(self, msg)
if msg ~= nil and msg ~= "" then
if myWIPsData[curCanvas.id] == nil then
table.insert(myWIPs, curCanvas.id)
end
saveData(Player.UserID, { WIPs = myWIPs })
curCanvas.name = msg
local data = canvasData(curCanvas)
myWIPsData[curCanvas.id] = data
saveData(curCanvas.id, data)
print(#myWIPs)
else
print("❌ That is not a valid name!")
end
end
ui.draw.back.OnPress = function()
cameraSetPos = true
cameraTargetPos = Player.Position + { 0, 7.55, 0 }
cameraTargetRot = cameraPrevRot
--print(canvasToString(curCanvas))
exitDrawMode()
end
ui.draw.load.OnPress = function()
loadShown = not loadShown
if loadShown then
ui.draw.load.Color = btnToggleON
ui:showScene("load")
for i = 1, maxWIPs do
local data = myWIPsData[myWIPs[i]]
local name = ""
local b = ui.load["loadBTN" .. i]
if not data then b:Remove() else name = "[ " .. data.name .. " ]" or "[ ??? ]" end
b.Text = name
end
else
ui.draw.load.Color = btnEnabled
ui:hideScene("load")
end
end
ui.curScene = "walk"
ui.draw.picker.Visible = false
function ui:showScene(scene)
for _, v in pairs(ui[scene]) do
if type(v) == "ColorPicker" then
v.Visible = true
else
v:Add()
end
end
end
function ui:hideScene(scene)
for _, v in pairs(ui[scene]) do
if type(v) == "ColorPicker" then
v.Visible = false
else
v:Remove()
end
end
end
function ui:switchScene(scene)
ui:hideScene(ui.curScene)
ui:showScene(scene)
ui.curScene = scene
end
function refreshUndoRedo()
--[[if not curCanvas.CanUndo then
undo:setColor(btnDisabled)
else
undo:setColor(btnEnabled)
end
if not curCanvas.CanRedo then
redo:setColor(btnDisabled)
else
redo:setColor(btnEnabled)
end]]
end
enterDrawMode = function()
UI.Crosshair = false
-- stop all ongoing motion
Player.Velocity = { 0, 0, 0 }
Player.Motion = { 0, 0, 0 }
dpadX = 0
dpadY = 0
observe = true
canMove = false
ui:switchScene("draw")
Pointer:Show()
Client.DirectionalPad = nil
Client.Action1 = nil
--for some reason :Remove() deletes them unlike other buttons
--undo, redo, save = Private:ItemEditorCreateUndoRedoAndGrid()
--save.Text = "➡ 💾"
--saveBTN = save
refreshUndoRedo()
ui.draw.load.Color = btnEnabled
--[[save.OnPress = function()
if #myWIPs < maxWIPs then
print("✏ painting ID: " .. curCanvas.id .. " Please name your drawing")
chatting = false
Client.OpenChatInput()
end
end
save.callBack = function(self, msg)
if msg ~= nil and msg ~= "" then
if myWIPsData[curCanvas.id] == nil then
table.insert(myWIPs, curCanvas.id)
end
saveData(Player.UserID, { WIPs = myWIPs })
curCanvas.name = msg
local data = canvasData(curCanvas)
myWIPsData[curCanvas.id] = data
saveData(curCanvas.id, data)
else
print("❌ That is not a valid name!")
end
end
undo.OnPress = function()
curCanvas:Undo()
refreshUndoRedo()
local e = Event()
e.action = EVENT_TYPE.UNDOREDO
e.undo = true
e:SendTo(OtherPlayers)
end
redo.OnPress = function()
curCanvas:Redo()
refreshUndoRedo()
local e = Event()
e.action = EVENT_TYPE.UNDOREDO
e.undo = false
e:SendTo(OtherPlayers)
end]]
end
exitDrawMode = function()
canDraw = false
UI.Crosshair = true
if loadShown then
loadShown = false
ui:hideScene("load")
end
observe = false
ui:switchScene("walk")
Pointer:Hide()
Client.DirectionalPad = padDirFunc
Client.Action1 = jumpFunc
--Private:ItemEditorClearUndoRedoAndGrid()
end
exitDrawMode()
ui:hideScene("draw")
ui:switchScene("start")
TimeCycle.On = false
Time.Current = Time.Noon
function spawnEasel(owner, pos)
local easel = Shape(Items.jacksbertox.easel)
easel.canva = canvasShape:Copy()
easel.canva.Pivot = canvasShape.Center
easel.LocalScale = 0.5
easel.owner = owner
easel.canva.owner = owner
easel:AddChild(easel.canva)
easel.canva.LocalPosition = Number3(0, 4.5, 2)
easels[owner.ID] = easel
easel.Position = pos
local p = Object()
p:SetParent(easel.canva)
p.LocalPosition = { 0, 0, 20 }
easel.cameraPoint = p
if owner.IsLocal then
curCanvas = easel.canva
myEasel = easel
easel.canva.id = createID()
end
easel.canva.History = true --item edit history
World:AddChild(easel)
easel.OnCollision = function(self, other)
self.collidesWithMap = type(other) == "Map"
end
return easel
end
spawnEasel(Player, Number3(0, 12, 16))
myEasel.PrivateDrawMode = 1
curCanvas.PrivateDrawMode = 1
myEasel:SetParent(Player)
myEasel.IsHidden = false
myEasel.LocalPosition = { 0, 12, 16 }
myEasel.LocalRotation = { 0, 3.14, 0 }
function Camera:smoothSetPos(tPos, tRot, minSpeed, dt)
Camera:SetModeFree()
local distance = Camera.Position - tPos
local distance2 = Camera.Forward - tRot
if distance.Length > 0.1 then
local speed = ((Camera.Position - tPos) * 2.0).Length
if speed < minSpeed then
speed = minSpeed
end
if speed * dt > distance.Length then
speed = distance.Length / dt
end
distance:Normalize()
Camera.Position = Camera.Position - distance * speed * dt
else
cameraSetPos = false
end
if distance2.Length > 0.1 then
local speed2 = ((Camera.Forward - tRot) * 0.5).Length
if speed2 < minSpeed * 0.1 then
speed2 = minSpeed * 0.1
end
if speed2 * dt > distance2.Length then
speed2 = distance2.Length / dt
end
distance2:Normalize()
Camera.Forward = Camera.Forward - distance2 * speed2 * dt
end
end
end
Client.OnPlayerJoin = function(p)
require("multi"):initPlayer(p)
if not p.IsLocal then
spawnEasel(p, Number3(0, 0, 0))
print("😬 " .. p.Username .. " Joined")
local o = Object()
o.name = p.Username
usrTags[p.ID] = o
o:TextBubble(o.name, -1, Number3(0, 35, 0), Color(1.0, 1.0, 1.0, 135), Color(0, 0, 0, 0))
o:SetParent(p)
local e = Event()
e.action = EVENT_TYPE.SENDCLIENTDATA
e.easelDown = easelPlaced
e.easelPos = myEasel.Position
e.easelRot = myEasel.Rotation
e.paint = canvasToString(curCanvas)
e:SendTo(p)
end
playerSpawn(p)
end
Client.AnalogPad = function(dx, dy)
Player.LocalRotation.Y = Player.LocalRotation.Y + dx * 0.01
Player.LocalRotation.X = Player.LocalRotation.X + -dy * 0.01
if dpadX ~= nil and dpadY ~= nil and canMove then
Player.Motion = (Player.Forward * dpadY + Player.Right * dpadX) * 50
end
end
padDirFunc = function(x, y)
-- storing globals here for AnalogPad to update Player.Motion
dpadX = x
dpadY = y
if canMove then Player.Motion = (Player.Forward * y + Player.Right * x) * 50 end
Player.moving = x ~= 0 or y ~= 0
end
Client.OnPlayerLeave = function(p)
require("multi"):removePlayer(p)
usrTags[p.ID]:ClearTextBubble()
usrTags[p.ID] = nil
print("🤨 " .. p.Username .. " Left")
easels[p.ID]:SetParent(nil)
easels[p.ID] = nil
end
Client.Tick = function(dt)
require("multi"):tick(dt)
if Player.Position.Y < -500 then
playerSpawn(Player)
end
if cameraSetPos then
Camera:smoothSetPos(cameraTargetPos, cameraTargetRot, 12, dt)
else
if not observe then
Camera:playerPOV()
canMove = true
else
canDraw = true
end
end
local Pos = Player.Forward + Player.Position
local Coords = Number3(
math.floor(Pos.X / Map.Scale.X),
math.floor(Pos.Y / Map.Scale.Y),
math.floor(Pos.Z / Map.Scale.Z)
)
if Map:GetBlock(Coords.X, Coords.Y, Coords.Z + 1) ~= nil and Map:GetBlock(Coords.X, Coords.Y + 1, Coords.Z + 1) ==
nil and Player.moving then
Player.Velocity.Y = 40
end
if Map:GetBlock(Coords.X, Coords.Y, Coords.Z - 1) ~= nil and Map:GetBlock(Coords.X, Coords.Y + 1, Coords.Z - 1) ==
nil and Player.moving then
Player.Velocity.Y = 40
end
if Map:GetBlock(Coords.X + 1, Coords.Y, Coords.Z) ~= nil and Map:GetBlock(Coords.X + 1, Coords.Y + 1, Coords.Z) ==
nil and Player.moving then
Player.Velocity.Y = 40
end
if Map:GetBlock(Coords.X - 1, Coords.Y, Coords.Z) ~= nil and Map:GetBlock(Coords.X - 1, Coords.Y + 1, Coords.Z) ==
nil and Player.moving then
Player.Velocity.Y = 40
end
end
-- jump function
jumpFunc = function()
if Player.IsOnGround then
Player.Velocity.Y = 100
end
end
Client.Action2 = function()
if easelPlaced then
-- Note from Cubzh: this is because Action2 is wrongfully triggered, to be fixed on our end
if not canMove then return end
local i = Player:CastRay(curCanvas)
if i.Object ~= nil and i.FaceTouched == Face.Front then
if i.Distance < 35 then
--Smooth transition to canvas
cameraPrevRot = Camera.Forward:Copy()
cameraSetPos = true
cameraTargetPos = myEasel.cameraPoint.Position
cameraTargetRot = curCanvas.Backward
enterDrawMode()
else
print("⚠ Too Far!")
end
end
else
if not myEasel.collidesWithMap and Player.IsOnGround then
ui:hideScene("start")
local pos1 = myEasel.Position:Copy()
World:AddChild(myEasel, true)
local pos2 = myEasel.Position:Copy()
easelPlaced = true
myEasel.PrivateDrawMode = 0
curCanvas.PrivateDrawMode = 0
local e = Event()
e.action = EVENT_TYPE.PLACEEASEL
e:SendTo(OtherPlayers)
end
end
end
Client.OnChat = function(message)
if chatting then
-- display bubble over local Player
-- when a message is posted
print("[" .. Player.Username .. "]: " .. message)
Player:TextBubble(message, 8, true)
-- send message to other players
local e = Event()
e.action = EVENT_TYPE.CHAT
e.msg = message
e:SendTo(OtherPlayers)
else
chatting = true
ui.draw.save:callBack(message)
end
end
Client.DidReceiveEvent = function(e)
require("multi"):receive(e)
if e.action == EVENT_TYPE.MOVE then
local sender = e.Sender
sender.Position = e.p
sender.Motion = e.m
sender.Velocity = e.v
sender.Rotation = e.r
elseif e.action == EVENT_TYPE.CHAT then
-- display messages from other players
print("[" .. e.Sender.Username .. "]: " .. e.msg)
e.Sender:TextBubble(e.msg, 8, true)
elseif e.action == EVENT_TYPE.SENDCLIENTDATA then
local ea = easels[e.Sender.ID] or spawnEasel(e.Sender, Number3(0, 0, 0))
if e.easelDown then
ea:SetParent(World)
ea.Position = e.easelPos
ea.Rotation = e.easelRot
stringToCanvas(e.paint, ea.canva)
else
ea:SetParent(e.Sender)
ea.PrivateDrawMode = 1
ea.canva.PrivateDrawMode = 1
ea.LocalPosition = { 0, 12, 16 }
ea.LocalRotation = { 0, 3.14, 0 }
end
elseif e.action == EVENT_TYPE.PLACEEASEL then
local ea = easels[e.Sender.ID]
ea.PrivateDrawMode = 0
ea.canva.PrivateDrawMode = 0
local p = ea.Position:Copy()
World:AddChild(ea, true)
local p = ea.Position:Copy()
elseif e.action == EVENT_TYPE.DRAW then
local ea = easels[e.Sender.ID]
ea.canva:GetBlock(e.pos.X, e.pos.Y, e.pos.Z):Replace(DefaultColors[e.color])
elseif e.action == EVENT_TYPE.UNDOREDO then
if e.undo then
easels[e.Sender.ID].canva:Undo()
else
easels[e.Sender.ID].canva:Redo()
end
elseif e.action == EVENT_TYPE.SENDDATA then
local data = JSON:Decode(e.data)
local queueElement = table.remove(loadQueue, 1)
if queueElement.type == DATA_TYPE.WIP then
myWIPsData[queueElement.key] = data
elseif queueElement.type == DATA_TYPE.USER then
myWIPs = data.WIPs or {}
for i = 1, #myWIPs do
loadData(myWIPs[i], painting_keys, DATA_TYPE.WIP)
end
elseif queueElement.type == DATA_TYPE.PAINTING then
end
end
end
placePixel = function(e)
if canDraw then
refreshUndoRedo()
local i = e:CastRay(curCanvas)
local pos
if i.Block then
i.Block:Replace(DefaultColors[default_color])
pos = i.Block.Coords
local e = Event()
e.action = EVENT_TYPE.DRAW
e.color = default_color
e.pos = pos
e:SendTo(OtherPlayers)
end
end
end
Pointer.Down = function(e)
placePixel(e)
end
Pointer.Drag = function(e)
placePixel(e)
end
Server.OnStart = function()
end
Server.DidReceiveEvent = function(e)
if e.action == EVENT_TYPE.SAVE then
print("💾 Saving Data...")
local store = KeyValueStore(e.key)
local data = JSON:Decode(e.data)
local i = 0
local count = 0
local progress = 0
for _ in pairs(data) do count = count + 1 end
for k, v in pairs(data) do
local value
if type(v) == "table" then value = JSON:Encode(v) else value = v end
store:Set(k, value, function(success)
if success then
i = i + 1
progress = math.floor(100 / count * i)
print(progress)
else
print("❌ Something went wrong!")
end
end)
end
else
local ev = Event()
ev.action = EVENT_TYPE.SENDDATA
ev.data = ""
local store = KeyValueStore(e.key)
local keys = JSON:Decode(e.keys)
local p = 0
local r = {}
for i = 1, #keys do
store:Get(keys[i], function(success, result)
if success then
local item = result[keys[i]]
if type(item) == "string" then
if item:sub(1, 1) == "[" and item:sub(#item, #item) == "]" then
item = JSON:Decode(item)
end
end
r[keys[i]] = item
else
print("❌ Something went wrong!")
end
p = i
if p == #keys then
ev.data = JSON:Encode(r)
ev:SendTo(e.Sender)
end
end)
end
end
end
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment