This page is used for previewing and submitting scripts for use with the Script Manager
Available Scripts
(2) TPTMulti by LBPHacker
(3) set wifi v2 by jacob1
(4) Script Paste by jacob1
(5) Random Element by jacob1
(6) Magical Merge Master 3000 by nucular
(7) More Fuel Mod-heavy by jward212
(9) Breakpoints (BRPT) by boxmein
(10) Cockroaches! by boxmein
(11) Random tree by ssccsscc
(12) Element with random properties by ssccsscc
(15) Minimalistic Element Dehider by nucular
(16) FPS Gauge by mniip
(17) TPT Radio by jward212
(18) Print Debugger by FeynmanLogomaker
(19) Powered BHOL by jacob1
(20) Light and lamps by electronic_steve
(21) Extremely Durable TTAN by QuentinADay
(22) New Buttons by QuentinADay
(23) Pure Energy by QuentinADay
(25) RCA's HUD XV Update I by RCAProduction
(27) 123456787654 vols by mjpowder
(28) Singularity Bomb by QuentinADay
(29) ES wifi set by electronic_steve
(30) Lua Elements Pack by FeynmanLogomaker
(31) stkm gun by jward212
(32) space building materials by kjack1111
(34) Rust bomb by Damian97
(35) Simple command block by ssccsscc
(36) Everlasting Fusion by QuentinADay
(37) Screenshot Organiser by mecha-man
(38) Napalm mod by cccp3
(39) Rocket fuel mod v0.15 by cccp3
(41) Useful web links by jward212
(42) TPT Logic Gates Mod by iamdumb
(43) ESTools by electronic_steve
(44) Head Crabs-HL2 by jward212
(45) Procedural Save Generator by boxmein
(46) smooth colours for nametag by jward212
(47) ZAKPACK by zak03
(49) Performance Monitor by FeynmanLogomaker
(51) Texter by byzod
(52) Texter default fonts by byzod
(53) Schicko's Font Pack for Texter by Schicko
(54) Realistic Element Names by Atomic10
(55) TPT's Mod V.3 Update 1 by Amy
(56) Temporaryaccount-Decorator by Temporaryaccount
(57) random save loader by jward212
(58) Tmp gradient display by ssccsscc
(59) particle re-orderer by mniip
(60) Electric Glow by jacob1
(61) More Fuel Mod-lite by jward212
(63) Rythidium by janekbe04
(64) Simple FPS GUI by Sfsjunior
(65) Enhanced Element Dehider by ChargedCreeper
(66) Graph of average temp by ssccsscc
(67) Template save loader by jacob1
(68) Lua Text Generator by jBot-42
(70) Pixel's Freezer by Pixelguru26
(71) Thingy | Fusion For Ever by TheChosenEvilOne
(72) op explosions by zolly_bro
(73) Scar by DorkyBobster
(74) ScreenShotMod by lill74
(77) Useful Things by TheEvilChosenOne
(78) Alchemy Mod by _MrN_
(79) Nuke v2 by Fnaf65
(80) Compressor mod by TheChosenEvilOne
(81) Custom Render Mode Loader by jacob1
(82) Spacewars by JosephMA
(83) MOAR - Alpha 0.1 by TheChosenEvilOne
(86) Element Creator by cxi
(88) Soapworm by LBPHacker
(90) Pressure Bomb by God_Kra
(91) SMFB by wntjq69
(92) Potato by cxi
(93) Subatomic Pack (BDS) by TPT_PL
(94) Acidic Pack (BDS) by TPT_PL
(95) Starbound Building Materials by Sanpypr
(96) Factory problems by TPT_PL
(97) Gamma Ray-diation by Kostia4381
(98) Magic by livingfossil
(99) Cross-window Copy/Cut/Paste by LBPHacker
(100) Langton's Ant with variations by LBPHacker
(101) Remote particle creator/deleter by TPT_PL
(102) Force fields by electronic_steve
(103) Reinforced Concrete by 12Me21
(105) TPT_PL's Lua Mod by TPT_PL
(106) Nuke v4 by Fnaf65
(107) CHEMMOD V1 by KevAK
(108) Chemicals by Ligan
(109) VonDaniel's Template by VonDaniel
(110) The Inaccurate Radioactivity Toy Mod by TuDoR2007
(112) textmonsterPack by textmonster404
(113) Meteor by TheScienceKid
(114) Tgpm by TuDoR2007
(115) Civilizations by TPT_PL
(116) RAD-MOD 1.2.1B by Kev_AK
(117) MicroLua by RamiLego4Game
(118) Extra customizable HUD by djy0212
(119) Ingame brush editor by ssccsscc
(120) Window Maker by Paul_31415
(121) CHEM-MOD V1.2B by Kev_AK
(122) Rainbow PHOT by Mrprocom
(123) stronger stickmanv by yuval
(124) 3D Pressure Visualizer by mniip
(125) Arkadian Liquid by JanKaszanka
(126) Fuel by nukers473
(127) Immersive Radioactivity v2.1 by Potbelly
(128) ElementLaunchingTool by juh9870
(129) CHEM-MOD_v1.2.2b by KevAK
(130) Slingshot by Mrprocom
(131) Perlin Noise Generator by DoubleF
(132) Element Replace by TomW1605
(133) Flooder V2 by TheAwesomeMutant
(134) Link Sign GUI by QuanTech
(135) Element dehider by 4e616d65
(136) Subphoton ROM Builder by mad-cow
(137) Hardened Dust by Liftski
(138) Bio-Vir by TheAwesomeMutant
(140) Orbit Simulator by Mrprocom
(141) johnnyou's Font for Texter by johnnyou (49796346)
(142) auto_wifi by phisically
(143) Layering helper by ssccsscc
(144) Layering Helper Extended by LuaMaster
(145) TPT Remade by TuDoR2007
(146) All-seeing sampler by djy0212
(147) Layering helper remastered by ssccsscc
(148) Eraser by thepowdertoy12
(149) EXPLOSIONS by olix3001
(150) Simple rocket fuel mod by ArseniyPlotnikov2006
(151) Pure Fission by Fnaf65
(154) Graph by ssccsscc
(155) Little's Pack! by LittleProgramming
(156) Lead by LoftisGaming
(157) WIFI Tuner by ssccsscc
(158) Previous Brush by TomW1605
(159) HUD Auto-Hider by Tim
(161) Stack tool by thepowdertoy12
(162) Oil and plastic by ArseniyPlotnikov2006
(163) Colored Ember by DUC
(164) Timer by ssccsscc
(165) Bacteria Mod by TuDoR2007
(166) Noise filter by LBPHacker
(167) Future-proof element dehider by LBPHacker
(168) RadioactiveNuke by DreamingWarlord
(169) Only Hot Element by DreamingWarlord
(170) Philosopher's Stone by Godhydra
(171) Conic section generator by LBPHacker
(172) Interface API by ssccsscc
(173) Metals&Materials by Ferrous26
(174) tpt.all by LBPHacker
(175) The Visual Elements Pack by Goblin01, vvv331
(176) Layering Helper Reforged by PowderNotSolid
(177) SNOWified SING by LBPHacker
(178) FPS Chart by Goblin01
(179) TPT font writer by Goblin01
(180) Simple Ruler by PowderNotSolid
(181) Heat Modifier by DreamingWarlord
(182) TPT Remade II by TuDoR2007
(183) Gravity simulator by ArseniyPlotnikov2k6
(184) Unobtainium by christheboss894
(185) TPTMIDI noteblock in tpt by djy0212
(186) DreamingWarlord's Lua Tool by DreamingWarlord
(187) Elements Tooltip by Goblin01
(189) Explodium script by 0d15ea5ebe57c0debadc0ffee0ddf00d
(190) more powered force elements by 6nop6nop
(191) Yzaak1Scifire Modpack by Yzaak1Scifire
(194) Fluor and more modpack! by galaktor
(195) Hot Powder by lieve_blendi
(196) Star by TUANMINHVIETNAM
(197) Heat Powders by lieve_blendi
(200) Tangeriinium (thx 2 cxi 4 code) by LostEditor
(201) Freezer by lieve_blendi
(202) Powder Power! by TPTSortaGuy
(203) PowderPlus v1.4 by PowderPlus Team
(204) fire by ME
(205) Stacked Goo Animations by Maticzpl
(206) Stickman Control for Android Version by PhauloRiquelme
(207) Spark Removal Button by Xyz
(208) More HEAC's! by Maxhd1234
(209) Immersive Radioactivity v3.0 by Potbelly
(210) Subframe Chipmaker Script by Maticzpl
(211) Realistic Propellants by ArseniyPlotnikov2k6
(212) Mass Equals Gravity by Maticzpl
(213) PhiMod v1 by ArolaunTech
(214) PC Controls for Android by Cracker1000
(217) Single-pixel pipe configurator by LBPHacker
(218) Omega Death Laser Gun by Dogeoum
(219) Notifications by Maticzpl
(221) Powderizer by ArolaunTech
(222) ElemDehider 1.2 by Inventor70
(223) Unobtainum V2 by DoomKittyAttack
(224) Organics Mod v0.2B by PowderPlus Team
(225) Gravity distortion by Avolte55
(226) tmp Wifi by PhauloRiquelme
(227) Alchemagica Mod v1.0 by RebMiami
(228) Fan Elements Mod by RebMiami
(229) Impossibilities by ArolaunTech
(230) Realistic Explosives by ArseniyPlotnikov2k6
(231) libactivation by anamorphic
(232) Alloy Brushes by Maticzpl
(233) Gravity bender by pres
(234) Slow Tick by Pixel
(235) Paste ID by Maticzpl
(236) many things by jadenflp2
(237) Territect by Rebmiami
(238) Better Descriptions v1.0.5 by ashyboi2022
(239) LIGHTNING SPRK by GOLmaster10101
(240) Small Bombs by juh9870
(241) Save Shop by aaccbb
(242) Moving solids v1.3.0 Beta by ArolaunTech
(244) Alchemistry by rdococ
(245) ETRD (Formerly PowderIM) by aaccbb
(246) RadonX by Justadirtblock
(248) Water-X by deuterium_oxide
(250) Indestructible INSL by CheekyRand0m
(252) Console's Mod by Console/Compec
(255) Slow motion by LBPHacker
(256) Powered Repeller by Hythonia
(257) Zeta's Electric Tools. by Zetalasis
(258) Azure serum (AZSR) by ALumpOfPowderToy
(259) COLORFULSAND by xert
(260) Lightning Circle by defaultuser0
(261) Powder Future Tech by JonaHungary
(262) TPTGlowingSolids by DestinyDyson
(263) Volcano Bomb by I_am_the_NugsWorld
(264) Neon Lights by Rebmiami
(265) Radioactive Materials by xyz
(266) Eater mod by VIPERGAMEZ
(267) the biology mod by someone
(268) Atomic Physics by qe
(269) Pure Radiation by ronansb
(270) Fake Elements by That_PowderToy_Guy
(271) Tachyons and MISC by RamenNoods
(272) Exotic Particles by rdococ
(273) FPS Slider by aaccbb
(274) Enphosian's Radioactive mod pack by Enphosian
(276) ROM Builder by QnpfvTPz
(277) acb's Idea Generator by aaccbb
(278) Bode's Grapher by Bodester
(279) Gradient Tool by Bodester
(280) Better FPS Slider by ngmoco
(281) MyWIFI by Bodester
(282) acb's Slow-Mo Script by aaccbb
(283) Fun n' Chemicals by 0xHenryMC
(284) Resistant coating by git4rker
(285) Nationwide script by Kit237
(286) weird alphabet.lua by alice_loona_ot12
+ Submit new script!
Title:
Author:
Script:
local env__ = setmetatable({}, { __index = function(_, key)
error("__index on env: " .. tostring(key), 2)
end, __newindex = function(_, key)
error("__newindex on env: " .. tostring(key), 2)
end })
for key, value in pairs(_G) do
rawset(env__, key, value)
end
local _ENV = env__
if rawget(_G, "setfenv") then
setfenv(1, env__)
end
math.randomseed(os.time())
local require_preload__ = {}
local require_loaded__ = {}
local function require(modname)
local mod = require_loaded__[modname]
if not mod then
mod = assert(assert(require_preload__[modname], "missing module " .. modname)())
require_loaded__[modname] = mod
end
return mod
end
rawset(env__, "require", require)
local unpack = rawget(_G, "unpack") or table.unpack
local function packn(...)
return { [ 0 ] = select("#", ...), ... }
end
local function unpackn(tbl, from, to)
return unpack(tbl, from or 1, to or tbl[0])
end
local function xpcall_wrap(func, handler)
return function(...)
local iargs = packn(...)
local oargs
xpcall(function()
oargs = packn(func(unpackn(iargs)))
end, function(err)
if handler then
handler(err)
end
print(debug.traceback(err, 2))
return err
end)
if oargs then
return unpackn(oargs)
end
end
end
rawset(env__, "xpcall_wrap", xpcall_wrap)
require_preload__["tptmp.client"] = function()
local common_util = require("tptmp.common.util")
local loadtime_error
local tptVersion = { tpt.version.major, tpt.version.minor }
if tpt.version.upstreamMajor then
tptVersion = { tpt.version.upstreamMajor, tpt.version.upstreamMinor }
end
local http = rawget(_G, "http")
local socket = rawget(_G, "socket")
if sim.CELL ~= 4 then -- * Required by cursor snapping functions.
loadtime_error = "CELL is not 4, try using the official version of the game"
elseif sim.XRES ~= 612 then -- * Required by lots of code dealing with positions.
loadtime_error = "XRES is not 612, try using the official version of the game"
elseif sim.YRES ~= 384 then -- * Required by lots of code dealing with positions.
loadtime_error = "XRES is not 384, try using the official version of the game"
elseif sim.PMAPBITS >= 13 then -- * Required by how non-element tools are encoded (extended tool IDs, XIDs).
loadtime_error = "PMAPBITS is too large, try using the official version of the game"
elseif not (tpt.version and tpt.version.upstreamBuild and tpt.version.upstreamBuild >= 356) then
loadtime_error = "game version not supported, try updating the game"
elseif not rawget(_G, "bit") then
loadtime_error = "no bit API, try updating the game"
elseif not http then
loadtime_error = "no http API, try updating the game"
elseif not socket or not socket.tcp then
loadtime_error = "no socket API, try updating the game"
elseif socket.bind then
loadtime_error = "outdated socket API, try updating the game"
elseif tpt.version.jacob1s_mod and not tpt.tab_menu then
loadtime_error = "mod version not supported, try updating the game"
elseif tpt.version.mobilemajor then
loadtime_error = "platform not supported" -- no good advice, can't quite tell the user to buy a computer
end
local config = require("tptmp.client.config")
local colours = not loadtime_error and require("tptmp.client.colours")
local window = not loadtime_error and require("tptmp.client.window")
local side_button = not loadtime_error and require("tptmp.client.side_button")
local localcmd = not loadtime_error and require("tptmp.client.localcmd")
local client = not loadtime_error and require("tptmp.client.client")
local util = not loadtime_error and require("tptmp.client.util")
local profile = not loadtime_error and require("tptmp.client.profile")
local format = not loadtime_error and require("tptmp.client.format")
local manager = not loadtime_error and require("tptmp.client.manager")
local function run()
if rawget(_G, "TPTMP") then
if TPTMP.version <= config.version then
TPTMP.disableMultiplayer()
else
loadtime_error = "newer version already running"
end
end
if loadtime_error then
print("TPTMP " .. config.versionstr .. ": Cannot load: " .. loadtime_error)
return
end
local hooks_enabled = false
local window_status = "hidden"
local window_hide_mode = "hidden"
local function set_floating(floating)
window_hide_mode = floating and "floating" or "hidden"
end
local function get_window_status()
return window_status
end
local TPTMP = {
version = config.version,
versionStr = config.versionstr,
}
local hide_window, show_window, begin_chat
setmetatable(TPTMP, { __newindex = function(tbl, key, value)
if key == "chatHidden" then
if value then
hide_window()
else
show_window()
end
return
end
rawset(tbl, key, value)
end, __index = function(tbl, key)
if key == "chatHidden" then
return window_status ~= "shown"
end
return rawget(tbl, key)
end })
rawset(_G, "TPTMP", TPTMP)
local current_id, current_hist = util.get_save_id()
local function set_id(id, hist)
current_id, current_hist = id, hist
end
local function get_id()
return current_id, current_hist
end
local quickauth = manager.get("quickauthToken", "")
local function set_qa(qa)
quickauth = qa
manager.set("quickauthToken", quickauth)
end
local function get_qa()
return quickauth
end
local function log_event(text)
print("\bt[TPTMP]\bw " .. text)
end
local last_trace_str
local handle_error
local should_reconnect_at
local cli
local prof = profile.new({
set_id_func = set_id,
get_id_func = get_id,
log_event_func = log_event,
registered_func = function()
return cli and cli:registered()
end
})
local win
local should_reconnect = false
local function kill_client()
win:set_subtitle("status", "Not connected")
cli:fps_sync(false)
cli:stop()
if should_reconnect then
should_reconnect = false
should_reconnect_at = socket.gettime() + config.reconnect_later_timeout
win:backlog_push_neutral("* Will attempt to reconnect in " .. config.reconnect_later_timeout .. " seconds")
end
cli = nil
end
function begin_chat()
show_window()
win.hide_when_chat_done = true
end
function hide_window()
window_status = window_hide_mode
win.in_focus = false
end
function show_window()
if not hooks_enabled then
TPTMP.enableMultiplayer()
end
window_status = "shown"
win:backlog_bump_marker()
win.in_focus = true
end
win = window.new({
hide_window_func = hide_window,
window_status_func = get_window_status,
log_event_func = log_event,
client_func = function()
return cli and cli:registered() and cli
end,
localcmd_parse_func = function(str)
return cmd:parse(str)
end,
should_ignore_mouse_func = function(str)
return prof:should_ignore_mouse()
end,
})
local cmd = localcmd.new({
window_status_func = get_window_status,
window_set_floating_func = set_floating,
client_func = function()
return cli and cli:registered() and cli
end,
new_client_func = function(params)
should_reconnect_at = nil
params.window = win
params.profile = prof
params.set_id_func = set_id
params.get_id_func = get_id
params.set_qa_func = set_qa
params.get_qa_func = get_qa
params.log_event_func = log_event
params.handle_error_func = handle_error
params.should_reconnect_func = function()
should_reconnect = true
end
params.should_not_reconnect_func = function()
should_reconnect = false
end
last_trace_str = nil
cli = client.new(params)
return cli
end,
kill_client_func = function()
should_reconnect = false
kill_client()
end,
window = win,
})
win.localcmd = cmd
local sbtn = side_button.new({
notif_count_func = function()
return win:backlog_notif_count()
end,
notif_important_func = function()
return win:backlog_notif_important()
end,
show_window_func = show_window,
hide_window_func = hide_window,
begin_chat_func = begin_chat,
window_status_func = get_window_status,
sync_func = function()
cmd:parse("/sync")
end,
})
local grab_drop_text_input
do
if rawget(_G, "ui") and ui.grabTextInput then
local text_input_grabbed = false
function grab_drop_text_input(should_grab)
if text_input_grabbed and not should_grab then
ui.dropTextInput()
elseif not text_input_grabbed and should_grab then
ui.grabTextInput()
end
text_input_grabbed = should_grab
end
end
end
function handle_error(err)
if not last_trace_str then
local handle = io.open(config.trace_path, "wb")
handle:write(("TPTMP %s %s\n"):format(config.versionstr, os.date("!%Y-%m-%dT%H:%M:%SZ")))
handle:close()
win:backlog_push_error("An error occurred and its trace has been saved to " .. config.trace_path .. "; please find this file in your data folder and attach it when reporting this to developers")
win:backlog_push_error("Top-level error: " .. tostring(err))
end
local str = debug.traceback(err, 2) .. "\n"
if last_trace_str ~= str then
last_trace_str = str
local handle = io.open(config.trace_path, "ab")
handle:write(str)
handle:close()
end
should_reconnect = false
if cli then
cli:stop("error handled")
kill_client()
end
end
local pcur_r, pcur_g, pcur_b, pcur_a = unpack(colours.common.player_cursor)
local bmode_to_repr = {
[ 0 ] = "",
[ 1 ] = " REPL",
[ 2 ] = " SDEL",
}
local handle_tick = xpcall_wrap(function()
local now = socket.gettime()
if should_reconnect_at and now >= should_reconnect_at then
should_reconnect_at = nil
win:backlog_push_neutral("* Attempting to reconnect")
cmd:parse("/reconnect")
end
if grab_drop_text_input then
grab_drop_text_input(window_status == "shown")
end
if cli then
cli:tick()
if cli:status() ~= "running" then
kill_client()
end
end
if cli then
for _, member in pairs(cli.id_to_member) do
if member:can_render() then
local px, py = member.pos_x, member.pos_y
local sx, sy = member.size_x, member.size_y
local rx, ry = member.rect_x, member.rect_y
local lx, ly = member.line_x, member.line_y
local zx, zy, zs = member.zoom_x, member.zoom_y, member.zoom_s
if rx then
sx, sy = 0, 0
end
local tool = member.last_tool or member.tool_l
local tool_class = tool and cli.xidr.xid_class[tool]
local tool_name = cli:tool_proper_name(tool)
local add_argb = false
if tool_name:find("^DEFAULT_DECOR_") then
add_argb = true
end
tool_name = tool_name:match("[^_]+$") or tool_name
if add_argb then
tool_name = ("%s %02X%02X%02X%02X"):format(tool_name, member.deco_a, member.deco_r, member.deco_g, member.deco_b)
end
local repl_tool_name
if member.bmode ~= 0 then
local repl_tool = member.tool_x
repl_tool_name = cli:tool_proper_name(repl_tool)
repl_tool_name = repl_tool_name:match("[^_]+$") or repl_tool_name
end
if zx and util.inside_rect(zx, zy, zs, zs, px, py) then
gfx.drawRect(zx - 1, zy - 1, zs + 2, zs + 2, pcur_r, pcur_g, pcur_b, pcur_a)
if zs > 8 then
gfx.drawText(zx, zy, "\238\129\165", pcur_r, pcur_g, pcur_b, pcur_a)
end
end
local offx, offy = 6, -9
local player_info = member.formatted_nick
if cli.fps_sync_ and member.fps_sync then
player_info = ("%s %s%+i"):format(player_info, colours.commonstr.brush, member.fps_sync_count_diff)
end
local brush_info
if member.select or member.place then
local xlo, ylo, xhi, yhi, action
if member.select then
xlo = math.min(px, member.select_x)
ylo = math.min(py, member.select_y)
xhi = math.max(px, member.select_x)
yhi = math.max(py, member.select_y)
action = member.select
else
xlo = px - math.floor(member.place_w / 2)
ylo = py - math.floor(member.place_h / 2)
xhi = xlo + member.place_w
yhi = ylo + member.place_h
xlo = math.min(sim.XRES, math.max(0, xlo))
ylo = math.min(sim.YRES, math.max(0, ylo))
xhi = math.min(sim.XRES, math.max(0, xhi))
yhi = math.min(sim.YRES, math.max(0, yhi))
action = member.place
end
gfx.drawRect(xlo, ylo, xhi - xlo + 1, yhi - ylo + 1, pcur_r, pcur_g, pcur_b, pcur_a)
brush_info = action
else
local dsx, dsy = sx * 2 + 1, sy * 2 + 1
if tool_class == "WL" then
px, py = util.wall_snap_coords(px, py)
sx, sy = util.wall_snap_coords(sx, sy)
offx, offy = offx + 3, offy + 1
dsx, dsy = 2 * sx + 4, 2 * sy + 4
end
if sx < 50 then
offx = offx + sx
end
brush_info = ("%s %ix%i%s %s"):format(tool_name, dsx, dsy, bmode_to_repr[member.bmode], repl_tool_name or "")
if not rx then
if not lx and member.kmod_s and member.kmod_c then
gfx.drawLine(px - 5, py, px + 5, py, pcur_r, pcur_g, pcur_b, pcur_a)
gfx.drawLine(px, py - 5, px, py + 5, pcur_r, pcur_g, pcur_b, pcur_a)
elseif tool_class == "WL" then
gfx.drawRect(px - sx, py - sy, dsx, dsy, pcur_r, pcur_g, pcur_b, pcur_a)
elseif member.shape == 0 then
gfx.drawCircle(px, py, sx, sy, pcur_r, pcur_g, pcur_b, pcur_a)
elseif member.shape == 1 then
gfx.drawRect(px - sx, py - sy, sx * 2 + 1, sy * 2 + 1, pcur_r, pcur_g, pcur_b, pcur_a)
elseif member.shape == 2 then
gfx.drawLine(px - sx, py + sy, px , py - sy, pcur_r, pcur_g, pcur_b, pcur_a)
gfx.drawLine(px - sx, py + sy, px + sx, py + sy, pcur_r, pcur_g, pcur_b, pcur_a)
gfx.drawLine(px , py - sy, px + sx, py + sy, pcur_r, pcur_g, pcur_b, pcur_a)
end
end
if lx then
if member.kmod_a then
px, py = util.line_snap_coords(lx, ly, px, py)
end
gfx.drawLine(lx, ly, px, py, pcur_r, pcur_g, pcur_b, pcur_a)
end
if rx then
if member.kmod_a then
px, py = util.rect_snap_coords(rx, ry, px, py)
end
local x, y, w, h = util.corners_to_rect(px, py, rx, ry)
gfx.drawRect(x, y, w, h, pcur_r, pcur_g, pcur_b, pcur_a)
end
end
gfx.drawText(px + offx, py + offy, player_info, pcur_r, pcur_g, pcur_b, pcur_a)
gfx.drawText(px + offx, py + offy + 12, brush_info, pcur_r, pcur_g, pcur_b, pcur_a)
end
end
end
if window_status ~= "hidden" and win:handle_tick() then
return false
end
if sbtn:handle_tick() then
return false
end
prof:handle_tick()
end, handle_error)
local handle_mousemove = xpcall_wrap(function(px, py, dx, dy)
if prof:handle_mousemove(px, py, dx, dy) then
return false
end
end, handle_error)
local handle_mousedown = xpcall_wrap(function(px, py, button)
if window_status == "shown" and win:handle_mousedown(px, py, button) then
return false
end
if sbtn:handle_mousedown(px, py, button) then
return false
end
if prof:handle_mousedown(px, py, button) then
return false
end
end, handle_error)
local handle_mouseup = xpcall_wrap(function(px, py, button, reason)
if window_status == "shown" and win:handle_mouseup(px, py, button, reason) then
return false
end
if sbtn:handle_mouseup(px, py, button, reason) then
return false
end
if prof:handle_mouseup(px, py, button, reason) then
return false
end
end, handle_error)
local handle_mousewheel = xpcall_wrap(function(px, py, dir)
if window_status == "shown" and win:handle_mousewheel(px, py, dir) then
return false
end
if sbtn:handle_mousewheel(px, py, dir) then
return false
end
if prof:handle_mousewheel(px, py, dir) then
return false
end
end, handle_error)
local handle_keypress = xpcall_wrap(function(key, scan, rep, shift, ctrl, alt)
if window_status == "shown" and win:handle_keypress(key, scan, rep, shift, ctrl, alt) then
return false
end
if sbtn:handle_keypress(key, scan, rep, shift, ctrl, alt) then
return false
end
if prof:handle_keypress(key, scan, rep, shift, ctrl, alt) then
return false
end
end, handle_error)
local handle_keyrelease = xpcall_wrap(function(key, scan, rep, shift, ctrl, alt)
if window_status == "shown" and win:handle_keyrelease(key, scan, rep, shift, ctrl, alt) then
return false
end
if sbtn:handle_keyrelease(key, scan, rep, shift, ctrl, alt) then
return false
end
if prof:handle_keyrelease(key, scan, rep, shift, ctrl, alt) then
return false
end
end, handle_error)
local handle_textinput = xpcall_wrap(function(text)
if window_status == "shown" and win:handle_textinput(text) then
return false
end
if sbtn:handle_textinput(text) then
return false
end
if prof:handle_textinput(text) then
return false
end
end, handle_error)
local handle_textediting = xpcall_wrap(function(text)
if window_status == "shown" and win:handle_textediting(text) then
return false
end
if sbtn:handle_textediting(text) then
return false
end
if prof:handle_textediting(text) then
return false
end
end, handle_error)
local handle_blur = xpcall_wrap(function()
if window_status == "shown" and win:handle_blur() then
return false
end
if sbtn:handle_blur() then
return false
end
if prof:handle_blur() then
return false
end
end, handle_error)
evt.register(evt.tick , handle_tick )
evt.register(evt.mousemove , handle_mousemove )
evt.register(evt.mousedown , handle_mousedown )
evt.register(evt.mouseup , handle_mouseup )
evt.register(evt.mousewheel, handle_mousewheel)
evt.register(evt.keypress , handle_keypress )
evt.register(evt.textinput , handle_textinput )
evt.register(evt.keyrelease, handle_keyrelease)
evt.register(evt.blur , handle_blur )
if evt.textediting then
evt.register(evt.textediting, handle_textediting)
end
function TPTMP.disableMultiplayer()
if cli then
cmd:parse("/fpssync off")
cmd:parse("/disconnect")
end
evt.unregister(evt.tick , handle_tick )
evt.unregister(evt.mousemove , handle_mousemove )
evt.unregister(evt.mousedown , handle_mousedown )
evt.unregister(evt.mouseup , handle_mouseup )
evt.unregister(evt.mousewheel, handle_mousewheel)
evt.unregister(evt.keypress , handle_keypress )
evt.unregister(evt.textinput , handle_textinput )
evt.unregister(evt.keyrelease, handle_keyrelease)
evt.unregister(evt.blur , handle_blur )
if evt.textediting then
evt.unregister(evt.textediting, handle_textediting)
end
_G.TPTMP = nil
end
function TPTMP.enableMultiplayer()
hooks_enabled = true
TPTMP.enableMultiplayer = nil
end
win:set_subtitle("status", "Not connected")
if tpt.version.snapshot then
win:backlog_push_neutral(colours.commonstr.error .. "* This is a snapshot version of TPT, expect breakage")
elseif tpt.version.beta then
win:backlog_push_neutral(colours.commonstr.error .. "* This is a beta version of TPT, expect breakage")
end
win:backlog_push_neutral("* Type " .. colours.commonstr.error .. "/connect" .. colours.commonstr.neutral .. " to join a server, " .. colours.commonstr.error .. "/list" .. colours.commonstr.neutral .. " for a list of commands, or " .. colours.commonstr.error .. "/help" .. colours.commonstr.neutral .. " for command help")
win:backlog_notif_reset()
end
return {
run = run,
loadtime_error = loadtime_error,
}
end
require_preload__["tptmp.client.client"] = function()
local buffer_list = require("tptmp.common.buffer_list")
local colours = require("tptmp.client.colours")
local config = require("tptmp.client.config")
local util = require("tptmp.client.util")
local format = require("tptmp.client.format")
local can_yield_xpcall = coroutine.resume(coroutine.create(function()
assert(pcall(coroutine.yield))
end))
local client_i = {}
local client_m = { __index = client_i }
local packet_handlers = {}
local function get_msec()
return math.floor(socket.gettime() * 1000)
end
local index_to_lrax = {
[ 0 ] = "tool_l",
[ 1 ] = "tool_r",
[ 2 ] = "tool_a",
[ 3 ] = "tool_x",
}
local function get_auth_token(audience)
local req = http.getAuthToken(audience)
local started_at = socket.gettime()
while req:status() == "running" do
if socket.gettime() > started_at + config.auth_backend_timeout then
return nil, "timeout", "failed to contact authentication backend"
end
coroutine.yield()
end
local body, code = req:finish()
if code == 403 then
return nil, "refused", body
end
if code ~= 200 then
return nil, "non200", code
end
return body
end
function client_i:proto_error_(...)
self:stop("protocol error: " .. string.format(...))
coroutine.yield()
end
function client_i:proto_close_(message)
self:stop(message)
coroutine.yield()
end
function client_i:read_(count)
while self.rx_:pending() < count do
coroutine.yield()
end
return self.rx_:get(count)
end
function client_i:read_bytes_(count)
while self.rx_:pending() < count do
coroutine.yield()
end
local data, first, last = self.rx_:next()
if last >= first + count - 1 then
-- * Less memory-intensive path.
self.rx_:pop(count)
return data:byte(first, first + count - 1)
end
return self.rx_:get(count):byte(1, count)
end
function client_i:read_str24_()
return self:read_(self:read_24be_())
end
function client_i:read_str8_()
return self:read_(self:read_bytes_(1))
end
function client_i:read_nullstr_(max)
local collect = {}
while true do
local byte = self:read_bytes_(1)
if byte == 0 then
break
end
if #collect == max then
self:proto_error_("overlong nullstr")
end
table.insert(collect, string.char(byte))
end
return table.concat(collect)
end
function client_i:read_24be_()
local hi, mi, lo = self:read_bytes_(3)
return bit.bor(lo, bit.lshift(mi, 8), bit.lshift(hi, 16))
end
function client_i:read_xy_12_()
local d24 = self:read_24be_()
return bit.rshift(d24, 12), bit.band(d24, 0xFFF)
end
function client_i:handle_disconnect_reason_2_()
local reason = self:read_str8_()
self.should_not_reconnect_func_()
self:stop(reason)
end
function client_i:handle_ping_3_()
self.last_ping_received_at_ = socket.gettime()
end
local member_i = {}
local member_m = { __index = member_i }
function member_i:can_render()
return self.can_render_
end
function member_i:update_can_render()
if not self.can_render_ then
if self.deco_a ~= nil and
self.kmod_c ~= nil and
self.shape ~= nil and
self.size_x ~= nil and
self.pos_x ~= nil then
self.can_render_ = true
end
end
end
function client_i:add_member_(id, nick)
if self.id_to_member[id] or id == self.self_id_ then
self:proto_close_("member already exists")
end
self.id_to_member[id] = setmetatable({
nick = nick,
fps_sync = false,
identifiers = {},
}, member_m)
end
function client_i:push_names(prefix)
self.window_:backlog_push_room(self.room_name_, self.id_to_member, prefix)
end
function client_i:push_fpssync()
local members = {}
for _, member in pairs(self.id_to_member) do
if member.fps_sync then
table.insert(members, member)
end
end
self.window_:backlog_push_fpssync(members)
end
function client_i:handle_room_16_()
sim.clearSim()
self.room_name_ = self:read_str8_()
local item_count
self.self_id_, item_count = self:read_bytes_(2)
self.id_to_member = {}
for i = 1, item_count do
local id = self:read_bytes_(1)
local nick = self:read_str8_()
self:add_member_(id, nick)
end
self:reformat_nicks_()
self:push_names("Joined ")
self.window_:set_subtitle("room", self.room_name_)
self.localcmd_:reconnect_commit({
room = self.room_name_,
host = self.host_,
port = self.port_,
secure = self.secure_,
})
self:user_sync_()
end
function client_i:user_sync_()
self:send_elemlist(util.element_identifiers())
self.profile_:user_sync()
end
function client_i:handle_join_17_()
local id = self:read_bytes_(1)
local nick = self:read_str8_()
self:add_member_(id, nick)
self:reformat_nicks_()
self.window_:backlog_push_join(self.id_to_member[id].formatted_nick)
self:rehash_supported_elements_()
self:user_sync_()
end
function client_i:member_prefix_()
local id = self:read_bytes_(1)
local member = self.id_to_member[id]
if not member then
self:proto_close_("no such member")
end
return member, id
end
function client_i:handle_leave_18_()
local member, id = self:member_prefix_()
local nick = member.nick
self.window_:backlog_push_leave(self.id_to_member[id].formatted_nick)
self.id_to_member[id] = nil
self:rehash_supported_elements_()
end
function client_i:handle_say_19_()
local member = self:member_prefix_()
local msg = self:read_str8_()
self.window_:backlog_push_say_other(member.formatted_nick, msg)
end
function client_i:handle_say3rd_20_()
local member = self:member_prefix_()
local msg = self:read_str8_()
self.window_:backlog_push_say3rd_other(member.formatted_nick, msg)
end
function client_i:handle_server_22_()
local msg = self:read_str8_()
self.window_:backlog_push_server(msg)
end
function client_i:handle_elemlist_23_()
local member = self:member_prefix_()
local length = self:read_24be_()
local cstr = self:read_str24_()
local str, _, err = bz2.decompress(cstr, length)
local identifiers = {}
if str then
for name in str:gmatch("[^ ]+") do
identifiers[name] = true
end
else
self.log_event_func_(colours.commonstr.error .. "Failed to parse supported element list from " .. member.formatted_nick .. colours.commonstr.error .. ": " .. err)
end
member.identifiers = identifiers
self:rehash_supported_elements_()
end
function client_i:rehash_supported_elements_()
local unsupported = {}
for key in pairs(self.identifiers_) do
for member_id, member in pairs(self.id_to_member) do
if not member.identifiers[key] then
if not unsupported[key] then
unsupported[key] = {}
end
table.insert(unsupported[key], member_id)
end
end
end
local supported = {}
for key in pairs(self.identifiers_) do
if not unsupported[key] then
table.insert(supported, key)
end
end
self.xidr = util.xid_registry(supported)
self.xidr_unsupported = unsupported
self.profile_:xidr_sync()
end
function client_i:handle_sync_30_()
local member = self:member_prefix_()
self:read_(3)
local data = self:read_str24_()
local ok, err = util.stamp_load(0, 0, data, true)
if ok then
self.log_event_func_(colours.commonstr.event .. "Sync from " .. member.formatted_nick)
else
self.log_event_func_(colours.commonstr.error .. "Failed to sync from " .. member.formatted_nick .. colours.commonstr.error .. ": " .. err)
end
end
function client_i:handle_pastestamp_31_()
local member = self:member_prefix_()
local x, y = self:read_xy_12_()
local data = self:read_str24_()
local ok, err = util.stamp_load(x, y, data, false)
if ok then
self.log_event_func_(colours.commonstr.event .. "Stamp from " .. member.formatted_nick) -- * Not really needed thanks to the stamp intent displays in init.lua.
else
self.log_event_func_(colours.commonstr.error .. "Failed to paste stamp from " .. member.formatted_nick .. colours.commonstr.error .. ": " .. err)
end
end
function client_i:handle_mousepos_32_()
local member = self:member_prefix_()
member.pos_x, member.pos_y = self:read_xy_12_()
member:update_can_render()
end
function client_i:handle_brushmode_33_()
local member = self:member_prefix_()
local bmode = self:read_bytes_(1)
member.bmode = bmode < 3 and bmode or 0
member:update_can_render()
end
function client_i:handle_brushsize_34_()
local member = self:member_prefix_()
local x, y = self:read_bytes_(2)
member.size_x = x
member.size_y = y
member:update_can_render()
end
function client_i:handle_brushshape_35_()
local member = self:member_prefix_()
member.shape = self:read_bytes_(1)
member:update_can_render()
end
function client_i:handle_keybdmod_36_()
local member = self:member_prefix_()
local kmod = self:read_bytes_(1)
member.kmod_c = bit.band(kmod, 1) ~= 0
member.kmod_s = bit.band(kmod, 2) ~= 0
member.kmod_a = bit.band(kmod, 4) ~= 0
member:update_can_render()
end
function client_i:handle_selecttool_37_()
local member = self:member_prefix_()
local hi, lo = self:read_bytes_(2)
local tool = bit.bor(lo, bit.lshift(hi, 8))
local index = bit.rshift(tool, 14)
local xtype = bit.band(tool, 0x3FFF)
member[index_to_lrax[index]] = self.xidr.to_tool[xtype] and xtype or self.xidr.unknown_xid
member.last_toolslot = index
end
local simstates = {
{
format = "Simulation %s by %s",
states = { "unpaused", "paused" },
func = tpt.set_pause,
shift = 0,
size = 1,
},
{
format = "Heat simulation %s by %s",
states = { "disabled", "enabled" },
func = tpt.heat,
shift = 1,
size = 1,
},
{
format = "Ambient heat simulation %s by %s",
states = { "disabled", "enabled" },
func = tpt.ambient_heat,
shift = 2,
size = 1,
},
{
format = "Newtonian gravity %s by %s",
states = { "disabled", "enabled" },
func = tpt.newtonian_gravity,
shift = 3,
size = 1,
},
{
format = "Sand effect %s by %s",
states = { "disabled", "enabled" },
func = sim.prettyPowders,
shift = 5,
size = 1,
},
{
format = "Water equalisation %s by %s",
states = { "disabled", "enabled" },
func = sim.waterEqualisation,
shift = 4,
size = 1,
},
{
format = "Gravity mode set to %s by %s",
states = { "vertical", "off", "radial", "custom" },
func = sim.gravityMode,
shift = 8,
size = 2,
},
{
format = "Air mode set to %s by %s",
states = { "on", "pressure off", "velocity off", "off", "no update" },
func = sim.airMode,
shift = 10,
size = 3,
},
{
format = "Edge mode set to %s by %s",
states = { "void", "solid", "loop" },
func = sim.edgeMode,
shift = 13,
size = 2,
},
}
function client_i:handle_simstate_38_()
local member = self:member_prefix_()
local lo, hi = self:read_bytes_(2)
local temp = self:read_24be_()
local gravx = self:read_24be_()
local gravy = self:read_24be_()
local bits = bit.bor(lo, bit.lshift(hi, 8))
for i = 1, #simstates do
local desc = simstates[i]
local value = bit.band(bit.rshift(bits, desc.shift), bit.lshift(1, desc.size) - 1)
if value + 1 > #desc.states then
value = 0
end
if desc.func() ~= value then
desc.func(value)
self.log_event_func_(colours.commonstr.event .. desc.format:format(desc.states[value + 1], member.formatted_nick))
end
end
if util.ambient_air_temp() ~= temp then
local set = util.ambient_air_temp(temp)
self.log_event_func_(colours.commonstr.event .. ("Ambient air temperature set to %.2f by %s"):format(set, member.formatted_nick))
end
do
local cgx, cgy = util.custom_gravity()
if cgx ~= gravx or cgy ~= gravy then
local setx, sety = util.custom_gravity(gravx, gravy)
if sim.gravityMode() == 3 then
self.log_event_func_(colours.commonstr.event .. ("Custom gravity set to (%+.2f, %+.2f) by %s"):format(setx, sety, member.formatted_nick))
end
end
end
self.profile_:sample_simstate()
end
function client_i:handle_flood_39_()
local member = self:member_prefix_()
local index = self:read_bytes_(1)
if index > 3 then
index = 0
end
member.last_tool = member[index_to_lrax[index]]
local x, y = self:read_xy_12_()
if member.last_tool then
util.flood_any(self.xidr, x, y, member.last_tool, -1, -1, member)
end
end
function client_i:handle_lineend_40_()
local member = self:member_prefix_()
local x1, y1 = member.line_x, member.line_y
local x2, y2 = self:read_xy_12_()
if member:can_render() and x1 and member.last_tool then
if member.kmod_a then
x2, y2 = util.line_snap_coords(x1, y1, x2, y2)
end
util.create_line_any(self.xidr, x1, y1, x2, y2, member.size_x, member.size_y, member.last_tool, member.shape, member, false)
end
member.line_x, member.line_y = nil, nil
end
function client_i:handle_rectend_41_()
local member = self:member_prefix_()
local x1, y1 = member.rect_x, member.rect_y
local x2, y2 = self:read_xy_12_()
if member:can_render() and x1 and member.last_tool then
if member.kmod_a then
x2, y2 = util.rect_snap_coords(x1, y1, x2, y2)
end
util.create_box_any(self.xidr, x1, y1, x2, y2, member.last_tool, member)
end
member.rect_x, member.rect_y = nil, nil
end
function client_i:handle_pointsstart_42_()
local member = self:member_prefix_()
local index = self:read_bytes_(1)
if index > 3 then
index = 0
end
member.last_tool = member[index_to_lrax[index]]
local x, y = self:read_xy_12_()
if member:can_render() and member.last_tool then
util.create_parts_any(self.xidr, x, y, member.size_x, member.size_y, member.last_tool, member.shape, member)
end
member.last_x = x
member.last_y = y
end
function client_i:handle_pointscont_43_()
local member = self:member_prefix_()
local x, y = self:read_xy_12_()
if member:can_render() and member.last_tool and member.last_x then
util.create_line_any(self.xidr, member.last_x, member.last_y, x, y, member.size_x, member.size_y, member.last_tool, member.shape, member, true)
end
member.last_x = x
member.last_y = y
end
function client_i:handle_linestart_44_()
local member = self:member_prefix_()
local index = self:read_bytes_(1)
if index > 3 then
index = 0
end
member.last_tool = member[index_to_lrax[index]]
member.line_x, member.line_y = self:read_xy_12_()
end
function client_i:handle_rectstart_45_()
local member = self:member_prefix_()
local index = self:read_bytes_(1)
if index > 3 then
index = 0
end
member.last_tool = member[index_to_lrax[index]]
member.rect_x, member.rect_y = self:read_xy_12_()
end
function client_i:handle_custgolinfo_46_()
local member = self:member_prefix_()
local ruleset = bit.band(self:read_24be_(), 0x1FFFFF)
local primary = self:read_24be_()
local secondary = self:read_24be_()
local begin = bit.band(bit.rshift(ruleset, 8), 0x1FE)
local stay = bit.band(ruleset, 0x1FF)
local states = bit.band(bit.rshift(ruleset, 17), 0xF) + 2
local repr = {}
table.insert(repr, "B")
for i = 0, 8 do
if bit.band(bit.lshift(1, i), begin) ~= 0 then
table.insert(repr, i)
end
end
table.insert(repr, "/")
table.insert(repr, "S")
for i = 0, 8 do
if bit.band(bit.lshift(1, i), stay) ~= 0 then
table.insert(repr, i)
end
end
if states ~= 2 then
table.insert(repr, "/")
table.insert(repr, states)
end
member[index_to_lrax[member.last_toolslot]] = {
type = "cgol",
repr = table.concat(repr),
ruleset = ruleset,
primary = primary,
secondary = secondary,
elem = bit.bor(elem.DEFAULT_PT_LIFE, bit.lshift(ruleset, sim.PMAPBITS)),
}
end
function client_i:handle_stepsim_50_()
local member = self:member_prefix_()
tpt.set_pause(1)
sim.framerender(1)
self.log_event_func_(colours.commonstr.event .. "Single-frame step from " .. member.formatted_nick)
end
function client_i:handle_sparkclear_60_()
local member = self:member_prefix_()
tpt.reset_spark()
self.log_event_func_(colours.commonstr.event .. "Sparks cleared by " .. member.formatted_nick)
end
function client_i:handle_airclear_61_()
local member = self:member_prefix_()
tpt.reset_velocity()
tpt.set_pressure()
self.log_event_func_(colours.commonstr.event .. "Pressure cleared by " .. member.formatted_nick)
end
function client_i:handle_airinv_62_()
-- * TODO[api]: add an api for this to tpt
local member = self:member_prefix_()
for x = 0, sim.XRES / sim.CELL - 1 do
for y = 0, sim.YRES / sim.CELL - 1 do
sim.pressure(x, y, -sim.pressure(x, y))
end
end
self.log_event_func_(colours.commonstr.event .. "Pressure inverted by " .. member.formatted_nick)
end
function client_i:handle_clearsim_63_()
local member = self:member_prefix_()
sim.clearSim()
self.set_id_func_(nil, nil)
self.log_event_func_(colours.commonstr.event .. "Simulation cleared by " .. member.formatted_nick)
end
function client_i:handle_heatclear_64_()
-- * TODO[api]: add an api for this to tpt
local member = self:member_prefix_()
util.heat_clear()
self.log_event_func_(colours.commonstr.event .. "Ambient heat reset by " .. member.formatted_nick)
end
function client_i:handle_brushdeco_65_()
local member = self:member_prefix_()
member.deco_a, member.deco_r, member.deco_g, member.deco_b = self:read_bytes_(4)
member:update_can_render()
end
function client_i:handle_clearrect_67_()
self:member_prefix_()
local x, y = self:read_xy_12_()
local w, h = self:read_xy_12_()
util.clear_rect(x, y, w, h)
end
function client_i:handle_canceldraw_68_()
local member = self:member_prefix_()
member.rect_x, member.rect_y = nil, nil
member.line_x, member.line_y = nil, nil
member.last_tool = nil
end
function client_i:handle_loadonline_69_()
local member = self:member_prefix_()
local id = self:read_24be_()
local histhi = self:read_24be_()
local histlo = self:read_24be_()
local hist = histhi * 0x1000000 + histlo
if id > 0 then
sim.loadSave(id, 1, hist)
coroutine.yield() -- * sim.loadSave seems to take effect one frame late.
self.set_id_func_(id, hist)
self.log_event_func_(colours.commonstr.event .. "Online save " .. (hist == 0 and "id" or "history") .. ":" .. id .. " loaded by " .. member.formatted_nick)
end
end
function client_i:handle_reloadsim_70_()
local member = self:member_prefix_()
if self.get_id_func_() then
sim.reloadSave()
end
self.log_event_func_(colours.commonstr.event .. "Simulation reloaded by " .. member.formatted_nick)
end
function client_i:handle_placestatus_71_()
local member = self:member_prefix_()
local k = self:read_bytes_(1)
local w, h = self:read_xy_12_()
if k == 0 then
member.place = nil
elseif k == 1 then
member.place = "Pasting"
end
member.place_w = w
member.place_h = h
end
function client_i:handle_selectstatus_72_()
local member = self:member_prefix_()
local k = self:read_bytes_(1)
local x, y = self:read_xy_12_()
if k == 0 then
member.select = nil
elseif k == 1 then
member.select = "Copying"
elseif k == 2 then
member.select = "Cutting"
elseif k == 3 then
member.select = "Stamping"
end
member.select_x = x
member.select_y = y
end
function client_i:handle_zoomstart_73_()
local member = self:member_prefix_()
local x, y = self:read_xy_12_()
local s = self:read_bytes_(1)
member.zoom_x = x
member.zoom_y = y
member.zoom_s = s
end
function client_i:handle_zoomend_74_()
local member = self:member_prefix_()
member.zoom_x = nil
member.zoom_y = nil
member.zoom_s = nil
end
function client_i:handle_sparksign_75_()
local member = self:member_prefix_()
local x, y = self:read_xy_12_()
sim.partCreate(-1, x, y, elem.DEFAULT_PT_SPRK)
end
function client_i:handle_fpssync_76_()
local member = self:member_prefix_()
local hi = self:read_24be_()
local mi = self:read_24be_()
local lo = self:read_24be_()
local elapsed = hi * 0x1000 + math.floor(mi / 0x1000)
local count = mi % 0x1000 * 0x1000000 + lo
if member.fps_sync and elapsed <= member.fps_sync_elapsed then
self:fps_sync_end_(member)
end
local now_msec = get_msec()
if not member.fps_sync then
member.fps_sync = true
member.fps_sync_count_diff = 0
member.fps_sync_first = now_msec
member.fps_sync_history = {}
if self.fps_sync_count_ then
member.fps_sync_count_offset = count - self.fps_sync_count_
end
if self.fps_sync_ then
self.window_:backlog_push_fpssync_enable(member.formatted_nick)
end
end
member.fps_sync_last = now_msec
member.fps_sync_elapsed = elapsed
member.fps_sync_count = count
local history_item = { elapsed = elapsed, count = count, now_msec = now_msec }
local history_size = #member.fps_sync_history
if history_size < 5 then
table.insert(member.fps_sync_history, 1, history_item)
else
for i = 1, history_size - 1 do
member.fps_sync_history[i + 1] = member.fps_sync_history[i]
end
member.fps_sync_history[1] = history_item
end
end
function client_i:handle_sync_request_128_()
self:send_sync_done()
end
function client_i:connect_()
self.server_probably_secure_ = nil
self.window_:set_subtitle("status", "Connecting")
self.socket_ = socket.tcp()
self.socket_:settimeout(0)
self.socket_:setoption("tcp-nodelay", true)
while true do
local ok, err = self.socket_:connect(self.host_, self.port_, self.secure_)
if ok then
break
elseif err == "timeout" then
coroutine.yield()
else
local errl = err:lower()
if errl:find("schannel") or errl:find("ssl") then
self.server_probably_secure_ = true
end
self:proto_close_(err)
end
end
self.connected_ = true
end
function client_i:handshake_()
self.window_:set_subtitle("status", "Registering")
local name = util.get_name()
self:write_bytes_(255, 255, config.version)
self:write_nullstr_((name or tpt.get_name() or ""):sub(1, 255))
self:write_24be_(tpt.version.upstreamBuild)
self:write_bytes_(0) -- * Flags, currently unused.
local qa_host, qa_port, qa_name, qa_token = self.get_qa_func_():match("^([^:]+):([^:]+):([^:]+):([^:]+)$")
self:write_str8_(qa_token and qa_name == name and qa_host == self.host_ and tonumber(qa_port) == self.port_ and qa_token or "")
self:write_str8_(self.initial_room_ or "")
self:write_flush_()
local conn_status = self:read_bytes_(1)
local auth_err
if conn_status == 4 then -- * Quickauth failed.
self.window_:set_subtitle("status", "Authenticating")
local token = ""
if name then
local fresh_token, err, info = get_auth_token(self.host_ .. ":" .. self.port_)
if fresh_token then
token = fresh_token
else
if err == "non200" then
auth_err = "authentication failed (status code " .. info .. "); try again later or try restarting TPT"
elseif err == "timeout" then
auth_err = "authentication failed (timeout: " .. info .. "); try again later or try restarting TPT"
else
auth_err = "authentication failed (" .. err .. ": " .. info .. "); try logging out and back in and restarting TPT"
end
end
end
self:write_str8_(token)
self:write_flush_()
conn_status = self:read_bytes_(1)
if name then
self.set_qa_func_((conn_status == 1) and (self.host_ .. ":" .. self.port_ .. ":" .. name .. ":" .. token) or "")
end
end
local downgrade_reason
if conn_status == 5 then -- * Downgraded to guest.
downgrade_reason = self:read_str8_()
conn_status = self:read_bytes_(1)
end
if conn_status == 1 then
self.should_reconnect_func_()
self.registered_ = true
self.nick_ = self:read_str8_()
self:reformat_nicks_()
self.flags_ = self:read_bytes_(1)
self.guest_ = bit.band(self.flags_, 1) ~= 0
self.last_ping_sent_at_ = socket.gettime()
self.connecting_since_ = nil
if tpt.get_name() and auth_err then
self.window_:backlog_push_error("Warning: " .. auth_err)
end
if downgrade_reason then
self.window_:backlog_push_error("Warning: " .. downgrade_reason)
end
self.window_:backlog_push_registered(self.formatted_nick_)
self.profile_:set_client(self)
elseif conn_status == 0 then
local reason = self:read_nullstr_(255)
self:proto_close_(auth_err or reason)
else
self:proto_error_("invalid connection status (%i)", conn_status)
end
end
function client_i:send_ping()
self:write_flush_("\3")
end
function client_i:send_say(str)
self:write_("\19")
self:write_str8_(str)
self:write_flush_()
end
function client_i:send_say3rd(str)
self:write_("\20")
self:write_str8_(str)
self:write_flush_()
end
function client_i:send_elemlist(identifiers)
self:write_("\23")
local arr = {}
for name in pairs(identifiers) do
table.insert(arr, name)
end
local str = table.concat(arr, " ")
local cstr = bz2.compress(str)
self:write_24be_(#str)
self:write_str24_(cstr)
self:write_flush_()
end
function client_i:send_mousepos(px, py)
self:write_("\32")
self:write_xy_12_(px, py)
self:write_flush_()
end
function client_i:send_brushmode(bmode)
self:write_("\33")
self:write_bytes_(bmode)
self:write_flush_()
end
function client_i:send_brushsize(sx, sy)
self:write_("\34")
self:write_bytes_(sx, sy)
self:write_flush_()
end
function client_i:send_brushshape(shape)
self:write_("\35")
self:write_bytes_(shape)
self:write_flush_()
end
function client_i:send_keybdmod(c, s, a)
self:write_("\36")
self:write_bytes_(bit.bor(c and 1 or 0, s and 2 or 0, a and 4 or 0))
self:write_flush_()
end
function client_i:send_selecttool(idx, xtype)
self:write_("\37")
local tool = bit.bor(xtype, bit.lshift(idx, 14))
local hi = bit.band(bit.rshift(tool, 8), 0xFF)
local lo = bit.band( tool , 0xFF)
self:write_bytes_(hi, lo)
self:write_flush_()
end
function client_i:send_simstate(ss_p, ss_h, ss_u, ss_n, ss_w, ss_g, ss_a, ss_e, ss_y, ss_t, ss_r, ss_s)
self:write_("\38")
local toggles = bit.bor(
ss_p ,
bit.lshift(ss_h, 1),
bit.lshift(ss_u, 2),
bit.lshift(ss_n, 3),
bit.lshift(ss_w, 4),
bit.lshift(ss_y, 5)
)
local multis = bit.bor(
ss_g ,
bit.lshift(ss_a, 2),
bit.lshift(ss_e, 5)
)
self:write_bytes_(toggles, multis)
self:write_24be_(ss_t)
self:write_24be_(ss_r)
self:write_24be_(ss_s)
self:write_flush_()
end
function client_i:send_flood(index, x, y)
self:write_("\39")
self:write_bytes_(index)
self:write_xy_12_(x, y)
self:write_flush_()
end
function client_i:send_lineend(x, y)
self:write_("\40")
self:write_xy_12_(x, y)
self:write_flush_()
end
function client_i:send_rectend(x, y)
self:write_("\41")
self:write_xy_12_(x, y)
self:write_flush_()
end
function client_i:send_pointsstart(index, x, y)
self:write_("\42")
self:write_bytes_(index)
self:write_xy_12_(x, y)
self:write_flush_()
end
function client_i:send_pointscont(x, y)
self:write_("\43")
self:write_xy_12_(x, y)
self:write_flush_()
end
function client_i:send_linestart(index, x, y)
self:write_("\44")
self:write_bytes_(index)
self:write_xy_12_(x, y)
self:write_flush_()
end
function client_i:send_rectstart(index, x, y)
self:write_("\45")
self:write_bytes_(index)
self:write_xy_12_(x, y)
self:write_flush_()
end
function client_i:send_custgolinfo(ruleset, primary, secondary)
self:write_("\46")
self:write_24be_(ruleset)
self:write_24be_(primary)
self:write_24be_(secondary)
self:write_flush_()
end
function client_i:send_stepsim()
self:write_flush_("\50")
end
function client_i:send_sparkclear()
self:write_flush_("\60")
end
function client_i:send_airclear()
self:write_flush_("\61")
end
function client_i:send_airinv()
self:write_flush_("\62")
end
function client_i:send_clearsim()
self:write_flush_("\63")
end
function client_i:send_heatclear()
self:write_flush_("\64")
end
function client_i:send_brushdeco(deco)
self:write_("\65")
self:write_bytes_(
bit.band(bit.rshift(deco, 24), 0xFF),
bit.band(bit.rshift(deco, 16), 0xFF),
bit.band(bit.rshift(deco, 8), 0xFF),
bit.band( deco , 0xFF)
)
self:write_flush_()
end
function client_i:send_clearrect(x, y, w, h)
self:write_("\67")
self:write_xy_12_(x, y)
self:write_xy_12_(w, h)
self:write_flush_()
end
function client_i:send_canceldraw()
self:write_flush_("\68")
end
function client_i:send_loadonline(id, hist)
self:write_("\69")
self:write_24be_(id)
self:write_24be_(math.floor(hist / 0x1000000))
self:write_24be_( hist % 0x1000000 )
self:write_flush_()
end
function client_i:send_pastestamp_data_(pid, x, y, w, h)
local data, err = util.stamp_save(x, y, w, h)
if not data then
return nil, err
end
self:write_(pid)
self:write_xy_12_(x, y)
self:write_str24_(data)
self:write_flush_()
return true
end
function client_i:send_pastestamp(x, y, w, h)
local ok, err = self:send_pastestamp_data_("\31", x, y, w, h)
if not ok then
self.log_event_func_(colours.commonstr.error .. "Failed to send stamp: " .. err)
end
end
function client_i:send_sync()
local ok, err = self:send_pastestamp_data_("\30", 0, 0, sim.XRES, sim.YRES)
if not ok then
self.log_event_func_(colours.commonstr.error .. "Failed to send screen: " .. err)
end
end
function client_i:send_reloadsim()
self:write_flush_("\70")
end
function client_i:send_placestatus(k, w, h)
self:write_("\71")
self:write_bytes_(k)
self:write_xy_12_(w, h)
self:write_flush_()
end
function client_i:send_selectstatus(k, x, y)
self:write_("\72")
self:write_bytes_(k)
self:write_xy_12_(x, y)
self:write_flush_()
end
function client_i:send_zoomstart(x, y, s)
self:write_("\73")
self:write_xy_12_(x, y)
self:write_bytes_(s)
self:write_flush_()
end
function client_i:send_zoomend()
self:write_flush_("\74")
end
function client_i:send_sparksign(x, y)
self:write_("\75")
self:write_xy_12_(x, y)
self:write_flush_()
end
function client_i:send_fpssync(elapsed, count)
self:write_("\76")
self:write_24be_(math.floor(elapsed / 0x1000))
self:write_24be_(elapsed % 0x1000 * 0x1000 + math.floor(count / 0x1000000))
self:write_24be_(count % 0x1000000)
self:write_flush_()
end
function client_i:send_sync_done()
self:write_flush_("\128")
local id, hist = self.get_id_func_()
self:send_loadonline(id or 0, hist or 0)
self:send_sync()
self.profile_:simstate_sync()
end
function client_i:start()
assert(self.status_ == "ready")
self.status_ = "running"
self.proto_coro_ = coroutine.create(function()
local wrap_traceback = can_yield_xpcall and xpcall or function(func)
-- * It doesn't matter if wrap_traceback is not a real xpcall
-- as the error would be re-thrown later anyway, but a real
-- xpcall is preferable because it lets us print a stack trace
-- from within the coroutine.
func()
return true
end
local ok, err = wrap_traceback(function()
self:connect_()
self:handshake_()
while true do
local packet_id = self:read_bytes_(1)
local handler = packet_handlers[packet_id]
if not handler then
self:proto_error_("invalid packet ID (%i)", packet_id)
end
handler(self)
end
end, function(err)
if self.handle_error_func_ then
self.handle_error_func_(err)
end
return err
end)
if not ok then
error(err)
end
end)
end
function client_i:tick_read_()
if self.connected_ and not self.read_closed_ then
while true do
local closed = false
local data, err, partial = self.socket_:receive(config.read_size)
if not data then
if err == "closed" then
data = partial
closed = true
elseif err == "timeout" then
data = partial
else
self:stop(err)
break
end
end
local pushed, count = self.rx_:push(data)
if pushed < count then
self:stop("recv queue limit exceeded")
break
end
if closed then
self:tick_resume_()
self:stop("connection closed: receive failed: " .. tostring(self.socket_lasterror_))
break
end
if #data < config.read_size then
break
end
end
end
end
function client_i:tick_resume_()
if self.proto_coro_ then
local ok, err = coroutine.resume(self.proto_coro_)
if not ok then
self.proto_coro_ = nil
error("proto coroutine: " .. err, 0)
end
if self.proto_coro_ and coroutine.status(self.proto_coro_) == "dead" then
error("proto coroutine terminated")
end
end
end
function client_i:tick_write_()
if self.connected_ then
while true do
local data, first, last = self.tx_:next()
if not data then
break
end
local closed = false
local count = last - first + 1
if self.socket_:status() ~= "connected" then
break
end
local written_up_to, err, partial_up_to = self.socket_:send(data, first, last)
if not written_up_to then
if err == "closed" then
written_up_to = partial_up_to
closed = true
elseif err == "timeout" then
written_up_to = partial_up_to
else
self:stop(err)
break
end
end
local written = written_up_to - first + 1
self.tx_:pop(written)
if closed then
self.socket_lasterror_ = self.socket_:lasterror()
self:stop("connection closed: send failed: " .. tostring(self.socket_lasterror_))
break
end
if written < count then
break
end
end
end
end
function client_i:tick_connect_()
if self.socket_ then
if self.connecting_since_ and self.connecting_since_ + config.connect_timeout < socket.gettime() then
self:stop("connect timeout")
end
end
end
function client_i:tick_ping_()
if self.registered_ then
local now = socket.gettime()
if self.last_ping_sent_at_ + config.ping_interval < now then
self:send_ping()
self.last_ping_sent_at_ = now
end
if self.last_ping_received_at_ + config.ping_timeout < now then
self:stop("ping timeout")
end
end
end
function client_i:tick_sim_()
for _, member in pairs(self.id_to_member) do
if member:can_render() then
local lx, ly = member.line_x, member.line_y
if lx and member.last_tool == self.xidr.from_tool.DEFAULT_UI_WIND and not (member.select or member.place) and lx then
local px, py = member.pos_x, member.pos_y
if member.kmod_a then
px, py = util.line_snap_coords(lx, ly, px, py)
end
util.create_line_any(self.xidr, lx, ly, px, py, member.size_x, member.size_y, member.last_tool, member.shape, member, false)
end
end
end
end
function client_i:fps_sync_end_(member)
if self.fps_sync_ then
self.window_:backlog_push_fpssync_disable(member.formatted_nick)
end
member.fps_sync = false
end
function client_i:tick_fpssync_invalidate_()
if self.registered_ then
local now_msec = get_msec()
for _, member in pairs(self.id_to_member) do
if member.fps_sync then
if member.fps_sync_last + config.fps_sync_timeout < now_msec then
self:fps_sync_end_(member)
end
end
end
end
end
function client_i:tick_fpssync_()
if self.registered_ then
if self.fps_sync_ then
local now_msec = get_msec()
if not self.fps_sync_first_ then
self.fps_sync_first_ = now_msec
self.fps_sync_last_ = 0
self.fps_sync_count_ = 0
for _, member in pairs(self.id_to_member) do
if member.fps_sync then
member.fps_sync_count_offset = member.fps_sync_count
end
end
end
self.fps_sync_count_ = self.fps_sync_count_ + 1
if now_msec >= self.fps_sync_last_ + 1000 then
self:send_fpssync(now_msec - self.fps_sync_first_, self.fps_sync_count_)
self.fps_sync_last_ = now_msec
end
local target_fps = self.fps_sync_target_
local smallest_target = self.fps_sync_count_ + math.floor(target_fps * config.fps_sync_plan_ahead_by / 1000)
if self.fps_sync_target_ == 2 then
smallest_target = math.huge
end
for _, member in pairs(self.id_to_member) do
if member.fps_sync and #member.fps_sync_history >= 2 then
local diff_count = member.fps_sync_history[1].count - member.fps_sync_history[2].count
local diff_elapsed = member.fps_sync_history[1].elapsed - member.fps_sync_history[2].elapsed
local slope = diff_count / (diff_elapsed / 1000)
if slope < 5 then slope = 5 end
if slope > 10000 then slope = 10000 end
local current_msec = now_msec - member.fps_sync_history[1].now_msec
local current_frames_remote = math.floor(member.fps_sync_history[1].count + slope * (current_msec / 1000))
local current_frames_local = current_frames_remote - member.fps_sync_count_offset
local target_msec = now_msec - member.fps_sync_history[1].now_msec + config.fps_sync_plan_ahead_by
local target_frames_remote = math.floor(member.fps_sync_history[1].count + slope * (target_msec / 1000))
local target_frames_local = target_frames_remote - member.fps_sync_count_offset
member.fps_sync_count_diff = current_frames_local - self.fps_sync_count_
if smallest_target > target_frames_local then
smallest_target = target_frames_local
end
end
end
if smallest_target == math.huge then
tpt.setfpscap(2)
else
local smallest_fps = (smallest_target - self.fps_sync_count_) / (config.fps_sync_plan_ahead_by / 1000)
local fps = math.floor((target_fps + (smallest_fps - target_fps) * config.fps_sync_homing_factor) + 0.5)
if fps < 10 then fps = 10 end
tpt.setfpscap(fps)
end
end
end
end
function client_i:tick()
if self.status_ ~= "running" then
return
end
self:tick_fpssync_invalidate_()
self:tick_read_()
self:tick_resume_()
self:tick_write_()
self:tick_connect_()
self:tick_ping_()
self:tick_sim_()
self:tick_fpssync_()
end
function client_i:stop(message)
if self.status_ == "dead" then
return
end
self.profile_:clear_client()
if self.socket_ then
if self.connected_ then
self.socket_:shutdown()
end
self.socket_:close()
self.socket_lasterror_ = self.socket_:lasterror()
self.socket_ = nil
self.connected_ = nil
self.registered_ = nil
end
self.proto_coro_ = nil
self.status_ = "dead"
local disconnected = "Disconnected"
if message then
disconnected = disconnected .. ": " .. message
end
self.window_:backlog_push_error(disconnected)
if self.server_probably_secure_ then
self.window_:backlog_push_error(("The server probably does not support secure connections, try /connect %s:%i"):format(self.host_, self.port_))
end
end
function client_i:write_(data)
if not self.write_buf_ then
self.write_buf_ = data
elseif type(self.write_buf_) == "string" then
self.write_buf_ = { self.write_buf_, data }
else
table.insert(self.write_buf_, data)
end
end
function client_i:write_flush_(data)
if data then
self:write_(data)
end
local buf = self.write_buf_
self.write_buf_ = nil
local pushed, count = self.tx_:push(type(buf) == "string" and buf or table.concat(buf))
if pushed < count then
self:stop("send queue limit exceeded")
end
end
function client_i:write_bytes_(...)
self:write_(string.char(...))
end
function client_i:write_str24_(str)
local length = math.min(#str, 0xFFFFFF)
self:write_24be_(length)
self:write_(str:sub(1, length))
end
function client_i:write_str8_(str)
local length = math.min(#str, 0xFF)
self:write_bytes_(length)
self:write_(str:sub(1, length))
end
function client_i:write_nullstr_(str)
self:write_(str:gsub("[^\1-\255]", ""))
self:write_("\0")
end
function client_i:write_24be_(d24)
local hi = bit.band(bit.rshift(d24, 16), 0xFF)
local mi = bit.band(bit.rshift(d24, 8), 0xFF)
local lo = bit.band( d24 , 0xFF)
self:write_bytes_(hi, mi, lo)
end
function client_i:write_xy_12_(x, y)
self:write_24be_(bit.bor(bit.lshift(x, 12), y))
end
function client_i:nick()
return self.nick_
end
function client_i:formatted_nick()
return self.formatted_nick_
end
function client_i:status()
return self.status_
end
function client_i:connected()
return self.connected_
end
function client_i:registered()
return self.registered_
end
function client_i:nick_colour_seed(seed)
self.nick_colour_seed_ = seed
self:reformat_nicks_()
end
function client_i:fps_sync(fps_sync)
if self.fps_sync_ and not fps_sync then
tpt.setfpscap(self.fps_sync_target_)
end
if not self.fps_sync_ and fps_sync then
self.fps_sync_first_ = nil
end
self.fps_sync_ = fps_sync and true or false
self.fps_sync_target_ = fps_sync or false
end
function client_i:reformat_nicks_()
if self.nick_ then
self.formatted_nick_ = format.nick(self.nick_, self.nick_colour_seed_)
end
for _, member in pairs(self.id_to_member) do
member.formatted_nick = format.nick(member.nick, self.nick_colour_seed_)
end
end
function client_i:tool_proper_name(tool)
return util.tool_proper_name(tool, self.xidr)
end
for key, value in pairs(client_i) do
local packet_id_str = key:match("^handle_.+_(%d+)_$")
if packet_id_str then
local packet_id = tonumber(packet_id_str)
assert(not packet_handlers[packet_id])
packet_handlers[packet_id] = value
end
end
local function new(params)
local now = socket.gettime()
local cli = setmetatable({
host_ = params.host,
port_ = params.port,
secure_ = params.secure,
event_log_ = params.event_log,
backlog_ = params.backlog,
rx_ = buffer_list.new({ limit = config.recvq_limit }),
tx_ = buffer_list.new({ limit = config.sendq_limit }),
connecting_since_ = now,
last_ping_sent_at_ = now,
last_ping_received_at_ = now,
status_ = "ready",
window_ = params.window,
profile_ = params.profile,
localcmd_ = params.localcmd,
initial_room_ = params.initial_room,
set_id_func_ = params.set_id_func,
get_id_func_ = params.get_id_func,
set_qa_func_ = params.set_qa_func,
get_qa_func_ = params.get_qa_func,
log_event_func_ = params.log_event_func,
handle_error_func_ = params.handle_error_func,
should_reconnect_func_ = params.should_reconnect_func,
should_not_reconnect_func_ = params.should_not_reconnect_func,
id_to_member = {},
nick_colour_seed_ = 0,
identifiers_ = util.element_identifiers(),
fps_sync_ = false,
}, client_m)
cli:rehash_supported_elements_()
return cli
end
return {
new = new,
}
end
require_preload__["tptmp.client.colours"] = function()
local utf8 = require("tptmp.client.utf8")
local function hsv_to_rgb(hue, saturation, value) -- * [0, 1), [0, 1), [0, 1)
local sector = math.floor(hue * 6)
local offset = hue * 6 - sector
local red, green, blue
if sector == 0 then
red, green, blue = 1, offset, 0
elseif sector == 1 then
red, green, blue = 1 - offset, 1, 0
elseif sector == 2 then
red, green, blue = 0, 1, offset
elseif sector == 3 then
red, green, blue = 0, 1 - offset, 1
elseif sector == 4 then
red, green, blue = offset, 0, 1
else
red, green, blue = 1, 0, 1 - offset
end
return {
math.floor((saturation * (red - 1) + 1) * 0xFF * value),
math.floor((saturation * (green - 1) + 1) * 0xFF * value),
math.floor((saturation * (blue - 1) + 1) * 0xFF * value),
}
end
local function escape(rgb)
return utf8.encode_multiple(15, rgb[1], rgb[2], rgb[3])
end
local common = {}
local commonstr = {}
for key, value in pairs({
brush = { 0, 255, 0 },
chat = { 255, 255, 255 },
error = { 255, 50, 50 },
event = { 255, 255, 255 },
join = { 100, 255, 100 },
leave = { 255, 255, 100 },
fpssyncenable = { 255, 100, 255 },
fpssyncdisable = { 130, 130, 255 },
lobby = { 0, 200, 200 },
neutral = { 200, 200, 200 },
room = { 200, 200, 0 },
status = { 150, 150, 150 },
notif_normal = { 100, 100, 100 },
notif_important = { 255, 50, 50 },
player_cursor = { 0, 255, 0, 128 },
}) do
common[key] = value
commonstr[key] = escape(value)
end
local appearance = {
hover = {
background = { 20, 20, 20 },
text = { 255, 255, 255 },
border = { 255, 255, 255 },
},
inactive = {
background = { 0, 0, 0 },
text = { 255, 255, 255 },
border = { 200, 200, 200 },
},
active = {
background = { 255, 255, 255 },
text = { 0, 0, 0 },
border = { 235, 235, 235 },
},
}
return {
escape = escape,
common = common,
commonstr = commonstr,
hsv_to_rgb = hsv_to_rgb,
appearance = appearance,
}
end
require_preload__["tptmp.client.config"] = function()
local common_config = require("tptmp.common.config")
local versionstr = "v2.1.0"
local config = {
-- ***********************************************************************
-- *** The following options are purely cosmetic and should be ***
-- *** customised in accordance with your taste. ***
-- ***********************************************************************
-- * Version string to display in the window title.
versionstr = versionstr,
-- * Amount of incoming messages to remember, counted from the
-- last one received.
backlog_size = 1000,
-- * Amount of outgoing messages to remember, counted from the
-- last one sent.
history_size = 1000,
-- * Default window width. Overridden by the value loaded from the manager
-- backend, if any.
default_width = 230,
-- * Default window height. Similar to default_width.
default_height = 155,
-- * Default window background alpha. Similar to default_width.
default_alpha = 150,
-- * Minimum window width.
min_width = 160,
-- * Minimum window height.
min_height = 107,
-- * Amount of time in seconds that elapses between a notification bubble
-- appearing and settling in its final position.
notif_fly_time = 0.1,
-- * Distance in pixels between the position where a notification appears
-- and the position where it settles.
notif_fly_distance = 3,
-- * Amount of time in seconds that elapses between a message arriving and
-- it beginning to fade out if the window is floating.
floating_linger_time = 3,
-- * Amount of time in seconds that elapses between a message beginning to
-- fade out and disappearing completely if the window is floating.
floating_fade_time = 1,
-- * Path to tptmp.client.manager.null configuration file relative to
-- current directory. Only relevant if the null manager is active.
null_manager_path = "tptmpsettings.txt",
-- * Path to error trace file relative to current directory.
trace_path = "tptmptrace.log",
-- ***********************************************************************
-- *** The following options should only be changed if you know what ***
-- *** you are doing. This usually involves consulting with the ***
-- *** developers. Otherwise, these are sane values you should trust. ***
-- ***********************************************************************
-- * Specifies whether connections made without specifying the port number
-- should be encrypted. Default should match the common setting.
default_secure = common_config.secure,
-- * Size of the buffer passed to the recv system call. Bigger values
-- consume more memory, smaller ones incur larger system call overhead.
read_size = 0x1000000,
-- * Receive queue limit. Specifies the maximum amount of data the server
-- is allowed to have sent but which the client has not yet had time to
-- process. The connection is closed if the size of the receive queue
-- exceeds this limit.
recvq_limit = 0x200000,
-- * Send queue limit. Specifies the maximum amount of data the server
-- is allowed to have not yet processed but which the client has already
-- queued. The connection is closed if the size of the send queue exceeds
-- this limit.
sendq_limit = 0x2000000,
-- * Maximum amount of time in seconds after which the connection attempt
-- should be deemed a failure, unless it succeeds.
connect_timeout = 15,
-- * Amount of time in seconds between pings being sent to the server.
-- Should be half of the ping_timeout option on the server side or less.
ping_interval = 60,
-- * Amount of time in seconds the connection is allowed to be maintained
-- without the server sending a ping. Should be twice the ping_interval
-- option on the server side or more.
ping_timeout = 120,
-- * Amount of time in seconds that elapses between a non-graceful
-- connection closure (anything that isn't the client willingly
-- disconnecting or the server explicitly dropping the client) and an
-- attempt to establish a new connection.
reconnect_later_timeout = 2,
-- * Path to the temporary stamp created when syncing.
stamp_temp = ".tptmp.stm",
-- * Pattern used to match word characters by the textbox. Used by cursor
-- control, mostly Ctrl+Left and Ctrl+Right and related shortcuts.
word_pattern = "^[A-Za-z0-9-_\128-\255]+$",
-- * Pattern used to match whitespace characters by the textbox. Similar to
-- word_pattern.
whitespace_pattern = "^ $",
-- * Namespace for settings stored in the manager backend.
manager_namespace = "tptmp",
-- * Grace period in milliseconds after which another client is deemed to
-- not have FPS synchronization enabled.
fps_sync_timeout = 10000,
-- * Interval to plan ahead in milliseconds, after which local number of
-- frames simulated should more or less match the number of frames
-- everyone else with FPS synchronization enabled has simulated.
fps_sync_plan_ahead_by = 3000,
-- * Coefficient of linear interpolation between the current target FPS and
-- that of the slowest client in the room with FPS synchronization
-- enabled used when slowing down to match the number of frames simulated
-- by this client. 0 means no slowing down at all, 1 means slowing down
-- to the framerate the other client seems to be running at.
fps_sync_homing_factor = 0.5,
-- ***********************************************************************
-- *** The following options should be changed in ***
-- *** tptmp/common/config.lua instead. Since these options should ***
-- *** align with the equivalent options on the server side, you ***
-- *** will most likely have to run your own version of the server ***
-- *** if you intend to change these. ***
-- ***********************************************************************
-- * Host to connect to by default.
default_host = common_config.host,
-- * Port to connect to by default.
default_port = common_config.port,
-- * Protocol version.
version = common_config.version,
-- * Client-to-server message size limit.
message_size = common_config.message_size,
-- * Client-to-server message rate limit.
message_interval = common_config.message_interval,
-- * Authentication backend URL. Only relevant if auth = true on the
-- server side.
auth_backend = common_config.auth_backend,
-- * Authentication backend timeout in seconds. Only relevant if
--- auth = true on the server side.
auth_backend_timeout = common_config.auth_backend_timeout,
}
config.default_x = math.floor((sim.XRES - config.default_width) / 2)
config.default_y = math.floor((sim.YRES - config.default_height) / 2)
return config
end
require_preload__["tptmp.client.format"] = function()
local colours = require("tptmp.client.colours")
local util = require("tptmp.client.util")
local function nick(unformatted, seed)
return colours.escape(colours.hsv_to_rgb(util.fnv1a32(seed .. unformatted .. "bagels") / 0x100000000, 0.5, 1)) .. unformatted
end
local names = {
[ "null" ] = "lobby",
[ "guest" ] = "guest lobby",
[ "kicked" ] = "a dark alley",
}
local function room(unformatted)
local name = names[unformatted]
return name and (colours.commonstr.lobby .. name) or (colours.commonstr.room .. unformatted)
end
local function troom(unformatted)
local name = names[unformatted]
return name and (colours.commonstr.lobby .. name) or ("room " .. colours.commonstr.room .. unformatted)
end
return {
nick = nick,
room = room,
troom = troom,
}
end
require_preload__["tptmp.client.localcmd"] = function()
local config = require("tptmp.client.config")
local format = require("tptmp.client.format")
local manager = require("tptmp.client.manager")
local command_parser = require("tptmp.common.command_parser")
local colours = require("tptmp.client.colours")
local localcmd_i = {}
local localcmd_m = { __index = localcmd_i }
local function parse_fps_sync(fps_sync)
fps_sync = fps_sync and tonumber(fps_sync) or false
fps_sync = fps_sync and math.floor(fps_sync) or false
fps_sync = fps_sync and fps_sync >= 2 and fps_sync or false
return fps_sync
end
local cmdp = command_parser.new({
commands = {
help = {
role = "help",
help = "/help <command>: displays command usage and notes (try /help list)",
},
list = {
role = "list",
help = "/list, no arguments: lists available commands",
},
size = {
func = function(localcmd, message, words, offsets)
local width = tonumber(words[2] and #words[2] > 0 and #words[2] <= 7 and not words[2]:find("[^0-9]") and words[2] or "")
local height = tonumber(words[3] and #words[3] > 0 and #words[3] <= 7 and not words[3]:find("[^0-9]") and words[3] or "")
if not width or not height then
return false
else
localcmd.window_:set_size(width, height)
end
return true
end,
help = "/size <width> <height>: sets the size of the chat window",
},
sync = {
func = function(localcmd, message, words, offsets)
local cli = localcmd.client_func_()
if cli then
cli:send_sync()
if localcmd.window_status_func_() ~= "hidden" then
localcmd.window_:backlog_push_neutral("* Simulation synchronized")
end
else
if localcmd.window_status_func_() ~= "hidden" then
localcmd.window_:backlog_push_error("Not connected, cannot sync")
end
end
return true
end,
help = "/sync, no arguments: synchronizes your simulation with everyone else's in the room; shortcut is Alt+S",
},
S = {
alias = "sync",
},
fpssync = {
func = function(localcmd, message, words, offsets)
local cli = localcmd.client_func_()
if words[2] == "on" then
if not localcmd.fps_sync_ then
localcmd.fps_sync_ = tpt.setfpscap()
end
if words[3] then
local fps_sync = parse_fps_sync(words[3])
if not fps_sync then
return false
end
localcmd.fps_sync_ = fps_sync
end
manager.set("fpsSync", tostring(localcmd.fps_sync_))
if cli then
cli:fps_sync(localcmd.fps_sync_)
end
localcmd.window_:backlog_push_neutral("* FPS synchronization enabled")
return true
elseif words[2] == "check" or not words[2] then
if localcmd.fps_sync_ then
local cli = localcmd.client_func_()
if cli then
cli:push_fpssync()
else
localcmd.window_:backlog_push_fpssync(true)
end
else
localcmd.window_:backlog_push_fpssync(false)
end
return true
elseif words[2] == "off" then
localcmd.fps_sync_ = false
manager.set("fpsSync", tostring(localcmd.fps_sync_))
if cli then
cli:fps_sync(localcmd.fps_sync_)
end
localcmd.window_:backlog_push_neutral("* FPS synchronization disabled")
return true
end
return false
end,
help = "/fpssync on [targetfps]\\check\\off: enables or disables FPS synchronization with those in the room who also have it enabled; targetfps defaults to the current FPS cap",
},
floating = {
func = function(localcmd, message, words, offsets)
local cli = localcmd.client_func_()
if words[2] == "on" then
localcmd.floating_ = true
localcmd.window_set_floating_func_(true)
manager.set("floating", "on")
localcmd.window_:backlog_push_neutral("* Floating mode enabled")
return true
elseif words[2] == "check" or not words[2] then
if localcmd.floating_ then
localcmd.window_:backlog_push_neutral("* Floating mode currenly enabled")
else
localcmd.window_:backlog_push_neutral("* Floating mode currenly disabled")
end
return true
elseif words[2] == "off" then
localcmd.floating_ = false
localcmd.window_set_floating_func_(false)
manager.set("floating", "false")
localcmd.window_:backlog_push_neutral("* Floating mode disabled")
return true
end
return false
end,
help = "/floating on\\check\\off: enables or disables floating mode: messages are drawn even when the window is hidden; chat shortcut is T",
},
connect = {
macro = function(localcmd, message, words, offsets)
return { "connectroom", "", unpack(words, 2) }
end,
help = "/connect [host[:[+]port]]: connects the default TPTMP server or the specified one, add + to connect securely",
},
C = {
alias = "connect",
},
reconnect = {
macro = function(localcmd, message, words, offsets)
if not localcmd.reconnect_ then
localcmd.window_:backlog_push_error("No successful connection on record, cannot reconnect")
return {}
end
return { "connectroom", localcmd.reconnect_.room, localcmd.reconnect_.host .. ":" .. localcmd.reconnect_.secr .. localcmd.reconnect_.port }
end,
help = "/reconnect, no arguments: connects back to the most recently visited server",
},
connectroom = {
func = function(localcmd, message, words, offsets)
local cli = localcmd.client_func_()
if not words[2] then
return false
elseif cli then
localcmd.window_:backlog_push_error("Already connected")
else
local host = words[3] or config.default_host
local host_without_port, port = host:match("^(.+):(%+?[^:]+)$")
host = host_without_port or host
local secure
if port then
secure = port:find("%+") and true
else
secure = config.default_secure
end
local new_cli = localcmd.new_client_func_({
host = host,
port = port and tonumber(port:gsub("[^0-9]", ""):sub(1, 5)) or config.default_port,
secure = secure,
initial_room = words[2],
localcmd = localcmd,
})
new_cli:nick_colour_seed(localcmd.nick_colour_seed_)
new_cli:fps_sync(localcmd.fps_sync_)
new_cli:start()
end
return true
end,
help = "/connectroom <room> [host[:[+]port]]: same as /connect, but skips the lobby and joins the specified room",
},
CR = {
alias = "connectroom",
},
disconnect = {
func = function(localcmd, message, words, offsets)
local cli = localcmd.client_func_()
if cli then
localcmd.kill_client_func_()
else
localcmd.window_:backlog_push_error("Not connected, cannot disconnect")
end
return true
end,
help = "/disconnect, no arguments: disconnects from the current server",
},
D = {
alias = "disconnect",
},
quit = {
alias = "disconnect",
},
Q = {
alias = "disconnect",
},
names = {
func = function(localcmd, message, words, offsets)
local cli = localcmd.client_func_()
if cli then
cli:push_names("Currently in ")
else
localcmd.window_:backlog_push_error("Not connected, cannot list users")
end
return true
end,
help = "/names, no arguments: tells you which room you are in and lists users present",
},
clear = {
func = function(localcmd, message, words, offsets)
localcmd.window_:backlog_reset()
localcmd.window_:backlog_push_neutral("* Backlog cleared")
return true
end,
help = "/clear, no arguments: clears the chat window",
},
hide = {
func = function(localcmd, message, words, offsets)
localcmd.window_.hide_window_func_()
return true
end,
help = "/hide, no arguments: hides the chat window; shortcut is Shift+Escape, this toggles window visibility (different from Escape without Shift, which defocuses the input box, and its counterpart Enter, which focuses it)",
},
me = {
func = function(localcmd, message, words, offsets)
local cli = localcmd.client_func_()
if not words[2] then
return false
elseif cli then
local msg = message:sub(offsets[2])
localcmd.window_:backlog_push_say3rd(cli:formatted_nick(), msg)
cli:send_say3rd(msg)
else
localcmd.window_:backlog_push_error("Not connected, message not sent")
end
return true
end,
help = "/me <message>: says something in third person",
},
ncseed = {
func = function(localcmd, message, words, offsets)
localcmd.nick_colour_seed_ = words[2] or tostring(math.random())
manager.set("nickColourSeed", tostring(localcmd.nick_colour_seed_))
local cli = localcmd.client_func_()
localcmd.window_:nick_colour_seed(localcmd.nick_colour_seed_)
if cli then
cli:nick_colour_seed(localcmd.nick_colour_seed_)
end
return true
end,
help = "/ncseed [seed]: set nick colour seed, randomize it if not specified, default is 0",
},
alpha = {
func = function(localcmd, message, words, offsets)
if words[2] then
if words[2]:find("[^0-9]") then
return false
end
local alpha = tonumber(words[2])
if not alpha then
return false
end
if alpha < 0 or alpha > 255 then
return false
end
localcmd.nick_colour_seed_ = words[2] or tostring(math.random())
manager.set("windowAlpha", tostring(alpha))
localcmd.window_:alpha(alpha)
end
localcmd.window_:backlog_push_neutral("* Current alpha value: " .. localcmd.window_:alpha())
return true
end,
help = "/alpha [value]: set or get the window alpha value, which goes from 0 (transparent) to 255 (opaque), default is " .. config.default_alpha,
},
},
respond = function(localcmd, message)
localcmd.window_:backlog_push_neutral(message)
end,
cmd_fallback = function(localcmd, message)
local cli = localcmd.client_func_()
if cli then
cli:send_say("/" .. message)
return true
end
return false
end,
help_fallback = function(localcmd, cmdstr)
local cli = localcmd.client_func_()
if cli then
cli:send_say("/shelp " .. cmdstr)
return true
end
return false
end,
list_extra = function(localcmd, cmdstr)
local cli = localcmd.client_func_()
if cli then
cli:send_say("/slist")
else
localcmd.window_:backlog_push_neutral("* Server commands are not currently available (connect to a server first)")
end
end,
help_format = colours.commonstr.neutral .. "* %s",
alias_format = colours.commonstr.neutral .. "* /%s is an alias for /%s",
list_format = colours.commonstr.neutral .. "* Client commands: %s",
unknown_format = colours.commonstr.error .. "* No such command, try /list (maybe it is server-only, connect and try again)",
})
function localcmd_i:parse(str)
if str:find("^/") and not str:find("^//") then
cmdp:parse(self, str:sub(2))
return true
end
end
function localcmd_i:reconnect_commit(reconnect)
self.reconnect_ = {
room = reconnect.room,
host = reconnect.host,
port = tostring(reconnect.port),
secr = reconnect.secure and "+" or "",
}
manager.set("reconnectRoom", self.reconnect_.room)
manager.set("reconnectHost", self.reconnect_.host)
manager.set("reconnectPort", self.reconnect_.port)
manager.set("reconnectSecure", self.reconnect_.secr)
end
local function new(params)
local reconnect = {
room = manager.get("reconnectRoom", ""),
host = manager.get("reconnectHost", ""),
port = manager.get("reconnectPort", ""),
secr = manager.get("reconnectSecure", ""),
}
if #reconnect.room == 0 or #reconnect.host == 0 or #reconnect.port == 0 then
reconnect = nil
end
local fps_sync = parse_fps_sync(manager.get("fpsSync", "0"))
local floating = manager.get("floating", "on") == "on"
local cmd = setmetatable({
fps_sync_ = fps_sync,
floating_ = floating,
reconnect_ = reconnect,
window_status_func_ = params.window_status_func,
window_set_floating_func_ = params.window_set_floating_func,
client_func_ = params.client_func,
new_client_func_ = params.new_client_func,
kill_client_func_ = params.kill_client_func,
nick_colour_seed_ = manager.get("nickColourSeed", "0"),
window_ = params.window,
}, localcmd_m)
cmd.window_:nick_colour_seed(cmd.nick_colour_seed_)
cmd.window_set_floating_func_(floating)
return cmd
end
return {
new = new,
}
end
require_preload__["tptmp.client.manager"] = function()
local jacobs = require("tptmp.client.manager.jacobs")
local null = require("tptmp.client.manager.null")
if rawget(_G, "MANAGER") then
return jacobs
else
return null
end
end
require_preload__["tptmp.client.manager.jacobs"] = function()
local config = require("tptmp.client.config")
local MANAGER = rawget(_G, "MANAGER")
local function get(key, default)
local value = MANAGER.getsetting(config.manager_namespace, key)
return type(value) == "string" and value or default
end
local function set(key, value)
MANAGER.savesetting(config.manager_namespace, key, value)
end
local function hidden()
return MANAGER.hidden
end
local function print(msg)
return MANAGER.print(msg)
end
return {
hidden = hidden,
get = get,
set = set,
print = print,
brand = "jacobs",
minimize_conflict = true,
side_button_conflict = true,
}
end
require_preload__["tptmp.client.manager.null"] = function()
local config = require("tptmp.client.config")
local data
local function load_data()
if data then
return
end
data = {}
local handle = io.open(config.null_manager_path, "r")
if not handle then
return
end
for line in handle:read("*a"):gmatch("[^\r\n]+") do
local key, value = line:match("^([^=]+)=(.*)$")
if key then
data[key] = value
end
end
handle:close()
end
local function save_data()
local handle = io.open(config.null_manager_path, "w")
if not handle then
return
end
local collect = {}
for key, value in pairs(data) do
table.insert(collect, tostring(key))
table.insert(collect, "=")
table.insert(collect, tostring(value))
table.insert(collect, "\n")
end
handle:write(table.concat(collect))
handle:close()
end
local function get(key, default)
load_data()
return data[key] or default
end
local function set(key, value)
data[key] = value
save_data()
end
local function print(msg)
print(msg)
end
return {
get = get,
set = set,
print = print,
brand = "null",
}
end
require_preload__["tptmp.client.profile"] = function()
local vanilla = require("tptmp.client.profile.vanilla")
local jacobs = require("tptmp.client.profile.jacobs")
if tpt.version.jacob1s_mod then
return jacobs
else
return vanilla
end
end
require_preload__["tptmp.client.profile.jacobs"] = function()
local vanilla = require("tptmp.client.profile.vanilla")
local config = require("tptmp.client.config")
local profile_i = {}
local profile_m = { __index = profile_i }
for key, value in pairs(vanilla.profile_i) do
profile_i[key] = value
end
function profile_i:handle_mousedown(px, py, button)
if self.client and (tpt.tab_menu() == 1 or self.kmod_c_) and px >= sim.XRES and py < 116 and not self.kmod_a_ then
self.log_event_func_(config.print_prefix .. "The tab menu is disabled because it does not sync (press the Alt key to override)")
return true
end
return vanilla.profile_i.handle_mousedown(self, px, py, button)
end
local function new(params)
local prof = vanilla.new(params)
prof.buttons_.clear = { x = gfx.WIDTH - 148, y = gfx.HEIGHT - 16, w = 17, h = 15 }
setmetatable(prof, profile_m)
return prof
end
return {
new = new,
brand = "jacobs",
}
end
require_preload__["tptmp.client.profile.vanilla"] = function()
local util = require("tptmp.client.util")
local config = require("tptmp.client.config")
local profile_i = {}
local profile_m = { __index = profile_i }
local index_to_lrax = {
[ 0 ] = "tool_l_",
[ 1 ] = "tool_r_",
[ 2 ] = "tool_a_",
[ 3 ] = "tool_x_",
}
local index_to_lraxid = {
[ 0 ] = "tool_lid_",
[ 1 ] = "tool_rid_",
[ 2 ] = "tool_aid_",
[ 3 ] = "tool_xid_",
}
local toolwarn_tools = {
[ "DEFAULT_UI_PROPERTY" ] = "prop",
[ "DEFAULT_TOOL_MIX" ] = "mix",
[ "DEFAULT_PT_LIGH" ] = "ligh",
[ "DEFAULT_PT_STKM" ] = "stkm",
[ "DEFAULT_PT_STKM2" ] = "stkm",
[ "DEFAULT_PT_SPAWN" ] = "stkm",
[ "DEFAULT_PT_SPAWN2" ] = "stkm",
[ "DEFAULT_PT_FIGH" ] = "stkm",
[ "UNKNOWN" ] = "unknown",
}
local toolwarn_messages = {
prop = "The PROP tool does not sync, you will have to use /sync",
mix = "The MIX tool does not sync, you will have to use /sync",
ligh = "LIGH does not sync, you will have to use /sync",
stkm = "Stickmen do not sync, you will have to use /sync",
cbrush = "Custom brushes do not sync, you will have to use /sync",
ipcirc = "The old circle brush does not sync, you will have to use /sync",
cgol = "This custom GOL type is not supported, please avoid using it while connected",
cgolcolor = "Custom GOL currently syncs without colours, use /sync to get colours across",
}
local BRUSH_COUNT = 3
local MOUSEUP_REASON_MOUSEUP = 0
local MOUSEUP_REASON_BLUR = 1
local MAX_SIGNS = 0
while sim.signs[MAX_SIGNS + 1] do
MAX_SIGNS = MAX_SIGNS + 1
end
local function rulestring_bits(str)
local bits = 0
for i = 1, #str do
bits = bit.bor(bits, bit.lshift(1, str:byte(i) - 48))
end
return bits
end
local function get_custgolinfo(identifier)
-- * TODO[api]: add an api for this to tpt
local pref = io.open("powder.pref")
if not pref then
return
end
local pref_data = pref:read("*a")
pref:close()
local types = pref_data:match([=["Types"%s*:%s*%[([^%]]+)%]]=])
if not types then
return
end
for name, ruleset, primary, secondary in types:gmatch([["(%S+)%s+(%S+)%s+(%S+)%s+(%S+)"]]) do
if "DEFAULT_PT_LIFECUST_" .. name == identifier then
local begin, stay, states = ruleset:match("^B([1-8]+)/S([0-8]+)/([0-9]+)$")
if not begin then
begin, stay = ruleset:match("^B([1-8]+)/S([0-8]+)$")
states = "2"
end
states = tonumber(states)
states = states >= 2 and states <= 17 and states
ruleset = begin and stay and states and bit.bor(bit.lshift(rulestring_bits(begin), 8), rulestring_bits(stay), bit.lshift(states - 2, 17))
primary = tonumber(primary)
secondary = tonumber(secondary)
if ruleset and primary and secondary then
return ruleset, primary, secondary
end
break
end
end
end
local function get_sign_data()
local sign_data = {}
for i = 1, MAX_SIGNS do
local text = sim.signs[i].text
if text then
sign_data[i] = {
tx = text,
ju = sim.signs[i].justification,
px = sim.signs[i].x,
py = sim.signs[i].y,
}
end
end
return sign_data
end
local function perfect_circle()
return sim.brush(1, 1, 1, 1, 0)() == 0
end
local props = {}
for key, value in pairs(sim) do
if key:find("^FIELD_") and key ~= "FIELD_TYPE" then
table.insert(props, value)
end
end
local function in_zoom_window(x, y)
local ax, ay = sim.adjustCoords(x, y)
return ren.zoomEnabled() and (ax ~= x or ay ~= y)
end
function profile_i:report_loadonline_(id, hist)
if self.registered_func_() then
self.client_:send_loadonline(id, hist)
end
end
function profile_i:report_pos_()
if self.registered_func_() then
self.client_:send_mousepos(self.pos_x_, self.pos_y_)
end
end
function profile_i:report_size_()
if self.registered_func_() then
self.client_:send_brushsize(self.size_x_, self.size_y_)
end
end
function profile_i:report_zoom_()
if self.registered_func_() then
if self.zenabled_ then
self.client_:send_zoomstart(self.zcx_, self.zcy_, self.zsize_)
else
self.client_:send_zoomend()
end
end
end
function profile_i:report_bmode_()
if self.registered_func_() then
self.client_:send_brushmode(self.bmode_)
end
end
function profile_i:report_shape_()
if self.registered_func_() then
self.client_:send_brushshape(self.shape_ < BRUSH_COUNT and self.shape_ or 0)
end
end
function profile_i:report_sparksign_(x, y)
if self.registered_func_() then
self.client_:send_sparksign(x, y)
end
end
function profile_i:report_flood_(i, x, y)
if self.registered_func_() then
self.client_:send_flood(i, x, y)
end
end
function profile_i:report_lineend_(x, y)
self.lss_i_ = nil
if self.registered_func_() then
self.client_:send_lineend(x, y)
end
end
function profile_i:report_rectend_(x, y)
self.rss_i_ = nil
if self.registered_func_() then
self.client_:send_rectend(x, y)
end
end
function profile_i:sync_linestart_(i, x, y)
if self.client_ and self.lss_i_ then
self.client_:send_linestart(self.lss_i_, self.lss_x_, self.lss_y_)
end
end
function profile_i:report_linestart_(i, x, y)
self.lss_i_ = i
self.lss_x_ = x
self.lss_y_ = y
if self.registered_func_() then
self.client_:send_linestart(i, x, y)
end
end
function profile_i:sync_rectstart_(i, x, y)
if self.client_ and self.rss_i_ then
self.client_:send_rectstart(self.rss_i_, self.rss_x_, self.rss_y_)
end
end
function profile_i:report_rectstart_(i, x, y)
self.rss_i_ = i
self.rss_x_ = x
self.rss_y_ = y
if self.registered_func_() then
self.client_:send_rectstart(i, x, y)
end
end
function profile_i:sync_pointsstart_()
if self.client_ and self.pts_i_ then
self.client_:send_pointsstart(self.pts_i_, self.pts_x_, self.pts_y_)
end
end
function profile_i:report_pointsstart_(i, x, y)
self.pts_i_ = i
self.pts_x_ = x
self.pts_y_ = y
if self.registered_func_() then
self.client_:send_pointsstart(i, x, y)
end
end
function profile_i:report_pointscont_(x, y, done)
if self.registered_func_() then
self.client_:send_pointscont(x, y)
end
self.pts_x_ = x
self.pts_y_ = y
if done then
self.pts_i_ = nil
end
end
function profile_i:report_kmod_()
if self.registered_func_() then
self.client_:send_keybdmod(self.kmod_c_, self.kmod_s_, self.kmod_a_)
end
end
function profile_i:report_framestep_()
if self.registered_func_() then
self.client_:send_stepsim()
end
end
function profile_i:report_airinvert_()
if self.registered_func_() then
self.client_:send_airinv()
end
end
function profile_i:report_reset_spark_()
if self.registered_func_() then
self.client_:send_sparkclear()
end
end
function profile_i:report_reset_air_()
if self.registered_func_() then
self.client_:send_airclear()
end
end
function profile_i:report_reset_airtemp_()
if self.registered_func_() then
self.client_:send_heatclear()
end
end
function profile_i:report_clearrect_(x, y, w, h)
if self.registered_func_() then
self.client_:send_clearrect(x, y, w, h)
end
end
function profile_i:report_clearsim_()
if self.registered_func_() then
self.client_:send_clearsim()
end
end
function profile_i:report_reloadsim_()
if self.registered_func_() then
self.client_:send_reloadsim()
end
end
function profile_i:simstate_sync()
if self.registered_func_() then
self.client_:send_simstate(self.ss_p_, self.ss_h_, self.ss_u_, self.ss_n_, self.ss_w_, self.ss_g_, self.ss_a_, self.ss_e_, self.ss_y_, self.ss_t_, self.ss_r_, self.ss_s_)
end
end
function profile_i:report_tool_(index)
if self.registered_func_() then
self.client_:send_selecttool(index, self[index_to_lrax[index]])
local identifier = self[index_to_lraxid[index]]
if identifier:find("^DEFAULT_PT_LIFECUST_") then
local ruleset, primary, secondary = get_custgolinfo(identifier)
if ruleset then
self.client_:send_custgolinfo(ruleset, primary, secondary)
-- * TODO[api]: add an api for setting gol colour
self.display_toolwarn_["cgolcolor"] = true
else
self.display_toolwarn_["cgol"] = true
end
end
end
end
function profile_i:report_deco_()
if self.registered_func_() then
self.client_:send_brushdeco(self.deco_)
end
end
function profile_i:sync_placestatus_()
if self.client_ and self.pes_k_ ~= 0 then
self.client_:send_placestatus(self.pes_k_, self.pes_w_, self.pes_h_)
end
end
function profile_i:report_placestatus_(k, w, h)
self.pes_k_ = k
self.pes_w_ = w
self.pes_h_ = h
if self.registered_func_() then
self.client_:send_placestatus(k, w, h)
end
end
function profile_i:sync_selectstatus_()
if self.client_ and self.sts_k_ ~= 0 then
self.client_:send_selectstatus(self.sts_k_, self.sts_x_, self.sts_y_)
end
end
function profile_i:report_selectstatus_(k, x, y)
self.sts_k_ = k
self.sts_x_ = x
self.sts_y_ = y
if self.registered_func_() then
self.client_:send_selectstatus(k, x, y)
end
end
function profile_i:report_pastestamp_(x, y, w, h)
if self.registered_func_() then
self.client_:send_pastestamp(x, y, w, h)
end
end
function profile_i:report_canceldraw_()
if self.registered_func_() then
self.client_:send_canceldraw()
end
end
function profile_i:get_stamp_size_()
local name = sim.listStamps()[1]
if not name then
return
end
local stamp = io.open("stamps/" .. name .. ".stm", "rb")
if not stamp then
return
end
local header = stamp:read(12)
stamp:close()
if type(header) ~= "string" or #header ~= 12 then
return
end
local bw, bh = header:byte(7, 8) -- * Works for OPS and PSv too.
return bw * 4, bh * 4
end
function profile_i:user_sync()
self:report_size_()
self:report_tool_(0)
self:report_tool_(1)
self:report_tool_(2)
self:report_tool_(3)
self:report_deco_()
self:report_bmode_()
self:report_shape_()
self:report_kmod_()
self:report_pos_()
self:sync_pointsstart_()
self:sync_placestatus_()
self:sync_selectstatus_()
self:sync_linestart_()
self:sync_rectstart_()
self:report_zoom_()
end
function profile_i:post_event_check_()
if self.placesave_postmsg_ then
local partcount = self.placesave_postmsg_.partcount
if partcount and (partcount ~= sim.NUM_PARTS or sim.NUM_PARTS == sim.XRES * sim.YRES) and self.registered_func_() then
-- * TODO[api]: get rid of all of this nonsense once redo-ui lands
if self.registered_func_() then
self.client_:send_sync()
end
end
self.placesave_postmsg_ = nil
end
if self.placesave_size_ then
local x1, y1, x2, y2 = self:end_placesave_size_()
if x1 then
local x, y, w, h = util.corners_to_rect(x1, y1, x2, y2)
self.simstate_invalid_ = true
if self.placesave_open_ then
local id, hist = util.get_save_id()
self.set_id_func_(id, hist)
if id then
self:report_loadonline_(id, hist)
else
self:report_pastestamp_(x, y, w, h)
end
elseif self.placesave_reload_ then
if not self.get_id_func_() then
self:report_pastestamp_(x, y, w, h)
end
self:report_reloadsim_()
elseif self.placesave_clear_ then
self.set_id_func_(nil, nil)
self:report_clearsim_()
else
self:report_pastestamp_(x, y, w, h)
end
end
self.placesave_open_ = nil
self.placesave_reload_ = nil
self.placesave_clear_ = nil
end
if self.zoom_invalid_ then
self.zoom_invalid_ = nil
self:update_zoom_()
end
if self.simstate_invalid_ then
self.simstate_invalid_ = nil
self:check_simstate()
end
if self.bmode_invalid_ then
self.bmode_invalid_ = nil
self:update_bmode_()
end
self:update_size_()
self:update_shape_()
self:update_tools_()
self:update_deco_()
end
function profile_i:sample_simstate()
local ss_p = tpt.set_pause()
local ss_h = tpt.heat()
local ss_u = tpt.ambient_heat()
local ss_n = tpt.newtonian_gravity()
local ss_w = sim.waterEqualisation()
local ss_g = sim.gravityMode()
local ss_a = sim.airMode()
local ss_e = sim.edgeMode()
local ss_y = sim.prettyPowders()
local ss_t = util.ambient_air_temp()
local ss_r, ss_s = util.custom_gravity()
if self.ss_p_ ~= ss_p or
self.ss_h_ ~= ss_h or
self.ss_u_ ~= ss_u or
self.ss_n_ ~= ss_n or
self.ss_w_ ~= ss_w or
self.ss_g_ ~= ss_g or
self.ss_a_ ~= ss_a or
self.ss_e_ ~= ss_e or
self.ss_y_ ~= ss_y or
self.ss_t_ ~= ss_t or
self.ss_r_ ~= ss_r or
self.ss_s_ ~= ss_s then
self.ss_p_ = ss_p
self.ss_h_ = ss_h
self.ss_u_ = ss_u
self.ss_n_ = ss_n
self.ss_w_ = ss_w
self.ss_g_ = ss_g
self.ss_a_ = ss_a
self.ss_e_ = ss_e
self.ss_y_ = ss_y
self.ss_t_ = ss_t
self.ss_r_ = ss_r
self.ss_s_ = ss_s
return true
end
return false
end
function profile_i:check_signs(old_data)
local new_data = get_sign_data()
local bw = sim.XRES / 4
local to_send = {}
local function key(x, y)
return math.floor(x / 4) + math.floor(y / 4) * bw
end
for i = 1, MAX_SIGNS do
if old_data[i] and new_data[i] then
if old_data[i].ju ~= new_data[i].ju or
old_data[i].tx ~= new_data[i].tx or
old_data[i].px ~= new_data[i].px or
old_data[i].py ~= new_data[i].py then
to_send[key(old_data[i].px, old_data[i].py)] = true
to_send[key(new_data[i].px, new_data[i].py)] = true
end
elseif old_data[i] then
to_send[key(old_data[i].px, old_data[i].py)] = true
elseif new_data[i] then
to_send[key(new_data[i].px, new_data[i].py)] = true
end
end
for k in pairs(to_send) do
local x, y, w, h = k % bw * 4, math.floor(k / bw) * 4, 4, 4
self:report_clearrect_(x, y, w, h)
self:report_pastestamp_(x, y, w, h)
end
end
function profile_i:check_simstate()
if self:sample_simstate() then
self:simstate_sync()
end
end
function profile_i:update_draw_mode_()
if self.kmod_c_ and self.kmod_s_ then
if self[index_to_lraxid[self.last_toolslot_]]:find("^DEFAULT_TOOL_") then
self.draw_mode_ = "points"
else
self.draw_mode_ = "flood"
end
elseif self.kmod_c_ then
self.draw_mode_ = "rect"
elseif self.kmod_s_ then
self.draw_mode_ = "line"
else
self.draw_mode_ = "points"
end
end
function profile_i:enable_shift_()
self.kmod_changed_ = true
self.kmod_s_ = true
if not self.dragging_mouse_ or self.select_mode_ ~= "none" then
self:update_draw_mode_()
end
end
function profile_i:enable_ctrl_()
self.kmod_changed_ = true
self.kmod_c_ = true
if not self.dragging_mouse_ or self.select_mode_ ~= "none" then
self:update_draw_mode_()
end
end
function profile_i:enable_alt_()
self.kmod_changed_ = true
self.kmod_a_ = true
end
function profile_i:disable_shift_()
self.kmod_changed_ = true
self.kmod_s_ = false
if not self.dragging_mouse_ or self.select_mode_ ~= "none" then
self:update_draw_mode_()
end
end
function profile_i:disable_ctrl_()
self.kmod_changed_ = true
self.kmod_c_ = false
if not self.dragging_mouse_ or self.select_mode_ ~= "none" then
self:update_draw_mode_()
end
end
function profile_i:disable_alt_()
self.kmod_changed_ = true
self.kmod_a_ = false
end
function profile_i:update_pos_(x, y)
x, y = sim.adjustCoords(x, y)
if x < 0 then x = 0 end
if x >= sim.XRES then x = sim.XRES - 1 end
if y < 0 then y = 0 end
if y >= sim.YRES then y = sim.YRES - 1 end
if self.pos_x_ ~= x or self.pos_y_ ~= y then
self.pos_x_ = x
self.pos_y_ = y
self:report_pos_(self.pos_x_, self.pos_y_)
end
end
function profile_i:update_size_()
local x, y = tpt.brushx, tpt.brushy
if x < 0 then x = 0 end
if x > 255 then x = 255 end
if y < 0 then y = 0 end
if y > 255 then y = 255 end
if self.size_x_ ~= x or self.size_y_ ~= y then
self.size_x_ = x
self.size_y_ = y
self:report_size_(self.size_x_, self.size_y_)
end
end
function profile_i:update_zoom_()
local zenabled = ren.zoomEnabled()
local zcx, zcy, zsize = ren.zoomScope()
if self.zenabled_ ~= zenabled or self.zcx_ ~= zcx or self.zcy_ ~= zcy or self.zsize_ ~= zsize then
self.zenabled_ = zenabled
self.zcx_ = zcx
self.zcy_ = zcy
self.zsize_ = zsize
self:report_zoom_()
end
end
function profile_i:update_bmode_()
local bmode = sim.replaceModeFlags()
if self.bmode_ ~= bmode then
self.bmode_ = bmode
self:report_bmode_()
end
end
function profile_i:update_shape_()
local pcirc = self.perfect_circle_
if self.perfect_circle_invalid_ then
pcirc = perfect_circle()
end
local shape = tpt.brushID
if self.shape_ ~= shape or self.perfect_circle_ ~= pcirc then
local old_cbrush = self.cbrush_
self.cbrush_ = shape >= BRUSH_COUNT or nil
if not old_cbrush and self.cbrush_ then
self.display_toolwarn_["cbrush"] = true
end
local old_ipcirc = self.ipcirc_
self.ipcirc_ = shape == 0 and not pcirc
if not old_ipcirc and self.ipcirc_ then
self.display_toolwarn_["ipcirc"] = true
end
self.shape_ = shape
self.perfect_circle_ = pcirc
self:report_shape_()
end
end
function profile_i:update_tools_()
local tlid = tpt.selectedl
local trid = tpt.selectedr
local taid = tpt.selecteda
local txid = tpt.selectedreplace
if self.tool_lid_ ~= tlid then
if self.registered_func_() then
self.tool_l_ = self.xidr_.from_tool[tlid] or self.xidr_.unknown_xid
end
self.tool_lid_ = tlid
self:report_tool_(0)
end
if self.tool_rid_ ~= trid then
if self.registered_func_() then
self.tool_r_ = self.xidr_.from_tool[trid] or self.xidr_.unknown_xid
end
self.tool_rid_ = trid
self:report_tool_(1)
end
if self.tool_aid_ ~= taid then
if self.registered_func_() then
self.tool_a_ = self.xidr_.from_tool[taid] or self.xidr_.unknown_xid
end
self.tool_aid_ = taid
self:report_tool_(2)
end
if self.tool_xid_ ~= txid then
if self.registered_func_() then
self.tool_x_ = self.xidr_.from_tool[txid] or self.xidr_.unknown_xid
end
self.tool_xid_ = txid
self:report_tool_(3)
end
if self.registered_func_() then
local new_tool = self.xidr_.to_tool[self[index_to_lrax[self.last_toolslot_]]]
local new_tool_id = self[index_to_lraxid[self.last_toolslot_]]
if self.last_toolid_ ~= new_tool_id then
if not new_tool_id:find("^DEFAULT_PT_LIFECUST_") then
if toolwarn_tools[new_tool] then
self.display_toolwarn_[toolwarn_tools[new_tool]] = true
self.display_toolwarn_identifier_ = new_tool_id
end
end
self.last_toolid_ = new_tool_id
end
end
end
function profile_i:update_kmod_()
if self.kmod_changed_ then
self.kmod_changed_ = nil
self:report_kmod_()
end
end
function profile_i:update_deco_()
local deco = sim.decoColour()
if self.deco_ ~= deco then
self.deco_ = deco
self:report_deco_()
end
end
function profile_i:begin_placesave_size_(x, y, aux_button)
local bx, by = math.floor(x / 4), math.floor(y / 4)
local p = 0
local pres = {}
local function push(x, y)
p = p + 2
local pr = sim.pressure(x, y)
if pr > 256 then pr = 256 end
if pr < -256 then pr = -256 end
local st = (math.floor(pr * 0x10) * 0x1000 + math.random(0x000, 0xFFF)) / 0x10000
pres[p - 1] = pr
pres[p] = st
sim.pressure(x, y, st)
end
for x = 0, sim.XRES / 4 - 1 do
push(x, by)
end
for y = 0, sim.YRES / 4 - 1 do
if y ~= by then
push(bx, y)
end
end
local pss = {
pres = pres,
bx = bx,
by = by,
aux_button = aux_button,
airmode = sim.airMode(),
partcount = sim.NUM_PARTS,
}
if aux_button then
-- * This means that begin_placesave_size_ was called from a button
-- callback, i.e. not really in response to pasting, but reloading /
-- clearing / opening a save. In this case, the air mode should
-- not be reset to the original air mode, but left to be whatever
-- value these actions set it to.
self.placesave_size_next_ = pss
else
self.placesave_size_ = pss
end
sim.airMode(4)
end
function profile_i:end_placesave_size_()
local bx, by = self.placesave_size_.bx, self.placesave_size_.by
local pres = self.placesave_size_.pres
local p = 0
local lx, ly, hx, hy = math.huge, math.huge, -math.huge, -math.huge
local function pop(x, y)
p = p + 2
if sim.pressure(x, y) == pres[p] then
sim.pressure(x, y, pres[p - 1])
else
lx = math.min(lx, x)
ly = math.min(ly, y)
hx = math.max(hx, x)
hy = math.max(hy, y)
end
end
for x = 0, sim.XRES / 4 - 1 do
pop(x, by)
end
for y = 0, sim.YRES / 4 - 1 do
if y ~= by then
pop(bx, y)
end
end
-- * Unlike normal stamp pastes, auxiliary button events (open, save, clear)
-- are guaranteed to have been cancelled if no air change is detected.
-- The following block roughly translates to resetting the air mode to
-- the sampled value if the change in the simulation occurred due to
-- a paste event, otherwise only if we actually detected a change in air.
if not self.placesave_size_.aux_button or lx == math.huge then
sim.airMode(self.placesave_size_.airmode)
end
local partcount = self.placesave_size_.partcount
self.placesave_size_ = nil
if lx == math.huge then
self.placesave_postmsg_ = {
partcount = partcount,
}
else
return math.max((lx - 2) * 4, 0),
math.max((ly - 2) * 4, 0),
math.min((hx + 2) * 4, sim.XRES) - 1,
math.min((hy + 2) * 4, sim.YRES) - 1
end
end
function profile_i:handle_tick()
self:post_event_check_()
if self.want_stamp_size_ then
self.want_stamp_size_ = nil
local w, h = self:get_stamp_size_()
if w then
self.place_x_, self.place_y_ = w, h
end
end
if self.signs_invalid_ then
local sign_data = self.signs_invalid_
self.signs_invalid_ = nil
self:check_signs(sign_data)
end
self:update_pos_(tpt.mousex, tpt.mousey)
-- * Here the assumption is made that no Lua hook cancels the tick event.
if self.placing_zoom_ then
self.zoom_invalid_ = true
end
if self.skip_draw_ then
self.skip_draw_ = nil
else
if self.select_mode_ == "none" and self.dragging_mouse_ then
if self.draw_mode_ == "flood" then
self:report_flood_(self.last_toolslot_, self.pos_x_, self.pos_y_)
end
if self.draw_mode_ == "points" then
self:report_pointscont_(self.pos_x_, self.pos_y_)
end
end
end
if self.simstate_invalid_next_ then
self.simstate_invalid_next_ = nil
self.simstate_invalid_ = true
end
if self.placesave_size_next_ then
self.placesave_size_ = self.placesave_size_next_
self.placesave_size_next_ = nil
end
local complete_select_mode = self.select_x_ and self.select_mode_
if self.prev_select_mode_ ~= complete_select_mode then
self.prev_select_mode_ = complete_select_mode
if self.select_x_ and (self.select_mode_ == "copy" or
self.select_mode_ == "cut" or
self.select_mode_ == "stamp") then
if self.select_mode_ == "copy" then
self:report_selectstatus_(1, self.select_x_, self.select_y_)
elseif self.select_mode_ == "cut" then
self:report_selectstatus_(2, self.select_x_, self.select_y_)
elseif self.select_mode_ == "stamp" then
self:report_selectstatus_(3, self.select_x_, self.select_y_)
end
else
self.select_x_, self.select_y_ = nil, nil
self:report_selectstatus_(0, 0, 0)
end
end
local complete_place_mode = self.place_x_ and self.select_mode_
if self.prev_place_mode_ ~= complete_place_mode then
self.prev_place_mode_ = complete_place_mode
if self.place_x_ and self.select_mode_ == "place" then
self:report_placestatus_(1, self.place_x_, self.place_y_)
else
self.place_x_, self.place_y_ = nil, nil
self:report_placestatus_(0, 0, 0)
end
end
end
function profile_i:handle_mousedown(px, py, button)
self:post_event_check_()
self:update_pos_(px, py)
self.last_in_zoom_window_ = in_zoom_window(px, py)
-- * Here the assumption is made that no Lua hook cancels the mousedown event.
if not self.kmod_c_ and not self.kmod_s_ and self.kmod_a_ and button == ui.SDL_BUTTON_LEFT then
button = 2
end
for _, btn in pairs(self.buttons_) do
if util.inside_rect(btn.x, btn.y, btn.w, btn.h, tpt.mousex, tpt.mousey) then
btn.active = true
end
end
if not self.placing_zoom_ then
if self.select_mode_ ~= "none" then
self.sel_x1_ = self.pos_x_
self.sel_y1_ = self.pos_y_
self.sel_x2_ = self.pos_x_
self.sel_y2_ = self.pos_y_
self.dragging_mouse_ = true
self.select_x_, self.select_y_ = self.pos_x_, self.pos_y_
return
end
if px < sim.XRES and py < sim.YRES then
if button == ui.SDL_BUTTON_LEFT then
self.last_toolslot_ = 0
elseif button == ui.SDL_BUTTON_MIDDLE then
self.last_toolslot_ = 2
elseif button == ui.SDL_BUTTON_RIGHT then
self.last_toolslot_ = 1
else
return
end
self:update_tools_()
if next(self.display_toolwarn_) then
if self.registered_func_() then
for key in pairs(self.display_toolwarn_) do
if key == "unknown" then
local identifier = self.display_toolwarn_identifier_
local ids = self.xidr_unsupported_[identifier]
local display_as = identifier
if elem[identifier] then
display_as = elem.property(elem[identifier], "Name")
end
self.log_event_func_(("The following users in the room cannot use %s, please avoid using it while connected"):format(display_as))
local str = ""
local function commit()
self.log_event_func_(" - " .. str)
str = ""
end
for i = 1, #ids do
str = str .. self.client_.id_to_member[ids[i]].formatted_nick
if i < #ids then
str = str .. "\bw, "
end
if gfx.textSize(str) > gfx.WIDTH / 2 then
commit()
end
end
commit()
else
self.log_event_func_(toolwarn_messages[key])
end
end
end
self.display_toolwarn_ = {}
end
self:update_draw_mode_()
self.dragging_mouse_ = true
if self.draw_mode_ == "rect" then
self:report_rectstart_(self.last_toolslot_, self.pos_x_, self.pos_y_)
end
if self.draw_mode_ == "line" then
self:report_linestart_(self.last_toolslot_, self.pos_x_, self.pos_y_)
end
if self.draw_mode_ == "flood" then
if self.registered_func_() and self[index_to_lraxid[self.last_toolslot_]]:find("^DEFAULT_DECOR_") then
self.log_event_func_("Decoration flooding does not sync, you will have to use /sync")
end
self:report_flood_(self.last_toolslot_, self.pos_x_, self.pos_y_)
end
if self.draw_mode_ == "points" then
self:report_pointsstart_(self.last_toolslot_, self.pos_x_, self.pos_y_)
end
end
end
end
function profile_i:cancel_drawing_()
if self.dragging_mouse_ then
self:report_canceldraw_()
self.dragging_mouse_ = false
end
end
function profile_i:handle_mousemove(px, py, delta_x, delta_y)
self:post_event_check_()
self:update_pos_(px, py)
for _, btn in pairs(self.buttons_) do
if not util.inside_rect(btn.x, btn.y, btn.w, btn.h, tpt.mousex, tpt.mousey) then
btn.active = false
end
end
-- * Here the assumption is made that no Lua hook cancels the mousemove event.
if self.select_mode_ ~= "none" then
if self.select_mode_ == "place" then
self.sel_x1_ = self.pos_x_
self.sel_y1_ = self.pos_y_
end
if self.sel_x1_ then
self.sel_x2_ = self.pos_x_
self.sel_y2_ = self.pos_y_
end
elseif self.dragging_mouse_ then
local last = self.last_in_zoom_window_
self.last_in_zoom_window_ = in_zoom_window(px, py)
if last ~= self.last_in_zoom_window_ and (self.draw_mode_ == "flood" or self.draw_mode_ == "points") then
self:cancel_drawing_()
return
end
if self.draw_mode_ == "flood" then
self:report_flood_(self.last_toolslot_, self.pos_x_, self.pos_y_)
self.skip_draw_ = true
end
if self.draw_mode_ == "points" then
self:report_pointscont_(self.pos_x_, self.pos_y_)
self.skip_draw_ = true
end
end
end
function profile_i:handle_mouseup(px, py, button, reason)
self:post_event_check_()
self:update_pos_(px, py)
for name, btn in pairs(self.buttons_) do
if btn.active then
self["button_" .. name .. "_"](self)
end
btn.active = false
end
-- * Here the assumption is made that no Lua hook cancels the mouseup event.
if px >= sim.XRES or py >= sim.YRES then
self.perfect_circle_invalid_ = true
self.simstate_invalid_next_ = true
end
if self.registered_func_() and ((reason == MOUSEUP_REASON_MOUSEUP and self[index_to_lraxid[self.last_toolslot_]] ~= "DEFAULT_UI_SIGN") or button ~= 1) then
for i = 1, MAX_SIGNS do
local x = sim.signs[i].screenX
if x then
local t = sim.signs[i].text
local y = sim.signs[i].screenY
local w = sim.signs[i].width + 1
local h = sim.signs[i].height
if util.inside_rect(x, y, w, h, self.pos_x_, self.pos_y_) then
if t:match("^{b|.*}$") then
self:report_sparksign_(sim.signs[i].x, sim.signs[i].y)
end
if t:match("^{c:[0-9]+|.*}$") then
if self.registered_func_() then
self.placesave_open_ = true
self:begin_placesave_size_(100, 100, true)
end
end
end
end
end
end
if self.placing_zoom_ then
self.placing_zoom_ = false
self.draw_mode_ = "points"
self:cancel_drawing_()
elseif self.dragging_mouse_ then
if self.select_mode_ ~= "none" then
if reason == MOUSEUP_REASON_MOUSEUP then
local x, y, w, h = util.corners_to_rect(self.sel_x1_, self.sel_y1_, self.sel_x2_, self.sel_y2_)
if self.select_mode_ == "place" then
if self.registered_func_() then
self:begin_placesave_size_(x, y)
end
elseif self.select_mode_ == "copy" then
self.clipsize_x_ = w
self.clipsize_y_ = h
elseif self.select_mode_ == "cut" then
self.clipsize_x_ = w
self.clipsize_y_ = h
self:report_clearrect_(x, y, w, h)
elseif self.select_mode_ == "stamp" then
-- * Nothing.
end
end
self.select_mode_ = "none"
self:cancel_drawing_()
return
end
if reason == MOUSEUP_REASON_MOUSEUP then
if self.draw_mode_ == "rect" then
self:report_rectend_(self.pos_x_, self.pos_y_)
end
if self.draw_mode_ == "line" then
self:report_lineend_(self.pos_x_, self.pos_y_)
end
if self.draw_mode_ == "flood" then
self:report_flood_(self.last_toolslot_, self.pos_x_, self.pos_y_)
end
if self.draw_mode_ == "points" then
self:report_pointscont_(self.pos_x_, self.pos_y_, true)
end
end
self:cancel_drawing_()
elseif self.select_mode_ ~= "none" and button ~= 1 then
if reason == MOUSEUP_REASON_MOUSEUP then
self.select_mode_ = "none"
end
end
self:update_draw_mode_()
end
function profile_i:handle_mousewheel(px, py, dir)
self:post_event_check_()
self:update_pos_(px, py)
-- * Here the assumption is made that no Lua hook cancels the mousewheel event.
if self.placing_zoom_ then
self.zoom_invalid_ = true
end
end
function profile_i:handle_keypress(key, scan, rep, shift, ctrl, alt)
self:post_event_check_()
if shift and not self.kmod_s_ then
self:enable_shift_()
end
if ctrl and not self.kmod_c_ then
self:enable_ctrl_()
end
if alt and not self.kmod_a_ then
self:enable_alt_()
end
self:update_kmod_()
-- * Here the assumption is made that no Lua hook cancels the keypress event.
if not rep then
if not self.stk2_out_ or ctrl then
if scan == ui.SDL_SCANCODE_W then
self.simstate_invalid_ = true
elseif scan == ui.SDL_SCANCODE_S then
self.select_mode_ = "stamp"
self:cancel_drawing_()
end
end
end
-- * Here the assumption is made that no debug hook cancels the keypress event.
if self.select_mode_ == "place" then
-- * Note: Sadly, there's absolutely no way to know how these operations
-- affect the save being placed, as it only grows if particles
-- in it would go beyond its border.
if key == ui.SDLK_RIGHT then
-- * Move. See note above.
return
elseif key == ui.SDLK_LEFT then
-- * Move. See note above.
return
elseif key == ui.SDLK_DOWN then
-- * Move. See note above.
return
elseif key == ui.SDLK_UP then
-- * Move. See note above.
return
elseif scan == ui.SDL_SCANCODE_R and not rep then
if ctrl and shift then
-- * Rotate. See note above.
elseif not ctrl and shift then
-- * Rotate. See note above.
else
-- * Rotate. See note above.
end
return
end
end
if rep then
return
end
local did_shortcut = true
if scan == ui.SDL_SCANCODE_SPACE then
self.simstate_invalid_ = true
elseif scan == ui.SDL_SCANCODE_GRAVE then
if self.registered_func_() and not alt then
self.log_event_func_("The console is disabled because it does not sync (press the Alt key to override)")
return true
end
elseif scan == ui.SDL_SCANCODE_Z then
if self.select_mode_ == "none" or not self.dragging_mouse_ then
if ctrl and not self.dragging_mouse_ then
if self.registered_func_() and not alt then
self.log_event_func_("Undo is disabled because it does not sync (press the Alt key to override)")
return true
end
else
self:cancel_drawing_()
self.placing_zoom_ = true
self.zoom_invalid_ = true
end
end
elseif scan == ui.SDL_SCANCODE_F5 or (ctrl and scan == ui.SDL_SCANCODE_R) then
self:button_reload_()
elseif scan == ui.SDL_SCANCODE_F and not ctrl then
if ren.debugHUD() == 1 and (shift or alt) then
if self.registered_func_() and not alt then
self.log_event_func_("Partial framesteps do not sync, you will have to use /sync")
end
end
self:report_framestep_()
self.simstate_invalid_ = true
elseif scan == ui.SDL_SCANCODE_B and not ctrl then
self.simstate_invalid_ = true
elseif scan == ui.SDL_SCANCODE_E and ctrl then
self.simstate_invalid_ = true
elseif scan == ui.SDL_SCANCODE_Y then
if ctrl then
if self.registered_func_() and not alt then
self.log_event_func_("Redo is disabled because it does not sync (press the Alt key to override)")
return true
end
else
self.simstate_invalid_ = true
end
elseif scan == ui.SDL_SCANCODE_U then
if ctrl then
self:report_reset_airtemp_()
else
self.simstate_invalid_ = true
end
elseif scan == ui.SDL_SCANCODE_N then
self.simstate_invalid_ = true
elseif scan == ui.SDL_SCANCODE_EQUALS then
if ctrl then
self:report_reset_spark_()
else
self:report_reset_air_()
end
elseif scan == ui.SDL_SCANCODE_C and ctrl then
self.select_mode_ = "copy"
self:cancel_drawing_()
elseif scan == ui.SDL_SCANCODE_X and ctrl then
self.select_mode_ = "cut"
self:cancel_drawing_()
elseif scan == ui.SDL_SCANCODE_V and ctrl then
if self.clipsize_x_ then
self.select_mode_ = "place"
self:cancel_drawing_()
self.place_x_, self.place_y_ = self.clipsize_x_, self.clipsize_y_
end
elseif scan == ui.SDL_SCANCODE_L then
self.select_mode_ = "place"
self:cancel_drawing_()
self.want_stamp_size_ = true
elseif scan == ui.SDL_SCANCODE_K then
self.select_mode_ = "place"
self:cancel_drawing_()
self.want_stamp_size_ = true
elseif scan == ui.SDL_SCANCODE_RIGHTBRACKET then
if self.placing_zoom_ then
self.zoom_invalid_ = true
end
elseif scan == ui.SDL_SCANCODE_LEFTBRACKET then
if self.placing_zoom_ then
self.zoom_invalid_ = true
end
elseif scan == ui.SDL_SCANCODE_I and not ctrl then
self:report_airinvert_()
elseif scan == ui.SDL_SCANCODE_SEMICOLON then
if self.registered_func_() then
self.bmode_invalid_ = true
end
end
if key == ui.SDLK_INSERT or key == ui.SDLK_DELETE then
if self.registered_func_() then
self.bmode_invalid_ = true
end
end
end
function profile_i:handle_keyrelease(key, scan, rep, shift, ctrl, alt)
self:post_event_check_()
if not shift and self.kmod_s_ then
self:disable_shift_()
end
if not ctrl and self.kmod_c_ then
self:disable_ctrl_()
end
if not alt and self.kmod_a_ then
self:disable_alt_()
end
self:update_kmod_()
-- * Here the assumption is made that no Lua hook cancels the keyrelease event.
-- * Here the assumption is made that no debug hook cancels the keyrelease event.
if rep then
return
end
if scan == ui.SDL_SCANCODE_Z then
if self.placing_zoom_ and not alt then
self.placing_zoom_ = false
self.zoom_invalid_ = true
end
end
end
function profile_i:handle_textinput(text)
self:post_event_check_()
end
function profile_i:handle_textediting(text)
self:post_event_check_()
end
function profile_i:handle_blur()
self:post_event_check_()
for _, btn in pairs(self.buttons_) do
btn.active = false
end
if self.registered_func_() and self[index_to_lraxid[self.last_toolslot_]] == "DEFAULT_UI_SIGN" then
self.signs_invalid_ = get_sign_data()
end
self:disable_shift_()
self:disable_ctrl_()
self:disable_alt_()
self:update_kmod_()
self:cancel_drawing_()
self.draw_mode_ = "points"
end
function profile_i:should_ignore_mouse()
return self.placing_zoom_ or self.select_mode_ ~= "none"
end
function profile_i:button_open_()
if self.registered_func_() then
self.placesave_open_ = true
self:begin_placesave_size_(100, 100, true)
end
end
function profile_i:button_reload_()
if self.registered_func_() then
self.placesave_reload_ = true
self:begin_placesave_size_(100, 100, true)
end
end
function profile_i:button_clear_()
if self.registered_func_() then
self.placesave_clear_ = true
self:begin_placesave_size_(100, 100, true)
end
end
function profile_i:set_client(client)
self.client_ = client
self.bmode_invalid_ = true
self.set_id_func_(util.get_save_id())
self:xidr_sync()
end
function profile_i:clear_client()
self.client_ = nil
self.xidr_ = nil
self.xidr_unsupported_ = nil
self.tool_l_ = nil
self.tool_r_ = nil
self.tool_a_ = nil
self.tool_x_ = nil
self.last_toolid_ = nil
self.tool_lid_ = nil
self.tool_rid_ = nil
self.tool_aid_ = nil
self.tool_xid_ = nil
end
function profile_i:xidr_sync()
if self.registered_func_() then
self.display_toolwarn_ = {}
self.display_toolwarn_identifier_ = nil
self.xidr_ = self.client_.xidr
self.xidr_unsupported_ = self.client_.xidr_unsupported
self.tool_l_ = self.xidr_.unknown_xid
self.tool_r_ = self.xidr_.unknown_xid
self.tool_a_ = self.xidr_.unknown_xid
self.tool_x_ = self.xidr_.unknown_xid
self.tool_lid_ = nil
self.tool_rid_ = nil
self.tool_aid_ = nil
self.tool_xid_ = nil
self.last_toolid_ = self.tool_lid_
self:update_tools_()
end
end
local function new(params)
local prof = setmetatable({
placing_zoom_ = false,
kmod_c_ = false,
kmod_s_ = false,
kmod_a_ = false,
bmode_ = 0,
dragging_mouse_ = false,
select_mode_ = "none",
prev_select_mode_ = false,
prev_place_mode_ = false,
draw_mode_ = "points",
last_toolslot_ = 0,
shape_ = 0,
stk2_out_ = false,
perfect_circle_invalid_ = true,
registered_func_ = params.registered_func,
log_event_func_ = params.log_event_func,
set_id_func_ = params.set_id_func,
get_id_func_ = params.get_id_func,
display_toolwarn_ = {},
buttons_ = {
open = { x = 1, y = gfx.HEIGHT - 16, w = 17, h = 15 },
reload = { x = 19, y = gfx.HEIGHT - 16, w = 17, h = 15 },
clear = { x = gfx.WIDTH - 159, y = gfx.HEIGHT - 16, w = 17, h = 15 },
},
}, profile_m)
prof.deco_ = sim.decoColour()
prof:update_pos_(tpt.mousex, tpt.mousey)
prof:update_size_()
prof:update_deco_()
prof:check_simstate()
prof:update_kmod_()
prof:update_bmode_()
prof:update_shape_()
prof:update_zoom_()
prof:update_tools_()
prof:check_signs({})
return prof
end
return {
new = new,
brand = "vanilla",
profile_i = profile_i,
}
end
require_preload__["tptmp.client.side_button"] = function()
local colours = require("tptmp.client.colours")
local util = require("tptmp.client.util")
local utf8 = require("tptmp.client.utf8")
local config = require("tptmp.client.config")
local manager = require("tptmp.client.manager")
local side_button_i = {}
local side_button_m = { __index = side_button_i }
function side_button_i:draw_button_()
local inside = util.inside_rect(self.pos_x_, self.pos_y_, self.width_, self.height_, util.mouse_pos())
if self.active_ and not inside then
self.active_ = false
end
local state
if self.active_ or self.window_status_func_() == "shown" then
state = "active"
elseif inside then
state = "hover"
else
state = "inactive"
end
local text_colour = colours.appearance[state].text
local border_colour = colours.appearance[state].border
local background_colour = colours.appearance[state].background
gfx.fillRect(self.pos_x_ + 1, self.pos_y_ + 1, self.width_ - 2, self.height_ - 2, unpack(background_colour))
gfx.drawRect(self.pos_x_, self.pos_y_, self.width_, self.height_, unpack(border_colour))
gfx.drawText(self.tx_, self.ty_, self.text_, unpack(text_colour))
end
function side_button_i:update_notif_count_()
local notif_count = self.notif_count_func_()
local notif_important = self.notif_important_func_()
if self.window_status_func_() == "floating" and not notif_important then
notif_count = 0
end
if self.notif_count_ ~= notif_count or self.notif_important_ ~= notif_important then
self.notif_count_ = notif_count
self.notif_important_ = notif_important
local notif_count_str = tostring(self.notif_count_)
self.notif_background_ = utf8.encode_multiple(0xE03B, 0xE039) .. utf8.encode_multiple(0xE03C):rep(#notif_count_str - 1) .. utf8.encode_multiple(0xE03A)
self.notif_border_ = utf8.encode_multiple(0xE02D, 0xE02B) .. utf8.encode_multiple(0xE02E):rep(#notif_count_str - 1) .. utf8.encode_multiple(0xE02C)
self.notif_text_ = notif_count_str:gsub(".", function(ch)
return utf8.encode_multiple(ch:byte() + 0xDFFF)
end)
self.notif_width_ = gfx.textSize(self.notif_background_)
self.notif_last_change_ = socket.gettime()
end
end
function side_button_i:draw_notif_count_()
if self.notif_count_ > 0 then
local since_last_change = socket.gettime() - self.notif_last_change_
local fly = since_last_change > config.notif_fly_time and 0 or ((1 - since_last_change / config.notif_fly_time) * config.notif_fly_distance)
gfx.drawText(self.pos_x_ - self.notif_width_ + 4, self.pos_y_ - 4 - fly, self.notif_background_, unpack(self.notif_important_ and colours.common.notif_important or colours.common.notif_normal))
gfx.drawText(self.pos_x_ - self.notif_width_ + 4, self.pos_y_ - 4 - fly, self.notif_border_)
gfx.drawText(self.pos_x_ - self.notif_width_ + 7, self.pos_y_ - 4 - fly, self.notif_text_)
end
end
function side_button_i:handle_tick()
self:draw_button_()
self:update_notif_count_()
self:draw_notif_count_()
end
function side_button_i:handle_mousedown(mx, my, button)
if button == ui.SDL_BUTTON_LEFT then
if util.inside_rect(self.pos_x_, self.pos_y_, self.width_, self.height_, util.mouse_pos()) then
self.active_ = true
end
end
end
function side_button_i:handle_mouseup(mx, my, button)
if button == ui.SDL_BUTTON_LEFT then
if self.active_ then
if manager.minimize_conflict and not manager.hidden() then
manager.print("minimize the manager before opening TPTMP")
else
if self.window_status_func_() == "shown" then
self.hide_window_func_()
else
self.show_window_func_()
end
end
self.active_ = false
end
end
end
function side_button_i:handle_mousewheel(pos_x, pos_y, dir)
end
function side_button_i:handle_keypress(key, scan, rep, shift, ctrl, alt)
if shift and not ctrl and not alt and scan == ui.SDL_SCANCODE_ESCAPE then
self.show_window_func_()
return true
elseif alt and not ctrl and not shift and scan == ui.SDL_SCANCODE_S then
self.sync_func_()
return true
elseif not alt and not ctrl and not shift and scan == ui.SDL_SCANCODE_T and self.window_status_func_() == "floating" then
self.begin_chat_func_()
return true
end
end
function side_button_i:handle_keyrelease(key, scan, rep, shift, ctrl, alt)
end
function side_button_i:handle_textinput(text)
end
function side_button_i:handle_textediting(text)
end
function side_button_i:handle_blur()
self.active_ = false
end
local function new(params)
local pos_x, pos_y, width, height = 613, 136, 15, 15
if tpt.version.jacob1s_mod and tpt.oldmenu and tpt.oldmenu() == 1 then
pos_y = 392
elseif tpt.num_menus then
pos_y = 392 - 16 * tpt.num_menus() - (not tpt.version.jacob1s_mod and 16 or 0)
end
if manager.side_button_conflict then
pos_y = pos_y - 17
end
local text = "<<"
local tw, th = gfx.textSize(text)
local tx = pos_x + math.ceil((width - tw) / 2)
local ty = pos_y + math.ceil((height - th) / 2)
return setmetatable({
text_ = text,
tx_ = tx,
pos_x_ = pos_x,
ty_ = ty,
pos_y_ = pos_y,
width_ = width,
height_ = height,
active_ = false,
notif_last_change_ = 0,
notif_count_ = 0,
notif_important_ = false,
notif_count_func_ = params.notif_count_func,
notif_important_func_ = params.notif_important_func,
show_window_func_ = params.show_window_func,
hide_window_func_ = params.hide_window_func,
begin_chat_func_ = params.begin_chat_func,
window_status_func_ = params.window_status_func,
sync_func_ = params.sync_func,
}, side_button_m)
end
return {
new = new,
}
end
require_preload__["tptmp.client.utf8"] = function()
local function code_points(str)
local cps = {}
local cursor = 0
while true do
local old_cursor = cursor
cursor = cursor + 1
local head = str:byte(cursor)
if not head then
break
end
local size = 1
if head >= 0x80 then
if head < 0xC0 then
return nil, cursor
end
size = 2
if head >= 0xE0 then
size = 3
end
if head >= 0xF0 then
size = 4
end
if head >= 0xF8 then
return nil, cursor
end
head = bit.band(head, bit.lshift(1, 7 - size) - 1)
for ix = 2, size do
local by = str:byte(cursor + ix - 1)
if not by then
return nil, cursor
end
if by < 0x80 or by >= 0xC0 then
return nil, cursor + ix
end
head = bit.bor(bit.lshift(head, 6), bit.band(by, 0x3F))
end
cursor = cursor - 1 + size
end
local pos = old_cursor + 1
if (head < 0x80 and size > 1)
or (head < 0x800 and size > 2)
or (head < 0x10000 and size > 3) then
return nil, pos
end
table.insert(cps, { cp = head, pos = pos, size = size })
end
return cps
end
local function encode(code_point)
if code_point < 0x80 then
return string.char(code_point)
elseif code_point < 0x800 then
return string.char(
bit.bor(0xC0, bit.rshift(code_point, 6) ),
bit.bor(0x80, bit.band( code_point , 0x3F))
)
elseif code_point < 0x10000 then
return string.char(
bit.bor(0xE0, bit.rshift(code_point, 12) ),
bit.bor(0x80, bit.band(bit.rshift(code_point, 6), 0x3F)),
bit.bor(0x80, bit.band( code_point , 0x3F))
)
elseif code_point < 0x200000 then
return string.char(
bit.bor(0xF0, bit.rshift(code_point, 18) ),
bit.bor(0x80, bit.band(bit.rshift(code_point, 12), 0x3F)),
bit.bor(0x80, bit.band(bit.rshift(code_point, 6), 0x3F)),
bit.bor(0x80, bit.band( code_point , 0x3F))
)
else
error("invalid code point")
end
end
local function encode_multiple(cp, ...)
if not ... then
return encode(cp)
end
local cps = { cp, ... }
local collect = {}
for i = 1, #cps do
table.insert(collect, encode(cps[i]))
end
return table.concat(collect)
end
if tpt.version.jacob1s_mod then
function code_points(str)
local cps = {}
for pos in str:gmatch("().") do
table.insert(cps, { cp = str:byte(pos), pos = pos, size = 1 })
end
return cps
end
function encode(cp)
if cp >= 0xE000 then
cp = cp - 0xDF80
end
return string.char(cp)
end
end
return {
code_points = code_points,
encode = encode,
encode_multiple = encode_multiple,
}
end
require_preload__["tptmp.client.util"] = function()
local config = require("tptmp.client.config")
local common_util = require("tptmp.common.util")
local jacobsmod = rawget(_G, "jacobsmod")
local PMAPBITS = sim.PMAPBITS
local tpt_version = { tpt.version.major, tpt.version.minor }
local has_ambient_heat_tools
do
local old_selectedl = tpt.selectedl
if old_selectedl == "DEFAULT_UI_PROPERTY" or old_selectedl == "DEFAULT_UI_ADDLIFE" then
old_selectedl = "DEFAULT_PT_DUST"
end
has_ambient_heat_tools = pcall(function() tpt.selectedl = "DEFAULT_TOOL_AMBM" end)
tpt.selectedl = old_selectedl
end
local function array_concat(...)
local tbl = {}
local arrays = { ... }
for i = 1, #arrays do
for j = 1, #arrays[i] do
table.insert(tbl, arrays[i][j])
end
end
return tbl
end
local tools = array_concat({
"DEFAULT_PT_LIFE_GOL",
"DEFAULT_PT_LIFE_HLIF",
"DEFAULT_PT_LIFE_ASIM",
"DEFAULT_PT_LIFE_2X2",
"DEFAULT_PT_LIFE_DANI",
"DEFAULT_PT_LIFE_AMOE",
"DEFAULT_PT_LIFE_MOVE",
"DEFAULT_PT_LIFE_PGOL",
"DEFAULT_PT_LIFE_DMOE",
"DEFAULT_PT_LIFE_3-4",
"DEFAULT_PT_LIFE_LLIF",
"DEFAULT_PT_LIFE_STAN",
"DEFAULT_PT_LIFE_SEED",
"DEFAULT_PT_LIFE_MAZE",
"DEFAULT_PT_LIFE_COAG",
"DEFAULT_PT_LIFE_WALL",
"DEFAULT_PT_LIFE_GNAR",
"DEFAULT_PT_LIFE_REPL",
"DEFAULT_PT_LIFE_MYST",
"DEFAULT_PT_LIFE_LOTE",
"DEFAULT_PT_LIFE_FRG2",
"DEFAULT_PT_LIFE_STAR",
"DEFAULT_PT_LIFE_FROG",
"DEFAULT_PT_LIFE_BRAN",
}, {
"DEFAULT_WL_ERASE",
"DEFAULT_WL_CNDTW",
"DEFAULT_WL_EWALL",
"DEFAULT_WL_DTECT",
"DEFAULT_WL_STRM",
"DEFAULT_WL_FAN",
"DEFAULT_WL_LIQD",
"DEFAULT_WL_ABSRB",
"DEFAULT_WL_WALL",
"DEFAULT_WL_AIR",
"DEFAULT_WL_POWDR",
"DEFAULT_WL_CNDTR",
"DEFAULT_WL_EHOLE",
"DEFAULT_WL_GAS",
"DEFAULT_WL_GRVTY",
"DEFAULT_WL_ENRGY",
"DEFAULT_WL_NOAIR",
"DEFAULT_WL_ERASEA",
"DEFAULT_WL_STASIS",
}, {
"DEFAULT_UI_SAMPLE",
"DEFAULT_UI_SIGN",
"DEFAULT_UI_PROPERTY",
"DEFAULT_UI_WIND",
"DEFAULT_UI_ADDLIFE",
}, {
"DEFAULT_TOOL_HEAT",
"DEFAULT_TOOL_COOL",
"DEFAULT_TOOL_AIR",
"DEFAULT_TOOL_VAC",
"DEFAULT_TOOL_PGRV",
"DEFAULT_TOOL_NGRV",
"DEFAULT_TOOL_MIX",
"DEFAULT_TOOL_CYCL",
has_ambient_heat_tools and "DEFAULT_TOOL_AMBM" or nil,
has_ambient_heat_tools and "DEFAULT_TOOL_AMBP" or nil,
}, {
"DEFAULT_DECOR_SET",
"DEFAULT_DECOR_CLR",
"DEFAULT_DECOR_ADD",
"DEFAULT_DECOR_SUB",
"DEFAULT_DECOR_MUL",
"DEFAULT_DECOR_DIV",
"DEFAULT_DECOR_SMDG",
})
local function xid_registry(supported)
table.sort(supported, function(lhs, rhs)
-- * Doesn't matter what this is as long as it's canonical. Built-in
-- __lt on strings is not trustworthy because it's based on the
-- current locale, so it's not necessarily canonical.
for i = 1, math.max(#lhs, #rhs) do
local lb = string.byte(lhs, i) or -math.huge
local rb = string.byte(rhs, i) or -math.huge
if lb < rb then return true end
if lb > rb then return false end
end
return false
end)
local xid_first = {}
local xid_class = {}
local from_tool = {}
local to_tool = {}
for i = 1, #tools do
local xtype = 0x2000 + i
local tool = tools[i]
from_tool[tool] = xtype
to_tool[xtype] = tool
local class = tool:match("^[^_]+_(.-)_[^_]+$")
xid_class[xtype] = class
xid_first[class] = math.min(xid_first[class] or math.huge, xtype)
end
for key, value in pairs(supported) do
assert(not to_tool[key])
assert(not from_tool[value])
to_tool[key] = value
from_tool[value] = key
end
local unknown_xid = 0x3FFF
assert(not to_tool[unknown_xid])
from_tool["UNKNOWN"] = unknown_xid
to_tool[unknown_xid] = "UNKNOWN"
local function assign_if_supported(tbl)
local res = {}
for key, value in pairs(tbl) do
if from_tool[key] then
res[from_tool[key]] = value
end
end
return res
end
local create_override = assign_if_supported({
[ "DEFAULT_PT_STKM" ] = function(rx, ry, c)
return 0, 0, c
end,
[ "DEFAULT_PT_LIGH" ] = function(rx, ry, c)
local tmp = rx + ry
if tmp > 55 then
tmp = 55
end
return 0, 0, c + bit.lshift(tmp, PMAPBITS)
end,
[ "DEFAULT_PT_TESC" ] = function(rx, ry, c)
local tmp = rx * 4 + ry * 4 + 7
if tmp > 300 then
tmp = 300
end
return rx, ry, c + bit.lshift(tmp, PMAPBITS)
end,
[ "DEFAULT_PT_STKM2" ] = function(rx, ry, c)
return 0, 0, c
end,
[ "DEFAULT_PT_FIGH" ] = function(rx, ry, c)
return 0, 0, c
end,
})
local no_flood = assign_if_supported({
[ "DEFAULT_PT_SPRK" ] = true,
[ "DEFAULT_PT_STKM" ] = true,
[ "DEFAULT_PT_LIGH" ] = true,
[ "DEFAULT_PT_STKM2" ] = true,
[ "DEFAULT_PT_FIGH" ] = true,
})
local no_shape = assign_if_supported({
[ "DEFAULT_PT_STKM" ] = true,
[ "DEFAULT_PT_LIGH" ] = true,
[ "DEFAULT_PT_STKM2" ] = true,
[ "DEFAULT_PT_FIGH" ] = true,
})
local no_create = assign_if_supported({
[ "DEFAULT_UI_PROPERTY" ] = true,
[ "DEFAULT_UI_SAMPLE" ] = true,
[ "DEFAULT_UI_SIGN" ] = true,
[ "UNKNOWN" ] = true,
})
local line_only = assign_if_supported({
[ "DEFAULT_UI_WIND" ] = true,
})
return {
xid_first = xid_first,
xid_class = xid_class,
from_tool = from_tool,
to_tool = to_tool,
create_override = create_override,
no_flood = no_flood,
no_shape = no_shape,
no_create = no_create,
line_only = line_only,
unknown_xid = unknown_xid,
}
end
local function heat_clear()
local temp = sim.ambientAirTemp()
for x = 0, sim.XRES / sim.CELL - 1 do
for y = 0, sim.YRES / sim.CELL - 1 do
sim.ambientHeat(x, y, temp)
end
end
end
local function stamp_load(x, y, data, reset)
if data == "" then -- * Is this check needed at all?
return nil, "no stamp data"
end
local stamp_temp = ("%s.%s.%s"):format(config.stamp_temp, tostring(socket.gettime()), tostring(math.random(10000, 99999)))
local handle = io.open(stamp_temp, "wb")
if not handle then
return nil, "cannot write stamp data"
end
handle:write(data)
handle:close()
if reset then
sim.clearRect(0, 0, sim.XRES, sim.YRES)
heat_clear()
tpt.reset_velocity()
tpt.set_pressure()
end
local ok, err = sim.loadStamp(stamp_temp, x, y)
if not ok then
os.remove(stamp_temp)
if err then
return nil, "cannot load stamp data: " .. err
else
return nil, "cannot load stamp data"
end
end
os.remove(stamp_temp)
return true
end
local function stamp_save(x, y, w, h)
local name = sim.saveStamp(x, y, w - 1, h - 1)
if not name then
return nil, "error saving stamp"
end
local handle = io.open("stamps/" .. name .. ".stm", "rb")
if not handle then
sim.deleteStamp(name)
return nil, "cannot read stamp data"
end
local data = handle:read("*a")
handle:close()
sim.deleteStamp(name)
return data
end
-- * Finds bynd, the smallest idx in [first, last] for which beyond(idx)
-- is true. Assumes that for all idx in [first, bynd-1] beyond(idx) is
-- false and for all idx in [bynd, last] beyond(idx) is true. beyond(first-1)
-- is implicitly false and beyond(last+1) is implicitly true, thus an
-- all-false field yields last+1 and an all-true field yields first.
local function binary_search_implicit(first, last, beyond)
local function beyond_wrap(idx)
if idx < first then
return false
end
if idx > last then
return true
end
return beyond(idx)
end
while first <= last do
local mid = math.floor((first + last) / 2)
if beyond_wrap(mid) then
if beyond_wrap(mid - 1) then
last = mid - 1
else
return mid
end
else
first = mid + 1
end
end
return first
end
local function inside_rect(pos_x, pos_y, width, height, check_x, check_y)
return pos_x <= check_x and pos_y <= check_y and pos_x + width > check_x and pos_y + height > check_y
end
local function mouse_pos()
return tpt.mousex, tpt.mousey
end
local function brush_size()
return tpt.brushx, tpt.brushy
end
local function selected_tools()
return tpt.selectedl, tpt.selecteda, tpt.selectedr, tpt.selectedreplace
end
local function wall_snap_coords(x, y)
return math.floor(x / 4) * 4, math.floor(y / 4) * 4
end
local function line_snap_coords(x1, y1, x2, y2)
local dx, dy = x2 - x1, y2 - y1
if math.abs(math.floor(dx / 2)) > math.abs(dy) then
return x2, y1
elseif math.abs(dx) < math.abs(math.floor(dy / 2)) then
return x1, y2
elseif dx * dy > 0 then
return x1 + math.floor((dx + dy) / 2), y1 + math.floor((dy + dx) / 2)
else
return x1 + math.floor((dx - dy) / 2), y1 + math.floor((dy - dx) / 2)
end
end
local function rect_snap_coords(x1, y1, x2, y2)
local dx, dy = x2 - x1, y2 - y1
if dx * dy > 0 then
return x1 + math.floor((dx + dy) / 2), y1 + math.floor((dy + dx) / 2)
else
return x1 + math.floor((dx - dy) / 2), y1 + math.floor((dy - dx) / 2)
end
end
local function create_parts_any(xidr, x, y, rx, ry, xtype, brush, member)
if not inside_rect(0, 0, sim.XRES, sim.YRES, x, y) then
return
end
if xidr.line_only[xtype] or xidr.no_create[xtype] then
return
end
local translate = true
local class = xidr.xid_class[xtype]
if class == "WL" then
if xtype == xidr.from_tool.DEFAULT_WL_STRM then
rx, ry = 0, 0
end
sim.createWalls(x, y, rx, ry, xtype - xidr.xid_first.WL, brush)
return
elseif class == "TOOL" then
local str = 1
if member.kmod_s then
str = 10
elseif member.kmod_c then
str = 0.1
end
sim.toolBrush(x, y, rx, ry, xtype - xidr.xid_first.TOOL, brush, str)
return
elseif class == "DECOR" then
sim.decoBrush(x, y, rx, ry, member.deco_r, member.deco_g, member.deco_b, member.deco_a, xtype - xidr.xid_first.DECOR, brush)
return
elseif class == "PT_LIFE" then
xtype = bit.bor(elem.DEFAULT_PT_LIFE, bit.lshift(xtype - xidr.xid_first.PT_LIFE, PMAPBITS))
translate = false
elseif type(xtype) == "table" and xtype.type == "cgol" then
-- * TODO[api]: add an api for setting gol colour
xtype = xtype.elem
translate = false
end
local ov = xidr.create_override[xtype]
if ov then
rx, ry, xtype = ov(rx, ry, xtype)
end
local selectedreplace
if member.bmode ~= 0 then
selectedreplace = tpt.selectedreplace
tpt.selectedreplace = xidr.to_tool[member.tool_x] or "DEFAULT_PT_NONE"
end
if translate then
xtype = elem[xidr.to_tool[xtype]]
end
sim.createParts(x, y, rx, ry, xtype, brush, member.bmode)
if member.bmode ~= 0 then
tpt.selectedreplace = selectedreplace
end
end
local function create_line_any(xidr, x1, y1, x2, y2, rx, ry, xtype, brush, member, cont)
-- * TODO[opt]: Revert jacob1's mod ball check.
if not inside_rect(0, 0, sim.XRES, sim.YRES, x1, y1) or
not inside_rect(0, 0, sim.XRES, sim.YRES, x2, y2) then
return
end
if xidr.no_create[xtype] or xidr.no_shape[xtype] then
return
end
local translate = true
local class = xidr.xid_class[xtype]
if class == "WL" then
local str = 1
if cont then
if member.kmod_s then
str = 10
elseif member.kmod_c then
str = 0.1
end
str = str * 5
end
local wl_fan = xidr.from_tool.DEFAULT_WL_FAN - xidr.xid_first.WL
if not cont and xtype == xidr.from_tool.DEFAULT_WL_FAN and tpt.get_wallmap(math.floor(x1 / 4), math.floor(y1 / 4)) == wl_fan then
local fvx = (x2 - x1) * 0.005
local fvy = (y2 - y1) * 0.005
local bw = sim.XRES / 4
local bh = sim.YRES / 4
local visit = {}
local mark = {}
local last = 0
local function enqueue(x, y)
if x >= 0 and y >= 0 and x < bw and y < bh and tpt.get_wallmap(x, y) == wl_fan then
local k = x + y * bw
if not mark[k] then
last = last + 1
visit[last] = k
mark[k] = true
end
end
end
enqueue(math.floor(x1 / 4), math.floor(y1 / 4))
local curr = 1
while visit[curr] do
local k = visit[curr]
local x, y = k % bw, math.floor(k / bw)
tpt.set_wallmap(x, y, 1, 1, fvx, fvy, wl_fan)
enqueue(x - 1, y)
enqueue(x, y - 1)
enqueue(x + 1, y)
enqueue(x, y + 1)
curr = curr + 1
end
return
end
if xtype == xidr.from_tool.DEFAULT_WL_STRM then
rx, ry = 0, 0
end
sim.createWallLine(x1, y1, x2, y2, rx, ry, xtype - xidr.xid_first.WL, brush)
return
elseif xtype == xidr.from_tool.DEFAULT_UI_WIND then
local str = 1
if cont then
if member.kmod_s then
str = 10
elseif member.kmod_c then
str = 0.1
end
str = str * 5
end
sim.toolLine(x1, y1, x2, y2, rx, ry, sim.TOOL_WIND, brush, str)
return
elseif class == "TOOL" then
local str = 1
if cont then
if member.kmod_s then
str = 10
elseif member.kmod_c then
str = 0.1
end
end
sim.toolLine(x1, y1, x2, y2, rx, ry, xtype - xidr.xid_first.TOOL, brush, str)
return
elseif class == "DECOR" then
sim.decoLine(x1, y1, x2, y2, rx, ry, member.deco_r, member.deco_g, member.deco_b, member.deco_a, xtype - xidr.xid_first.DECOR, brush)
return
elseif class == "PT_LIFE" then
xtype = bit.bor(elem.DEFAULT_PT_LIFE, bit.lshift(xtype - xidr.xid_first.PT_LIFE, PMAPBITS))
translate = false
elseif type(xtype) == "table" and xtype.type == "cgol" then
-- * TODO[api]: add an api for setting gol colour
xtype = xtype.elem
translate = false
end
local ov = xidr.create_override[xtype]
if ov then
rx, ry, xtype = ov(rx, ry, xtype)
end
local selectedreplace
if member.bmode ~= 0 then
selectedreplace = tpt.selectedreplace
tpt.selectedreplace = xidr.to_tool[member.tool_x] or "DEFAULT_PT_NONE"
end
if translate then
xtype = elem[xidr.to_tool[xtype]]
end
sim.createLine(x1, y1, x2, y2, rx, ry, xtype, brush, member.bmode)
if member.bmode ~= 0 then
tpt.selectedreplace = selectedreplace
end
end
local function create_box_any(xidr, x1, y1, x2, y2, xtype, member)
if not inside_rect(0, 0, sim.XRES, sim.YRES, x1, y1) or
not inside_rect(0, 0, sim.XRES, sim.YRES, x2, y2) then
return
end
if xidr.line_only[xtype] or xidr.no_create[xtype] or xidr.no_shape[xtype] then
return
end
local translate = true
local class = xidr.xid_class[xtype]
if class == "WL" then
sim.createWallBox(x1, y1, x2, y2, xtype - xidr.xid_first.WL)
return
elseif class == "TOOL" then
sim.toolBox(x1, y1, x2, y2, xtype - xidr.xid_first.TOOL)
return
elseif class == "DECOR" then
sim.decoBox(x1, y1, x2, y2, member.deco_r, member.deco_g, member.deco_b, member.deco_a, xtype - xidr.xid_first.DECOR)
return
elseif class == "PT_LIFE" then
xtype = bit.bor(elem.DEFAULT_PT_LIFE, bit.lshift(xtype - xidr.xid_first.PT_LIFE, PMAPBITS))
translate = false
elseif type(xtype) == "table" and xtype.type == "cgol" then
-- * TODO[api]: add an api for setting gol colour
xtype = xtype.elem
translate = false
end
local _
local ov = xidr.create_override[xtype]
if ov then
_, _, xtype = ov(member.size_x, member.size_y, xtype)
end
local selectedreplace
if member.bmode ~= 0 then
selectedreplace = tpt.selectedreplace
tpt.selectedreplace = xidr.to_tool[member.tool_x] or "DEFAULT_PT_NONE"
end
if translate then
xtype = elem[xidr.to_tool[xtype]]
end
sim.createBox(x1, y1, x2, y2, xtype, member and member.bmode)
if member.bmode ~= 0 then
tpt.selectedreplace = selectedreplace
end
end
local function flood_any(xidr, x, y, xtype, part_flood_hint, wall_flood_hint, member)
if not inside_rect(0, 0, sim.XRES, sim.YRES, x, y) then
return
end
if xidr.line_only[xtype] or xidr.no_create[xtype] or xidr.no_flood[xtype] then
return
end
local translate = true
local class = xidr.xid_class[xtype]
if class == "WL" then
sim.floodWalls(x, y, xtype - xidr.xid_first.WL, wall_flood_hint)
return
elseif class == "DECOR" or class == "TOOL" then
return
elseif class == "PT_LIFE" then
xtype = bit.bor(elem.DEFAULT_PT_LIFE, bit.lshift(xtype - xidr.xid_first.PT_LIFE, PMAPBITS))
translate = false
elseif type(xtype) == "table" and xtype.type == "cgol" then
-- * TODO[api]: add an api for setting gol colour
xtype = xtype.elem
translate = false
end
local _
local ov = xidr.create_override[xtype]
if ov then
_, _, xtype = ov(member.size_x, member.size_y, xtype)
end
local selectedreplace
if member.bmode ~= 0 then
selectedreplace = tpt.selectedreplace
tpt.selectedreplace = xidr.to_tool[member.tool_x] or "DEFAULT_PT_NONE"
end
if translate then
xtype = elem[xidr.to_tool[xtype]]
end
sim.floodParts(x, y, xtype, part_flood_hint, member.bmode)
if member.bmode ~= 0 then
tpt.selectedreplace = selectedreplace
end
end
local function clear_rect(x, y, w, h)
if not inside_rect(0, 0, sim.XRES, sim.YRES, x + w, y + h) then
return
end
sim.clearRect(x, y, w, h)
end
local function corners_to_rect(x1, y1, x2, y2)
local xl = math.min(x1, x2)
local yl = math.min(y1, y2)
local xh = math.max(x1, x2)
local yh = math.max(y1, y2)
return xl, yl, xh - xl + 1, yh - yl + 1
end
local function escape_regex(str)
return (str:gsub("[%$%%%(%)%*%+%-%.%?%[%^%]]", "%%%1"))
end
local function fnv1a32(data)
local hash = 2166136261
for i = 1, #data do
hash = bit.bxor(hash, data:byte(i))
hash = bit.band(bit.lshift(hash, 24), 0xFFFFFFFF) + bit.band(bit.lshift(hash, 8), 0xFFFFFFFF) + hash * 147
end
hash = bit.band(hash, 0xFFFFFFFF)
return hash < 0 and (hash + 0x100000000) or hash
end
local function ambient_air_temp(temp)
if temp then
local set = temp / 0x400
sim.ambientAirTemp(set)
return set
else
return math.max(0x000000, math.min(0xFFFFFF, math.floor(sim.ambientAirTemp() * 0x400)))
end
end
local function custom_gravity(x, y)
if x then
if x >= 0x800000 then x = x - 0x1000000 end
if y >= 0x800000 then y = y - 0x1000000 end
local setx, sety = x / 0x400, y / 0x400
sim.customGravity(setx, sety)
return setx, sety
else
local getx, gety = sim.customGravity()
getx = math.max(-0x800000, math.min(0x7FFFFF, math.floor(getx * 0x400)))
gety = math.max(-0x800000, math.min(0x7FFFFF, math.floor(gety * 0x400)))
if getx < 0 then getx = getx + 0x1000000 end
if gety < 0 then gety = gety + 0x1000000 end
return getx, gety
end
end
local function get_save_id()
local id, hist = sim.getSaveID()
if id and not hist then
hist = 0
end
return id, hist
end
local function urlencode(str)
return (str:gsub("[^ !'()*%-%.0-9A-Z_a-z]", function(cap)
return ("%%%02x"):format(cap:byte())
end))
end
local function get_name()
local name = tpt.get_name()
return name ~= "" and name or nil
end
local function element_identifiers()
local identifiers = {}
for name in pairs(elem) do
if name:find("^[^_]*_PT_[^_]*$") then
identifiers[name] = true
end
end
return identifiers
end
local function decode_rulestring(tool)
if type(tool) == "table" and tool.type == "cgol" then
return tool.repr
end
end
local function tool_proper_name(tool, xidr)
local tool_name = (tool and xidr.to_tool[tool] or decode_rulestring(tool)) or "UNKNOWN"
if elem[tool_name] and xidr.to_tool[tool] and tool ~= 0 and tool_name ~= "UNKNOWN" then
local real_name = elem.property(elem[tool_name], "Name")
if real_name ~= "" then
tool_name = real_name
end
end
return tool_name
end
return {
get_name = get_name,
stamp_load = stamp_load,
stamp_save = stamp_save,
binary_search_implicit = binary_search_implicit,
inside_rect = inside_rect,
mouse_pos = mouse_pos,
brush_size = brush_size,
selected_tools = selected_tools,
wall_snap_coords = wall_snap_coords,
line_snap_coords = line_snap_coords,
rect_snap_coords = rect_snap_coords,
create_parts_any = create_parts_any,
create_line_any = create_line_any,
create_box_any = create_box_any,
flood_any = flood_any,
clear_rect = clear_rect,
xid_registry = xid_registry,
corners_to_rect = corners_to_rect,
escape_regex = escape_regex,
fnv1a32 = fnv1a32,
ambient_air_temp = ambient_air_temp,
custom_gravity = custom_gravity,
get_save_id = get_save_id,
version_less = common_util.version_less,
version_equal = common_util.version_equal,
tpt_version = tpt_version,
urlencode = urlencode,
heat_clear = heat_clear,
element_identifiers = element_identifiers,
tool_proper_name = tool_proper_name,
}
end
require_preload__["tptmp.client.window"] = function()
local config = require("tptmp.client.config")
local colours = require("tptmp.client.colours")
local format = require("tptmp.client.format")
local utf8 = require("tptmp.client.utf8")
local util = require("tptmp.client.util")
local manager = require("tptmp.client.manager")
local notif_important = colours.common.notif_important
local text_bg_high = { notif_important[1] / 2, notif_important[2] / 2, notif_important[3] / 2 }
local text_bg_high_floating = { notif_important[1] / 3, notif_important[2] / 3, notif_important[3] / 3 }
local text_bg = { 0, 0, 0 }
local window_i = {}
local window_m = { __index = window_i }
local wrap_padding = 11 -- * Width of "* "
function window_i:backlog_push_join(formatted_nick)
self:backlog_push_str(colours.commonstr.join .. "* " .. formatted_nick .. colours.commonstr.join .. " has joined", true)
end
function window_i:backlog_push_leave(formatted_nick)
self:backlog_push_str(colours.commonstr.leave .. "* " .. formatted_nick .. colours.commonstr.leave .. " has left", true)
end
function window_i:backlog_push_fpssync_enable(formatted_nick)
self:backlog_push_str(colours.commonstr.fpssyncenable .. "* " .. formatted_nick .. colours.commonstr.fpssyncenable .. " has enabled FPS synchronization", true)
end
function window_i:backlog_push_fpssync_disable(formatted_nick)
self:backlog_push_str(colours.commonstr.fpssyncdisable .. "* " .. formatted_nick .. colours.commonstr.fpssyncdisable .. " has disabled FPS synchronization", true)
end
function window_i:backlog_push_error(str)
self:backlog_push_str(colours.commonstr.error .. "* " .. str, true)
end
function window_i:get_important_(str)
local cli = self.client_func_()
if cli then
if (" " .. str .. " "):lower():find("[^a-z0-9-_]" .. cli:nick():lower() .. "[^a-z0-9-_]") then
return "highlight"
end
end
end
function window_i:backlog_push_say_other(formatted_nick, str)
self:backlog_push_say(formatted_nick, str, self:get_important_(str))
end
function window_i:backlog_push_say3rd_other(formatted_nick, str)
self:backlog_push_say3rd(formatted_nick, str, self:get_important_(str))
end
function window_i:backlog_push_say(formatted_nick, str, important)
self:backlog_push_str(colours.commonstr.chat .. "<" .. formatted_nick .. colours.commonstr.chat .. "> " .. str, important)
end
function window_i:backlog_push_say3rd(formatted_nick, str, important)
self:backlog_push_str(colours.commonstr.chat .. "* " .. formatted_nick .. colours.commonstr.chat .. " " .. str, important)
end
function window_i:backlog_push_room(room, members, prefix)
local sep = colours.commonstr.neutral .. ", "
local collect = { colours.commonstr.neutral, "* ", prefix, format.troom(room), sep }
if next(members) then
table.insert(collect, "present: ")
local first = true
for id, member in pairs(members) do
if first then
first = false
else
table.insert(collect, sep)
end
table.insert(collect, member.formatted_nick)
end
else
table.insert(collect, "nobody else present")
end
self:backlog_push_str(table.concat(collect), true)
end
function window_i:backlog_push_fpssync(members)
local sep = colours.commonstr.neutral .. ", "
local collect = { colours.commonstr.neutral, "* " }
if members == true then
table.insert(collect, "FPS synchronization is enabled")
elseif members then
if next(members) then
table.insert(collect, "FPS synchronization is enabled, in sync with: ")
local first = true
for id, member in pairs(members) do
if first then
first = false
else
table.insert(collect, sep)
end
table.insert(collect, member.formatted_nick)
end
else
table.insert(collect, "FPS synchronization is enabled, not in sync with anyone")
end
else
table.insert(collect, "FPS synchronization is disabled")
end
self:backlog_push_str(table.concat(collect), true)
end
function window_i:backlog_push_registered(formatted_nick)
self:backlog_push_str(colours.commonstr.neutral .. "* Connected as " .. formatted_nick, true)
end
local server_colours = {
n = colours.commonstr.neutral,
e = colours.commonstr.error,
j = colours.commonstr.join,
l = colours.commonstr.leave,
}
function window_i:backlog_push_server(str)
local formatted = str
:gsub("\au([A-Za-z0-9-_#]+)", function(cap) return format.nick(cap, self.nick_colour_seed_) end)
:gsub("\ar([A-Za-z0-9-_#]+)", function(cap) return format.room(cap) end)
:gsub("\a([nejl])" , function(cap) return server_colours[cap] end)
self:backlog_push_str(formatted, true)
end
function window_i:nick_colour_seed(seed)
self.nick_colour_seed_ = seed
end
function window_i:backlog_push_neutral(str)
self:backlog_push_str(colours.commonstr.neutral .. str, true)
end
function window_i:backlog_wrap_(msg)
if msg == self.backlog_first_ then
return
end
if msg.wrapped_to ~= self.width_ then
local line = {}
local wrapped = {}
local collect = msg.collect
local i = 0
local word = {}
local word_width = 0
local line_width = 0
local max_width = self.width_ - 8
local line_empty = true
local red, green, blue = 255, 255, 255
local initial_block
local function insert_block(block)
if initial_block then
table.insert(line, initial_block)
initial_block = nil
end
table.insert(line, block)
end
local function flush_line()
if not line_empty then
table.insert(wrapped, table.concat(line))
line = {}
initial_block = colours.escape({ red, green, blue })
line_width = wrap_padding
line_empty = true
end
end
local function flush_word()
if #word > 0 then
for i = 1, #word do
insert_block(word[i])
end
line_empty = false
line_width = line_width + word_width
word = {}
word_width = 0
end
end
while i < #collect do
i = i + 1
if collect[i] == "\15" and i + 3 <= #collect then
local rgb = utf8.code_points(table.concat(collect, nil, i + 1, i + 3))
if rgb then
for j = i, i + 3 do
table.insert(word, collect[j])
end
red, green, blue = rgb[1].cp, rgb[2].cp, rgb[3].cp
end
i = i + 3
else
local i_width = gfx.textSize(collect[i])
if collect[i]:find(config.whitespace_pattern) then
flush_word()
if line_width + i_width > max_width then
flush_line()
end
if not line_empty then
insert_block(collect[i])
line_width = line_width + i_width
end
line_empty = false
else
if line_width + word_width + i_width > max_width then
flush_line()
if line_width + word_width + i_width > max_width then
flush_word()
if line_width + word_width + i_width > max_width then
flush_line()
end
end
end
table.insert(word, collect[i])
word_width = word_width + i_width
end
end
end
flush_word()
flush_line()
if #wrapped > 1 and wrapped[#wrapped] == "" then
wrapped[#wrapped] = nil
end
msg.wrapped_to = self.width_
msg.wrapped = wrapped
self.backlog_last_wrapped_ = math.max(self.backlog_last_wrapped_, msg.unique)
end
end
function window_i:backlog_update_()
local max_lines = math.floor((self.height_ - 35) / 12)
local lines_reverse = {}
self:backlog_wrap_(self.backlog_last_visible_msg_)
if self.backlog_auto_scroll_ then
while self.backlog_last_visible_msg_.next ~= self.backlog_last_ do
self.backlog_last_visible_msg_ = self.backlog_last_visible_msg_.next
end
self:backlog_wrap_(self.backlog_last_visible_msg_)
self.backlog_last_visible_line_ = #self.backlog_last_visible_msg_.wrapped
end
self:backlog_wrap_(self.backlog_last_visible_msg_)
self.backlog_last_visible_line_ = math.min(#self.backlog_last_visible_msg_.wrapped, self.backlog_last_visible_line_)
local source_msg = self.backlog_last_visible_msg_
local source_line = self.backlog_last_visible_line_
while #lines_reverse < max_lines do
if source_msg == self.backlog_first_ then
break
end
self:insert_wrapped_line_(lines_reverse, source_msg, source_line)
source_line = source_line - 1
if source_line == 0 then
source_msg = source_msg.prev
self:backlog_wrap_(source_msg)
source_line = #source_msg.wrapped
end
end
if source_msg ~= self.backlog_first_ and source_msg.unique - 1 <= self.backlog_unique_ - config.backlog_size then
source_msg.prev = self.backlog_first_
self.backlog_first_.next = source_msg
end
local lines = {}
for i = #lines_reverse, 1, -1 do
table.insert(lines, lines_reverse[i])
end
while #lines < max_lines do
if self.backlog_last_visible_line_ == #self.backlog_last_visible_msg_.wrapped then
if self.backlog_last_visible_msg_.next == self.backlog_last_ then
break
end
self.backlog_last_visible_msg_ = self.backlog_last_visible_msg_.next
self:backlog_wrap_(self.backlog_last_visible_msg_)
self.backlog_last_visible_line_ = 1
else
self.backlog_last_visible_line_ = self.backlog_last_visible_line_ + 1
end
self:insert_wrapped_line_(lines, self.backlog_last_visible_msg_, self.backlog_last_visible_line_)
end
self.backlog_text_ = {}
local marker_after
for i = 1, #lines do
local text_width = gfx.textSize(lines[i].wrapped)
local padding = lines[i].needs_padding and wrap_padding or 0
local box_width = lines[i].extend_box and self.width_ or (padding + text_width + 10)
table.insert(self.backlog_text_, {
padding = padding,
pushed_at = lines[i].msg.pushed_at,
highlight = lines[i].msg.important == "highlight",
text = lines[i].wrapped,
box_width = box_width,
})
if lines[i].marker then
marker_after = i
end
end
self.backlog_lines_ = lines
self.backlog_text_y_ = self.height_ - #lines * 12 - 15
self.backlog_marker_y_ = self.backlog_enable_marker_ and marker_after and marker_after ~= #lines and (self.backlog_text_y_ + marker_after * 12 - 2)
end
function window_i:backlog_push_(collect, important)
self.backlog_unique_ = self.backlog_unique_ + 1
local msg = {
unique = self.backlog_unique_,
collect = collect,
prev = self.backlog_last_.prev,
next = self.backlog_last_,
important = important,
pushed_at = socket.gettime(),
}
self.backlog_last_.prev.next = msg
self.backlog_last_.prev = msg
if important then
self.backlog_unique_important_ = self.backlog_unique_
end
self:backlog_update_()
end
function window_i:backlog_push_str(str, important)
local collect = {}
local cps = utf8.code_points(str)
if cps then
for i = 1, #cps do
table.insert(collect, str:sub(cps[i].pos, cps[i].pos + cps[i].size - 1))
end
self:backlog_push_(collect, important)
end
end
function window_i:backlog_bump_marker()
self.backlog_enable_marker_ = false
if self.backlog_last_seen_ < self.backlog_unique_ then
self.backlog_enable_marker_ = true
self.backlog_marker_at_ = self.backlog_last_seen_
end
self:backlog_update_()
end
function window_i:backlog_notif_reset()
self.backlog_last_seen_ = self.backlog_unique_
self:backlog_bump_marker()
end
function window_i:backlog_notif_count()
return self.backlog_unique_ - self.backlog_last_seen_
end
function window_i:backlog_notif_important()
return self.backlog_unique_important_ - self.backlog_last_seen_ > 0
end
function window_i:backlog_reset()
self.backlog_unique_ = 0
self.backlog_unique_important_ = 0
self.backlog_last_wrapped_ = 0
self.backlog_last_seen_ = 0
self.backlog_marker_at_ = 0
self.backlog_last_ = { wrapped = {}, unique = 0 }
self.backlog_first_ = { wrapped = {} }
self.backlog_last_.prev = self.backlog_first_
self.backlog_first_.next = self.backlog_last_
self.backlog_last_visible_msg_ = self.backlog_first_
self.backlog_last_visible_line_ = 0
self.backlog_auto_scroll_ = true
self.backlog_enable_marker_ = false
self:backlog_update_()
end
local close_button_off_x = -12
local close_button_off_y = 3
if tpt.version.jacob1s_mod then
close_button_off_x = -11
close_button_off_y = 4
end
function window_i:tick_close_()
local border_colour = colours.appearance.inactive.border
local close_fg = colours.appearance.inactive.text
local close_bg
local inside_close = util.inside_rect(self.pos_x_ + self.width_ - 15, self.pos_y_, 15, 15, util.mouse_pos())
if self.close_active_ then
close_fg = colours.appearance.active.text
close_bg = colours.appearance.active.background
elseif inside_close then
close_fg = colours.appearance.hover.text
close_bg = colours.appearance.hover.background
end
if close_bg then
gfx.fillRect(self.pos_x_ + self.width_ - 14, self.pos_y_ + 1, 13, 13, unpack(close_bg))
end
gfx.drawLine(self.pos_x_ + self.width_ - 15, self.pos_y_ + 1, self.pos_x_ + self.width_ - 15, self.pos_y_ + 13, unpack(border_colour))
gfx.drawText(self.pos_x_ + self.width_ + close_button_off_x, self.pos_y_ + close_button_off_y, utf8.encode_multiple(0xE02A), unpack(close_fg))
if self.close_active_ and not inside_close then
self.close_active_ = false
end
end
function window_i:handle_tick()
local floating = self.window_status_func_() == "floating"
local now = socket.gettime()
if self.backlog_auto_scroll_ and not floating then
self.backlog_last_seen_ = self.backlog_last_wrapped_
else
if self.backlog_last_seen_ < self.backlog_unique_ and not self.backlog_enable_marker_ then
self:backlog_bump_marker()
end
end
if self.resizer_active_ then
local resizer_x, resizer_y = util.mouse_pos()
local prev_x, prev_y = self.pos_x_, self.pos_y_
self.pos_x_ = math.min(math.max(1, self.pos_x_ + resizer_x - self.resizer_last_x_), self.pos_x_ + self.width_ - config.min_width)
self.pos_y_ = math.min(math.max(1, self.pos_y_ + resizer_y - self.resizer_last_y_), self.pos_y_ + self.height_ - config.min_height)
local diff_x, diff_y = self.pos_x_ - prev_x, self.pos_y_ - prev_y
self.resizer_last_x_ = self.resizer_last_x_ + diff_x
self.resizer_last_y_ = self.resizer_last_y_ + diff_y
self.width_ = self.width_ - diff_x
self.height_ = self.height_ - diff_y
self:input_update_()
self:backlog_update_()
self:subtitle_update_()
self:save_window_rect_()
end
if self.dragger_active_ then
local dragger_x, dragger_y = util.mouse_pos()
local prev_x, prev_y = self.pos_x_, self.pos_y_
self.pos_x_ = math.min(math.max(1, self.pos_x_ + dragger_x - self.dragger_last_x_), sim.XRES - self.width_)
self.pos_y_ = math.min(math.max(1, self.pos_y_ + dragger_y - self.dragger_last_y_), sim.YRES - self.height_)
local diff_x, diff_y = self.pos_x_ - prev_x, self.pos_y_ - prev_y
self.dragger_last_x_ = self.dragger_last_x_ + diff_x
self.dragger_last_y_ = self.dragger_last_y_ + diff_y
self:save_window_rect_()
end
local border_colour = colours.appearance[self.in_focus and "active" or "inactive"].border
local background_colour = colours.appearance.inactive.background
if not floating then
gfx.fillRect(self.pos_x_ + 1, self.pos_y_ + 1, self.width_ - 2, self.height_ - 2, background_colour[1], background_colour[2], background_colour[3], self.alpha_)
gfx.drawRect(self.pos_x_, self.pos_y_, self.width_, self.height_, unpack(border_colour))
self:tick_close_()
local subtitle_blue = 255
if #self.input_collect_ > 0 and self.input_last_say_ + config.message_interval >= now then
subtitle_blue = 0
end
gfx.drawText(self.pos_x_ + 18, self.pos_y_ + 4, self.subtitle_text_, 255, 255, subtitle_blue)
gfx.drawText(self.pos_x_ + self.width_ - self.title_width_ - 17, self.pos_y_ + 4, self.title_)
for i = 1, 3 do
gfx.drawLine(self.pos_x_ + i * 3 + 1, self.pos_y_ + 3, self.pos_x_ + 3, self.pos_y_ + i * 3 + 1, unpack(border_colour))
end
gfx.drawLine(self.pos_x_ + 1, self.pos_y_ + 14, self.pos_x_ + self.width_ - 2, self.pos_y_ + 14, unpack(border_colour))
gfx.drawLine(self.pos_x_ + 14, self.pos_y_ + 1, self.pos_x_ + 14, self.pos_y_ + 13, unpack(border_colour))
end
local prev_text, prev_fades_at, prev_alpha, prev_box_width, prev_highlight
for i = 1, #self.backlog_text_ + 1 do
local fades_at, alpha, box_width, highlight
if self.backlog_text_[i] then
fades_at = self.backlog_text_[i].pushed_at + config.floating_linger_time + config.floating_fade_time
alpha = math.max(0, math.min(1, (fades_at - now) / config.floating_fade_time))
box_width = self.backlog_text_[i].box_width
highlight = self.backlog_text_[i].highlight
end
if not prev_fades_at then
prev_fades_at, prev_alpha, prev_box_width, prev_highlight = fades_at, alpha, box_width, highlight
elseif not fades_at then
fades_at, alpha, box_width, highlight = prev_fades_at, prev_alpha, prev_box_width, prev_highlight
end
local comm_box_width = math.max(box_width, prev_box_width)
local min_box_width = math.min(box_width, prev_box_width)
local comm_fades_at = math.max(fades_at, prev_fades_at)
local comm_alpha = math.max(alpha, prev_alpha)
local comm_highlight = highlight or prev_highlight
local diff_fades_at = prev_fades_at
local diff_alpha = prev_alpha
local diff_highlight = prev_highlight
if box_width > prev_box_width then
diff_fades_at = fades_at
diff_alpha = alpha
diff_highlight = highlight
end
if floating and diff_fades_at > now then
local rgb = diff_highlight and text_bg_high_floating or text_bg
gfx.fillRect(self.pos_x_ - 1 + min_box_width, self.pos_y_ + self.backlog_text_y_ + i * 12 - 15, comm_box_width - min_box_width, 2, rgb[1], rgb[2], rgb[3], diff_alpha * self.alpha_)
end
if floating and comm_fades_at > now then
local rgb = comm_highlight and text_bg_high_floating or text_bg
local alpha = 1
if not highlight and prev_alpha < comm_alpha then
alpha = prev_alpha
end
gfx.fillRect(self.pos_x_ - 1, self.pos_y_ + self.backlog_text_y_ + i * 12 - 15, min_box_width, 2, alpha * rgb[1], alpha * rgb[2], alpha * rgb[3], comm_alpha * self.alpha_)
end
if prev_text then
local alpha = 1
if floating then
alpha = math.min(1, (prev_fades_at - now) / config.floating_fade_time)
end
if floating and prev_fades_at > now then
local rgb = prev_highlight and text_bg_high_floating or text_bg
gfx.fillRect(self.pos_x_ - 1, self.pos_y_ + self.backlog_text_y_ + i * 12 - 25, prev_box_width, 10, rgb[1], rgb[2], rgb[3], alpha * self.alpha_)
end
if not floating and prev_highlight then
gfx.fillRect(self.pos_x_ + 1, self.pos_y_ + self.backlog_text_y_ + i * 12 - 26, self.width_ - 2, 12, text_bg_high[1], text_bg_high[2], text_bg_high[3], alpha * self.alpha_)
end
if not floating or prev_fades_at > now then
gfx.drawText(self.pos_x_ + 4 + prev_text.padding, self.pos_y_ + self.backlog_text_y_ + i * 12 - 24, prev_text.text, 255, 255, 255, alpha * 255)
end
end
prev_text, prev_alpha, prev_fades_at, prev_box_width, prev_highlight = self.backlog_text_[i], alpha, fades_at, box_width, highlight
end
if not floating then
if self.backlog_marker_y_ then
gfx.drawLine(self.pos_x_ + 1, self.pos_y_ + self.backlog_marker_y_, self.pos_x_ + self.width_ - 2, self.pos_y_ + self.backlog_marker_y_, unpack(notif_important))
end
gfx.drawLine(self.pos_x_ + 1, self.pos_y_ + self.height_ - 15, self.pos_x_ + self.width_ - 2, self.pos_y_ + self.height_ - 15, unpack(border_colour))
if self.input_has_selection_ then
gfx.fillRect(self.pos_x_ + self.input_sel_low_x_ + self.input_scroll_x_, self.pos_y_ + self.height_ - 13, self.input_sel_high_x_ - self.input_sel_low_x_, 11)
end
gfx.drawText(self.pos_x_ + 4 + self.input_text_1x_, self.pos_y_ + self.height_ - 11, self.input_text_1_)
gfx.drawText(self.pos_x_ + 4 + self.input_text_2x_, self.pos_y_ + self.height_ - 11, self.input_text_2_, 0, 0, 0)
gfx.drawText(self.pos_x_ + 4 + self.input_text_3x_, self.pos_y_ + self.height_ - 11, self.input_text_3_)
if self.in_focus and now % 1 < 0.5 then
gfx.drawLine(self.pos_x_ + self.input_cursor_x_ + self.input_scroll_x_, self.pos_y_ + self.height_ - 13, self.pos_x_ + self.input_cursor_x_ + self.input_scroll_x_, self.pos_y_ + self.height_ - 3)
end
end
end
function window_i:handle_mousedown(px, py, button)
if self.should_ignore_mouse_func_() then
return
end
-- * TODO[opt]: mouse selection
if button == ui.SDL_BUTTON_LEFT then
if util.inside_rect(self.pos_x_, self.pos_y_, self.width_, self.height_, util.mouse_pos()) then
self.in_focus = true
end
if util.inside_rect(self.pos_x_, self.pos_y_, 15, 15, util.mouse_pos()) then
self.resizer_active_ = true
self.resizer_last_x_, self.resizer_last_y_ = util.mouse_pos()
return true
end
if util.inside_rect(self.pos_x_ + 15, self.pos_y_, self.width_ - 30, 15, util.mouse_pos()) then
self.dragger_active_ = true
self.dragger_last_x_, self.dragger_last_y_ = util.mouse_pos()
return true
end
if util.inside_rect(self.pos_x_ + self.width_ - 15, self.pos_y_, 15, 15, util.mouse_pos()) then
self.close_active_ = true
return true
end
elseif button == ui.SDL_BUTTON_RIGHT then
if util.inside_rect(self.pos_x_ + 1, self.pos_y_ + 15, self.width_ - 2, self.height_ - 30, util.mouse_pos()) then
local _, y = util.mouse_pos()
local line = 1 + math.floor((y - self.backlog_text_y_ - self.pos_y_) / 12)
if self.backlog_lines_[line] then
local collect = self.backlog_lines_[line].msg.collect
local collect_sane = {}
local i = 0
while i < #collect do
i = i + 1
if collect[i] == "\15" then
i = i + 3
elseif collect[i]:byte() >= 32 then
table.insert(collect_sane, collect[i])
end
end
plat.clipboardPaste(table.concat(collect_sane))
self.log_event_func_("Message copied to clipboard")
end
return true
end
end
if util.inside_rect(self.pos_x_, self.pos_y_, self.width_, self.height_, util.mouse_pos()) then
return true
elseif self.in_focus then
self.in_focus = false
end
end
function window_i:handle_mouseup(px, py, button)
if button == ui.SDL_BUTTON_LEFT then
if self.close_active_ then
self.hide_window_func_()
end
self.resizer_active_ = false
self.dragger_active_ = false
self.close_active_ = false
end
end
function window_i:handle_mousewheel(px, py, dir)
if util.inside_rect(self.pos_x_, self.pos_y_ + 15, self.width_, self.height_ - 30, util.mouse_pos()) then
self:backlog_wrap_(self.backlog_last_visible_msg_)
while dir > 0 do
if self.backlog_last_visible_line_ > 1 then
self.backlog_last_visible_line_ = self.backlog_last_visible_line_ - 1
self.backlog_auto_scroll_ = false
elseif self.backlog_last_visible_msg_ ~= self.backlog_first_ then
self.backlog_last_visible_msg_ = self.backlog_last_visible_msg_.prev
self:backlog_wrap_(self.backlog_last_visible_msg_)
self.backlog_last_visible_line_ = #self.backlog_last_visible_msg_.wrapped
self.backlog_auto_scroll_ = false
end
dir = dir - 1
end
while dir < 0 do
if self.backlog_last_visible_line_ < #self.backlog_last_visible_msg_.wrapped then
self.backlog_last_visible_line_ = self.backlog_last_visible_line_ + 1
elseif self.backlog_last_visible_msg_.next ~= self.backlog_last_ then
self.backlog_last_visible_msg_ = self.backlog_last_visible_msg_.next
self.backlog_last_visible_line_ = 1
end
self:backlog_wrap_(self.backlog_last_visible_msg_)
if self.backlog_last_visible_msg_.next == self.backlog_last_ and self.backlog_last_visible_line_ == #self.backlog_last_visible_msg_.wrapped then
self.backlog_auto_scroll_ = true
end
dir = dir + 1
end
self:backlog_update_()
return true
end
if util.inside_rect(self.pos_x_, self.pos_y_, self.width_, self.height_, util.mouse_pos()) then
return true
end
end
local modkey_scan = {
[ ui.SDL_SCANCODE_LCTRL ] = true,
[ ui.SDL_SCANCODE_LSHIFT ] = true,
[ ui.SDL_SCANCODE_LALT ] = true,
[ ui.SDL_SCANCODE_RCTRL ] = true,
[ ui.SDL_SCANCODE_RSHIFT ] = true,
[ ui.SDL_SCANCODE_RALT ] = true,
}
function window_i:handle_keypress(key, scan, rep, shift, ctrl, alt)
if not self.in_focus and self.window_status_func_() == "shown" and scan == ui.SDL_SCANCODE_RETURN then
self.in_focus = true
return true
end
if self.in_focus then
if not ctrl and not alt and scan == ui.SDL_SCANCODE_ESCAPE then
if self.in_focus then
self.in_focus = false
self.input_autocomplete_ = nil
local force_hide = false
if self.hide_when_chat_done then
self.hide_when_chat_done = false
force_hide = true
self:input_reset_()
end
if shift or force_hide then
self.hide_window_func_()
end
else
self.in_focus = true
end
elseif not ctrl and not shift and not alt and scan == ui.SDL_SCANCODE_TAB then
local left_word_first, left_word
local cursor = self.input_cursor_
local check_offset = 0
while self.input_collect_[cursor + check_offset] and not self.input_collect_[cursor + check_offset]:find(config.whitespace_pattern) do
check_offset = check_offset - 1
end
if check_offset < 0 then
left_word_first = cursor + check_offset + 1
left_word = table.concat(self.input_collect_, "", left_word_first, cursor)
end
local cli = self.client_func_()
if left_word and cli then
left_word = left_word:lower()
if self.input_autocomplete_ and not left_word:find("^" .. util.escape_regex(self.input_autocomplete_)) then
self.input_autocomplete_ = nil
end
if not self.input_autocomplete_ then
self.input_autocomplete_ = left_word
end
local nicks = {}
local function try_complete(nick)
if nick:lower():find("^" .. util.escape_regex(self.input_autocomplete_)) then
table.insert(nicks, nick)
end
end
try_complete(cli:nick())
for _, member in pairs(cli.id_to_member) do
try_complete(member.nick)
end
if next(nicks) then
table.sort(nicks)
local index = 1
for i = 1, #nicks do
if nicks[i]:lower() == left_word and nicks[i + 1] then
index = i + 1
end
end
self.input_sel_first_ = left_word_first - 1
self.input_sel_second_ = cursor
self:input_update_()
self:input_insert_(nicks[index])
end
else
self.input_autocomplete_ = nil
end
elseif not shift and not alt and (scan == ui.SDL_SCANCODE_BACKSPACE or scan == ui.SDL_SCANCODE_DELETE) then
local start, length
if self.input_has_selection_ then
start = self.input_sel_low_
length = self.input_sel_high_ - self.input_sel_low_
self.input_cursor_ = self.input_sel_low_
elseif (scan == ui.SDL_SCANCODE_BACKSPACE and self.input_cursor_ > 0) or (scan == ui.SDL_SCANCODE_DELETE and self.input_cursor_ < #self.input_collect_) then
if ctrl then
local cursor_step = scan == ui.SDL_SCANCODE_DELETE and 1 or -1
local check_offset = scan == ui.SDL_SCANCODE_DELETE and 1 or 0
local cursor = self.input_cursor_
while self.input_collect_[cursor + check_offset] and self.input_collect_[cursor + check_offset]:find(config.whitespace_pattern) do
cursor = cursor + cursor_step
end
while self.input_collect_[cursor + check_offset] and self.input_collect_[cursor + check_offset]:find(config.word_pattern) do
cursor = cursor + cursor_step
end
if cursor == self.input_cursor_ then
cursor = cursor + cursor_step
end
start = self.input_cursor_
length = cursor - self.input_cursor_
if length < 0 then
start = start + length
length = -length
end
self.input_cursor_ = start
else
if scan == ui.SDL_SCANCODE_BACKSPACE then
self.input_cursor_ = self.input_cursor_ - 1
end
start = self.input_cursor_
length = 1
end
end
if start then
self:input_remove_(start, length)
self:input_update_()
end
self.input_autocomplete_ = nil
elseif not ctrl and not shift and not alt and scan == ui.SDL_SCANCODE_RETURN then
if #self.input_collect_ > 0 then
local str = self:input_text_to_send_()
local sent = str ~= "" and not self.message_overlong_
if sent then
local cli = self.client_func_()
if self.localcmd and self.localcmd:parse(str) then
-- * Nothing.
elseif cli then
local cps = utf8.code_points(str)
local last = 0
for i = 1, #cps do
local new_last = cps[i].pos + cps[i].size - 1
if new_last > config.message_size then
break
end
last = new_last
end
local now = socket.gettime()
if self.input_last_say_ + config.message_interval >= now then
sent = false
else
self.input_last_say_ = now
local limited_str = str:sub(1, last)
self:backlog_push_say(cli:formatted_nick(), limited_str:gsub("^//", "/"))
cli:send_say(limited_str)
end
else
self:backlog_push_error("Not connected, message not sent")
end
end
if sent then
self.input_history_[self.input_history_next_] = self.input_editing_[self.input_history_select_]
self.input_history_next_ = self.input_history_next_ + 1
self.input_history_[self.input_history_next_] = {}
self.input_history_[self.input_history_next_ - config.history_size] = nil
self:input_reset_()
if self.hide_when_chat_done then
self.hide_when_chat_done = false
self.in_focus = false
self.hide_window_func_()
end
end
else
self.in_focus = false
end
self.input_autocomplete_ = nil
elseif not ctrl and not shift and not alt and scan == ui.SDL_SCANCODE_UP then
local to_select = self.input_history_select_ - 1
if self.input_history_[to_select] then
self:input_select_(to_select)
end
self.input_autocomplete_ = nil
elseif not ctrl and not shift and not alt and scan == ui.SDL_SCANCODE_DOWN then
local to_select = self.input_history_select_ + 1
if self.input_history_[to_select] then
self:input_select_(to_select)
end
self.input_autocomplete_ = nil
elseif not alt and (scan == ui.SDL_SCANCODE_HOME or scan == ui.SDL_SCANCODE_END or scan == ui.SDL_SCANCODE_RIGHT or scan == ui.SDL_SCANCODE_LEFT) then
self.input_cursor_prev_ = self.input_cursor_
if scan == ui.SDL_SCANCODE_HOME then
self.input_cursor_ = 0
elseif scan == ui.SDL_SCANCODE_END then
self.input_cursor_ = #self.input_collect_
else
if (scan == ui.SDL_SCANCODE_RIGHT and self.input_cursor_ < #self.input_collect_) or (scan == ui.SDL_SCANCODE_LEFT and self.input_cursor_ > 0) then
local cursor_step = scan == ui.SDL_SCANCODE_RIGHT and 1 or -1
local check_offset = scan == ui.SDL_SCANCODE_RIGHT and 1 or 0
if ctrl then
local cursor = self.input_cursor_
while self.input_collect_[cursor + check_offset] and self.input_collect_[cursor + check_offset]:find(config.whitespace_pattern) do
cursor = cursor + cursor_step
end
while self.input_collect_[cursor + check_offset] and self.input_collect_[cursor + check_offset]:find(config.word_pattern) do
cursor = cursor + cursor_step
end
if cursor == self.input_cursor_ then
cursor = cursor + cursor_step
end
self.input_cursor_ = cursor
else
self.input_cursor_ = self.input_cursor_ + cursor_step
end
end
end
if shift then
if self.input_sel_first_ == self.input_sel_second_ then
self.input_sel_first_ = self.input_cursor_prev_
end
else
self.input_sel_first_ = self.input_cursor_
end
self.input_sel_second_ = self.input_cursor_
self:input_update_()
self.input_autocomplete_ = nil
elseif ctrl and not shift and not alt and scan == ui.SDL_SCANCODE_A then
self.input_cursor_ = #self.input_collect_
self.input_sel_first_ = 0
self.input_sel_second_ = self.input_cursor_
self:input_update_()
self.input_autocomplete_ = nil
elseif ctrl and not shift and not alt and scan == ui.SDL_SCANCODE_C then
if self.input_has_selection_ then
plat.clipboardPaste(self:input_collect_range_(self.input_sel_low_ + 1, self.input_sel_high_))
end
self.input_autocomplete_ = nil
elseif ctrl and not shift and not alt and scan == ui.SDL_SCANCODE_V then
local text = plat.clipboardCopy()
if text then
self:input_insert_(text)
end
self.input_autocomplete_ = nil
elseif ctrl and not shift and not alt and scan == ui.SDL_SCANCODE_X then
if self.input_has_selection_ then
local start = self.input_sel_low_
local length = self.input_sel_high_ - self.input_sel_low_
self.input_cursor_ = self.input_sel_low_
plat.clipboardPaste(self:input_collect_range_(self.input_sel_low_ + 1, self.input_sel_high_))
self:input_remove_(start, length)
self:input_update_()
end
self.input_autocomplete_ = nil
end
return not modkey_scan[scan]
else
if not ctrl and not alt and scan == ui.SDL_SCANCODE_ESCAPE then
self.hide_window_func_()
return true
end
end
end
function window_i:handle_keyrelease(key, scan, rep, shift, ctrl, alt)
if self.in_focus then
return not modkey_scan[scan]
end
end
function window_i:handle_textinput(text)
if self.in_focus then
self:input_insert_(text)
self.input_autocomplete_ = nil
return true
end
end
function window_i:handle_textediting(text)
if self.in_focus then
return true
end
end
function window_i:handle_blur()
end
function window_i:save_window_rect_()
manager.set("windowLeft", tostring(self.pos_x_))
manager.set("windowTop", tostring(self.pos_y_))
manager.set("windowWidth", tostring(self.width_))
manager.set("windowHeight", tostring(self.height_))
manager.set("windowAlpha", tostring(self.alpha_))
end
function window_i:insert_wrapped_line_(tbl, msg, line)
table.insert(tbl, {
wrapped = msg.wrapped[line],
needs_padding = line > 1,
extend_box = line < #msg.wrapped,
msg = msg,
marker = self.backlog_marker_at_ == msg.unique and #msg.wrapped == line,
})
end
local function set_size_clamp(new_width, new_height, new_pos_x, new_pos_y)
local width = math.min(math.max(new_width, config.min_width), sim.XRES - 1)
local height = math.min(math.max(new_height, config.min_height), sim.YRES - 1)
local pos_x = math.min(math.max(1, new_pos_x), sim.XRES - width)
local pos_y = math.min(math.max(1, new_pos_y), sim.YRES - height)
return width, height, pos_x, pos_y
end
function window_i:set_size(new_width, new_height)
self.width_, self.height_, self.pos_x_, self.pos_y_ = set_size_clamp(new_width, new_height, self.pos_x_, self.pos_y_)
self:input_update_()
self:backlog_update_()
self:subtitle_update_()
self:save_window_rect_()
end
function window_i:subtitle_update_()
self.subtitle_text_ = self.subtitle_secondary_ or self.subtitle_ or ""
local max_width = self.width_ - self.title_width_ - 43
if gfx.textSize(self.subtitle_text_) > max_width then
self.subtitle_text_ = self.subtitle_text_:sub(1, util.binary_search_implicit(1, #self.subtitle_text_, function(idx)
local str = self.subtitle_text_:sub(1, idx)
str = str:gsub("\15[\194\195].", "\15"):gsub("\15[^\128-\255]", "\15")
str = str:gsub("\15[\194\195].", "\15"):gsub("\15[^\128-\255]", "\15")
str = str:gsub("\15[\194\195].", "\15"):gsub("\15[^\128-\255]", "\15")
str = str:gsub("\15", "")
return gfx.textSize(str .. "...") > max_width
end) - 1) .. "..."
end
end
function window_i:input_select_(history_index)
self.input_history_select_ = history_index
local editing = self.input_editing_[history_index]
if not editing then
editing = {}
local original = self.input_history_[history_index]
for i = 1, #original do
editing[i] = original[i]
end
self.input_editing_[history_index] = editing
end
self.input_collect_ = editing
self.input_cursor_ = #self.input_collect_
self.input_sel_first_ = self.input_cursor_
self.input_sel_second_ = self.input_cursor_
self:input_update_()
end
function window_i:input_reset_()
self.input_editing_ = {}
self:input_select_(self.input_history_next_)
end
function window_i:input_remove_(start, length)
for i = start + 1, #self.input_collect_ - length do
self.input_collect_[i] = self.input_collect_[i + length]
end
for i = #self.input_collect_, #self.input_collect_ - length + 1, -1 do
self.input_collect_[i] = nil
end
self.input_sel_first_ = self.input_cursor_
self.input_sel_second_ = self.input_cursor_
end
function window_i:input_insert_(text)
local cps = {}
local unfiltered_cps = utf8.code_points(text)
if unfiltered_cps then
for i = 1, #unfiltered_cps do
if unfiltered_cps[i].cp >= 32 then
table.insert(cps, unfiltered_cps[i])
end
end
end
if #cps > 0 then
if self.input_has_selection_ then
local start = self.input_sel_low_
local length = self.input_sel_high_ - self.input_sel_low_
self.input_cursor_ = self.input_sel_low_
self:input_remove_(start, length)
end
for i = #self.input_collect_, self.input_cursor_ + 1, -1 do
self.input_collect_[i + #cps] = self.input_collect_[i]
end
for i = 1, #cps do
self.input_collect_[self.input_cursor_ + i] = text:sub(cps[i].pos, cps[i].pos + cps[i].size - 1)
end
self.input_cursor_ = self.input_cursor_ + #cps
self:input_update_()
end
end
function window_i:input_clamp_text_(start, first, last)
local shave_off_left = -start
local shave_off_right = gfx.textSize(self:input_collect_range_(first, last)) + start - self.width_ + 10
local new_first = util.binary_search_implicit(first, last, function(pos)
return gfx.textSize(self:input_collect_range_(first, pos - 1)) >= shave_off_left
end)
local new_last = util.binary_search_implicit(first, last, function(pos)
return gfx.textSize(self:input_collect_range_(pos, last)) < shave_off_right
end) - 1
local new_start = start + gfx.textSize(self:input_collect_range_(first, new_first - 1))
return new_start, self:input_collect_range_(new_first, new_last)
end
function window_i:input_update_()
self.input_sel_low_ = math.min(self.input_sel_first_, self.input_sel_second_)
self.input_sel_high_ = math.max(self.input_sel_first_, self.input_sel_second_)
self.input_text_1_ = self:input_collect_range_(1, self.input_sel_low_)
self.input_text_1w_ = gfx.textSize(self.input_text_1_)
self.input_text_2_ = self:input_collect_range_(self.input_sel_low_ + 1, self.input_sel_high_)
self.input_text_2w_ = gfx.textSize(self.input_text_2_)
self.input_text_3_ = self:input_collect_range_(self.input_sel_high_ + 1, #self.input_collect_)
self.input_text_3w_ = gfx.textSize(self.input_text_3_)
self.input_cursor_x_ = 4 + gfx.textSize(self:input_collect_range_(1, self.input_cursor_))
self.input_sel_low_x_ = 3 + self.input_text_1w_
self.input_sel_high_x_ = self.input_sel_low_x_ + 1 + self.input_text_2w_
self.input_has_selection_ = self.input_sel_first_ ~= self.input_sel_second_
local min_cursor_x = 4
local max_cursor_x = self.width_ - 5
if self.input_cursor_x_ + self.input_scroll_x_ < min_cursor_x then
self.input_scroll_x_ = min_cursor_x - self.input_cursor_x_
end
if self.input_cursor_x_ + self.input_scroll_x_ > max_cursor_x then
self.input_scroll_x_ = max_cursor_x - self.input_cursor_x_
end
local min_if_active = self.width_ - self.input_text_1w_ - self.input_text_2w_ - self.input_text_3w_ - 9
if self.input_scroll_x_ < 0 and self.input_scroll_x_ < min_if_active then
self.input_scroll_x_ = min_if_active
end
if min_if_active > 0 then
self.input_scroll_x_ = 0
end
if self.input_sel_low_x_ < 1 - self.input_scroll_x_ then
self.input_sel_low_x_ = 1 - self.input_scroll_x_
end
if self.input_sel_high_x_ > self.width_ - self.input_scroll_x_ - 1 then
self.input_sel_high_x_ = self.width_ - self.input_scroll_x_ - 1
end
self.input_text_1x_ = self.input_scroll_x_
self.input_text_2x_ = self.input_text_1x_ + self.input_text_1w_
self.input_text_3x_ = self.input_text_2x_ + self.input_text_2w_
self.input_text_1x_, self.input_text_1_ = self:input_clamp_text_(self.input_text_1x_, 1, self.input_sel_low_)
self.input_text_2x_, self.input_text_2_ = self:input_clamp_text_(self.input_text_2x_, self.input_sel_low_ + 1, self.input_sel_high_)
self.input_text_3x_, self.input_text_3_ = self:input_clamp_text_(self.input_text_3x_, self.input_sel_high_ + 1, #self.input_collect_)
self:set_subtitle_secondary(self:input_status_())
end
function window_i:input_text_to_send_()
return self:input_collect_range_():gsub("[\1-\31]", ""):gsub("^ *(.-) *$", "%1")
end
function window_i:input_status_()
if #self.input_collect_ == 0 then
return
end
local str = self:input_text_to_send_()
local max_size = config.message_size
if str:find("^/") and not str:find("^//") then
max_size = 255
end
local byte_length = #str
local bytes_left = max_size - byte_length
if bytes_left < 0 then
self.message_overlong_ = true
return colours.commonstr.error .. tostring(bytes_left)
else
self.message_overlong_ = nil
return tostring(bytes_left)
end
end
function window_i:input_collect_range_(first, last)
return table.concat(self.input_collect_, nil, first, last)
end
function window_i:set_subtitle(template, text)
if template == "status" then
self.subtitle_ = colours.commonstr.status .. text
elseif template == "room" then
self.subtitle_ = "In " .. format.troom(text)
end
self:subtitle_update_()
end
function window_i:alpha(alpha)
if not alpha then
return self.alpha_
end
self.alpha_ = alpha
end
function window_i:set_subtitle_secondary(formatted_text)
self.subtitle_secondary_ = formatted_text
self:subtitle_update_()
end
local function new(params)
local width, height, pos_x, pos_y = set_size_clamp(
tonumber(manager.get("windowWidth", "")) or config.default_width,
tonumber(manager.get("windowHeight", "")) or config.default_height,
tonumber(manager.get("windowLeft", "")) or config.default_x,
tonumber(manager.get("windowTop", "")) or config.default_y
)
local alpha = tonumber(manager.get("windowAlpha", "")) or config.default_alpha
local title = "TPT Multiplayer " .. config.versionstr
local title_width = gfx.textSize(title)
local win = setmetatable({
in_focus = false,
pos_x_ = pos_x,
pos_y_ = pos_y,
width_ = width,
height_ = height,
alpha_ = alpha,
title_ = title,
title_width_ = title_width,
input_scroll_x_ = 0,
resizer_active_ = false,
dragger_active_ = false,
close_active_ = false,
window_status_func_ = params.window_status_func,
log_event_func_ = params.log_event_func,
client_func_ = params.client_func,
hide_window_func_ = params.hide_window_func,
should_ignore_mouse_func_ = params.should_ignore_mouse_func,
input_history_ = { {} },
input_history_next_ = 1,
input_editing_ = {},
input_last_say_ = 0,
nick_colour_seed_ = 0,
hide_when_chat_done = false,
}, window_m)
win:input_reset_()
win:backlog_reset()
return win
end
return {
new = new,
}
end
require_preload__["tptmp.common.buffer_list"] = function()
local buffer_list_i = {}
local buffer_list_m = { __index = buffer_list_i }
function buffer_list_i:push(data)
local count = #data
local want = count
if self.limit then
want = math.min(want, self.limit - self:pending())
end
if want > 0 then
local buf = {
data = data,
curr = 0,
last = want,
prev = self.last_.prev,
next = self.last_,
}
self.last_.prev.next = buf
self.last_.prev = buf
self.pushed_ = self.pushed_ + want
end
return want, count
end
function buffer_list_i:next()
local buf = self.first_.next
if buf == self.last_ then
return
end
return buf.data, buf.curr + 1, buf.last
end
function buffer_list_i:pop(count)
local buf = self.first_.next
assert(buf ~= self.last_)
assert(buf.last - buf.curr >= count)
buf.curr = buf.curr + count
if buf.curr == buf.last then
buf.prev.next = buf.next
buf.next.prev = buf.prev
end
self.popped_ = self.popped_ + count
end
function buffer_list_i:pushed()
return self.pushed_
end
function buffer_list_i:popped()
return self.popped_
end
function buffer_list_i:pending()
return self.pushed_ - self.popped_
end
function buffer_list_i:get(count)
assert(count <= self.pushed_ - self.popped_)
local collect = {}
while count > 0 do
local data, first, last = self:next()
local want = math.min(count, last - first + 1)
local want_last = first - 1 + want
table.insert(collect, first == 1 and want_last == #data and data or data:sub(first, want_last))
self:pop(want)
count = count - want
end
return table.concat(collect)
end
local function new(params)
local bl = setmetatable({
first_ = {},
last_ = {},
limit = params.limit,
pushed_ = 0,
popped_ = 0,
}, buffer_list_m)
bl.first_.next = bl.last_
bl.last_.prev = bl.first_
return bl
end
return {
new = new,
}
end
require_preload__["tptmp.common.command_parser"] = function()
local command_parser_i = {}
local command_parser_m = { __index = command_parser_i }
function command_parser_i:parse(ctx, message)
local words = {}
local offsets = {}
for offset, word in message:gmatch("()(%S+)") do
table.insert(offsets, offset)
table.insert(words, word)
end
if not words[1] then
self:list_(ctx)
return
end
local initial_cmd = words[1]
words[1] = words[1]:lower()
while true do
local cmd = self.commands_[self.aliases_[words[1]] or words[1]]
if not cmd then
if self.cmd_fallback_ then
if self.cmd_fallback_(ctx, message) then
return
end
end
self.respond_(ctx, self.unknown_format_)
return
end
if cmd.macro then
words = cmd.macro(ctx, message, words, offsets)
if not words then
self:help_(ctx, initial_cmd)
return
end
if #words == 0 then
return
end
words[1] = words[1]:lower()
offsets = {}
local offset = 0
for i = 1, #words do
offsets[i] = offset + 1
offset = offset + #words[i] + 1
end
message = table.concat(words, " ")
else
local ok = cmd.func(ctx, message, words, offsets)
if not ok then
self:help_(ctx, initial_cmd)
end
return
end
end
end
function command_parser_i:list_(ctx)
self.respond_(ctx, self.list_format_:format(self.list_str_))
if self.list_extra_ then
self.list_extra_(ctx)
end
return true
end
function command_parser_i:help_(ctx, from)
from = from or self.help_name_
local initial_from = from
from = from:lower()
local to = self.aliases_[from]
if to then
self.respond_(ctx, self.alias_format_:format(from, to))
from = to
end
local cmd = self.commands_[from]
if cmd then
self.respond_(ctx, self.help_format_:format(cmd.help))
return true
end
if self.help_fallback_ then
if self.help_fallback_(ctx, initial_from) then
return true
end
end
self.respond_(ctx, self.unknown_format_)
return true
end
local function new(params)
local cmd = setmetatable({
respond_ = params.respond,
help_fallback_ = params.help_fallback,
list_extra_ = params.list_extra,
help_format_ = params.help_format,
alias_format_ = params.alias_format,
list_format_ = params.list_format,
unknown_format_ = params.unknown_format,
cmd_fallback_ = params.cmd_fallback,
commands_ = {},
aliases_ = {},
}, command_parser_m)
local collect = {}
for name, info in pairs(params.commands) do
if not info.hidden then
table.insert(collect, "/" .. name)
end
name = name:lower()
if info.role == "help" then
cmd.help_name_ = name
cmd.commands_[name] = {
func = function(ctx, _, words)
cmd:help_(ctx, words[2])
return true
end,
help = info.help,
}
elseif info.role == "list" then
cmd.commands_[name] = {
func = function(ctx)
cmd:list_(ctx)
return true
end,
help = info.help,
}
elseif info.alias then
cmd.aliases_[name] = info.alias
elseif info.macro then
cmd.commands_[name] = {
macro = info.macro,
help = info.help,
}
else
cmd.commands_[name] = {
func = info.func,
help = info.help,
}
end
end
table.sort(collect)
cmd.list_str_ = table.concat(collect, " ")
return cmd
end
return {
new = new,
}
end
require_preload__["tptmp.common.config"] = function()
return {
-- ***********************************************************************
-- *** The following options apply to both the server and the clients. ***
-- *** Handle with care; changing options here means having to update ***
-- *** the client you ship. ***
-- ***********************************************************************
-- * Protocol version, between 0 and 254. 255 is reserved for future use.
version = 33,
-- * Client-to-server message size limit, between 0 and 255, the latter
-- limit being imposted by the protocol.
message_size = 200, -- * Upper limit is 255.
-- * Client-to-server message rate limit. Specifies the amount of time in
-- seconds that must have elapsed since the previous message in order
-- for the current message to be processed.
message_interval = 1,
-- * Authentication backend URL.
auth_backend = "https://powdertoy.co.uk/ExternalAuth.api",
-- * Authentication backend timeout in seconds.
auth_backend_timeout = 15,
-- * Username to UID backend URL.
uid_backend = "https://powdertoy.co.uk/User.json",
-- * Username to UID backend timeout in seconds.
uid_backend_timeout = 15,
-- * Host to connect to by default.
host = "tptmp.starcatcher.us",
-- * Port to connect to by default.
port = 34403,
-- * Encrypt traffic between player clients and the server.
secure = true,
}
end
require_preload__["tptmp.common.util"] = function()
local function version_less(lhs, rhs)
for i = 1, math.max(#lhs, #rhs) do
local left = lhs[i] or 0
local right = rhs[i] or 0
if left < right then
return true
end
if left > right then
return false
end
end
return false
end
local function version_equal(lhs, rhs)
for i = 1, math.max(#lhs, #rhs) do
local left = lhs[i] or 0
local right = rhs[i] or 0
if left ~= right then
return false
end
end
return true
end
return {
version_less = version_less,
version_equal = version_equal,
}
end
xpcall_wrap(function()
require("tptmp.client").run()
end)()