Compare commits
8 Commits
develop-1.
...
39caa32908
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
39caa32908 | ||
|
|
9103c44658 | ||
|
|
882894685c | ||
|
|
ba49f7ca7d | ||
|
|
f3c35afe07 | ||
|
|
8a6896e276 | ||
|
|
a18a8b7140 | ||
|
|
6d6b43daf7 |
@@ -51,7 +51,7 @@ local config = {
|
||||
}
|
||||
Config.load('Overview', config)
|
||||
|
||||
local extSupport = Util.supportsExtChars()
|
||||
local extSupport = Util.getVersion() >= 1.76
|
||||
|
||||
local applications = { }
|
||||
local buttons = { }
|
||||
|
||||
@@ -1,22 +1,14 @@
|
||||
local SHA = require("opus.crypto.sha2")
|
||||
|
||||
local acceptableCharacters = {}
|
||||
for c = 0, 127 do
|
||||
local char = string.char(c)
|
||||
-- exclude potentially ambiguous characters
|
||||
if char:match("[1-9a-zA-Z]") and char:match("[^OIl]") then
|
||||
table.insert(acceptableCharacters, char)
|
||||
end
|
||||
end
|
||||
local acceptableCharacters = {"0", "1", "2", "3", "4", "5", "6", "7", "8", "9", "a", "b", "c", "d", "e", "f", "g", "h", "i", "j", "k", "l", "m", "n", "o", "p", "q", "r", "s", "t", "u", "v", "w", "x", "y", "z"}
|
||||
local acceptableCharactersLen = #acceptableCharacters
|
||||
|
||||
local password = ""
|
||||
|
||||
for i = 1, 10 do
|
||||
for _i = 1, 8 do
|
||||
password = password .. acceptableCharacters[math.random(acceptableCharactersLen)]
|
||||
end
|
||||
|
||||
os.queueEvent("set_otp", SHA.compute(password))
|
||||
|
||||
print("This allows one other device to permanently gain access to this device.")
|
||||
print("Use the trust settings in System to revert this.")
|
||||
print("Your one-time password is: " .. password)
|
||||
print("Your one-time password is: " .. password)
|
||||
200
sys/apps/memprofile.lua
Normal file
200
sys/apps/memprofile.lua
Normal file
@@ -0,0 +1,200 @@
|
||||
-- Memory Profiler for CC:Tweaked / Opus OS
|
||||
-- Usage: memprofile [--watch] [--interval <seconds>]
|
||||
--
|
||||
-- Shows current Lua memory usage with breakdown estimates.
|
||||
-- Use --watch to continuously monitor.
|
||||
-- Useful for detecting memory leaks and understanding overhead.
|
||||
|
||||
local args = { ... }
|
||||
local watch = false
|
||||
local interval = 3
|
||||
|
||||
for i, arg in ipairs(args) do
|
||||
if arg == '--watch' or arg == '-w' then
|
||||
watch = true
|
||||
elseif arg == '--interval' or arg == '-i' then
|
||||
interval = tonumber(args[i + 1]) or 3
|
||||
elseif arg == '--help' or arg == '-h' then
|
||||
print('Usage: memprofile [--watch] [--interval <secs>]')
|
||||
print('')
|
||||
print('Options:')
|
||||
print(' --watch, -w Continuously monitor memory')
|
||||
print(' --interval, -i N Update interval in seconds (default: 3)')
|
||||
print(' --help, -h Show this help')
|
||||
return
|
||||
end
|
||||
end
|
||||
|
||||
local term = _G.term
|
||||
local os = _G.os
|
||||
|
||||
local function formatBytes(bytes)
|
||||
if bytes < 1024 then
|
||||
return string.format('%d B', bytes)
|
||||
elseif bytes < 1024 * 1024 then
|
||||
return string.format('%.1f KB', bytes / 1024)
|
||||
else
|
||||
return string.format('%.2f MB', bytes / (1024 * 1024))
|
||||
end
|
||||
end
|
||||
|
||||
local function countTable(t, seen)
|
||||
if type(t) ~= 'table' or seen[t] then return 0, 0 end
|
||||
seen[t] = true
|
||||
local entries = 0
|
||||
local nested = 0
|
||||
for k, v in pairs(t) do
|
||||
entries = entries + 1
|
||||
if type(v) == 'table' then
|
||||
local e, n = countTable(v, seen)
|
||||
nested = nested + 1 + e
|
||||
entries = entries + n
|
||||
end
|
||||
if type(k) == 'table' then
|
||||
local e, n = countTable(k, seen)
|
||||
nested = nested + 1 + e
|
||||
entries = entries + n
|
||||
end
|
||||
end
|
||||
return entries, nested
|
||||
end
|
||||
|
||||
local function getSnapshot()
|
||||
-- Force a full GC cycle to get accurate usage
|
||||
collectgarbage('collect')
|
||||
collectgarbage('collect')
|
||||
local memKB = collectgarbage('count') -- returns KB as float
|
||||
|
||||
local snapshot = {
|
||||
totalKB = memKB,
|
||||
totalBytes = math.floor(memKB * 1024),
|
||||
timestamp = os.clock(),
|
||||
}
|
||||
|
||||
-- Count entries in major global tables
|
||||
local seen = {}
|
||||
local globals = {}
|
||||
|
||||
local interesting = {
|
||||
{ name = '_G (globals)', tbl = _G },
|
||||
{ name = 'kernel', tbl = _G.kernel },
|
||||
{ name = 'network', tbl = _G.network },
|
||||
{ name = 'device', tbl = _G.device },
|
||||
}
|
||||
|
||||
for _, item in ipairs(interesting) do
|
||||
if type(item.tbl) == 'table' then
|
||||
local entries, nested = countTable(item.tbl, seen)
|
||||
table.insert(globals, {
|
||||
name = item.name,
|
||||
entries = entries,
|
||||
nested = nested,
|
||||
})
|
||||
end
|
||||
end
|
||||
|
||||
snapshot.globals = globals
|
||||
|
||||
-- Count routines if kernel is available
|
||||
if _G.kernel and _G.kernel.routines then
|
||||
snapshot.routines = #_G.kernel.routines
|
||||
end
|
||||
|
||||
-- Count loaded modules
|
||||
if package and package.loaded then
|
||||
local count = 0
|
||||
for _ in pairs(package.loaded) do
|
||||
count = count + 1
|
||||
end
|
||||
snapshot.loadedModules = count
|
||||
end
|
||||
|
||||
return snapshot
|
||||
end
|
||||
|
||||
local function printSnapshot(snap, prev)
|
||||
term.clear()
|
||||
term.setCursorPos(1, 1)
|
||||
|
||||
local w = term.getSize()
|
||||
local sep = string.rep('-', w)
|
||||
|
||||
term.setTextColor(colors.yellow)
|
||||
print('=== Memory Profile ===')
|
||||
term.setTextColor(colors.white)
|
||||
print('')
|
||||
|
||||
-- Total memory
|
||||
local memStr = formatBytes(snap.totalBytes)
|
||||
local deltaStr = ''
|
||||
if prev then
|
||||
local delta = snap.totalBytes - prev.totalBytes
|
||||
if delta > 0 then
|
||||
deltaStr = string.format(' (+%s)', formatBytes(delta))
|
||||
term.setTextColor(colors.red)
|
||||
elseif delta < 0 then
|
||||
deltaStr = string.format(' (-%s)', formatBytes(-delta))
|
||||
term.setTextColor(colors.green)
|
||||
end
|
||||
end
|
||||
term.setTextColor(colors.white)
|
||||
print(string.format('Total Memory: %s%s', memStr, deltaStr))
|
||||
print(string.format('Uptime: %.1fs', snap.timestamp))
|
||||
print('')
|
||||
|
||||
-- Table sizes
|
||||
term.setTextColor(colors.lightBlue)
|
||||
print('Global Tables:')
|
||||
term.setTextColor(colors.white)
|
||||
print(sep)
|
||||
print(string.format(' %-20s %8s %8s', 'Name', 'Entries', 'Nested'))
|
||||
print(sep)
|
||||
for _, g in ipairs(snap.globals) do
|
||||
print(string.format(' %-20s %8d %8d', g.name, g.entries, g.nested))
|
||||
end
|
||||
print(sep)
|
||||
print('')
|
||||
|
||||
-- Kernel info
|
||||
if snap.routines then
|
||||
term.setTextColor(colors.lightBlue)
|
||||
print('Kernel:')
|
||||
term.setTextColor(colors.white)
|
||||
print(string.format(' Active routines: %d', snap.routines))
|
||||
end
|
||||
|
||||
if snap.loadedModules then
|
||||
print(string.format(' Loaded modules: %d', snap.loadedModules))
|
||||
end
|
||||
|
||||
print('')
|
||||
|
||||
-- CC:Tweaked limits
|
||||
term.setTextColor(colors.gray)
|
||||
print('Note: CC:Tweaked default memory limit is ~128MB per computer.')
|
||||
print('High memory usage may cause slowdowns or crashes.')
|
||||
|
||||
if watch then
|
||||
print('')
|
||||
term.setTextColor(colors.yellow)
|
||||
print(string.format('Refreshing every %ds... (Ctrl+T to stop)', interval))
|
||||
end
|
||||
end
|
||||
|
||||
local function run()
|
||||
local prev = nil
|
||||
|
||||
if watch then
|
||||
while true do
|
||||
local snap = getSnapshot()
|
||||
printSnapshot(snap, prev)
|
||||
prev = snap
|
||||
os.sleep(interval)
|
||||
end
|
||||
else
|
||||
local snap = getSnapshot()
|
||||
printSnapshot(snap, nil)
|
||||
end
|
||||
end
|
||||
|
||||
run()
|
||||
@@ -59,10 +59,6 @@ local function sambaConnection(socket)
|
||||
print('samba: Connection closed')
|
||||
end
|
||||
|
||||
local function sanitizeLabel(computer)
|
||||
return (computer.id.."_"..computer.label:gsub("[%c%.\"'/%*]", "")):sub(1, 40)
|
||||
end
|
||||
|
||||
Event.addRoutine(function()
|
||||
print('samba: listening on port 139')
|
||||
|
||||
@@ -83,10 +79,10 @@ Event.addRoutine(function()
|
||||
end)
|
||||
|
||||
Event.on('network_attach', function(_, computer)
|
||||
fs.mount(fs.combine('network', sanitizeLabel(computer)), 'netfs', computer.id)
|
||||
fs.mount(fs.combine('network', computer.label), 'netfs', computer.id)
|
||||
end)
|
||||
|
||||
Event.on('network_detach', function(_, computer)
|
||||
print('samba: detaching ' .. sanitizeLabel(computer))
|
||||
fs.unmount(fs.combine('network', sanitizeLabel(computer)))
|
||||
print('samba: detaching ' .. computer.label)
|
||||
fs.unmount(fs.combine('network', computer.label))
|
||||
end)
|
||||
|
||||
@@ -152,7 +152,7 @@ local function getSlots()
|
||||
end
|
||||
|
||||
local function sendInfo()
|
||||
if os.clock() - infoTimer >= 5 then -- don't flood
|
||||
if os.clock() - infoTimer >= 1 then -- don't flood
|
||||
infoTimer = os.clock()
|
||||
info.label = os.getComputerLabel()
|
||||
info.uptime = math.floor(os.clock())
|
||||
@@ -194,25 +194,16 @@ local function sendInfo()
|
||||
end
|
||||
end
|
||||
|
||||
local function cleanNetwork()
|
||||
-- every 10 seconds, send out this computer's info
|
||||
Event.onInterval(10, function()
|
||||
sendInfo()
|
||||
for _,c in pairs(_G.network) do
|
||||
local elapsed = os.clock()-c.timestamp
|
||||
if c.active and elapsed > 50 then
|
||||
if c.active and elapsed > 15 then
|
||||
c.active = false
|
||||
os.queueEvent('network_detach', c)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
-- every 30 seconds, send out this computer's info
|
||||
-- send with offset so that messages are evenly distributed and do not all come at once
|
||||
Event.onTimeout(math.random() * 30, function()
|
||||
sendInfo()
|
||||
cleanNetwork()
|
||||
Event.onInterval(30, function()
|
||||
sendInfo()
|
||||
cleanNetwork()
|
||||
end)
|
||||
end)
|
||||
|
||||
Event.on('turtle_response', function()
|
||||
@@ -222,5 +213,4 @@ Event.on('turtle_response', function()
|
||||
end
|
||||
end)
|
||||
|
||||
-- send info early so that computers show soon after booting
|
||||
Event.onTimeout(math.random() * 2 + 1, sendInfo)
|
||||
Event.onTimeout(1, sendInfo)
|
||||
|
||||
@@ -1,10 +1,9 @@
|
||||
local Security = require('opus.security')
|
||||
local SHA = require('opus.crypto.sha2')
|
||||
local Terminal = require('opus.terminal')
|
||||
|
||||
local password = Terminal.readPassword('Enter new password: ')
|
||||
|
||||
if password then
|
||||
Security.updatePassword(SHA.compute(password))
|
||||
Security.updatePassword(password)
|
||||
print('Password updated')
|
||||
end
|
||||
|
||||
@@ -26,12 +26,12 @@ return UI.Tab {
|
||||
x = 2, y = 5, ex = -2, ey = -2,
|
||||
values = {
|
||||
{ name = '', value = '' },
|
||||
{ name = 'CC version', value = ("%d.%d"):format(Util.getVersion()) },
|
||||
{ name = 'Lua version', value = _VERSION },
|
||||
{ name = 'MC version', value = Util.getMinecraftVersion() },
|
||||
{ name = 'Disk free', value = Util.toBytes(fs.getFreeSpace('/')) },
|
||||
{ name = 'Computer ID', value = tostring(os.getComputerID()) },
|
||||
{ name = 'Day', value = tostring(os.day()) },
|
||||
{ name = 'CC version', value = Util.getVersion() },
|
||||
{ name = 'Lua version', value = _VERSION },
|
||||
{ name = 'MC version', value = Util.getMinecraftVersion() },
|
||||
{ name = 'Disk free', value = Util.toBytes(fs.getFreeSpace('/')) },
|
||||
{ name = 'Computer ID', value = tostring(os.getComputerID()) },
|
||||
{ name = 'Day', value = tostring(os.day()) },
|
||||
},
|
||||
disableHeader = true,
|
||||
inactive = true,
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
sys/apps/pain.lua urlfs https://github.com/LDDestroier/CC/raw/master/pain.lua
|
||||
sys/apps/update.lua urlfs http://pastebin.com/raw/UzGHLbNC
|
||||
sys/apps/update.lua urlfs https://pastebin.com/raw/UzGHLbNC
|
||||
sys/apps/Enchat.lua urlfs https://raw.githubusercontent.com/LDDestroier/enchat/master/enchat3.lua
|
||||
sys/apps/cloud.lua urlfs https://cloud-catcher.squiddev.cc/cloud.lua
|
||||
rom/modules/main/opus linkfs sys/modules/opus
|
||||
@@ -14,7 +14,7 @@ local parentTerm = _G.device.terminal
|
||||
local w,h = parentTerm.getSize()
|
||||
local overviewId
|
||||
local tabsDirty = false
|
||||
local closeInd = Util.supportsExtChars() and '\215' or '*'
|
||||
local closeInd = Util.getVersion() >= 1.76 and '\215' or '*'
|
||||
local multishell = { }
|
||||
|
||||
_ENV.multishell = multishell
|
||||
|
||||
@@ -5,7 +5,7 @@ local cbor = require('opus.cbor')
|
||||
local sha2 = require('opus.crypto.sha2')
|
||||
local Util = require('opus.util')
|
||||
|
||||
local ROUNDS = 8 -- Adjust this for speed tradeoff
|
||||
local ROUNDS = 20 -- Standard ChaCha20 (was 8, upgraded for security)
|
||||
|
||||
local bxor = bit32.bxor
|
||||
local band = bit32.band
|
||||
|
||||
@@ -15,7 +15,11 @@ for _,m in pairs(methods) do
|
||||
end
|
||||
|
||||
function linkfs.resolve(node, dir)
|
||||
return dir:gsub(node.mountPoint, node.source, 1)
|
||||
local mp = node.mountPoint
|
||||
if dir:sub(1, #mp) == mp then
|
||||
return node.source .. dir:sub(#mp + 1)
|
||||
end
|
||||
return dir
|
||||
end
|
||||
|
||||
function linkfs.mount(path, source)
|
||||
@@ -41,8 +45,8 @@ function linkfs.mount(path, source)
|
||||
end
|
||||
|
||||
function linkfs.copy(node, s, t)
|
||||
s = s:gsub(node.mountPoint, node.source, 1)
|
||||
t = t:gsub(node.mountPoint, node.source, 1)
|
||||
s = linkfs.resolve(node, s)
|
||||
t = linkfs.resolve(node, t)
|
||||
return fs.copy(s, t)
|
||||
end
|
||||
|
||||
@@ -50,25 +54,29 @@ function linkfs.delete(node, dir)
|
||||
if dir == node.mountPoint then
|
||||
fs.unmount(node.mountPoint)
|
||||
else
|
||||
dir = dir:gsub(node.mountPoint, node.source, 1)
|
||||
dir = linkfs.resolve(node, dir)
|
||||
return fs.delete(dir)
|
||||
end
|
||||
end
|
||||
|
||||
function linkfs.find(node, spec)
|
||||
spec = spec:gsub(node.mountPoint, node.source, 1)
|
||||
spec = linkfs.resolve(node, spec)
|
||||
|
||||
local list = fs.find(spec)
|
||||
local src = node.source
|
||||
local mp = node.mountPoint
|
||||
for k,f in ipairs(list) do
|
||||
list[k] = f:gsub(node.source, node.mountPoint, 1)
|
||||
if f:sub(1, #src) == src then
|
||||
list[k] = mp .. f:sub(#src + 1)
|
||||
end
|
||||
end
|
||||
|
||||
return list
|
||||
end
|
||||
|
||||
function linkfs.move(node, s, t)
|
||||
s = s:gsub(node.mountPoint, node.source, 1)
|
||||
t = t:gsub(node.mountPoint, node.source, 1)
|
||||
s = linkfs.resolve(node, s)
|
||||
t = linkfs.resolve(node, t)
|
||||
return fs.move(s, t)
|
||||
end
|
||||
|
||||
|
||||
@@ -35,8 +35,10 @@ end
|
||||
local methods = { 'delete', 'exists', 'getFreeSpace', 'makeDir', 'list', 'listEx', 'attributes' }
|
||||
|
||||
local function resolve(node, dir)
|
||||
-- TODO: Wrong ! (does not support names with dashes)
|
||||
dir = dir:gsub(node.mountPoint, '', 1)
|
||||
local mp = node.mountPoint
|
||||
if dir:sub(1, #mp) == mp then
|
||||
dir = dir:sub(#mp + 1)
|
||||
end
|
||||
return fs.combine(node.source, dir)
|
||||
end
|
||||
|
||||
@@ -53,7 +55,7 @@ end
|
||||
|
||||
function netfs.mount(_, id, source)
|
||||
if not id or not tonumber(id) then
|
||||
error('ramfs syntax: computerId [directory]')
|
||||
error('netfs syntax: computerId [directory]')
|
||||
end
|
||||
return {
|
||||
id = tonumber(id),
|
||||
|
||||
@@ -15,7 +15,7 @@ function git.list(repository)
|
||||
|
||||
local user = table.remove(t, 1)
|
||||
local repo = table.remove(t, 1)
|
||||
local branch = table.remove(t, 1) or 'master'
|
||||
local branch = table.remove(t, 1) or 'main'
|
||||
local path
|
||||
|
||||
if not Util.empty(t) then
|
||||
|
||||
@@ -39,8 +39,7 @@ if register_global_module_table then
|
||||
_G[global_module_name] = json
|
||||
end
|
||||
|
||||
-- this was incompatible because we use fs later
|
||||
--local _ENV = nil -- blocking globals in Lua 5.2
|
||||
local _ENV = nil -- blocking globals in Lua 5.2
|
||||
|
||||
pcall (function()
|
||||
-- Enable access to blocked metatables.
|
||||
|
||||
@@ -1,10 +1,38 @@
|
||||
local Config = require('opus.config')
|
||||
local SHA = require('opus.crypto.sha2')
|
||||
local Util = require('opus.util')
|
||||
|
||||
local PBKDF2_ITERATIONS = 100
|
||||
|
||||
local Security = { }
|
||||
|
||||
local function generateSalt()
|
||||
local salt = { }
|
||||
for _ = 1, 16 do
|
||||
salt[#salt + 1] = math.random(0, 0xFF)
|
||||
end
|
||||
return setmetatable(salt, Util.byteArrayMT):toHex()
|
||||
end
|
||||
|
||||
function Security.verifyPassword(password)
|
||||
local current = Security.getPassword()
|
||||
return current and password == current
|
||||
local stored = Security.getPassword()
|
||||
if not stored then
|
||||
return false
|
||||
end
|
||||
|
||||
-- New format: { hash = hex, salt = hex, iter = N }
|
||||
if type(stored) == 'table' and stored.hash and stored.salt then
|
||||
local iter = stored.iter or PBKDF2_ITERATIONS
|
||||
local derived = SHA.pbkdf2(password, Util.hexToByteArray(stored.salt), iter)
|
||||
return derived:toHex() == stored.hash
|
||||
end
|
||||
|
||||
-- Legacy format: plain SHA-256 hex string
|
||||
if type(stored) == 'string' then
|
||||
return SHA.compute(password) == stored
|
||||
end
|
||||
|
||||
return false
|
||||
end
|
||||
|
||||
function Security.hasPassword()
|
||||
@@ -28,8 +56,15 @@ function Security.getIdentifier()
|
||||
end
|
||||
|
||||
function Security.updatePassword(password)
|
||||
local salt = generateSalt()
|
||||
local derived = SHA.pbkdf2(password, Util.hexToByteArray(salt), PBKDF2_ITERATIONS)
|
||||
|
||||
local config = Config.load('os')
|
||||
config.password = password
|
||||
config.password = {
|
||||
hash = derived:toHex(),
|
||||
salt = salt,
|
||||
iter = PBKDF2_ITERATIONS,
|
||||
}
|
||||
Config.update('os', config)
|
||||
end
|
||||
|
||||
|
||||
@@ -44,7 +44,7 @@ function UI:init()
|
||||
tertiary = colors.gray,
|
||||
}
|
||||
}
|
||||
self.extChars = Util.supportsExtChars()
|
||||
self.extChars = Util.getVersion() >= 1.76
|
||||
|
||||
local function keyFunction(event, code, held)
|
||||
local ie = Input:translate(event, code, held)
|
||||
@@ -115,12 +115,16 @@ function UI:init()
|
||||
local ie = Input:translate('mouse_up', button, x, y)
|
||||
local currentPage = self:getActivePage()
|
||||
|
||||
if ie.code == 'control-shift-mouse_click' then -- hack
|
||||
local event = currentPage:pointToChild(x, y)
|
||||
_ENV.multishell.openTab(_ENV, {
|
||||
path = 'sys/apps/Lua.lua',
|
||||
args = { event.element, self, _ENV },
|
||||
focused = true })
|
||||
if ie.code == 'control-shift-mouse_click' then -- debug inspector
|
||||
local Config = require('opus.config')
|
||||
local debugCfg = Config.load('os', { debug_inspector = false })
|
||||
if debugCfg.debug_inspector then
|
||||
local event = currentPage:pointToChild(x, y)
|
||||
_ENV.multishell.openTab(_ENV, {
|
||||
path = 'sys/apps/Lua.lua',
|
||||
args = { event.element, self, _ENV },
|
||||
focused = true })
|
||||
end
|
||||
|
||||
elseif ie and currentPage and currentPage.parent.device.side == side then
|
||||
self:click(currentPage, ie)
|
||||
|
||||
@@ -170,19 +170,16 @@ function Util.print(pattern, ...)
|
||||
end
|
||||
|
||||
function Util.getVersion()
|
||||
local versionString = _G._HOST or _G._CC_VERSION
|
||||
local versionMajor, versionMinor = versionString:match("(%d+)%.(%d+)")
|
||||
-- ex.: 1.89 would return 1, 89
|
||||
return tonumber(versionMajor), tonumber(versionMinor)
|
||||
end
|
||||
local version
|
||||
|
||||
function Util.compareVersion(major, minor)
|
||||
local currentMajor, currentMinor = Util.getVersion()
|
||||
return currentMajor > major or currentMajor == major and currentMinor >= minor
|
||||
end
|
||||
if _G._CC_VERSION then
|
||||
version = tonumber(_G._CC_VERSION:match('[%d]+%.?[%d][%d]'))
|
||||
end
|
||||
if not version and _G._HOST then
|
||||
version = tonumber(_G._HOST:match('[%d]+%.?[%d][%d]'))
|
||||
end
|
||||
|
||||
function Util.supportsExtChars()
|
||||
return Util.compareVersion(1, 76)
|
||||
return version or 1.7
|
||||
end
|
||||
|
||||
function Util.getMinecraftVersion()
|
||||
|
||||
Reference in New Issue
Block a user