@@ -1,4 +1,5 @@
|
||||
{
|
||||
"texture": "coal_ore",
|
||||
"tags": ["base:ore"],
|
||||
"base:durability": 16.0
|
||||
}
|
||||
|
||||
@@ -7,5 +7,6 @@
|
||||
"obstacle": false,
|
||||
"selectable": false,
|
||||
"replaceable": true,
|
||||
"translucent": true
|
||||
"translucent": true,
|
||||
"tags": ["core:liquid"]
|
||||
}
|
||||
|
||||
@@ -1,2 +1,3 @@
|
||||
generator = "base:demo"
|
||||
player-entity = "base:player"
|
||||
hand-skeleton = "base:hand"
|
||||
|
||||
@@ -1,6 +1,13 @@
|
||||
{
|
||||
"components": [
|
||||
"base:drop"
|
||||
{
|
||||
"name": "base:drop",
|
||||
"args": {
|
||||
"item": "base:stone.item",
|
||||
"count": 1
|
||||
}
|
||||
}
|
||||
|
||||
],
|
||||
"hitbox": [0.4, 0.25, 0.4],
|
||||
"sensors": [
|
||||
|
||||
@@ -1,5 +1,12 @@
|
||||
{
|
||||
"components": [
|
||||
{
|
||||
"name": "core:mob",
|
||||
"args": {
|
||||
"jump_force": 8.0
|
||||
}
|
||||
},
|
||||
"core:player",
|
||||
"base:player_animator"
|
||||
],
|
||||
"hitbox": [0.6, 1.8, 0.6]
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"id": "base",
|
||||
"title": "Base",
|
||||
"version": "0.28",
|
||||
"version": "0.29",
|
||||
"description": "basic content package"
|
||||
}
|
||||
|
||||
@@ -8,6 +8,9 @@ timer = 0.3
|
||||
|
||||
local def_index = entity:def_index()
|
||||
dropitem = ARGS
|
||||
if dropitem.item then
|
||||
dropitem.id = item.index(dropitem.item)
|
||||
end
|
||||
if dropitem then
|
||||
timer = dropitem.pickup_delay or timer
|
||||
end
|
||||
|
||||
@@ -1,11 +1,10 @@
|
||||
local tsf = entity.transform
|
||||
local body = entity.rigidbody
|
||||
local rig = entity.skeleton
|
||||
local mob = entity:require_component("core:mob")
|
||||
|
||||
local itemid = 0
|
||||
local headIndex = rig:index("head")
|
||||
local itemIndex = rig:index("item")
|
||||
local bodyIndex = rig:index("body")
|
||||
|
||||
local function refresh_model(id)
|
||||
itemid = id
|
||||
@@ -18,10 +17,11 @@ function on_render()
|
||||
if pid == -1 then
|
||||
return
|
||||
end
|
||||
|
||||
local rx, ry, rz = player.get_rot(pid, pid ~= hud.get_player())
|
||||
rig:set_matrix(headIndex, mat4.rotate({1, 0, 0}, ry))
|
||||
rig:set_matrix(bodyIndex, mat4.rotate({0, 1, 0}, rx))
|
||||
|
||||
local rx, _, _ = player.get_rot(pid, pid ~= hud.get_player())
|
||||
|
||||
local dir = vec2.rotate({0, -1}, -rx)
|
||||
mob.set_dir({dir[1], 0, dir[2]})
|
||||
|
||||
local invid, slotid = player.get_inventory(pid)
|
||||
local id, _ = inventory.get(invid, slotid)
|
||||
|
||||
@@ -21,6 +21,9 @@ function on_hud_open()
|
||||
local ppos = vec3.add({player.get_pos(pid)}, {0, 0.7, 0})
|
||||
local throw_force = vec3.mul(player.get_dir(pid), DROP_FORCE)
|
||||
local drop = base_util.drop(ppos, itemid, 1, data, 1.5)
|
||||
if not drop then
|
||||
return
|
||||
end
|
||||
local velocity = vec3.add(throw_force, vec3.add(pvel, DROP_INIT_VEL))
|
||||
drop.rigidbody:set_vel(velocity)
|
||||
end)
|
||||
|
||||
@@ -1,6 +1,11 @@
|
||||
function on_block_broken(id, x, y, z, playerid)
|
||||
if gfx then
|
||||
gfx.particles.emit({x+0.5, y+0.5, z+0.5}, 64, {
|
||||
local size = {block.get_size(id)}
|
||||
gfx.particles.emit({
|
||||
x + size[1] * 0.5,
|
||||
y + size[1] * 0.5,
|
||||
z + size[1] * 0.5
|
||||
}, 64, {
|
||||
lifetime=1.0,
|
||||
spawn_interval=0.0001,
|
||||
explosion={4, 4, 4},
|
||||
@@ -8,7 +13,7 @@ function on_block_broken(id, x, y, z, playerid)
|
||||
random_sub_uv=0.1,
|
||||
size={0.1, 0.1, 0.1},
|
||||
spawn_shape="box",
|
||||
spawn_spread={0.4, 0.4, 0.4}
|
||||
spawn_spread=vec3.mul(size, 0.4)
|
||||
})
|
||||
end
|
||||
|
||||
|
||||
@@ -0,0 +1,9 @@
|
||||
{
|
||||
"root": {
|
||||
"nodes": [
|
||||
{
|
||||
"name": "item"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
@@ -81,6 +81,11 @@ local function refresh_file_title()
|
||||
document.saveIcon.enabled = edited
|
||||
document.title.text = gui.str('File')..' - '..current_file.filename
|
||||
..(edited and ' *' or '')
|
||||
|
||||
local info = registry.get_info(current_file.filename)
|
||||
if info and info.type == "model" then
|
||||
pcall(run_current_file)
|
||||
end
|
||||
end
|
||||
|
||||
function on_control_combination(keycode)
|
||||
@@ -118,7 +123,6 @@ function run_current_file()
|
||||
local unit = info and info.unit
|
||||
|
||||
if script_type == "model" then
|
||||
print(current_file.filename)
|
||||
clear_output()
|
||||
local _, err = pcall(reload_model, current_file.filename, unit)
|
||||
if err then
|
||||
@@ -256,7 +260,7 @@ function open_file_in_editor(filename, line, mutable)
|
||||
end
|
||||
|
||||
function on_open(mode)
|
||||
registry = require "core:internal/scripts_registry"
|
||||
registry = __vc_scripts_registry
|
||||
|
||||
document.codePanel:setInterval(200, refresh_file_title)
|
||||
|
||||
|
||||
@@ -4,7 +4,7 @@ history = session.get_entry("commands_history")
|
||||
history_pointer = #history
|
||||
|
||||
events.on("core:open_traceback", function()
|
||||
if modes then
|
||||
if modes and modes.current ~= 'debug' then
|
||||
modes:set('debug')
|
||||
end
|
||||
end)
|
||||
|
||||
@@ -43,11 +43,8 @@ function build_files_list(filenames, highlighted_part)
|
||||
end
|
||||
end
|
||||
|
||||
function on_open(mode)
|
||||
registry = require "core:internal/scripts_registry"
|
||||
|
||||
local files_list = document.filesList
|
||||
|
||||
function on_open()
|
||||
registry = __vc_scripts_registry
|
||||
filenames = registry.filenames
|
||||
table.sort(filenames)
|
||||
build_files_list(filenames)
|
||||
|
||||
@@ -176,18 +176,105 @@ function place_pack(panel, packinfo, callback, position_func)
|
||||
end
|
||||
end
|
||||
|
||||
local Version = {};
|
||||
|
||||
function Version.matches_pattern(version)
|
||||
for _, letter in string.gmatch(version, "%.+") do
|
||||
if type(letter) ~= "number" or letter ~= "." then
|
||||
return false;
|
||||
end
|
||||
|
||||
local t = string.split(version, ".");
|
||||
|
||||
return #t == 2 or #t == 3;
|
||||
end
|
||||
end
|
||||
|
||||
function Version.__equal(ver1, ver2)
|
||||
return ver1[1] == ver2[1] and ver1[2] == ver2[2] and ver1[3] == ver2[3];
|
||||
end
|
||||
|
||||
function Version.__more(ver1, ver2)
|
||||
if ver1[1] ~= ver2[1] then return ver1[1] > ver2[1] end;
|
||||
if ver1[2] ~= ver2[2] then return ver1[2] > ver2[2] end;
|
||||
return ver1[3] > ver2[3];
|
||||
end
|
||||
|
||||
function Version.__less(ver1, ver2)
|
||||
return Version.__more(ver2, ver1);
|
||||
end
|
||||
|
||||
function Version.__more_or_equal(ver1, ver2)
|
||||
return not Version.__less(ver1, ver2);
|
||||
end
|
||||
|
||||
function Version.__less_or_equal(ver1, ver2)
|
||||
return not Version.__more(ver1, ver2);
|
||||
end
|
||||
|
||||
function Version.compare(op, ver1, ver2)
|
||||
ver1 = string.split(ver1, ".");
|
||||
ver2 = string.split(ver2, ".");
|
||||
|
||||
if op == "=" then return Version.__equal(ver1, ver2);
|
||||
elseif op == ">" then return Version.__more(ver1, ver2);
|
||||
elseif op == "<" then return Version.__less(ver1, ver2);
|
||||
elseif op == ">=" then return Version.__more_or_equal(ver1, ver2);
|
||||
elseif op == "<=" then return Version.__less_or_equal(ver1, ver2);
|
||||
else return false; end
|
||||
end
|
||||
|
||||
function Version.parse(version)
|
||||
local op = string.sub(version, 1, 2);
|
||||
if op == ">=" or op == "=>" then
|
||||
return ">=", string.sub(version, #op + 1);
|
||||
elseif op == "<=" or op == "=<" then
|
||||
return "<=", string.sub(version, #op + 1);
|
||||
end
|
||||
|
||||
op = string.sub(version, 1, 1);
|
||||
if op == ">" or op == "<" then
|
||||
return op, string.sub(version, #op + 1);
|
||||
end
|
||||
|
||||
return "=", version;
|
||||
end
|
||||
|
||||
local function compare_version(dependent_version, actual_version)
|
||||
if Version.matches_pattern(dependent_version) and Version.matches_pattern(actual_version) then
|
||||
local op, dep_ver = Version.parse_version(dependent_version);
|
||||
Version.compare(op, dep_ver, actual_version);
|
||||
elseif dependent_version == "*" or dependent_version == actual_version then
|
||||
return true;
|
||||
else
|
||||
return false;
|
||||
end
|
||||
end
|
||||
|
||||
function check_dependencies(packinfo)
|
||||
if packinfo.dependencies == nil then
|
||||
return
|
||||
end
|
||||
for i,dep in ipairs(packinfo.dependencies) do
|
||||
local depid = dep:sub(2,-1)
|
||||
if dep:sub(1,1) == '!' then
|
||||
local depid, depver = unpack(string.split(dep:sub(2,-1), "@"))
|
||||
|
||||
if dep:sub(1,1) == '!' then
|
||||
if not table.has(packs_all, depid) then
|
||||
return string.format(
|
||||
"%s (%s)", gui.str("error.dependency-not-found"), depid
|
||||
)
|
||||
end
|
||||
|
||||
|
||||
local dep_pack = pack.get_info(depid);
|
||||
|
||||
if not compare_version(depver, dep_pack.version) then
|
||||
local op, ver = Version.parse(depver);
|
||||
|
||||
print(string.format("%s: %s !%s %s (%s)", gui.str("error.dependency-version-not-met"), dep_pack.version, op, ver, depid));
|
||||
return string.format("%s: %s != %s (%s)", gui.str("error.dependency-version-not-met"), dep_pack.version, ver, depid);
|
||||
end
|
||||
|
||||
if table.has(packs_installed, packinfo.id) then
|
||||
table.insert(required, depid)
|
||||
end
|
||||
|
||||
@@ -13,9 +13,9 @@ end
|
||||
function refresh()
|
||||
document.list:clear()
|
||||
|
||||
local available = pack.get_available()
|
||||
local infos = pack.get_info(available)
|
||||
for _, name in ipairs(available) do
|
||||
local allpacks = table.merge(pack.get_available(), pack.get_installed())
|
||||
local infos = pack.get_info(allpacks)
|
||||
for _, name in ipairs(allpacks) do
|
||||
local info = infos[name]
|
||||
local scripts_dir = info.path.."/scripts/app"
|
||||
if not file.exists(scripts_dir) then
|
||||
|
||||
@@ -1,3 +1,12 @@
|
||||
local WARNING_COLORS = {
|
||||
{208, 104, 107, 255},
|
||||
{250, 75, 139, 255},
|
||||
{250, 151, 75, 255},
|
||||
{246, 233, 44, 255},
|
||||
{252, 200, 149, 255}
|
||||
}
|
||||
|
||||
local GENERAL_WARNING_COLOR = {208, 138, 0, 255}
|
||||
|
||||
function refresh_search()
|
||||
local search_text = document.search_textbox.text
|
||||
@@ -40,6 +49,49 @@ function change_sensitivity(val)
|
||||
refresh_sensitivity()
|
||||
end
|
||||
|
||||
function refresh_binding_marks()
|
||||
local prev_bindings = {}
|
||||
local conflicts_colors = {}
|
||||
local available_colors = table.copy(WARNING_COLORS)
|
||||
|
||||
local bindings = input.get_bindings()
|
||||
table.sort(bindings, function(a, b) return a > b end)
|
||||
|
||||
for _, bind_name in ipairs(bindings) do
|
||||
local key = input.get_binding_text(bind_name)
|
||||
local prev = prev_bindings[key]
|
||||
if prev then
|
||||
local color = GENERAL_WARNING_COLOR
|
||||
local conflict_color = conflicts_colors[key]
|
||||
local available_colors_len = #available_colors
|
||||
if conflict_color then
|
||||
color = conflict_color
|
||||
elseif available_colors_len > 0 then
|
||||
color = available_colors[available_colors_len]
|
||||
conflicts_colors[key] = color
|
||||
table.remove(available_colors, available_colors_len)
|
||||
end
|
||||
|
||||
local tooltip = gui.str("settings.Conflict", "settings")
|
||||
|
||||
local prev_bindmark = "bindmark_" .. prev
|
||||
local cur_bindmark = "bindmark_" .. bind_name
|
||||
document[prev_bindmark].visible = true
|
||||
document[cur_bindmark].visible = true
|
||||
|
||||
document[prev_bindmark].color = color
|
||||
document[cur_bindmark].color = color
|
||||
|
||||
document["bind_" .. prev].tooltip = tooltip
|
||||
document["bind_" .. bind_name].tooltip = tooltip
|
||||
else
|
||||
document["bindmark_" .. bind_name].visible = false
|
||||
document["bind_" .. bind_name].tooltip = ''
|
||||
prev_bindings[key] = bind_name
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
function on_open()
|
||||
document.sensitivity_track.value = core.get_setting("camera.sensitivity")
|
||||
refresh_sensitivity()
|
||||
@@ -52,4 +104,8 @@ function on_open()
|
||||
id=name, name=gui.str(name)
|
||||
}))
|
||||
end
|
||||
|
||||
document.bindings_panel:setInterval(100, function ()
|
||||
refresh_binding_marks()
|
||||
end)
|
||||
end
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
<panel size='400,40' padding='4' color='0' orientation='horizontal'>
|
||||
<panel id='bind_%{id}' size='400,40' padding='4' color='0' orientation='horizontal'>
|
||||
<bindbox binding='%{id}'/>
|
||||
<label margin='6'>%{name}</label>
|
||||
<image id="bindmark_%{id}"src='gui/left_half_block' size='4,28' visible="false"/>
|
||||
<label id='bindlabel_%{id}' color="#ffffffff" margin='6'>%{name}</label>
|
||||
</panel>
|
||||
|
||||
@@ -0,0 +1,10 @@
|
||||
local this = {}
|
||||
|
||||
function this.equals(expected, fact)
|
||||
assert(fact == expected, string.format(
|
||||
"(fact == expected) assertion failed\n Expected: %s\n Fact: %s",
|
||||
expected, fact
|
||||
))
|
||||
end
|
||||
|
||||
return this
|
||||
@@ -0,0 +1,48 @@
|
||||
local events = {
|
||||
handlers = {}
|
||||
}
|
||||
|
||||
function events.on(event, func)
|
||||
if events.handlers[event] == nil then
|
||||
events.handlers[event] = {}
|
||||
end
|
||||
table.insert(events.handlers[event], func)
|
||||
end
|
||||
|
||||
function events.reset(event, func)
|
||||
if func == nil then
|
||||
events.handlers[event] = nil
|
||||
else
|
||||
events.handlers[event] = {func}
|
||||
end
|
||||
end
|
||||
|
||||
function events.remove_by_prefix(prefix)
|
||||
for name, handlers in pairs(events.handlers) do
|
||||
local actualname = name
|
||||
if type(name) == 'table' then
|
||||
actualname = name[1]
|
||||
end
|
||||
if actualname:sub(1, #prefix+1) == prefix..':' then
|
||||
events.handlers[actualname] = nil
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
function events.emit(event, ...)
|
||||
local result = nil
|
||||
local handlers = events.handlers[event]
|
||||
if handlers == nil then
|
||||
return nil
|
||||
end
|
||||
for _, func in ipairs(handlers) do
|
||||
local status, newres = xpcall(func, __vc__error, ...)
|
||||
if not status then
|
||||
debug.error("error in event ("..event..") handler: "..newres)
|
||||
else
|
||||
result = result or newres
|
||||
end
|
||||
end
|
||||
return result
|
||||
end
|
||||
return events
|
||||
@@ -0,0 +1,240 @@
|
||||
-- =================================================== --
|
||||
-- ====================== vec3 ======================= --
|
||||
-- =================================================== --
|
||||
function vec3.add(a, b, dst)
|
||||
local btype = type(b)
|
||||
if dst then
|
||||
if btype == "table" then
|
||||
dst[1] = a[1] + b[1]
|
||||
dst[2] = a[2] + b[2]
|
||||
dst[3] = a[3] + b[3]
|
||||
else
|
||||
dst[1] = a[1] + b
|
||||
dst[2] = a[2] + b
|
||||
dst[3] = a[3] + b
|
||||
end
|
||||
return dst
|
||||
else
|
||||
if btype == "table" then
|
||||
return {a[1] + b[1], a[2] + b[2], a[3] + b[3]}
|
||||
else
|
||||
return {a[1] + b, a[2] + b, a[3] + b}
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
function vec3.sub(a, b, dst)
|
||||
local btype = type(b)
|
||||
if dst then
|
||||
if btype == "table" then
|
||||
dst[1] = a[1] - b[1]
|
||||
dst[2] = a[2] - b[2]
|
||||
dst[3] = a[3] - b[3]
|
||||
else
|
||||
dst[1] = a[1] - b
|
||||
dst[2] = a[2] - b
|
||||
dst[3] = a[3] - b
|
||||
end
|
||||
return dst
|
||||
else
|
||||
if btype == "table" then
|
||||
return {a[1] - b[1], a[2] - b[2], a[3] - b[3]}
|
||||
else
|
||||
return {a[1] - b, a[2] - b, a[3] - b}
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
function vec3.mul(a, b, dst)
|
||||
local btype = type(b)
|
||||
if dst then
|
||||
if btype == "table" then
|
||||
dst[1] = a[1] * b[1]
|
||||
dst[2] = a[2] * b[2]
|
||||
dst[3] = a[3] * b[3]
|
||||
else
|
||||
dst[1] = a[1] * b
|
||||
dst[2] = a[2] * b
|
||||
dst[3] = a[3] * b
|
||||
end
|
||||
return dst
|
||||
else
|
||||
if btype == "table" then
|
||||
return {a[1] * b[1], a[2] * b[2], a[3] * b[3]}
|
||||
else
|
||||
return {a[1] * b, a[2] * b, a[3] * b}
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
function vec3.div(a, b, dst)
|
||||
local btype = type(b)
|
||||
if dst then
|
||||
if btype == "table" then
|
||||
dst[1] = a[1] / b[1]
|
||||
dst[2] = a[2] / b[2]
|
||||
dst[3] = a[3] / b[3]
|
||||
else
|
||||
dst[1] = a[1] / b
|
||||
dst[2] = a[2] / b
|
||||
dst[3] = a[3] / b
|
||||
end
|
||||
return dst
|
||||
else
|
||||
if btype == "table" then
|
||||
return {a[1] / b[1], a[2] / b[2], a[3] / b[3]}
|
||||
else
|
||||
return {a[1] / b, a[2] / b, a[3] / b}
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
function vec3.abs(a, dst)
|
||||
local x = a[1]
|
||||
local y = a[2]
|
||||
local z = a[3]
|
||||
if dst then
|
||||
dst[1] = x < 0.0 and -x or x
|
||||
dst[2] = y < 0.0 and -y or y
|
||||
dst[3] = z < 0.0 and -z or z
|
||||
else
|
||||
return {
|
||||
x < 0.0 and -x or x,
|
||||
y < 0.0 and -y or y,
|
||||
z < 0.0 and -z or z,
|
||||
}
|
||||
end
|
||||
end
|
||||
|
||||
function vec3.dot(a, b)
|
||||
return a[1] * b[1] + a[2] * b[2] + a[3] * b[3]
|
||||
end
|
||||
|
||||
function vec3.mix(a, b, t, dest)
|
||||
if dest then
|
||||
dest[1] = a[1] * (1.0 - t) + b[1] * t
|
||||
dest[2] = a[2] * (1.0 - t) + b[2] * t
|
||||
dest[3] = a[3] * (1.0 - t) + b[3] * t
|
||||
return dest
|
||||
else
|
||||
return {
|
||||
a[1] * (1.0 - t) + b[1] * t,
|
||||
a[2] * (1.0 - t) + b[2] * t,
|
||||
a[3] * (1.0 - t) + b[3] * t,
|
||||
}
|
||||
end
|
||||
end
|
||||
|
||||
-- =================================================== --
|
||||
-- ====================== vec2 ======================= --
|
||||
-- =================================================== --
|
||||
function vec2.add(a, b, dst)
|
||||
local btype = type(b)
|
||||
if dst then
|
||||
if btype == "table" then
|
||||
dst[1] = a[1] + b[1]
|
||||
dst[2] = a[2] + b[2]
|
||||
else
|
||||
dst[1] = a[1] + b
|
||||
dst[2] = a[2] + b
|
||||
end
|
||||
return dst
|
||||
else
|
||||
if btype == "table" then
|
||||
return {a[1] + b[1], a[2] + b[2]}
|
||||
else
|
||||
return {a[1] + b, a[2] + b}
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
function vec2.sub(a, b, dst)
|
||||
local btype = type(b)
|
||||
if dst then
|
||||
if btype == "table" then
|
||||
dst[1] = a[1] - b[1]
|
||||
dst[2] = a[2] - b[2]
|
||||
else
|
||||
dst[1] = a[1] - b
|
||||
dst[2] = a[2] - b
|
||||
end
|
||||
return dst
|
||||
else
|
||||
if btype == "table" then
|
||||
return {a[1] - b[1], a[2] - b[2]}
|
||||
else
|
||||
return {a[1] - b, a[2] - b}
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
function vec2.mul(a, b, dst)
|
||||
local btype = type(b)
|
||||
if dst then
|
||||
if btype == "table" then
|
||||
dst[1] = a[1] * b[1]
|
||||
dst[2] = a[2] * b[2]
|
||||
else
|
||||
dst[1] = a[1] * b
|
||||
dst[2] = a[2] * b
|
||||
end
|
||||
return dst
|
||||
else
|
||||
if btype == "table" then
|
||||
return {a[1] * b[1], a[2] * b[2]}
|
||||
else
|
||||
return {a[1] * b, a[2] * b}
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
function vec2.div(a, b, dst)
|
||||
local btype = type(b)
|
||||
if dst then
|
||||
if btype == "table" then
|
||||
dst[1] = a[1] / b[1]
|
||||
dst[2] = a[2] / b[2]
|
||||
else
|
||||
dst[1] = a[1] / b
|
||||
dst[2] = a[2] / b
|
||||
end
|
||||
return dst
|
||||
else
|
||||
if btype == "table" then
|
||||
return {a[1] / b[1], a[2] / b[2]}
|
||||
else
|
||||
return {a[1] / b, a[2] / b}
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
function vec2.abs(a, dst)
|
||||
local x = a[1]
|
||||
local y = a[2]
|
||||
if dst then
|
||||
dst[1] = x < 0.0 and -x or x
|
||||
dst[2] = y < 0.0 and -y or y
|
||||
else
|
||||
return {
|
||||
x < 0.0 and -x or x,
|
||||
y < 0.0 and -y or y,
|
||||
}
|
||||
end
|
||||
end
|
||||
|
||||
function vec2.dot(a, b)
|
||||
return a[1] * b[1] + a[2] * b[2]
|
||||
end
|
||||
|
||||
function vec2.mix(a, b, t, dest)
|
||||
if dest then
|
||||
dest[1] = a[1] * (1.0 - t) + b[1] * t
|
||||
dest[2] = a[2] * (1.0 - t) + b[2] * t
|
||||
return dest
|
||||
else
|
||||
return {
|
||||
a[1] * (1.0 - t) + b[1] * t,
|
||||
a[2] * (1.0 - t) + b[2] * t,
|
||||
}
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,35 @@
|
||||
local Random = {}
|
||||
|
||||
local M = 2 ^ 31
|
||||
local A = 1103515245
|
||||
local C = 12345
|
||||
|
||||
function Random.randint(self)
|
||||
self._seed = (A * self._seed + C) % M
|
||||
return self._seed
|
||||
end
|
||||
|
||||
function Random.random(self, a, b)
|
||||
local num = self:randint() % M / M
|
||||
if b then
|
||||
return math.floor(num * (b - a + 1) + a)
|
||||
elseif a then
|
||||
return math.floor(num * a + 1)
|
||||
else
|
||||
return num
|
||||
end
|
||||
end
|
||||
|
||||
function Random.seed(self, number)
|
||||
if type(number) ~= "number" then
|
||||
error("number expected")
|
||||
end
|
||||
self._seed = number
|
||||
end
|
||||
|
||||
return function(seed)
|
||||
if seed and type(seed) ~= "number" then
|
||||
error("number expected")
|
||||
end
|
||||
return setmetatable({_seed = seed or random.random(M)}, {__index = Random})
|
||||
end
|
||||
@@ -25,6 +25,7 @@ local Rigidbody = {__index={
|
||||
get_linear_damping=function(self) return __rigidbody.get_linear_damping(self.eid) end,
|
||||
set_linear_damping=function(self, f) return __rigidbody.set_linear_damping(self.eid, f) end,
|
||||
is_vdamping=function(self) return __rigidbody.is_vdamping(self.eid) end,
|
||||
get_vdamping=function(self) return __rigidbody.get_vdamping(self.eid) end,
|
||||
set_vdamping=function(self, b) return __rigidbody.set_vdamping(self.eid, b) end,
|
||||
is_grounded=function(self) return __rigidbody.is_grounded(self.eid) end,
|
||||
is_crouching=function(self) return __rigidbody.is_crouching(self.eid) end,
|
||||
@@ -63,6 +64,13 @@ local Entity = {__index={
|
||||
get_skeleton=function(self) return entities.get_skeleton(self.eid) end,
|
||||
set_skeleton=function(self, s) return entities.set_skeleton(self.eid, s) end,
|
||||
get_component=function(self, name) return self.components[name] end,
|
||||
require_component=function(self, name)
|
||||
local component = self.components[name]
|
||||
if not component then
|
||||
error(("entity has no required component '%s'"):format(name))
|
||||
end
|
||||
return component
|
||||
end,
|
||||
has_component=function(self, name) return self.components[name] ~= nil end,
|
||||
get_uid=function(self) return self.eid end,
|
||||
def_index=function(self) return entities.get_def(self.eid) end,
|
||||
@@ -125,6 +133,19 @@ return {
|
||||
::continue::
|
||||
end
|
||||
end,
|
||||
physics_update = function(delta)
|
||||
for uid, entity in pairs(entities) do
|
||||
for _, component in pairs(entity.components) do
|
||||
local callback = component.on_physics_update
|
||||
if not component.__disabled and callback then
|
||||
local result, err = pcall(callback, delta)
|
||||
if err then
|
||||
debug.error(err)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end,
|
||||
render = function(delta)
|
||||
for _,entity in pairs(entities) do
|
||||
for _, component in pairs(entity.components) do
|
||||
|
||||
@@ -0,0 +1,17 @@
|
||||
local io_stream = require "core:io_stream"
|
||||
|
||||
local lib = {
|
||||
read = file.__read_descriptor,
|
||||
write = file.__write_descriptor,
|
||||
flush = file.__flush_descriptor,
|
||||
is_alive = file.__has_descriptor,
|
||||
close = file.__close_descriptor
|
||||
}
|
||||
|
||||
return function(path, mode)
|
||||
return io_stream.new(
|
||||
file.__open_descriptor(path, mode),
|
||||
mode:find('b') ~= nil,
|
||||
lib
|
||||
)
|
||||
end
|
||||
@@ -0,0 +1,7 @@
|
||||
local FFI = ffi
|
||||
|
||||
if FFI.os == "Windows" then
|
||||
return require "core:internal/stream_providers/named_pipe_windows"
|
||||
else
|
||||
return require "core:internal/stream_providers/named_pipe_unix"
|
||||
end
|
||||
@@ -0,0 +1,21 @@
|
||||
local forbiddenPaths = {
|
||||
"/..\\", "\\../",
|
||||
"/../", "\\..\\"
|
||||
}
|
||||
|
||||
return function(path)
|
||||
local corrected = true
|
||||
|
||||
if path:starts_with("../") or path:starts_with("..\\") then
|
||||
corrected = false
|
||||
else
|
||||
for _, forbiddenPath in ipairs(forbiddenPaths) do
|
||||
if path:find(forbiddenPath) then
|
||||
corrected = false
|
||||
break
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
if not corrected then error "special path \"../\" is not allowed in path to named pipe" end
|
||||
end
|
||||
@@ -0,0 +1,104 @@
|
||||
local path_validate = require "core:internal/stream_providers/named_pipe_path_validate"
|
||||
local io_stream = require "core:io_stream"
|
||||
|
||||
local FFI = ffi
|
||||
|
||||
FFI.cdef[[
|
||||
int open(const char *pathname, int flags);
|
||||
int close(int fd);
|
||||
ssize_t read(int fd, void *buf, size_t count);
|
||||
ssize_t write(int fd, const void *buf, size_t count);
|
||||
int fcntl(int fd, int cmd, ...);
|
||||
|
||||
const char *strerror(int errnum);
|
||||
]]
|
||||
|
||||
local C = FFI.C
|
||||
|
||||
local O_RDONLY = 0x0
|
||||
local O_WRONLY = 0x1
|
||||
local O_RDWR = 0x2
|
||||
local O_NONBLOCK = 0x800
|
||||
local F_GETFL = 3
|
||||
|
||||
local function getError()
|
||||
local err = FFI.errno()
|
||||
|
||||
return FFI.string(C.strerror(err)).." ("..err..")"
|
||||
end
|
||||
|
||||
local lib = {}
|
||||
|
||||
function lib.read(fd, len)
|
||||
local buffer = FFI.new("uint8_t[?]", len)
|
||||
local result = tonumber(C.read(fd, buffer, len))
|
||||
|
||||
local out = Bytearray()
|
||||
|
||||
if result <= 0 then
|
||||
return out
|
||||
end
|
||||
|
||||
for i = 0, result - 1 do
|
||||
out[i+1] = buffer[i]
|
||||
end
|
||||
|
||||
return out
|
||||
end
|
||||
|
||||
function lib.write(fd, bytearray)
|
||||
local len = #bytearray
|
||||
local buffer = FFI.new("uint8_t[?]", len)
|
||||
for i = 1, len do
|
||||
buffer[i-1] = bytearray[i]
|
||||
end
|
||||
|
||||
if C.write(fd, buffer, len) == -1 then
|
||||
error("failed to write to named pipe: "..getError())
|
||||
end
|
||||
end
|
||||
|
||||
function lib.flush(fd)
|
||||
-- no flush on unix
|
||||
end
|
||||
|
||||
function lib.is_alive(fd)
|
||||
if fd == nil or fd < 0 then return false end
|
||||
|
||||
return C.fcntl(fd, F_GETFL) ~= -1
|
||||
end
|
||||
|
||||
function lib.close(fd)
|
||||
C.close(fd)
|
||||
end
|
||||
|
||||
return function(path, mode)
|
||||
path_validate(path)
|
||||
|
||||
path = "/tmp/"..path
|
||||
|
||||
local read = mode:find('r') ~= nil
|
||||
local write = mode:find('w') ~= nil
|
||||
|
||||
local flags
|
||||
|
||||
if read and write then
|
||||
flags = O_RDWR
|
||||
elseif read then
|
||||
flags = O_RDONLY
|
||||
elseif write then
|
||||
flags = O_WRONLY
|
||||
else
|
||||
error "mode must contain read or write flag"
|
||||
end
|
||||
|
||||
flags = bit.bor(flags, O_NONBLOCK)
|
||||
|
||||
local fd = C.open(path, flags)
|
||||
|
||||
if fd == -1 then
|
||||
error("failed to open named pipe: "..getError())
|
||||
end
|
||||
|
||||
return io_stream.new(fd, mode:find('b') ~= nil, lib)
|
||||
end
|
||||
@@ -0,0 +1,144 @@
|
||||
local path_validate = require "core:internal/stream_providers/named_pipe_path_validate"
|
||||
local io_stream = require "core:io_stream"
|
||||
|
||||
local FFI = ffi
|
||||
|
||||
FFI.cdef[[
|
||||
typedef void* HANDLE;
|
||||
typedef uint32_t DWORD;
|
||||
typedef int BOOL;
|
||||
typedef void* LPVOID;
|
||||
typedef const char* LPCSTR;
|
||||
|
||||
BOOL CloseHandle(HANDLE hObject);
|
||||
DWORD GetFileType(HANDLE hFile);
|
||||
BOOL ReadFile(HANDLE hFile, void* lpBuffer, DWORD nNumberOfBytesToRead,
|
||||
DWORD* lpNumberOfBytesRead, void* lpOverlapped);
|
||||
BOOL WriteFile(HANDLE hFile, const void* lpBuffer, DWORD nNumberOfBytesToWrite,
|
||||
DWORD* lpNumberOfBytesWritten, void* lpOverlapped);
|
||||
HANDLE CreateFileA(LPCSTR lpFileName, DWORD dwDesiredAccess, DWORD dwShareMode,
|
||||
void* lpSecurityAttributes, DWORD dwCreationDisposition,
|
||||
DWORD dwFlagsAndAttributes, HANDLE hTemplateFile);
|
||||
|
||||
BOOL PeekNamedPipe(
|
||||
HANDLE hNamedPipe,
|
||||
LPVOID lpBuffer,
|
||||
DWORD nBufferSize,
|
||||
DWORD* lpBytesRead,
|
||||
DWORD* lpTotalBytesAvail,
|
||||
DWORD* lpBytesLeftThisMessage
|
||||
);
|
||||
|
||||
DWORD GetLastError(void);
|
||||
BOOL FlushFileBuffers(HANDLE hFile);
|
||||
]]
|
||||
|
||||
local C = FFI.C
|
||||
|
||||
local GENERIC_READ = 0x80000000
|
||||
local GENERIC_WRITE = 0x40000000
|
||||
local OPEN_EXISTING = 3
|
||||
local FILE_ATTRIBUTE_NORMAL = 0x00000080
|
||||
local FILE_TYPE_UNKNOWN = 0x0000
|
||||
local INVALID_HANDLE_VALUE = FFI.cast("HANDLE", -1)
|
||||
|
||||
local lib = {}
|
||||
|
||||
local function is_data_available(handle)
|
||||
local bytes_available = FFI.new("DWORD[1]")
|
||||
local success = FFI.C.PeekNamedPipe(handle, nil, 0, nil, bytes_available, nil)
|
||||
|
||||
if success == 0 then
|
||||
return -1
|
||||
end
|
||||
|
||||
return bytes_available[0] > 0
|
||||
end
|
||||
|
||||
function lib.read(handle, len)
|
||||
local out = Bytearray()
|
||||
|
||||
local has_data, err = is_data_available(handle)
|
||||
|
||||
if not has_data then
|
||||
return out
|
||||
elseif hasData == -1 then
|
||||
error("failed to read from named pipe: "..tostring(C.GetLastError()))
|
||||
end
|
||||
|
||||
local buffer = FFI.new("uint8_t[?]", len)
|
||||
local read = FFI.new("DWORD[1]")
|
||||
|
||||
local ok = C.ReadFile(handle, buffer, len, read, nil)
|
||||
|
||||
if ok == 0 or read[0] == 0 then
|
||||
return out
|
||||
end
|
||||
|
||||
for i = 0, read[0] - 1 do
|
||||
out[i+1] = buffer[i]
|
||||
end
|
||||
|
||||
return out
|
||||
end
|
||||
|
||||
function lib.write(handle, bytearray)
|
||||
local len = #bytearray
|
||||
|
||||
local buffer = FFI.new("uint8_t[?]", len)
|
||||
for i = 1, len do
|
||||
buffer[i-1] = bytearray[i]
|
||||
end
|
||||
|
||||
local written = FFI.new("DWORD[1]")
|
||||
|
||||
if C.WriteFile(handle, buffer, len, written, nil) == 0 then
|
||||
error("failed to write to named pipe: "..tostring(C.GetLastError()))
|
||||
end
|
||||
end
|
||||
|
||||
function lib.flush(handle)
|
||||
C.FlushFileBuffers(handle)
|
||||
end
|
||||
|
||||
function lib.is_alive(handle)
|
||||
if handle == nil or handle == INVALID_HANDLE_VALUE then
|
||||
return false
|
||||
else
|
||||
return C.GetFileType(handle) ~= FILE_TYPE_UNKNOWN
|
||||
end
|
||||
end
|
||||
|
||||
function lib.close(handle)
|
||||
C.CloseHandle(handle)
|
||||
end
|
||||
|
||||
return function(path, mode)
|
||||
path_validate(path)
|
||||
|
||||
path = "\\\\.\\pipe\\"..path
|
||||
|
||||
local read = mode:find('r') ~= nil
|
||||
local write = mode:find('w') ~= nil
|
||||
|
||||
local flags
|
||||
|
||||
if read and write then
|
||||
flags = bit.bor(GENERIC_READ, GENERIC_WRITE)
|
||||
elseif read then
|
||||
flags = GENERIC_READ
|
||||
elseif write then
|
||||
flags = GENERIC_WRITE
|
||||
else
|
||||
error("mode must contain read or write flag")
|
||||
end
|
||||
|
||||
local handle = C.CreateFileA(path, flags, 0, nil, OPEN_EXISTING,
|
||||
FILE_ATTRIBUTE_NORMAL, nil)
|
||||
|
||||
if handle == INVALID_HANDLE_VALUE then
|
||||
error("failed to open named pipe: "..tostring(C.GetLastError()))
|
||||
end
|
||||
|
||||
return io_stream.new(handle, mode:find('b') ~= nil, lib)
|
||||
end
|
||||
@@ -0,0 +1,398 @@
|
||||
local io_stream = { }
|
||||
|
||||
io_stream.__index = io_stream
|
||||
|
||||
local MAX_BUFFER_SIZE = 8192
|
||||
|
||||
local DEFAULT_MODE = "default"
|
||||
local BUFFERED_MODE = "buffered"
|
||||
local YIELD_MODE = "yield"
|
||||
|
||||
local ALL_MODES = {
|
||||
DEFAULT_MODE,
|
||||
BUFFERED_MODE,
|
||||
YIELD_MODE
|
||||
}
|
||||
|
||||
local FLUSH_MODE_ALL = "all"
|
||||
local FLUSH_MODE_ONLY_BUFFER = "buffer"
|
||||
|
||||
local ALL_FLUSH_MODES = {
|
||||
FLUSH_MODE_ALL,
|
||||
FLUSH_MODE_ONLY_BUFFER
|
||||
}
|
||||
|
||||
local CR = string.byte('\r')
|
||||
local LF = string.byte('\n')
|
||||
|
||||
local function readFully(result, readFunc)
|
||||
local isTable = type(result) == "table"
|
||||
|
||||
local buf
|
||||
|
||||
repeat
|
||||
buf = readFunc(MAX_BUFFER_SIZE)
|
||||
|
||||
if isTable then
|
||||
for i = 1, #buf do
|
||||
result[#result + 1] = buf[i]
|
||||
end
|
||||
else result:append(buf) end
|
||||
until #buf == 0
|
||||
end
|
||||
|
||||
--[[
|
||||
|
||||
descriptor - descriptor of stream for provided I/O library
|
||||
binaryMode - if enabled, most methods will expect bytes instead of strings
|
||||
ioLib - I/O library. Should include the following functions:
|
||||
read(descriptor: int, length: int) -> Bytearray
|
||||
May return bytearray with a smaller size if bytes have not arrived yet or have run out
|
||||
write(descriptor: int, data: Bytearray)
|
||||
flush(descriptor: int)
|
||||
is_alive(descriptor: int) -> bool
|
||||
close(descriptor: int)
|
||||
--]]
|
||||
|
||||
function io_stream.new(descriptor, binaryMode, ioLib, mode, flushMode)
|
||||
mode = mode or DEFAULT_MODE
|
||||
flushMode = flushMode or FLUSH_MODE_ALL
|
||||
|
||||
local self = setmetatable({}, io_stream)
|
||||
|
||||
self.descriptor = descriptor
|
||||
self.binaryMode = binaryMode
|
||||
self.maxBufferSize = MAX_BUFFER_SIZE
|
||||
self.ioLib = ioLib
|
||||
|
||||
self:set_mode(mode)
|
||||
self:set_flush_mode(flushMode)
|
||||
|
||||
return self
|
||||
end
|
||||
|
||||
function io_stream:is_binary_mode()
|
||||
return self.binaryMode
|
||||
end
|
||||
|
||||
function io_stream:set_binary_mode(binaryMode)
|
||||
self.binaryMode = binaryMode ~= nil
|
||||
end
|
||||
|
||||
function io_stream:get_mode()
|
||||
return self.mode
|
||||
end
|
||||
|
||||
function io_stream:set_mode(mode)
|
||||
if not table.has(ALL_MODES, mode) then
|
||||
error("invalid stream mode: "..mode)
|
||||
end
|
||||
|
||||
if self.mode == BUFFERED_MODE then
|
||||
self.writeBuffer:clear()
|
||||
self.readBuffer:clear()
|
||||
end
|
||||
|
||||
if mode == BUFFERED_MODE and not self.writeBuffer then
|
||||
self.writeBuffer = Bytearray()
|
||||
self.readBuffer = Bytearray()
|
||||
end
|
||||
|
||||
self.mode = mode
|
||||
end
|
||||
|
||||
function io_stream:get_flush_mode()
|
||||
return self.flushMode
|
||||
end
|
||||
|
||||
function io_stream:set_flush_mode(flushMode)
|
||||
if not table.has(ALL_FLUSH_MODES, flushMode) then
|
||||
error("invalid flush mode: "..flushMode)
|
||||
end
|
||||
|
||||
self.flushMode = flushMode
|
||||
end
|
||||
|
||||
function io_stream:get_max_buffer_size()
|
||||
return self.maxBufferSize
|
||||
end
|
||||
|
||||
function io_stream:set_max_buffer_size(maxBufferSize)
|
||||
self.maxBufferSize = maxBufferSize
|
||||
end
|
||||
|
||||
function io_stream:available(length)
|
||||
if self.mode == BUFFERED_MODE then
|
||||
self:__update_read_buffer()
|
||||
|
||||
if not length then
|
||||
return #self.readBuffer
|
||||
else
|
||||
return #self.readBuffer >= length
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
function io_stream:__update_read_buffer()
|
||||
local readed = Bytearray()
|
||||
|
||||
readFully(readed, function(length) return self.ioLib.read(self.descriptor, length) end)
|
||||
|
||||
self.readBuffer:append(readed)
|
||||
|
||||
if #self.readBuffer > self.maxBufferSize then
|
||||
error "buffer overflow"
|
||||
end
|
||||
end
|
||||
|
||||
function io_stream:__read(length)
|
||||
if self.mode == YIELD_MODE then
|
||||
local buffer = Bytearray()
|
||||
|
||||
while #buffer < length do
|
||||
buffer:append(self.ioLib.read(self.descriptor, length - #buffer))
|
||||
|
||||
if #buffer < length then coroutine.yield() end
|
||||
end
|
||||
|
||||
return buffer
|
||||
elseif self.mode == BUFFERED_MODE then
|
||||
self:__update_read_buffer()
|
||||
|
||||
if #self.readBuffer < length then
|
||||
error "buffer underflow"
|
||||
end
|
||||
|
||||
local copy
|
||||
|
||||
if #self.readBuffer == length then
|
||||
copy = Bytearray()
|
||||
|
||||
copy:append(self.readBuffer)
|
||||
|
||||
self.readBuffer:clear()
|
||||
else
|
||||
copy = Bytearray()
|
||||
|
||||
for i = 1, length do
|
||||
copy[i] = self.readBuffer[i]
|
||||
end
|
||||
|
||||
self.readBuffer:remove(1, length)
|
||||
end
|
||||
|
||||
return copy
|
||||
elseif self.mode == DEFAULT_MODE then
|
||||
return self.ioLib.read(self.descriptor, length)
|
||||
end
|
||||
end
|
||||
|
||||
function io_stream:__write(data)
|
||||
if self.mode == BUFFERED_MODE then
|
||||
self.writeBuffer:append(data)
|
||||
|
||||
if #self.writeBuffer > self.maxBufferSize then
|
||||
error "buffer overflow"
|
||||
end
|
||||
elseif self.mode == DEFAULT_MODE or self.mode == YIELD_MODE then
|
||||
return self.ioLib.write(self.descriptor, data)
|
||||
end
|
||||
end
|
||||
|
||||
function io_stream:read_fully(useTable)
|
||||
if self.binaryMode then
|
||||
local result = useTable and Bytearray() or { }
|
||||
|
||||
readFully(result, function() return self:__read(self.maxBufferSize) end)
|
||||
else
|
||||
if useTable then
|
||||
local lines = { }
|
||||
|
||||
local line
|
||||
|
||||
repeat
|
||||
line = self:read_line()
|
||||
|
||||
lines[#lines + 1] = line
|
||||
until not line
|
||||
|
||||
return lines
|
||||
else
|
||||
local result = Bytearray()
|
||||
|
||||
readFully(result, function() return self:__read(self.maxBufferSize) end)
|
||||
|
||||
return utf8.tostring(result)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
function io_stream:read_line()
|
||||
local result = Bytearray()
|
||||
|
||||
local first = true
|
||||
|
||||
while true do
|
||||
local char = self:__read(1)
|
||||
|
||||
if #char == 0 then
|
||||
if first then return else break end
|
||||
end
|
||||
|
||||
char = char[1]
|
||||
|
||||
if char == LF then break
|
||||
elseif char == CR then
|
||||
char = self:__read(1)
|
||||
|
||||
if char[1] == LF then break
|
||||
else
|
||||
result:append(CR)
|
||||
result:append(char[1])
|
||||
end
|
||||
else result:append(char) end
|
||||
|
||||
first = false
|
||||
end
|
||||
|
||||
return utf8.tostring(result)
|
||||
end
|
||||
|
||||
function io_stream:write_line(str)
|
||||
self:__write(utf8.tobytes(str .. LF))
|
||||
end
|
||||
|
||||
function io_stream:read(arg, useTable)
|
||||
local argType = type(arg)
|
||||
|
||||
if self.binaryMode then
|
||||
local byteArr
|
||||
|
||||
if argType == "number" then
|
||||
-- using 'arg' as length
|
||||
|
||||
byteArr = self:__read(arg)
|
||||
|
||||
if useTable == true then
|
||||
local t = { }
|
||||
|
||||
for i = 1, #byteArr do
|
||||
t[i] = byteArr[i]
|
||||
end
|
||||
|
||||
return t
|
||||
else
|
||||
return byteArr
|
||||
end
|
||||
elseif argType == "string" then
|
||||
return byteutil.unpack(
|
||||
arg,
|
||||
self:__read(byteutil.get_size(arg))
|
||||
)
|
||||
elseif argType == nil then
|
||||
error(
|
||||
"in binary mode the first argument must be a string data format"..
|
||||
" for the library \"byteutil\" or the number of bytes to read"
|
||||
)
|
||||
else
|
||||
error("unknown argument type: "..argType)
|
||||
end
|
||||
else
|
||||
if not arg then
|
||||
return self:read_line()
|
||||
else
|
||||
local linesCount = arg
|
||||
local trimLastEmptyLines = useTable or true
|
||||
|
||||
if linesCount < 0 then error "count of lines to read must be positive" end
|
||||
|
||||
local result = { }
|
||||
|
||||
for i = 1, linesCount do
|
||||
result[i] = self:read_line()
|
||||
end
|
||||
|
||||
if trimLastEmptyLines then
|
||||
local i = #result
|
||||
|
||||
while i >= 0 do
|
||||
local length = utf8.length(result[i])
|
||||
|
||||
if length > 0 then break
|
||||
else result[i] = nil end
|
||||
|
||||
i = i - 1
|
||||
end
|
||||
|
||||
local i = 1
|
||||
|
||||
while #result > 0 do
|
||||
local length = utf8.length(result[i])
|
||||
|
||||
if length > 0 then break
|
||||
else table.remove(result, i) end
|
||||
end
|
||||
end
|
||||
|
||||
return result
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
function io_stream:write(arg, ...)
|
||||
local argType = type(arg)
|
||||
|
||||
if self.binaryMode then
|
||||
local byteArr
|
||||
|
||||
if argType ~= "string" then
|
||||
-- using arg as bytes table/bytearray
|
||||
|
||||
if argType == "table" then
|
||||
byteArr = Bytearray(arg)
|
||||
else
|
||||
byteArr = arg
|
||||
end
|
||||
else
|
||||
byteArr = byteutil.pack(arg, ...)
|
||||
end
|
||||
|
||||
self:__write(byteArr)
|
||||
else
|
||||
if argType == "string" then
|
||||
self:write_line(arg)
|
||||
elseif argType == "table" then
|
||||
for i = 1, #arg do
|
||||
self:write_line(arg[i])
|
||||
end
|
||||
else error("unknown argument type: "..argType) end
|
||||
end
|
||||
end
|
||||
|
||||
function io_stream:is_alive()
|
||||
return self.ioLib.is_alive(self.descriptor)
|
||||
end
|
||||
|
||||
function io_stream:is_closed()
|
||||
return not self:is_alive()
|
||||
end
|
||||
|
||||
function io_stream:close()
|
||||
if self.mode == BUFFERED_MODE then
|
||||
self.readBuffer:clear()
|
||||
self.writeBuffer:clear()
|
||||
end
|
||||
|
||||
return self.ioLib.close(self.descriptor)
|
||||
end
|
||||
|
||||
function io_stream:flush()
|
||||
if self.mode == BUFFERED_MODE and #self.writeBuffer > 0 then
|
||||
self.ioLib.write(self.descriptor, self.writeBuffer)
|
||||
self.writeBuffer:clear()
|
||||
end
|
||||
|
||||
if self.flushMode ~= FLUSH_MODE_ONLY_BUFFER then self.ioLib.flush(self.descriptor) end
|
||||
end
|
||||
|
||||
return io_stream
|
||||
@@ -0,0 +1,50 @@
|
||||
local Schedule = {
|
||||
__index = {
|
||||
set_interval = function(self, ms, callback, repetions)
|
||||
local id = self._next_interval
|
||||
self._intervals[id] = {
|
||||
last_called = 0.0,
|
||||
delay = ms / 1000.0,
|
||||
callback = callback,
|
||||
repetions = repetions,
|
||||
}
|
||||
self._next_interval = id + 1
|
||||
return id
|
||||
end,
|
||||
set_timeout = function(self, ms, callback)
|
||||
self:set_interval(ms, callback, 1)
|
||||
end,
|
||||
tick = function(self, dt)
|
||||
local timer = self._timer + dt
|
||||
for id, interval in pairs(self._intervals) do
|
||||
if timer - interval.last_called >= interval.delay then
|
||||
local stack_size = debug.count_frames()
|
||||
xpcall(interval.callback, function(msg)
|
||||
__vc__error(msg, 1, 1, stack_size)
|
||||
end)
|
||||
interval.last_called = timer
|
||||
local repetions = interval.repetions
|
||||
if repetions then
|
||||
if repetions <= 1 then
|
||||
self:remove_interval(id)
|
||||
else
|
||||
interval.repetions = repetions - 1
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
self._timer = timer
|
||||
end,
|
||||
remove_interval = function (self, id)
|
||||
self._intervals[id] = nil
|
||||
end
|
||||
}
|
||||
}
|
||||
|
||||
return function ()
|
||||
return setmetatable({
|
||||
_next_interval = 1,
|
||||
_timer = 0.0,
|
||||
_intervals = {},
|
||||
}, Schedule)
|
||||
end
|
||||
+2
-1
@@ -47,7 +47,8 @@
|
||||
"gui/info",
|
||||
"gui/world",
|
||||
"gui/hud",
|
||||
"gui/entity"
|
||||
"gui/entity",
|
||||
"gui/half_block"
|
||||
],
|
||||
"fonts": [
|
||||
{
|
||||
|
||||
@@ -0,0 +1,27 @@
|
||||
local menubg
|
||||
|
||||
function on_menu_clear()
|
||||
if menubg then
|
||||
menubg:destruct()
|
||||
menubg = nil
|
||||
end
|
||||
end
|
||||
|
||||
function on_menu_setup()
|
||||
local controller = {}
|
||||
function controller.resize_menu_bg()
|
||||
local w, h = unpack(gui.get_viewport())
|
||||
if menubg then
|
||||
menubg.region = {0, math.floor(h / 48), math.floor(w / 48), 0}
|
||||
menubg.pos = {0, 0}
|
||||
end
|
||||
return w, h
|
||||
end
|
||||
gui.root.root:add(
|
||||
"<image id='menubg' src='gui/menubg' size-func='DATA.resize_menu_bg' "..
|
||||
"z-index='-1' interactive='true'/>", controller)
|
||||
menubg = gui.root.menubg
|
||||
controller.resize_menu_bg()
|
||||
menu.page = "main"
|
||||
menu.visible = true
|
||||
end
|
||||
+195
-14
@@ -46,18 +46,67 @@ local Socket = {__index={
|
||||
get_address=function(self) return network.__get_address(self.id) end,
|
||||
}}
|
||||
|
||||
local WriteableSocket = {__index={
|
||||
send=function(self, ...) return network.__send(self.id, ...) end,
|
||||
close=function(self) return network.__close(self.id) end,
|
||||
is_open=function(self) return network.__is_alive(self.id) end,
|
||||
get_address=function(self) return network.__get_address(self.id) end,
|
||||
}}
|
||||
|
||||
local ServerSocket = {__index={
|
||||
close=function(self) return network.__closeserver(self.id) end,
|
||||
is_open=function(self) return network.__is_serveropen(self.id) end,
|
||||
get_port=function(self) return network.__get_serverport(self.id) end,
|
||||
}}
|
||||
|
||||
local DatagramServerSocket = {__index={
|
||||
close=function(self) return network.__closeserver(self.id) end,
|
||||
is_open=function(self) return network.__is_serveropen(self.id) end,
|
||||
get_port=function(self) return network.__get_serverport(self.id) end,
|
||||
send=function(self, ...) return network.__udp_server_send_to(self.id, ...) end
|
||||
}}
|
||||
|
||||
local _tcp_server_callbacks = {}
|
||||
local _tcp_client_callbacks = {}
|
||||
|
||||
local _udp_server_callbacks = {}
|
||||
local _udp_client_datagram_callbacks = {}
|
||||
local _udp_client_open_callbacks = {}
|
||||
local _http_response_callbacks = {}
|
||||
local _http_error_callbacks = {}
|
||||
|
||||
network.get = function(url, callback, errorCallback, headers)
|
||||
local id = network.__get(url, headers)
|
||||
if callback then
|
||||
_http_response_callbacks[id] = callback
|
||||
end
|
||||
if errorCallback then
|
||||
_http_error_callbacks[id] = errorCallback
|
||||
end
|
||||
end
|
||||
|
||||
network.get_binary = function(url, callback, errorCallback, headers)
|
||||
local id = network.__get_binary(url, headers)
|
||||
if callback then
|
||||
_http_response_callbacks[id] = callback
|
||||
end
|
||||
if errorCallback then
|
||||
_http_error_callbacks[id] = errorCallback
|
||||
end
|
||||
end
|
||||
|
||||
network.post = function(url, data, callback, errorCallback, headers)
|
||||
local id = network.__post(url, data, headers)
|
||||
if callback then
|
||||
_http_response_callbacks[id] = callback
|
||||
end
|
||||
if errorCallback then
|
||||
_http_error_callbacks[id] = errorCallback
|
||||
end
|
||||
end
|
||||
|
||||
network.tcp_open = function (port, handler)
|
||||
local socket = setmetatable({id=network.__open(port)}, ServerSocket)
|
||||
local socket = setmetatable({id=network.__open_tcp(port)}, ServerSocket)
|
||||
|
||||
_tcp_server_callbacks[socket.id] = function(id)
|
||||
handler(setmetatable({id=id}, Socket))
|
||||
@@ -67,19 +116,133 @@ end
|
||||
|
||||
network.tcp_connect = function(address, port, callback)
|
||||
local socket = setmetatable({id=0}, Socket)
|
||||
socket.id = network.__connect(address, port)
|
||||
socket.id = network.__connect_tcp(address, port)
|
||||
_tcp_client_callbacks[socket.id] = function() callback(socket) end
|
||||
return socket
|
||||
end
|
||||
|
||||
network.udp_open = function (port, datagramHandler)
|
||||
if type(datagramHandler) ~= 'function' then
|
||||
error "udp server cannot be opened without datagram handler"
|
||||
end
|
||||
|
||||
local socket = setmetatable({id=network.__open_udp(port)}, DatagramServerSocket)
|
||||
|
||||
_udp_server_callbacks[socket.id] = function(address, port, data)
|
||||
datagramHandler(address, port, data, socket)
|
||||
end
|
||||
|
||||
return socket
|
||||
end
|
||||
|
||||
network.udp_connect = function (address, port, datagramHandler, openCallback)
|
||||
if type(datagramHandler) ~= 'function' then
|
||||
error "udp client socket cannot be opened without datagram handler"
|
||||
end
|
||||
|
||||
local socket = setmetatable({id=0}, WriteableSocket)
|
||||
socket.id = network.__connect_udp(address, port)
|
||||
|
||||
_udp_client_datagram_callbacks[socket.id] = datagramHandler
|
||||
_udp_client_open_callbacks[socket.id] = openCallback
|
||||
|
||||
return socket
|
||||
end
|
||||
|
||||
local function clean(iterable, checkFun, ...)
|
||||
local tables = { ... }
|
||||
|
||||
for id, _ in pairs(iterable) do
|
||||
if not checkFun(id) then
|
||||
for i = 1, #tables do
|
||||
tables[i][id] = nil
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
local updating_blocks = {}
|
||||
local TYPE_REGISTER = 0
|
||||
local TYPE_UNREGISTER = 1
|
||||
|
||||
block.__perform_ticks = function(delta)
|
||||
for id, entry in pairs(updating_blocks) do
|
||||
entry.timer = entry.timer + delta
|
||||
local steps = math.floor(entry.timer / entry.delta * #entry / 3)
|
||||
if steps == 0 then
|
||||
goto continue
|
||||
end
|
||||
entry.timer = 0.0
|
||||
local event = entry.event
|
||||
local tps = entry.tps
|
||||
for i=1, steps do
|
||||
local x = entry[entry.pointer + 1]
|
||||
local y = entry[entry.pointer + 2]
|
||||
local z = entry[entry.pointer + 3]
|
||||
entry.pointer = (entry.pointer + 3) % #entry
|
||||
events.emit(event, x, y, z, tps)
|
||||
end
|
||||
::continue::
|
||||
end
|
||||
end
|
||||
|
||||
block.__process_register_events = function()
|
||||
local register_events = block.__pull_register_events()
|
||||
if not register_events then
|
||||
return
|
||||
end
|
||||
for i=1, #register_events, 4 do
|
||||
local header = register_events[i]
|
||||
local type = bit.band(header, 0xFFFF)
|
||||
local id = bit.rshift(header, 16)
|
||||
local x = register_events[i + 1]
|
||||
local y = register_events[i + 2]
|
||||
local z = register_events[i + 3]
|
||||
|
||||
local list = updating_blocks[id]
|
||||
if type == TYPE_REGISTER then
|
||||
if not list then
|
||||
list = {}
|
||||
list.event = block.name(id) .. ".blocktick"
|
||||
list.tps = 20 / (block.properties[id]["tick-interval"] or 1)
|
||||
list.delta = 1.0 / list.tps
|
||||
list.timer = 0.0
|
||||
list.pointer = 0
|
||||
updating_blocks[id] = list
|
||||
end
|
||||
table.insert(list, x)
|
||||
table.insert(list, y)
|
||||
table.insert(list, z)
|
||||
elseif type == TYPE_UNREGISTER then
|
||||
if list then
|
||||
for j=1, #list, 3 do
|
||||
if list[j] == x and list[j + 1] == y and list[j + 2] == z then
|
||||
for k=1,3 do
|
||||
table.remove(list, j)
|
||||
end
|
||||
j = j - 3
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
print(type, id, x, y, z)
|
||||
end
|
||||
end
|
||||
|
||||
network.__process_events = function()
|
||||
local CLIENT_CONNECTED = 1
|
||||
local CONNECTED_TO_SERVER = 2
|
||||
local DATAGRAM = 3
|
||||
local RESPONSE = 4
|
||||
|
||||
local ON_SERVER = 1
|
||||
local ON_CLIENT = 2
|
||||
|
||||
local cleaned = false
|
||||
local events = network.__pull_events()
|
||||
for i, event in ipairs(events) do
|
||||
local etype, sid, cid = unpack(event)
|
||||
local etype, sid, cid, addr, port, side, data = unpack(event)
|
||||
|
||||
if etype == CLIENT_CONNECTED then
|
||||
local callback = _tcp_server_callbacks[sid]
|
||||
@@ -87,24 +250,42 @@ network.__process_events = function()
|
||||
callback(cid)
|
||||
end
|
||||
elseif etype == CONNECTED_TO_SERVER then
|
||||
local callback = _tcp_client_callbacks[cid]
|
||||
local callback = _tcp_client_callbacks[cid] or _udp_client_open_callbacks[cid]
|
||||
if callback then
|
||||
callback()
|
||||
end
|
||||
elseif etype == DATAGRAM then
|
||||
if side == ON_CLIENT then
|
||||
_udp_client_datagram_callbacks[cid](data)
|
||||
elseif side == ON_SERVER then
|
||||
_udp_server_callbacks[sid](addr, port, data)
|
||||
end
|
||||
elseif etype == RESPONSE then
|
||||
if event[2] / 100 == 2 then
|
||||
local callback = _http_response_callbacks[event[3]]
|
||||
_http_response_callbacks[event[3]] = nil
|
||||
_http_error_callbacks[event[3]] = nil
|
||||
if callback then
|
||||
callback(event[4])
|
||||
end
|
||||
else
|
||||
local callback = _http_error_callbacks[event[3]]
|
||||
_http_response_callbacks[event[3]] = nil
|
||||
_http_error_callbacks[event[3]] = nil
|
||||
if callback then
|
||||
callback(event[2], event[4])
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
-- remove dead servers
|
||||
if not cleaned then
|
||||
for sid, _ in pairs(_tcp_server_callbacks) do
|
||||
if not network.__is_serveropen(sid) then
|
||||
_tcp_server_callbacks[sid] = nil
|
||||
end
|
||||
end
|
||||
for cid, _ in pairs(_tcp_client_callbacks) do
|
||||
if not network.__is_alive(cid) then
|
||||
_tcp_client_callbacks[cid] = nil
|
||||
end
|
||||
end
|
||||
clean(_tcp_server_callbacks, network.__is_serveropen, _tcp_server_callbacks)
|
||||
clean(_tcp_client_callbacks, network.__is_alive, _tcp_client_callbacks)
|
||||
|
||||
clean(_udp_server_callbacks, network.__is_serveropen, _udp_server_callbacks)
|
||||
clean(_udp_client_datagram_callbacks, network.__is_alive, _udp_client_open_callbacks, _udp_client_datagram_callbacks)
|
||||
|
||||
cleaned = true
|
||||
end
|
||||
end
|
||||
|
||||
@@ -0,0 +1,177 @@
|
||||
local body = entity.rigidbody
|
||||
local tsf = entity.transform
|
||||
local rig = entity.skeleton
|
||||
|
||||
local props = {}
|
||||
|
||||
local function def_prop(name, def_value)
|
||||
props[name] = SAVED_DATA[name] or ARGS[name] or def_value
|
||||
this["get_"..name] = function() return props[name] end
|
||||
this["set_"..name] = function(value)
|
||||
props[name] = value
|
||||
if math.abs(value - def_value) < 1e-7 then
|
||||
SAVED_DATA[name] = nil
|
||||
else
|
||||
SAVED_DATA[name] = value
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
def_prop("jump_force", 0.0)
|
||||
def_prop("air_damping", 1.0)
|
||||
def_prop("ground_damping", 1.0)
|
||||
def_prop("movement_speed", 3.0)
|
||||
def_prop("run_speed_mul", 1.5)
|
||||
def_prop("crouch_speed_mul", 0.35)
|
||||
def_prop("flight_speed_mul", 2.0)
|
||||
def_prop("gravity_scale", 1.0)
|
||||
|
||||
local function normalize_angle(angle)
|
||||
while angle > 180 do
|
||||
angle = angle - 360
|
||||
end
|
||||
while angle <= -180 do
|
||||
angle = angle + 360
|
||||
end
|
||||
return angle
|
||||
end
|
||||
|
||||
local function angle_delta(a, b)
|
||||
return normalize_angle(a - b)
|
||||
end
|
||||
|
||||
local dir = mat4.mul(tsf:get_rot(), {0, 0, -1})
|
||||
local flight = false
|
||||
|
||||
function jump(multiplier)
|
||||
local vel = body:get_vel()
|
||||
body:set_vel(
|
||||
vec3.add(vel, {0, props.jump_force * (multiplier or 1.0), 0}, vel))
|
||||
end
|
||||
|
||||
function move_vertical(speed, vel)
|
||||
vel = vel or body:get_vel()
|
||||
vel[2] = vel[2] * 0.2 + props.movement_speed * speed * 0.8
|
||||
body:set_vel(vel)
|
||||
end
|
||||
|
||||
local function move_horizontal(speed, dir, vel)
|
||||
vel = vel or body:get_vel()
|
||||
if vec2.length(dir) > 0.0 then
|
||||
vec2.normalize(dir, dir)
|
||||
|
||||
local magnitude = vec2.length({vel[1], vel[3]})
|
||||
|
||||
if magnitude <= 1e-4 or (magnitude < speed or vec2.dot(
|
||||
{vel[1] / magnitude, vel[3] / magnitude}, dir) < 0.9)
|
||||
then
|
||||
vel[1] = vel[1] * 0.2 + dir[1] * speed * 0.8
|
||||
vel[3] = vel[3] * 0.2 + dir[2] * speed * 0.8
|
||||
end
|
||||
magnitude = vec3.length({vel[1], 0, vel[3]})
|
||||
if vec2.dot({vel[1] / magnitude, vel[3] / magnitude}, dir) > 0.5 then
|
||||
vel[1] = vel[1] / magnitude * speed
|
||||
vel[3] = vel[3] / magnitude * speed
|
||||
end
|
||||
end
|
||||
body:set_vel(vel)
|
||||
end
|
||||
|
||||
function go(dir, speed_multiplier, sprint, crouch, vel)
|
||||
local speed = props.movement_speed * speed_multiplier
|
||||
if flight then
|
||||
speed = speed * props.flight_speed_mul
|
||||
end
|
||||
if sprint then
|
||||
speed = speed * props.run_speed_mul
|
||||
elseif crouch then
|
||||
speed = speed * props.crouch_speed_mul
|
||||
end
|
||||
move_horizontal(speed, dir, vel)
|
||||
end
|
||||
|
||||
local headIndex = rig:index("head")
|
||||
|
||||
function look_at(point, change_dir)
|
||||
local pos = tsf:get_pos()
|
||||
local viewdir = vec3.normalize(vec3.sub(point, pos))
|
||||
|
||||
local dot = vec3.dot(viewdir, dir)
|
||||
if dot < 0.0 and not change_dir then
|
||||
viewdir = mat4.mul(tsf:get_rot(), {0, 0, -1})
|
||||
else
|
||||
dir[1] = dir[1] * 0.8 + viewdir[1] * 0.13
|
||||
dir[3] = dir[3] * 0.8 + viewdir[3] * 0.13
|
||||
end
|
||||
|
||||
if not headIndex then
|
||||
return
|
||||
end
|
||||
|
||||
local headrot = mat4.idt()
|
||||
local curdir = mat4.mul(mat4.mul(tsf:get_rot(),
|
||||
rig:get_matrix(headIndex)), {0, 0, -1})
|
||||
|
||||
vec3.mix(curdir, viewdir, 0.2, viewdir)
|
||||
|
||||
headrot = mat4.inverse(mat4.look_at({0,0,0}, viewdir, {0, 1, 0}))
|
||||
headrot = mat4.mul(mat4.inverse(tsf:get_rot()), headrot)
|
||||
rig:set_matrix(headIndex, headrot)
|
||||
end
|
||||
|
||||
function follow_waypoints(pathfinding)
|
||||
pathfinding = pathfinding or entity:require_component("core:pathfinding")
|
||||
local pos = tsf:get_pos()
|
||||
local waypoint = pathfinding.next_waypoint()
|
||||
if not waypoint then
|
||||
return
|
||||
end
|
||||
local speed = props.movement_speed
|
||||
local vel = body:get_vel()
|
||||
dir = vec3.sub(
|
||||
vec3.add(waypoint, {0.5, 0, 0.5}),
|
||||
{pos[1], math.floor(pos[2]), pos[3]}
|
||||
)
|
||||
local upper = dir[2] > 0
|
||||
dir[2] = 0.0
|
||||
vec3.normalize(dir, dir)
|
||||
move_horizontal(speed, {dir[1], dir[3]}, vel)
|
||||
if upper and body:is_grounded() then
|
||||
jump(1.0)
|
||||
end
|
||||
end
|
||||
|
||||
function set_dir(new_dir)
|
||||
dir = new_dir
|
||||
end
|
||||
|
||||
function is_flight() return flight end
|
||||
|
||||
function set_flight(flag) flight = flag end
|
||||
|
||||
local prev_angle = (vec2.angle({dir[3], dir[1]})) % 360
|
||||
|
||||
function on_physics_update(delta)
|
||||
local grounded = body:is_grounded()
|
||||
body:set_vdamping(flight)
|
||||
body:set_gravity_scale({0, flight and 0.0 or props.gravity_scale, 0})
|
||||
body:set_linear_damping(
|
||||
(flight or not grounded) and props.air_damping or props.ground_damping
|
||||
)
|
||||
|
||||
local new_angle = (vec2.angle({dir[3], dir[1]})) % 360
|
||||
local angle = prev_angle
|
||||
|
||||
local adelta = angle_delta(
|
||||
normalize_angle(new_angle),
|
||||
normalize_angle(prev_angle)
|
||||
)
|
||||
local rotate_speed = entity:get_player() == -1 and 200 or 400
|
||||
|
||||
if math.abs(adelta) > 5 then
|
||||
angle = angle + delta * rotate_speed * (adelta > 0 and 1 or -1)
|
||||
end
|
||||
|
||||
tsf:set_rot(mat4.rotate({0, 1, 0}, angle + 180))
|
||||
prev_angle = angle
|
||||
end
|
||||
@@ -0,0 +1,71 @@
|
||||
local target
|
||||
local route
|
||||
local started
|
||||
|
||||
local tsf = entity.transform
|
||||
local body = entity.rigidbody
|
||||
|
||||
agent = pathfinding.create_agent()
|
||||
pathfinding.set_max_visited(agent, 1e3)
|
||||
pathfinding.avoid_tag(agent, "core:liquid", 8)
|
||||
|
||||
function set_target(new_target)
|
||||
target = new_target
|
||||
end
|
||||
|
||||
function set_jump_height(height)
|
||||
pathfinding.set_jump_height(agent, height)
|
||||
end
|
||||
|
||||
function get_target()
|
||||
return target
|
||||
end
|
||||
|
||||
function get_route()
|
||||
return route
|
||||
end
|
||||
|
||||
function next_waypoint()
|
||||
if not route or #route == 0 then
|
||||
return
|
||||
end
|
||||
local waypoint = route[#route]
|
||||
local pos = tsf:get_pos()
|
||||
local dst = vec2.length({
|
||||
math.floor(waypoint[1] - math.floor(pos[1])),
|
||||
math.floor(waypoint[3] - math.floor(pos[3]))
|
||||
})
|
||||
if dst < 1.0 then
|
||||
table.remove(route, #route)
|
||||
end
|
||||
return route[#route]
|
||||
end
|
||||
|
||||
local refresh_internal = 100
|
||||
local frameid = math.random(0, refresh_internal)
|
||||
|
||||
function set_refresh_interval(interval)
|
||||
refresh_internal = interval
|
||||
end
|
||||
|
||||
function on_update()
|
||||
if not started then
|
||||
frameid = frameid + 1
|
||||
if body:is_grounded() then
|
||||
if target and (frameid % refresh_internal == 1 or not route) then
|
||||
pathfinding.make_route_async(agent, tsf:get_pos(), target)
|
||||
started = true
|
||||
end
|
||||
end
|
||||
else
|
||||
local new_route = pathfinding.pull_route(agent)
|
||||
if new_route then
|
||||
route = new_route
|
||||
started = false
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
function on_despawn()
|
||||
pathfinding.remove_agent(agent)
|
||||
end
|
||||
@@ -0,0 +1,62 @@
|
||||
local tsf = entity.transform
|
||||
local body = entity.rigidbody
|
||||
local mob = entity:require_component("core:mob")
|
||||
|
||||
local cheat_speed_mul = 10.0
|
||||
|
||||
local function process_player_inputs(pid, delta)
|
||||
if not hud or hud.is_inventory_open() or menu.page ~= "" then
|
||||
return
|
||||
end
|
||||
local cam = cameras.get("core:first-person")
|
||||
local front = cam:get_front()
|
||||
local right = cam:get_right()
|
||||
front[2] = 0.0
|
||||
vec3.normalize(front, front)
|
||||
|
||||
local isjump = input.is_active('movement.jump')
|
||||
local issprint = input.is_active('movement.sprint')
|
||||
local iscrouch = input.is_active('movement.crouch')
|
||||
local isforward = input.is_active('movement.forward')
|
||||
local ischeat = input.is_active('movement.cheat')
|
||||
local isback = input.is_active('movement.back')
|
||||
local isleft = input.is_active('movement.left')
|
||||
local isright = input.is_active('movement.right')
|
||||
mob.set_flight(player.is_flight(pid))
|
||||
body:set_body_type(player.is_noclip(pid) and "kinematic" or "dynamic")
|
||||
body:set_crouching(iscrouch)
|
||||
|
||||
local vel = body:get_vel()
|
||||
local speed = ischeat and cheat_speed_mul or 1.0
|
||||
|
||||
local dir = {0, 0, 0}
|
||||
|
||||
if isforward then vec3.add(dir, front, dir) end
|
||||
if isback then vec3.sub(dir, front, dir) end
|
||||
if isright then vec3.add(dir, right, dir) end
|
||||
if isleft then vec3.sub(dir, right, dir) end
|
||||
|
||||
if vec3.length(dir) > 0.0 then
|
||||
mob.go({dir[1], dir[3]}, speed, issprint, iscrouch, vel)
|
||||
end
|
||||
|
||||
if mob.is_flight() then
|
||||
if isjump then
|
||||
mob.move_vertical(speed * 3)
|
||||
elseif iscrouch then
|
||||
mob.move_vertical(-speed * 3)
|
||||
end
|
||||
elseif body:is_grounded() and isjump then
|
||||
mob.jump()
|
||||
end
|
||||
end
|
||||
|
||||
function on_physics_update(delta)
|
||||
local pid = entity:get_player()
|
||||
if pid ~= -1 then
|
||||
local pos = tsf:get_pos()
|
||||
local cam = cameras.get("core:first-person")
|
||||
process_player_inputs(pid, delta)
|
||||
mob.look_at(vec3.add(pos, cam:get_front()))
|
||||
end
|
||||
end
|
||||
+44
-1
@@ -22,6 +22,39 @@ local function configure_SSAO()
|
||||
-- for test purposes
|
||||
end
|
||||
|
||||
local function update_hand()
|
||||
local skeleton = gfx.skeletons
|
||||
local pid = hud.get_player()
|
||||
local invid, slot = player.get_inventory(pid)
|
||||
local itemid = inventory.get(invid, slot)
|
||||
|
||||
local cam = cameras.get("core:first-person")
|
||||
local bone = skeleton.index("hand", "item")
|
||||
|
||||
local offset = vec3.mul(vec3.sub(cam:get_pos(), {player.get_pos(pid)}), -1)
|
||||
|
||||
local rotation = cam:get_rot()
|
||||
|
||||
local angle = player.get_rot(pid) - 90
|
||||
local cos = math.cos(angle / (180 / math.pi))
|
||||
local sin = math.sin(angle / (180 / math.pi))
|
||||
|
||||
local newX = offset[1] * cos - offset[3] * sin
|
||||
local newZ = offset[1] * sin + offset[3] * cos
|
||||
|
||||
offset[1] = newX
|
||||
offset[3] = newZ
|
||||
|
||||
local mat = mat4.translate(mat4.idt(), {0.06, 0.035, -0.1})
|
||||
mat4.scale(mat, {0.1, 0.1, 0.1}, mat)
|
||||
mat4.mul(rotation, mat, mat)
|
||||
mat4.rotate(mat, {0, 1, 0}, -90, mat)
|
||||
mat4.translate(mat, offset, mat)
|
||||
|
||||
skeleton.set_matrix("hand", bone, mat)
|
||||
skeleton.set_model("hand", bone, item.model_name(itemid))
|
||||
end
|
||||
|
||||
function on_hud_open()
|
||||
input.add_callback("player.pick", function ()
|
||||
if hud.is_paused() or hud.is_inventory_open() then
|
||||
@@ -63,7 +96,7 @@ function on_hud_open()
|
||||
player.set_noclip(pid, true)
|
||||
end
|
||||
end)
|
||||
|
||||
|
||||
input.add_callback("player.flight", function ()
|
||||
if hud.is_paused() or hud.is_inventory_open() then
|
||||
return
|
||||
@@ -81,4 +114,14 @@ function on_hud_open()
|
||||
end)
|
||||
|
||||
configure_SSAO()
|
||||
|
||||
hud.default_hand_controller = update_hand
|
||||
end
|
||||
|
||||
function on_hud_render()
|
||||
if hud.hand_controller then
|
||||
hud.hand_controller()
|
||||
else
|
||||
update_hand()
|
||||
end
|
||||
end
|
||||
|
||||
@@ -12,7 +12,28 @@ local Text3D = {__index={
|
||||
update_settings=function(self, t) return gfx.text3d.update_settings(self.id, t) end,
|
||||
}}
|
||||
|
||||
local Skeleton = {__index={
|
||||
index=function(self, s) return gfx.skeletons.index(self.name, s) end,
|
||||
get_model=function(self, i) return gfx.skeletons.get_model(self.name, i) end,
|
||||
set_model=function(self, i, s) return gfx.skeletons.set_model(self.name, i, s) end,
|
||||
get_matrix=function(self, i) return gfx.skeletons.get_matrix(self.name, i) end,
|
||||
set_matrix=function(self, i, m) return gfx.skeletons.set_matrix(self.name, i, m) end,
|
||||
get_texture=function(self, i) return gfx.skeletons.get_texture(self.name, i) end,
|
||||
set_texture=function(self, i, s) return gfx.skeletons.set_texture(self.name, i, s) end,
|
||||
is_visible=function(self, i) return gfx.skeletons.is_visible(self.name, i) end,
|
||||
set_visible=function(self, i, b) return gfx.skeletons.set_visible(self.name, i, b) end,
|
||||
get_color=function(self, i) return gfx.skeletons.get_color(self.name, i) end,
|
||||
set_color=function(self, i, c) return gfx.skeletons.set_color(self.name, i, c) end,
|
||||
}}
|
||||
|
||||
gfx.text3d.new = function(pos, text, preset, extension)
|
||||
local id = gfx.text3d.show(pos, text, preset, extension)
|
||||
return setmetatable({id=id}, Text3D)
|
||||
end
|
||||
|
||||
gfx.skeletons = __skeleton
|
||||
gfx.skeletons.get = function(name)
|
||||
if gfx.skeletons.exists(name) then
|
||||
return setmetatable({name=name}, Skeleton)
|
||||
end
|
||||
end
|
||||
|
||||
@@ -6,28 +6,29 @@ local names = {
|
||||
"shadeless", "ambient-occlusion", "breakable", "selectable", "grounded",
|
||||
"hidden", "draw-group", "picking-item", "surface-replacement", "script-name",
|
||||
"ui-layout", "inventory-size", "tick-interval", "overlay-texture",
|
||||
"translucent", "fields", "particles", "icon-type", "icon", "placing-block",
|
||||
"translucent", "fields", "particles", "icon-type", "icon", "placing-block",
|
||||
"stack-size", "name", "script-file", "culling"
|
||||
}
|
||||
for name, _ in pairs(user_props) do
|
||||
table.insert(names, name)
|
||||
end
|
||||
-- remove undefined properties
|
||||
for id, blockprops in pairs(block.properties) do
|
||||
for propname, value in pairs(blockprops) do
|
||||
if not table.has(names, propname) then
|
||||
blockprops[propname] = nil
|
||||
end
|
||||
end
|
||||
end
|
||||
for id, itemprops in pairs(item.properties) do
|
||||
for propname, value in pairs(itemprops) do
|
||||
if not table.has(names, propname) then
|
||||
itemprops[propname] = nil
|
||||
|
||||
-- remove undefined properties and build tags set
|
||||
local function process_properties(lib)
|
||||
for id, props in pairs(lib.properties) do
|
||||
for propname, _ in pairs(props) do
|
||||
if not table.has(names, propname) then
|
||||
props[propname] = nil
|
||||
end
|
||||
end
|
||||
|
||||
props.tags_set = lib.__get_tags(id)
|
||||
end
|
||||
end
|
||||
|
||||
process_properties(block)
|
||||
process_properties(item)
|
||||
|
||||
local function make_read_only(t)
|
||||
setmetatable(t, {
|
||||
__newindex = function()
|
||||
@@ -57,10 +58,22 @@ local function cache_names(library)
|
||||
function library.index(name)
|
||||
return indices[name]
|
||||
end
|
||||
|
||||
function library.has_tag(id, tag)
|
||||
if id == nil then
|
||||
error("id is nil")
|
||||
end
|
||||
local props = library.properties[id]
|
||||
local tags_set = props.tags_set
|
||||
if tags_set then
|
||||
return tags_set[tag]
|
||||
else
|
||||
return false
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
cache_names(block)
|
||||
cache_names(item)
|
||||
|
||||
local scripts_registry = require "core:internal/scripts_registry"
|
||||
scripts_registry.build_registry()
|
||||
__vc_scripts_registry.build_registry()
|
||||
|
||||
@@ -157,6 +157,16 @@ console.add_command(
|
||||
end
|
||||
)
|
||||
|
||||
|
||||
console.add_command(
|
||||
"entity.spawn name:str x:num~pos.x y:num~pos.y z:num~pos.z",
|
||||
"Spawn entity with default parameters",
|
||||
function(args, kwargs)
|
||||
local eid = entities.spawn(args[1], {args[2], args[3], args[4]})
|
||||
return string.format("spawned %s at %s, %s, %s", unpack(args))
|
||||
end
|
||||
)
|
||||
|
||||
console.add_command(
|
||||
"entity.despawn entity:sel=$entity.selected",
|
||||
"Despawn entity",
|
||||
|
||||
+123
-54
@@ -1,3 +1,5 @@
|
||||
local enable_experimental = core.get_setting("debug.enable-experimental")
|
||||
|
||||
------------------------------------------------
|
||||
------ Extended kit of standard functions ------
|
||||
------------------------------------------------
|
||||
@@ -77,13 +79,20 @@ local function complete_app_lib(app)
|
||||
coroutine.yield()
|
||||
end
|
||||
|
||||
function app.sleep_until(predicate, max_ticks)
|
||||
function app.sleep_until(predicate, max_ticks, max_time)
|
||||
max_ticks = max_ticks or 1e9
|
||||
max_time = max_time or 1e9
|
||||
local ticks = 0
|
||||
while ticks < max_ticks and not predicate() do
|
||||
local start_time = os.clock()
|
||||
while ticks < max_ticks and
|
||||
os.clock() - start_time < max_time
|
||||
and not predicate() do
|
||||
app.tick()
|
||||
ticks = ticks + 1
|
||||
end
|
||||
if os.clock() - start_time >= max_time then
|
||||
error("timeout")
|
||||
end
|
||||
if ticks == max_ticks then
|
||||
error("max ticks exceed")
|
||||
end
|
||||
@@ -130,61 +139,55 @@ function inventory.decrement(invid, slot, count)
|
||||
end
|
||||
end
|
||||
|
||||
------------------------------------------------
|
||||
------------------- Events ---------------------
|
||||
------------------------------------------------
|
||||
events = {
|
||||
handlers = {}
|
||||
}
|
||||
function inventory.get_caption(invid, slot)
|
||||
local item_id, count = inventory.get(invid, slot)
|
||||
local caption = inventory.get_data(invid, slot, "caption")
|
||||
if not caption then return item.caption(item_id) end
|
||||
|
||||
function events.on(event, func)
|
||||
if events.handlers[event] == nil then
|
||||
events.handlers[event] = {}
|
||||
end
|
||||
table.insert(events.handlers[event], func)
|
||||
return caption
|
||||
end
|
||||
|
||||
function events.reset(event, func)
|
||||
if func == nil then
|
||||
events.handlers[event] = nil
|
||||
else
|
||||
events.handlers[event] = {func}
|
||||
function inventory.set_caption(invid, slot, caption)
|
||||
local itemid, itemcount = inventory.get(invid, slot)
|
||||
if itemid == 0 then
|
||||
return
|
||||
end
|
||||
if caption == nil or type(caption) ~= "string" then
|
||||
caption = ""
|
||||
end
|
||||
inventory.set_data(invid, slot, "caption", caption)
|
||||
end
|
||||
|
||||
function events.remove_by_prefix(prefix)
|
||||
for name, handlers in pairs(events.handlers) do
|
||||
local actualname = name
|
||||
if type(name) == 'table' then
|
||||
actualname = name[1]
|
||||
end
|
||||
if actualname:sub(1, #prefix+1) == prefix..':' then
|
||||
events.handlers[actualname] = nil
|
||||
end
|
||||
end
|
||||
function inventory.get_description(invid, slot)
|
||||
local item_id, count = inventory.get(invid, slot)
|
||||
local description = inventory.get_data(invid, slot, "description")
|
||||
if not description then return item.description(item_id) end
|
||||
|
||||
return description
|
||||
end
|
||||
|
||||
function inventory.set_description(invid, slot, description)
|
||||
local itemid, itemcount = inventory.get(invid, slot)
|
||||
if itemid == 0 then
|
||||
return
|
||||
end
|
||||
if description == nil or type(description) ~= "string" then
|
||||
description = ""
|
||||
end
|
||||
inventory.set_data(invid, slot, "description", description)
|
||||
end
|
||||
|
||||
if enable_experimental then
|
||||
require "core:internal/maths_inline"
|
||||
end
|
||||
|
||||
asserts = require "core:internal/asserts"
|
||||
events = require "core:internal/events"
|
||||
|
||||
function pack.unload(prefix)
|
||||
events.remove_by_prefix(prefix)
|
||||
end
|
||||
|
||||
function events.emit(event, ...)
|
||||
local result = nil
|
||||
local handlers = events.handlers[event]
|
||||
if handlers == nil then
|
||||
return nil
|
||||
end
|
||||
for _, func in ipairs(handlers) do
|
||||
local status, newres = xpcall(func, __vc__error, ...)
|
||||
if not status then
|
||||
debug.error("error in event ("..event..") handler: "..newres)
|
||||
else
|
||||
result = result or newres
|
||||
end
|
||||
end
|
||||
return result
|
||||
end
|
||||
|
||||
gui_util = require "core:internal/gui_util"
|
||||
|
||||
Document = gui_util.Document
|
||||
@@ -199,6 +202,7 @@ end
|
||||
_GUI_ROOT = Document.new("core:root")
|
||||
_MENU = _GUI_ROOT.menu
|
||||
menu = _MENU
|
||||
gui.root = _GUI_ROOT
|
||||
|
||||
--- Console library extension ---
|
||||
console.cheats = {}
|
||||
@@ -278,11 +282,33 @@ entities.get_all = function(uids)
|
||||
return stdcomp.get_all(uids)
|
||||
end
|
||||
end
|
||||
|
||||
local bytearray = require "core:internal/bytearray"
|
||||
Bytearray = bytearray.FFIBytearray
|
||||
Bytearray_as_string = bytearray.FFIBytearray_as_string
|
||||
Bytearray_construct = function(...) return Bytearray(...) end
|
||||
|
||||
__vc_scripts_registry = require "core:internal/scripts_registry"
|
||||
|
||||
file.open = require "core:internal/stream_providers/file"
|
||||
file.open_named_pipe = require "core:internal/stream_providers/named_pipe"
|
||||
|
||||
if ffi.os == "Windows" then
|
||||
ffi.cdef[[
|
||||
unsigned long GetCurrentProcessId();
|
||||
]]
|
||||
|
||||
os.pid = ffi.C.GetCurrentProcessId()
|
||||
else
|
||||
ffi.cdef[[
|
||||
int getpid(void);
|
||||
]]
|
||||
|
||||
os.pid = ffi.C.getpid()
|
||||
end
|
||||
|
||||
ffi = nil
|
||||
__vc_lock_internal_modules()
|
||||
|
||||
math.randomseed(time.uptime() * 1536227939)
|
||||
|
||||
@@ -411,8 +437,41 @@ function __vc_on_hud_open()
|
||||
hud.open_permanent("core:ingame_chat")
|
||||
end
|
||||
|
||||
local Schedule = require "core:schedule"
|
||||
|
||||
local ScheduleGroup_mt = {
|
||||
__index = {
|
||||
publish = function(self, schedule)
|
||||
local id = self._next_schedule
|
||||
self._schedules[id] = schedule
|
||||
self._next_schedule = id + 1
|
||||
end,
|
||||
tick = function(self, dt)
|
||||
for id, schedule in pairs(self._schedules) do
|
||||
schedule:tick(dt)
|
||||
end
|
||||
self.common:tick(dt)
|
||||
end,
|
||||
remove = function(self, id)
|
||||
self._schedules[id] = nil
|
||||
end,
|
||||
}
|
||||
}
|
||||
|
||||
local function ScheduleGroup()
|
||||
return setmetatable({
|
||||
_next_schedule = 1,
|
||||
_schedules = {},
|
||||
common = Schedule()
|
||||
}, ScheduleGroup_mt)
|
||||
end
|
||||
|
||||
time.schedules = {}
|
||||
|
||||
local RULES_FILE = "world:rules.toml"
|
||||
function __vc_on_world_open()
|
||||
time.schedules.world = ScheduleGroup()
|
||||
|
||||
if not file.exists(RULES_FILE) then
|
||||
return
|
||||
end
|
||||
@@ -422,6 +481,10 @@ function __vc_on_world_open()
|
||||
end
|
||||
end
|
||||
|
||||
function __vc_on_world_tick(tps)
|
||||
time.schedules.world:tick(1.0 / tps)
|
||||
end
|
||||
|
||||
function __vc_on_world_save()
|
||||
local rule_values = {}
|
||||
for name, rule in pairs(rules.rules) do
|
||||
@@ -434,6 +497,7 @@ function __vc_on_world_quit()
|
||||
_rules.clear()
|
||||
gui_util:__reset_local()
|
||||
stdcomp.__reset()
|
||||
file.__close_all_descriptors()
|
||||
end
|
||||
|
||||
local __vc_coroutines = {}
|
||||
@@ -475,15 +539,18 @@ function start_coroutine(chunk, name)
|
||||
local co = coroutine.create(function()
|
||||
local status, error = xpcall(chunk, function(err)
|
||||
local fullmsg = "error: "..string.match(err, ": (.+)").."\n"..debug.traceback()
|
||||
gui.alert(fullmsg, function()
|
||||
if world.is_open() then
|
||||
__vc_app.close_world()
|
||||
else
|
||||
__vc_app.reset_content()
|
||||
menu:reset()
|
||||
menu.page = "main"
|
||||
end
|
||||
end)
|
||||
|
||||
if hud then
|
||||
gui.alert(fullmsg, function()
|
||||
if world.is_open() then
|
||||
__vc_app.close_world()
|
||||
else
|
||||
__vc_app.reset_content()
|
||||
menu:reset()
|
||||
menu.page = "main"
|
||||
end
|
||||
end)
|
||||
end
|
||||
return fullmsg
|
||||
end)
|
||||
if not status then
|
||||
@@ -521,6 +588,8 @@ function __process_post_runnables()
|
||||
end
|
||||
|
||||
network.__process_events()
|
||||
block.__process_register_events()
|
||||
block.__perform_ticks(time.delta())
|
||||
end
|
||||
|
||||
function time.post_runnable(runnable)
|
||||
|
||||
@@ -20,6 +20,18 @@ if not ipairs_mt_supported then
|
||||
end
|
||||
end
|
||||
|
||||
function await(co)
|
||||
local res, err
|
||||
while coroutine.status(co) ~= 'dead' do
|
||||
coroutine.yield()
|
||||
res, err = coroutine.resume(co)
|
||||
if err then
|
||||
return res, err
|
||||
end
|
||||
end
|
||||
return res, err
|
||||
end
|
||||
|
||||
local _ffi = ffi
|
||||
function __vc_Canvas_set_data(self, data)
|
||||
if type(data) == "cdata" then
|
||||
@@ -538,6 +550,8 @@ function reload_module(name)
|
||||
end
|
||||
end
|
||||
|
||||
local internal_locked = false
|
||||
|
||||
-- Load script with caching
|
||||
--
|
||||
-- path - script path `contentpack:filename`.
|
||||
@@ -547,6 +561,11 @@ end
|
||||
function __load_script(path, nocache)
|
||||
local packname, filename = parse_path(path)
|
||||
|
||||
if internal_locked and (packname == "res" or packname == "core")
|
||||
and filename:starts_with("modules/internal") then
|
||||
error("access to core:internal modules outside of [core]")
|
||||
end
|
||||
|
||||
-- __cached_scripts used in condition because cached result may be nil
|
||||
if not nocache and __cached_scripts[path] ~= nil then
|
||||
return package.loaded[path]
|
||||
@@ -567,6 +586,10 @@ function __load_script(path, nocache)
|
||||
return result
|
||||
end
|
||||
|
||||
function __vc_lock_internal_modules()
|
||||
internal_locked = true
|
||||
end
|
||||
|
||||
function require(path)
|
||||
if not string.find(path, ':') then
|
||||
local prefix, _ = parse_path(_debug_getinfo(2).source)
|
||||
@@ -645,3 +668,5 @@ end
|
||||
|
||||
bit.compile = require "core:bitwise/compiler"
|
||||
bit.execute = require "core:bitwise/executor"
|
||||
|
||||
random.Random = require "core:internal/random_generator"
|
||||
|
||||
@@ -7,6 +7,7 @@ world.convert-block-layouts=Blocks fields have changes! Convert world files?
|
||||
pack.remove-confirm=Do you want to erase all pack(s) content from the world forever?
|
||||
error.pack-not-found=Could not to find pack
|
||||
error.dependency-not-found=Dependency pack is not found
|
||||
error.dependency-version-not-met=Dependency pack version is not met.
|
||||
world.delete-confirm=Do you want to delete world forever?
|
||||
world.generators.default=Default
|
||||
world.generators.flat=Flat
|
||||
@@ -22,6 +23,7 @@ graphics.dense-render.tooltip=Enables transparency in blocks like leaves
|
||||
|
||||
# settings
|
||||
settings.Controls Search Mode=Search by attached button name
|
||||
settings.Conflict=Possible conflicts found
|
||||
|
||||
# Bindings
|
||||
chunks.reload=Reload Chunks
|
||||
|
||||
@@ -32,6 +32,7 @@ devtools.output=Вывод
|
||||
|
||||
error.pack-not-found=Не удалось найти пакет
|
||||
error.dependency-not-found=Используемая зависимость не найдена
|
||||
error.dependency-version-not-met=Версия зависимости не соответствует необходимой
|
||||
pack.remove-confirm=Удалить весь поставляемый паком/паками контент из мира (безвозвратно)?
|
||||
|
||||
# Подсказки
|
||||
@@ -100,6 +101,7 @@ settings.Controls Search Mode=Поиск по привязанной кнопк
|
||||
settings.Limit Background FPS=Ограничить фоновую частоту кадров
|
||||
settings.Advanced render=Продвинутый рендер
|
||||
settings.Shadows quality=Качество теней
|
||||
settings.Conflict=Найдены возможные конфликты
|
||||
|
||||
# Управление
|
||||
chunks.reload=Перезагрузить Чанки
|
||||
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 531 B |
Reference in New Issue
Block a user