diff --git a/doc/en/audio.md b/doc/en/audio.md
index cacd7107..09510402 100644
--- a/doc/en/audio.md
+++ b/doc/en/audio.md
@@ -203,3 +203,79 @@ audio.count_speakers() -> integer
-- get current number of playing streams
audio.count_streams() -> integer
```
+
+### audio.PCMStream
+
+```lua
+-- Creating a PCM data source
+local stream = audio.PCMStream(
+ -- Sample rate
+ sample_rate: integer,
+ -- Number of channels (1 - mono, 2 - stereo)
+ channels: integer,
+ -- Number of bits per sample (8 or 16)
+ bits_per_sample: integer,
+)
+
+-- Feeding PCM data into the stream
+stream:feed(
+ -- PCM data to be fed into the stream
+ data: Bytearray
+)
+
+-- Publishing the PCM data source for use by the engine systems
+stream:share(
+ -- Alias of the audio stream, which can be referenced in audio.play_stream
+ alias: string
+)
+
+-- Creating a sound from the PCM data in the stream
+stream:create_sound(
+ -- Name of the created sound
+ name: string
+)
+```
+
+### Audio Recording
+
+```lua
+-- Requests access to audio recording
+-- On confirmation, the callback receives a token for use in audio.input.fetch
+audio.input.request_open(callback: function(string))
+
+-- Reads new PCM audio input data
+audio.input.fetch(
+ -- Token obtained through audio.input.request_open
+ access_token: string,
+ -- Maximum buffer size in bytes (optional)
+ [optional] max_read_size: integer
+)
+```
+
+### Example of Audio Generation:
+
+```lua
+-- For working with 16-bit samples, use a U16view over Bytearray
+-- Example:
+local max_amplitude = 32767
+local sample_rate = 44100
+local total_samples = sample_rate * 5 -- 5 seconds of mono
+local bytes = Bytearray(total_samples * 2) -- 5 seconds of 16-bit mono
+local samples = I16view(bytes)
+
+local frequency_hz = 400
+for i=1, total_samples do
+ local value = math.sin(i * math.pi * 2 / sample_rate * frequency_hz)
+ samples[i] = value * max_amplitude
+end
+
+local stream_name = "test-stream"
+local stream = audio.PCMStream(sample_rate, 1, 16)
+stream:feed(bytes)
+stream:share(stream_name)
+
+local volume = 1.0
+local pitch = 1.0
+local channel = "ui"
+audio.play_stream_2d(stream_name, volume, pitch, channel)
+```
diff --git a/doc/ru/audio.md b/doc/ru/audio.md
index 1fce6c35..8291e7ba 100644
--- a/doc/ru/audio.md
+++ b/doc/ru/audio.md
@@ -29,6 +29,7 @@
Доступ к спикерам производится по целочисленным id, которые не повторяются за время работы движка, следует избегать хранения прямых указателей на объекты класса.
Нумерация ID спикеров начинается с 1. ID 0 означает невозможность воспроизведения, по какой-либо причине.
+
### Звук (Sound)
Звуковые данные загруженные в память для возможности одновременного воспроизведения из нескольких источников. Может предоставлять доступ к PCM данным.
@@ -203,3 +204,79 @@ audio.count_speakers() -> integer
-- получить текущее число проигрываемых аудио-потоков
audio.count_streams() -> integer
```
+
+### audio.PCMStream
+
+```lua
+-- создание источника PCM данных
+local stream = audio.PCMStream(
+ -- частота дискретизации
+ sample_rate: integer,
+ -- число каналов (1 - моно, 2 - стерео)
+ channels: integer,
+ -- число бит на сэмпл (8 или 16)
+ bits_per_sample: integer,
+)
+
+-- подача PCM данных в поток
+stream:feed(
+ -- PCM данные для подачи в поток
+ data: Bytearray
+)
+
+-- публикация источника PCM данных для использования системами движка
+stream:share(
+ -- имя потокового аудио, которое можно будет указать в audio.play_stream
+ alias: string
+)
+
+-- создание звука из имеющихся в потоке PCM данных
+stream:create_sound(
+ -- имя создаваемого звука
+ name: string
+)
+```
+
+### Запись звука
+
+```lua
+-- запрашивает доступ к записи звука
+-- при подтверждении, в callback передаётся токен для использовании в audio.input.fetch
+audio.input.request_open(callback: function(string))
+
+-- читает новые PCM данные аудио ввода
+audio.input.fetch(
+ -- токен, полученный через audio.input.request_open
+ access_token: string,
+ -- максимальное размер буфера в байтах
+ [опционально] max_read_size: integer
+)
+```
+
+### Пример генерации аудио:
+
+```lua
+-- для работы с 16-битными семплами используйте U16view поверх Bytearray
+-- пример:
+local max_amplitude = 32767
+local sample_rate = 44100
+local total_samples = sample_rate * 5 -- 5 секунд моно
+local bytes = Bytearray(total_samples * 2) -- 5 секунд 16 бит моно
+local samples = I16view(bytes)
+
+local frequency_hz = 400
+for i=1, total_samples do
+ local value = math.sin(i * math.pi * 2 / sample_rate * frequency_hz)
+ samples[i] = value * max_amplitude
+end
+
+local stream_name = "test-stream"
+local stream = audio.PCMStream(sample_rate, 1, 16)
+stream:feed(bytes)
+stream:share(stream_name)
+
+local volume = 1.0
+local pitch = 1.0
+local channel = "ui"
+audio.play_stream_2d(stream_name, volume, pitch, channel)
+```
diff --git a/res/layouts/pages/settings_audio.xml.lua b/res/layouts/pages/settings_audio.xml.lua
index 9b29bd52..29859819 100644
--- a/res/layouts/pages/settings_audio.xml.lua
+++ b/res/layouts/pages/settings_audio.xml.lua
@@ -24,10 +24,46 @@ function update_setting(x, id, name, postfix)
)
end
+local initialized = false
+
function on_open()
+ if not initialized then
+ initialized = true
+ local token = audio.input.__get_core_token()
+ document.root:add("")
+ local prev_amplitude = 0.0
+ document.tm:setInterval(16, function()
+ audio.input.fetch(token)
+ local amplitude = audio.input.get_max_amplitude()
+ if amplitude > 0.0 then
+ amplitude = math.sqrt(amplitude)
+ end
+ amplitude = math.max(amplitude, prev_amplitude - time.delta())
+ document.input_volume_inner.size = {
+ prev_amplitude *
+ document.input_volume_outer.size[1],
+ document.input_volume_outer.size[2]
+ }
+ prev_amplitude = amplitude
+ end)
+ end
create_setting("audio.volume-master", "Master Volume", 0.01)
create_setting("audio.volume-regular", "Regular Sounds", 0.01)
create_setting("audio.volume-ui", "UI Sounds", 0.01)
create_setting("audio.volume-ambient", "Ambient", 0.01)
create_setting("audio.volume-music", "Music", 0.01)
+ document.root:add("")
+ document.root:add("")
+ document.root:add(""
+ ..""
+ .."")
+ local selectbox = document.input_device_select
+ local devices = {}
+ local names = audio.get_input_devices_names()
+ for i, name in ipairs(names) do
+ table.insert(devices, {value=name, text=name})
+ end
+ selectbox.options = devices
+ selectbox.value = audio.get_input_info().device_specifier
end
diff --git a/res/layouts/templates/problem.xml b/res/layouts/templates/problem.xml
index f4382dbb..b2bfdfe9 100644
--- a/res/layouts/templates/problem.xml
+++ b/res/layouts/templates/problem.xml
@@ -1,7 +1,7 @@
-
+
diff --git a/res/layouts/templates/script_file.xml b/res/layouts/templates/script_file.xml
index f5d55729..69a63342 100644
--- a/res/layouts/templates/script_file.xml
+++ b/res/layouts/templates/script_file.xml
@@ -6,7 +6,7 @@
onclick='%{open_func}("%{filename}")'
markup='md'
tooltip='%{unit}'
- sizefunc="-1,-1">
+ size-func="-1,-1">
[#FFFFFF80]%{path}[#FFFFFFFF]%{name}
diff --git a/res/modules/internal/audio_input.lua b/res/modules/internal/audio_input.lua
new file mode 100644
index 00000000..adf44b97
--- /dev/null
+++ b/res/modules/internal/audio_input.lua
@@ -0,0 +1,63 @@
+local _base64_encode_urlsafe = base64.encode_urlsafe
+local _random_bytes = random.bytes
+local core_token = _base64_encode_urlsafe(_random_bytes(18))
+
+local audio_input_tokens_store = {[core_token] = "core"}
+audio.input = {}
+
+local _gui_confirm = gui.confirm
+local _debug_pack_by_frame = debug.get_pack_by_frame
+local _audio_fetch_input = audio.__fetch_input
+audio.__fetch_input = nil
+local MAX_FETCH = 44100 * 4
+local MAX_AMPLITUDE = 32768
+
+local total_fetch = Bytearray()
+local max_amplitude = 0.0
+
+function audio.__reset_fetch_buffer()
+ total_fetch:clear()
+ max_amplitude = 0.0
+end
+
+function audio.input.get_max_amplitude()
+ return max_amplitude / MAX_AMPLITUDE
+end
+
+function audio.input.fetch(token, size)
+ size = size or MAX_FETCH
+ if audio_input_tokens_store[token] then
+ if #total_fetch >= size then
+ return total_fetch:slice(1, size)
+ end
+ local fetched = _audio_fetch_input(size - #total_fetch)
+ if not fetched then
+ return
+ end
+ for i, sample in ipairs(I16view(fetched)) do
+ max_amplitude = math.max(math.abs(sample))
+ end
+ total_fetch:append(fetched)
+ return total_fetch:slice()
+ end
+ error("access denied")
+end
+
+local GRANT_PERMISSION_MSG = "Grant '%{0}' pack audio recording permission?"
+
+function audio.input.request_open(callback)
+ local token = _base64_encode_urlsafe(_random_bytes(18))
+ local caller = _debug_pack_by_frame(1)
+ _gui_confirm(gui.str(GRANT_PERMISSION_MSG):gsub("%%{0}", caller), function()
+ audio_input_tokens_store[token] = caller
+ callback(token)
+ menu:reset()
+ end)
+end
+
+function audio.input.__get_core_token()
+ local caller = _debug_pack_by_frame(1)
+ if caller == "core" then
+ return core_token
+ end
+end
diff --git a/res/modules/internal/bytearray.lua b/res/modules/internal/bytearray.lua
index 56795499..c292d6a8 100644
--- a/res/modules/internal/bytearray.lua
+++ b/res/modules/internal/bytearray.lua
@@ -14,6 +14,8 @@ FFI.cdef[[
local malloc = FFI.C.malloc
local free = FFI.C.free
+local FFIBytearray
+local bytearray_type
local function grow_buffer(self, elems)
local new_capacity = math.ceil(self.capacity / 0.75 + elems)
@@ -119,6 +121,23 @@ local function get_capacity(self)
return self.capacity
end
+local function slice(self, offset, length)
+ offset = offset or 1
+ length = length or (self.size - offset + 1)
+ if offset < 1 or offset > self.size then
+ return FFIBytearray(0)
+ end
+ if offset + length - 1 > self.size then
+ length = self.size - offset + 1
+ end
+ local buffer = malloc(length)
+ if not buffer then
+ error("malloc(" .. length .. ") returned NULL")
+ end
+ FFI.copy(buffer, self.bytes + (offset - 1), length)
+ return bytearray_type(buffer, length, length)
+end
+
local bytearray_methods = {
append=append,
insert=insert,
@@ -127,6 +146,7 @@ local bytearray_methods = {
clear=clear,
reserve=reserve,
get_capacity=get_capacity,
+ slice=slice,
}
local bytearray_mt = {
@@ -168,9 +188,9 @@ local bytearray_mt = {
}
bytearray_mt.__pairs = bytearray_mt.__ipairs
-local bytearray_type = FFI.metatype("bytearray_t", bytearray_mt)
+bytearray_type = FFI.metatype("bytearray_t", bytearray_mt)
-local FFIBytearray = {
+FFIBytearray = {
__call = function (self, n)
local t = type(n)
if t == "string" then
@@ -210,7 +230,59 @@ local function FFIBytearray_as_string(bytes)
end
end
+local function create_FFIview_class(name, typename, typesize)
+ local FFIU16view_mt = {
+ __index = function(self, key)
+ if key <= 0 or key > self.size then
+ return
+ end
+ return self.ptr[key - 1]
+ end,
+ __newindex = function(self, key, value)
+ if key == self.size + 1 then
+ return append(self, value)
+ elseif key <= 0 or key > self.size then
+ return
+ end
+ self.ptr[key - 1] = value
+ end,
+ __len = function(self)
+ return self.size
+ end,
+ __tostring = function(self)
+ return string.format(name .. "[%s]{...}", tonumber(self.size))
+ end,
+ __ipairs = function(self)
+ local i = 0
+ return function()
+ i = i + 1
+ if i <= self.size then
+ return i, self.ptr[i - 1]
+ end
+ end
+ end
+ }
+ return function (bytes)
+ local ptr = FFI.cast(typename .. "*", bytes.bytes)
+ local x = setmetatable({
+ bytes=bytes,
+ ptr=ptr,
+ size=math.floor(bytes.size / typesize),
+ }, FFIU16view_mt)
+ return x
+ end
+end
+
+local FFII16view = create_FFIview_class("FFII16view", "int16_t", 2)
+local FFIU16view = create_FFIview_class("FFIU16view", "uint16_t", 2)
+local FFII32view = create_FFIview_class("FFII32view", "int32_t", 4)
+local FFIU32view = create_FFIview_class("FFIU32view", "uint32_t", 4)
+
return {
FFIBytearray = setmetatable(FFIBytearray, FFIBytearray),
- FFIBytearray_as_string = FFIBytearray_as_string
+ FFIBytearray_as_string = FFIBytearray_as_string,
+ FFIU16view = FFIU16view,
+ FFII16view = FFII16view,
+ FFIU32view = FFIU32view,
+ FFII32view = FFII32view,
}
diff --git a/res/modules/internal/debugging.lua b/res/modules/internal/debugging.lua
new file mode 100644
index 00000000..cef40202
--- /dev/null
+++ b/res/modules/internal/debugging.lua
@@ -0,0 +1,146 @@
+local breakpoints = {}
+local dbg_steps_mode = false
+local dbg_step_into_func = false
+local hook_lock = false
+local current_func
+local current_func_stack_size
+
+local __parse_path = parse_path
+local _debug_getinfo = debug.getinfo
+local _debug_getlocal = debug.getlocal
+local __pause = debug.pause
+local __error = error
+local __sethook = debug.sethook
+
+-- 'return' hook not called for some functions
+-- todo: speedup
+local function calc_stack_size()
+ local s = debug.traceback("", 2)
+ local count = 0
+ for i in s:gmatch("\n") do
+ count = count + 1
+ end
+ return count
+end
+
+local is_debugging = debug.is_debugging()
+if is_debugging then
+ __sethook(function (e, line)
+ if e == "return" then
+ local info = _debug_getinfo(2)
+ if info.func == current_func then
+ current_func = nil
+ end
+ end
+ if dbg_steps_mode and not hook_lock then
+ hook_lock = true
+
+ if not dbg_step_into_func then
+ local func = _debug_getinfo(2).func
+ if func ~= current_func then
+ return
+ end
+ if current_func_stack_size ~= calc_stack_size() then
+ return
+ end
+ end
+ current_func = func
+ __pause("step")
+ debug.pull_events()
+ end
+ hook_lock = false
+ local bps = breakpoints[line]
+ if not bps then
+ return
+ end
+ local source = _debug_getinfo(2).source
+ if not bps[source] then
+ return
+ end
+ current_func = _debug_getinfo(2).func
+ current_func_stack_size = calc_stack_size()
+ __pause("breakpoint")
+ debug.pull_events()
+ end, "lr")
+end
+
+local DBG_EVENT_SET_BREAKPOINT = 1
+local DBG_EVENT_RM_BREAKPOINT = 2
+local DBG_EVENT_STEP = 3
+local DBG_EVENT_STEP_INTO_FUNCTION = 4
+local DBG_EVENT_RESUME = 5
+local DBG_EVENT_GET_VALUE = 6
+local __pull_events = debug.__pull_events
+local __sendvalue = debug.__sendvalue
+debug.__pull_events = nil
+debug.__sendvalue = nil
+
+function debug.get_pack_by_frame(func)
+ local prefix, _ = __parse_path(_debug_getinfo(func, "S").source)
+ return prefix
+end
+
+function debug.pull_events()
+ if not is_debugging then
+ return
+ end
+ if not debug.is_debugging() then
+ is_debugging = false
+ __sethook()
+ end
+ local events = __pull_events()
+ if not events then
+ return
+ end
+ for i, event in ipairs(events) do
+ if event[1] == DBG_EVENT_SET_BREAKPOINT then
+ debug.set_breakpoint(event[2], event[3])
+ elseif event[1] == DBG_EVENT_RM_BREAKPOINT then
+ debug.remove_breakpoint(event[2], event[3])
+ elseif event[1] == DBG_EVENT_STEP then
+ dbg_steps_mode = true
+ dbg_step_into_func = false
+ elseif event[1] == DBG_EVENT_STEP_INTO_FUNCTION then
+ dbg_steps_mode = true
+ dbg_step_into_func = true
+ elseif event[1] == DBG_EVENT_RESUME then
+ dbg_steps_mode = false
+ dbg_step_into_func = false
+ elseif event[1] == DBG_EVENT_GET_VALUE then
+ local _, value = _debug_getlocal(event[2] + 3, event[3])
+ for _, key in ipairs(event[4]) do
+ if value == nil then
+ value = "error: index nil value"
+ break
+ end
+ value = value[key]
+ end
+ __sendvalue(value, event[2], event[3], event[4])
+ __pause()
+ end
+ end
+end
+
+function debug.set_breakpoint(source, line)
+ local bps = breakpoints[line]
+ if not bps then
+ bps = {}
+ breakpoints[line] = bps
+ end
+ bps[source] = true
+end
+
+function debug.remove_breakpoint(source, line)
+ local bps = breakpoints[line]
+ if not bps then
+ return
+ end
+ bps[source] = nil
+end
+
+function error(message, level)
+ if is_debugging then
+ __pause("exception", message)
+ end
+ __error(message, level)
+end
diff --git a/res/modules/internal/deprecated.lua b/res/modules/internal/deprecated.lua
new file mode 100644
index 00000000..0be82795
--- /dev/null
+++ b/res/modules/internal/deprecated.lua
@@ -0,0 +1,48 @@
+-- --------- Deprecated functions ------ --
+local function wrap_deprecated(func, name, alternatives)
+ return function (...)
+ on_deprecated_call(name, alternatives)
+ return func(...)
+ end
+end
+
+block_index = wrap_deprecated(block.index, "block_index", "block.index")
+block_name = wrap_deprecated(block.name, "block_name", "block.name")
+blocks_count = wrap_deprecated(block.defs_count, "blocks_count", "block.defs_count")
+is_solid_at = wrap_deprecated(block.is_solid_at, "is_solid_at", "block.is_solid_at")
+is_replaceable_at = wrap_deprecated(block.is_replaceable_at, "is_replaceable_at", "block.is_replaceable_at")
+set_block = wrap_deprecated(block.set, "set_block", "block.set")
+get_block = wrap_deprecated(block.get, "get_block", "block.get")
+get_block_X = wrap_deprecated(block.get_X, "get_block_X", "block.get_X")
+get_block_Y = wrap_deprecated(block.get_Y, "get_block_Y", "block.get_Y")
+get_block_Z = wrap_deprecated(block.get_Z, "get_block_Z", "block.get_Z")
+get_block_states = wrap_deprecated(block.get_states, "get_block_states", "block.get_states")
+set_block_states = wrap_deprecated(block.set_states, "set_block_states", "block.set_states")
+get_block_rotation = wrap_deprecated(block.get_rotation, "get_block_rotation", "block.get_rotation")
+set_block_rotation = wrap_deprecated(block.set_rotation, "set_block_rotation", "block.set_rotation")
+get_block_user_bits = wrap_deprecated(block.get_user_bits, "get_block_user_bits", "block.get_user_bits")
+set_block_user_bits = wrap_deprecated(block.set_user_bits, "set_block_user_bits", "block.set_user_bits")
+
+function load_script(path, nocache)
+ on_deprecated_call("load_script", "require or loadstring")
+ return __load_script(path, nocache)
+end
+
+_dofile = dofile
+-- Replaces dofile('*/content/packid/*') with load_script('packid:*')
+function dofile(path)
+ on_deprecated_call("dofile", "require or loadstring")
+ local index = string.find(path, "/content/")
+ if index then
+ local newpath = string.sub(path, index+9)
+ index = string.find(newpath, "/")
+ if index then
+ local label = string.sub(newpath, 1, index-1)
+ newpath = label..':'..string.sub(newpath, index+1)
+ if file.isfile(newpath) then
+ return __load_script(newpath, true)
+ end
+ end
+ end
+ return _dofile(path)
+end
diff --git a/res/modules/internal/extensions/file.lua b/res/modules/internal/extensions/file.lua
new file mode 100644
index 00000000..0f104012
--- /dev/null
+++ b/res/modules/internal/extensions/file.lua
@@ -0,0 +1,45 @@
+function file.name(path)
+ return path:match("([^:/\\]+)$")
+end
+
+function file.stem(path)
+ local name = file.name(path)
+ return name:match("(.+)%.[^%.]+$") or name
+end
+
+function file.ext(path)
+ return path:match("%.([^:/\\]+)$")
+end
+
+function file.prefix(path)
+ return path:match("^([^:]+)")
+end
+
+function file.parent(path)
+ local dir = path:match("(.*)/")
+ if not dir then
+ return file.prefix(path)..":"
+ end
+ return dir
+end
+
+function file.path(path)
+ local pos = path:find(':')
+ return path:sub(pos + 1)
+end
+
+function file.join(a, b)
+ if a[#a] == ':' then
+ return a .. b
+ end
+ return a .. "/" .. b
+end
+
+function file.readlines(path)
+ local str = file.read(path)
+ local lines = {}
+ for s in str:gmatch("[^\r\n]+") do
+ table.insert(lines, s)
+ end
+ return lines
+end
diff --git a/res/modules/internal/extensions/inventory.lua b/res/modules/internal/extensions/inventory.lua
new file mode 100644
index 00000000..799eecb0
--- /dev/null
+++ b/res/modules/internal/extensions/inventory.lua
@@ -0,0 +1,71 @@
+function inventory.get_uses(invid, slot)
+ local uses = inventory.get_data(invid, slot, "uses")
+ if uses == nil then
+ return item.uses(inventory.get(invid, slot))
+ end
+ return uses
+end
+
+function inventory.use(invid, slot)
+ local itemid, count = inventory.get(invid, slot)
+ if itemid == nil then
+ return
+ end
+ local item_uses = inventory.get_uses(invid, slot)
+ if item_uses == nil then
+ return
+ end
+ if item_uses == 1 then
+ inventory.set(invid, slot, itemid, count - 1)
+ elseif item_uses > 1 then
+ inventory.set_data(invid, slot, "uses", item_uses - 1)
+ end
+end
+
+function inventory.decrement(invid, slot, count)
+ count = count or 1
+ local itemid, itemcount = inventory.get(invid, slot)
+ if itemcount <= count then
+ inventory.set(invid, slot, 0)
+ else
+ inventory.set_count(invid, slot, itemcount - count)
+ end
+end
+
+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
+
+ return caption
+end
+
+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 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
diff --git a/res/modules/internal/extensions/math.lua b/res/modules/internal/extensions/math.lua
new file mode 100644
index 00000000..d906e94a
--- /dev/null
+++ b/res/modules/internal/extensions/math.lua
@@ -0,0 +1,37 @@
+function math.clamp(_in, low, high)
+ return math.min(math.max(_in, low), high)
+end
+
+function math.rand(low, high)
+ return low + (high - low) * math.random()
+end
+
+function math.normalize(num, conf)
+ conf = conf or 1
+
+ return (num / conf) % 1
+end
+
+function math.round(num, places)
+ places = places or 0
+
+ local mult = 10 ^ places
+ return math.floor(num * mult + 0.5) / mult
+end
+
+function math.sum(...)
+ local numbers = nil
+ local sum = 0
+
+ if type(...) == "table" then
+ numbers = ...
+ else
+ numbers = {...}
+ end
+
+ for _, v in ipairs(numbers) do
+ sum = sum + v
+ end
+
+ return sum
+end
diff --git a/res/modules/internal/extensions/pack.lua b/res/modules/internal/extensions/pack.lua
new file mode 100644
index 00000000..c43c1203
--- /dev/null
+++ b/res/modules/internal/extensions/pack.lua
@@ -0,0 +1,13 @@
+function pack.is_installed(packid)
+ return file.isfile(packid..":package.json")
+end
+
+function pack.data_file(packid, name)
+ file.mkdirs("world:data/"..packid)
+ return "world:data/"..packid.."/"..name
+end
+
+function pack.shared_file(packid, name)
+ file.mkdirs("config:"..packid)
+ return "config:"..packid.."/"..name
+end
diff --git a/res/modules/internal/extensions/string.lua b/res/modules/internal/extensions/string.lua
new file mode 100644
index 00000000..f76470df
--- /dev/null
+++ b/res/modules/internal/extensions/string.lua
@@ -0,0 +1,126 @@
+local pattern_escape_replacements = {
+ ["("] = "%(",
+ [")"] = "%)",
+ ["."] = "%.",
+ ["%"] = "%%",
+ ["+"] = "%+",
+ ["-"] = "%-",
+ ["*"] = "%*",
+ ["?"] = "%?",
+ ["["] = "%[",
+ ["]"] = "%]",
+ ["^"] = "%^",
+ ["$"] = "%$",
+ ["\0"] = "%z"
+}
+
+function string.pattern_safe(str)
+ return string.gsub(str, ".", pattern_escape_replacements)
+end
+
+local string_sub = string.sub
+local string_find = string.find
+local string_len = string.len
+function string.explode(separator, str, withpattern)
+ if (withpattern == nil) then withpattern = false end
+
+ local ret = {}
+ local current_pos = 1
+
+ for i = 1, string_len(str) do
+ local start_pos, end_pos = string_find(
+ str, separator, current_pos, not withpattern)
+ if (not start_pos) then break end
+ ret[i] = string_sub(str, current_pos, start_pos - 1)
+ current_pos = end_pos + 1
+ end
+
+ ret[#ret + 1] = string_sub(str, current_pos)
+
+ return ret
+end
+
+function string.split(str, delimiter)
+ return string.explode(delimiter, str)
+end
+
+function string.formatted_time(seconds, format)
+ if (not seconds) then seconds = 0 end
+ local hours = math.floor(seconds / 3600)
+ local minutes = math.floor((seconds / 60) % 60)
+ local millisecs = (seconds - math.floor(seconds)) * 1000
+ seconds = math.floor(seconds % 60)
+
+ if (format) then
+ return string.format(format, minutes, seconds, millisecs)
+ else
+ return { h = hours, m = minutes, s = seconds, ms = millisecs }
+ end
+end
+
+function string.replace(str, tofind, toreplace)
+ local tbl = string.explode(tofind, str)
+ if (tbl[1]) then return table.concat(tbl, toreplace) end
+ return str
+end
+
+function string.trim(s, char)
+ if char then char = string.pattern_safe(char) else char = "%s" end
+ return string.match(s, "^" .. char .. "*(.-)" .. char .. "*$") or s
+end
+
+function string.trim_right(s, char)
+ if char then char = string.pattern_safe(char) else char = "%s" end
+ return string.match(s, "^(.-)" .. char .. "*$") or s
+end
+
+function string.trim_left(s, char)
+ if char then char = string.pattern_safe(char) else char = "%s" end
+ return string.match(s, "^" .. char .. "*(.+)$") or s
+end
+
+function string.pad(str, size, char)
+ char = char == nil and " " or char
+
+ local padding = math.floor((size - #str) / 2)
+ local extra_padding = (size - #str) % 2
+
+ return string.rep(char, padding) .. str .. string.rep(char, padding + extra_padding)
+end
+
+function string.left_pad(str, size, char)
+ char = char == nil and " " or char
+
+ local left_padding = size - #str
+ return string.rep(char, left_padding) .. str
+end
+
+function string.right_pad(str, size, char)
+ char = char == nil and " " or char
+
+ local right_padding = size - #str
+ return str .. string.rep(char, right_padding)
+end
+
+string.lower = utf8.lower
+string.upper = utf8.upper
+string.escape = utf8.escape
+
+local meta = getmetatable("")
+
+function meta:__index(key)
+ local val = string[key]
+ if (val ~= nil) then
+ return val
+ elseif (tonumber(key)) then
+ return string.sub(self, key, key)
+ end
+end
+
+function string.starts_with(str, start)
+ return string.sub(str, 1, string.len(start)) == start
+end
+
+function string.ends_with(str, endStr)
+ return endStr == "" or string.sub(str, -string.len(endStr)) == endStr
+end
diff --git a/res/modules/internal/extensions/table.lua b/res/modules/internal/extensions/table.lua
new file mode 100644
index 00000000..33047df3
--- /dev/null
+++ b/res/modules/internal/extensions/table.lua
@@ -0,0 +1,179 @@
+function table.copy(t)
+ local copied = {}
+
+ for k, v in pairs(t) do
+ copied[k] = v
+ end
+
+ return copied
+end
+
+function table.deep_copy(t)
+ local copied = {}
+
+ for k, v in pairs(t) do
+ if type(v) == "table" then
+ copied[k] = table.deep_copy(v)
+ else
+ copied[k] = v
+ end
+ end
+
+ return setmetatable(copied, getmetatable(t))
+end
+
+function table.count_pairs(t)
+ local count = 0
+
+ for k, v in pairs(t) do
+ count = count + 1
+ end
+
+ return count
+end
+
+function table.random(t)
+ return t[math.random(1, #t)]
+end
+
+function table.shuffle(t)
+ for i = #t, 2, -1 do
+ local j = math.random(i)
+ t[i], t[j] = t[j], t[i]
+ end
+
+ return t
+end
+
+function table.merge(t1, t2)
+ for i, v in pairs(t2) do
+ if type(i) == "number" then
+ t1[#t1 + 1] = v
+ elseif t1[i] == nil then
+ t1[i] = v
+ end
+ end
+
+ return t1
+end
+
+function table.map(t, func)
+ for i, v in pairs(t) do
+ t[i] = func(i, v)
+ end
+
+ return t
+end
+
+function table.filter(t, func)
+
+ for i = #t, 1, -1 do
+ if not func(i, t[i]) then
+ table.remove(t, i)
+ end
+ end
+
+ local size = #t
+
+ for i, v in pairs(t) do
+ local i_type = type(i)
+ if i_type == "number" then
+ if i < 1 or i > size then
+ if not func(i, v) then
+ t[i] = nil
+ end
+ end
+ else
+ if not func(i, v) then
+ t[i] = nil
+ end
+ end
+ end
+
+ return t
+end
+
+function table.set_default(t, key, default)
+ if t[key] == nil then
+ t[key] = default
+ return default
+ end
+
+ return t[key]
+end
+
+function table.flat(t)
+ local flat = {}
+
+ for _, v in pairs(t) do
+ if type(v) == "table" then
+ table.merge(flat, v)
+ else
+ table.insert(flat, v)
+ end
+ end
+
+ return flat
+end
+
+function table.deep_flat(t)
+ local flat = {}
+
+ for _, v in pairs(t) do
+ if type(v) == "table" then
+ table.merge(flat, table.deep_flat(v))
+ else
+ table.insert(flat, v)
+ end
+ end
+
+ return flat
+end
+
+function table.sub(arr, start, stop)
+ local res = {}
+ start = start or 1
+ stop = stop or #arr
+
+ for i = start, stop do
+ table.insert(res, arr[i])
+ end
+
+ return res
+end
+
+function table.has(t, x)
+ for i,v in ipairs(t) do
+ if v == x then
+ return true
+ end
+ end
+ return false
+end
+
+function table.index(t, x)
+ for i,v in ipairs(t) do
+ if v == x then
+ return i
+ end
+ end
+ return -1
+end
+
+function table.remove_value(t, x)
+ local index = table.index(t, x)
+ if index ~= -1 then
+ table.remove(t, index)
+ end
+end
+
+function table.tostring(t)
+ local s = '['
+ for i,v in ipairs(t) do
+ s = s..tostring(v)
+ if i < #t then
+ s = s..', '
+ end
+ end
+ return s..']'
+end
diff --git a/res/modules/internal/rules.lua b/res/modules/internal/rules.lua
new file mode 100644
index 00000000..a1a5a8c3
--- /dev/null
+++ b/res/modules/internal/rules.lua
@@ -0,0 +1,70 @@
+local rules = {nexid = 1, rules = {}}
+
+function rules.get_rule(name)
+ local rule = rules.rules[name]
+ if rule == nil then
+ rule = {listeners={}}
+ rules.rules[name] = rule
+ end
+ return rule
+end
+
+function rules.get(name)
+ local rule = rules.rules[name]
+ if rule == nil then
+ return nil
+ end
+ return rule.value
+end
+
+function rules.set(name, value)
+ local rule = rules.get_rule(name)
+ rule.value = value
+ for _, handler in pairs(rule.listeners) do
+ handler(value)
+ end
+end
+
+function rules.reset(name)
+ local rule = rules.get_rule(name)
+ rules.set(rule.default)
+end
+
+function rules.listen(name, handler)
+ local rule = rules.get_rule(name)
+ local id = rules.nexid
+ rules.nextid = rules.nexid + 1
+ rule.listeners[utf8.encode(id)] = handler
+ return id
+end
+
+function rules.create(name, value, handler)
+ local rule = rules.get_rule(name)
+ rule.default = value
+
+ local handlerid
+ if handler ~= nil then
+ handlerid = rules.listen(name, handler)
+ end
+ if rules.get(name) == nil then
+ rules.set(name, value)
+ elseif handler then
+ handler(rules.get(name))
+ end
+ return handlerid
+end
+
+function rules.unlisten(name, id)
+ local rule = rules.rules[name]
+ if rule == nil then
+ return
+ end
+ rule.listeners[utf8.encode(id)] = nil
+end
+
+function rules.clear()
+ rules.rules = {}
+ rules.nextid = 1
+end
+
+return rules
diff --git a/res/scripts/stdlib.lua b/res/scripts/stdlib.lua
index eedc9d43..a9199132 100644
--- a/res/scripts/stdlib.lua
+++ b/res/scripts/stdlib.lua
@@ -105,82 +105,10 @@ elseif __vc_app then
complete_app_lib(__vc_app)
end
-function inventory.get_uses(invid, slot)
- local uses = inventory.get_data(invid, slot, "uses")
- if uses == nil then
- return item.uses(inventory.get(invid, slot))
- end
- return uses
-end
-
-function inventory.use(invid, slot)
- local itemid, count = inventory.get(invid, slot)
- if itemid == nil then
- return
- end
- local item_uses = inventory.get_uses(invid, slot)
- if item_uses == nil then
- return
- end
- if item_uses == 1 then
- inventory.set(invid, slot, itemid, count - 1)
- elseif item_uses > 1 then
- inventory.set_data(invid, slot, "uses", item_uses - 1)
- end
-end
-
-function inventory.decrement(invid, slot, count)
- count = count or 1
- local itemid, itemcount = inventory.get(invid, slot)
- if itemcount <= count then
- inventory.set(invid, slot, 0)
- else
- inventory.set_count(invid, slot, itemcount - count)
- end
-end
-
-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
-
- return caption
-end
-
-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 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
-
+require "core:internal/maths_inline"
+require "core:internal/debugging"
+require "core:internal/audio_input"
+require "core:internal/extensions/inventory"
asserts = require "core:internal/asserts"
events = require "core:internal/events"
@@ -296,11 +224,6 @@ entities.get_all = function(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"
@@ -320,86 +243,20 @@ else
os.pid = ffi.C.getpid()
end
-ffi = nil
-__vc_lock_internal_modules()
-
math.randomseed(time.uptime() * 1536227939)
-rules = {nexid = 1, rules = {}}
+rules = require "core:internal/rules"
local _rules = rules
-function _rules.get_rule(name)
- local rule = _rules.rules[name]
- if rule == nil then
- rule = {listeners={}}
- _rules.rules[name] = rule
- end
- return rule
-end
-
-function _rules.get(name)
- local rule = _rules.rules[name]
- if rule == nil then
- return nil
- end
- return rule.value
-end
-
-function _rules.set(name, value)
- local rule = _rules.get_rule(name)
- rule.value = value
- for _, handler in pairs(rule.listeners) do
- handler(value)
- end
-end
-
-function _rules.reset(name)
- local rule = _rules.get_rule(name)
- _rules.set(rule.default)
-end
-
-function _rules.listen(name, handler)
- local rule = _rules.get_rule(name)
- local id = _rules.nexid
- _rules.nextid = _rules.nexid + 1
- rule.listeners[utf8.encode(id)] = handler
- return id
-end
-
-function _rules.create(name, value, handler)
- local rule = _rules.get_rule(name)
- rule.default = value
-
- local handlerid
- if handler ~= nil then
- handlerid = _rules.listen(name, handler)
- end
- if _rules.get(name) == nil then
- _rules.set(name, value)
- elseif handler then
- handler(_rules.get(name))
- end
- return handlerid
-end
-
-function _rules.unlisten(name, id)
- local rule = _rules.rules[name]
- if rule == nil then
- return
- end
- rule.listeners[utf8.encode(id)] = nil
-end
-
-function _rules.clear()
- _rules.rules = {}
- _rules.nextid = 1
-end
-
function __vc_on_hud_open()
+ local _hud_is_content_access = hud._is_content_access
+ local _hud_set_content_access = hud._set_content_access
+ local _hud_set_debug_cheats = hud._set_debug_cheats
+
_rules.create("allow-cheats", true)
- _rules.create("allow-content-access", hud._is_content_access(), function(value)
- hud._set_content_access(value)
+ _rules.create("allow-content-access", _hud_is_content_access(), function(value)
+ _hud_set_content_access(value)
end)
_rules.create("allow-flight", true, function(value)
input.set_enabled("player.flight", value)
@@ -420,7 +277,7 @@ function __vc_on_hud_open()
input.set_enabled("player.fast_interaction", value)
end)
_rules.create("allow-debug-cheats", true, function(value)
- hud._set_debug_cheats(value)
+ _hud_set_debug_cheats(value)
end)
input.add_callback("devtools.console", function()
if menu.page ~= "" then
@@ -577,6 +434,9 @@ end
local __post_runnables = {}
+local fn_audio_reset_fetch_buffer = audio.__reset_fetch_buffer
+audio.__reset_fetch_buffer = nil
+
function __process_post_runnables()
if #__post_runnables then
for _, func in ipairs(__post_runnables) do
@@ -602,6 +462,7 @@ function __process_post_runnables()
__vc_named_coroutines[name] = nil
end
+ fn_audio_reset_fetch_buffer()
debug.pull_events()
network.__process_events()
block.__process_register_events()
@@ -621,6 +482,7 @@ local _getinfo = debug.getinfo
for i, name in ipairs(removed_names) do
debug[name] = nil
end
+
debug.getinfo = function(lvl, fields)
if type(lvl) == "number" then
lvl = lvl + 1
@@ -630,51 +492,7 @@ debug.getinfo = function(lvl, fields)
return debuginfo
end
--- --------- Deprecated functions ------ --
-local function wrap_deprecated(func, name, alternatives)
- return function (...)
- on_deprecated_call(name, alternatives)
- return func(...)
- end
-end
+require "core:internal/deprecated"
-block_index = wrap_deprecated(block.index, "block_index", "block.index")
-block_name = wrap_deprecated(block.name, "block_name", "block.name")
-blocks_count = wrap_deprecated(block.defs_count, "blocks_count", "block.defs_count")
-is_solid_at = wrap_deprecated(block.is_solid_at, "is_solid_at", "block.is_solid_at")
-is_replaceable_at = wrap_deprecated(block.is_replaceable_at, "is_replaceable_at", "block.is_replaceable_at")
-set_block = wrap_deprecated(block.set, "set_block", "block.set")
-get_block = wrap_deprecated(block.get, "get_block", "block.get")
-get_block_X = wrap_deprecated(block.get_X, "get_block_X", "block.get_X")
-get_block_Y = wrap_deprecated(block.get_Y, "get_block_Y", "block.get_Y")
-get_block_Z = wrap_deprecated(block.get_Z, "get_block_Z", "block.get_Z")
-get_block_states = wrap_deprecated(block.get_states, "get_block_states", "block.get_states")
-set_block_states = wrap_deprecated(block.set_states, "set_block_states", "block.set_states")
-get_block_rotation = wrap_deprecated(block.get_rotation, "get_block_rotation", "block.get_rotation")
-set_block_rotation = wrap_deprecated(block.set_rotation, "set_block_rotation", "block.set_rotation")
-get_block_user_bits = wrap_deprecated(block.get_user_bits, "get_block_user_bits", "block.get_user_bits")
-set_block_user_bits = wrap_deprecated(block.set_user_bits, "set_block_user_bits", "block.set_user_bits")
-
-function load_script(path, nocache)
- on_deprecated_call("load_script", "require or loadstring")
- return __load_script(path, nocache)
-end
-
-_dofile = dofile
--- Replaces dofile('*/content/packid/*') with load_script('packid:*')
-function dofile(path)
- on_deprecated_call("dofile", "require or loadstring")
- local index = string.find(path, "/content/")
- if index then
- local newpath = string.sub(path, index+9)
- index = string.find(newpath, "/")
- if index then
- local label = string.sub(newpath, 1, index-1)
- newpath = label..':'..string.sub(newpath, index+1)
- if file.isfile(newpath) then
- return __load_script(newpath, true)
- end
- end
- end
- return _dofile(path)
-end
+ffi = nil
+__vc_lock_internal_modules()
diff --git a/res/scripts/stdmin.lua b/res/scripts/stdmin.lua
index 0f90cea6..f41e660d 100644
--- a/res/scripts/stdmin.lua
+++ b/res/scripts/stdmin.lua
@@ -1,201 +1,8 @@
-local breakpoints = {}
-local dbg_steps_mode = false
-local dbg_step_into_func = false
-local hook_lock = false
-local current_func
-local current_func_stack_size
-
-local _debug_getinfo = debug.getinfo
-local _debug_getlocal = debug.getlocal
-local __pause = debug.pause
-local __error = error
-local __sethook = debug.sethook
-
--- 'return' hook not called for some functions
--- todo: speedup
-local function calc_stack_size()
- local s = debug.traceback("", 2)
- local count = 0
- for i in s:gmatch("\n") do
- count = count + 1
- end
- return count
-end
-
-local is_debugging = debug.is_debugging()
-if is_debugging then
- __sethook(function (e, line)
- if e == "return" then
- local info = _debug_getinfo(2)
- if info.func == current_func then
- current_func = nil
- end
- end
- if dbg_steps_mode and not hook_lock then
- hook_lock = true
-
- if not dbg_step_into_func then
- local func = _debug_getinfo(2).func
- if func ~= current_func then
- return
- end
- if current_func_stack_size ~= calc_stack_size() then
- return
- end
- end
- current_func = func
- __pause("step")
- debug.pull_events()
- end
- hook_lock = false
- local bps = breakpoints[line]
- if not bps then
- return
- end
- local source = _debug_getinfo(2).source
- if not bps[source] then
- return
- end
- current_func = _debug_getinfo(2).func
- current_func_stack_size = calc_stack_size()
- __pause("breakpoint")
- debug.pull_events()
- end, "lr")
-end
-
-local DBG_EVENT_SET_BREAKPOINT = 1
-local DBG_EVENT_RM_BREAKPOINT = 2
-local DBG_EVENT_STEP = 3
-local DBG_EVENT_STEP_INTO_FUNCTION = 4
-local DBG_EVENT_RESUME = 5
-local DBG_EVENT_GET_VALUE = 6
-local __pull_events = debug.__pull_events
-local __sendvalue = debug.__sendvalue
-debug.__pull_events = nil
-debug.__sendvalue = nil
-
-function debug.pull_events()
- if not is_debugging then
- return
- end
- if not debug.is_debugging() then
- is_debugging = false
- __sethook()
- end
- local events = __pull_events()
- if not events then
- return
- end
- for i, event in ipairs(events) do
- if event[1] == DBG_EVENT_SET_BREAKPOINT then
- debug.set_breakpoint(event[2], event[3])
- elseif event[1] == DBG_EVENT_RM_BREAKPOINT then
- debug.remove_breakpoint(event[2], event[3])
- elseif event[1] == DBG_EVENT_STEP then
- dbg_steps_mode = true
- dbg_step_into_func = false
- elseif event[1] == DBG_EVENT_STEP_INTO_FUNCTION then
- dbg_steps_mode = true
- dbg_step_into_func = true
- elseif event[1] == DBG_EVENT_RESUME then
- dbg_steps_mode = false
- dbg_step_into_func = false
- elseif event[1] == DBG_EVENT_GET_VALUE then
- local _, value = _debug_getlocal(event[2] + 3, event[3])
- for _, key in ipairs(event[4]) do
- if value == nil then
- value = "error: index nil value"
- break
- end
- value = value[key]
- end
- __sendvalue(value, event[2], event[3], event[4])
- __pause()
- end
- end
-end
-
-function debug.set_breakpoint(source, line)
- local bps = breakpoints[line]
- if not bps then
- bps = {}
- breakpoints[line] = bps
- end
- bps[source] = true
-end
-
-function debug.remove_breakpoint(source, line)
- local bps = breakpoints[line]
- if not bps then
- return
- end
- bps[source] = nil
-end
-
-function error(message, level)
- if is_debugging then
- __pause("exception", message)
- end
- __error(message, level)
-end
-
--- Lua has no parallelizm, also _set_data does not call any lua functions so
--- may be reused one global ffi buffer per lua_State
-local canvas_ffi_buffer
-local canvas_ffi_buffer_size = 0
-
-local ipairs_mt_supported = false
-for i, _ in ipairs(setmetatable({l={1}}, {
- __ipairs=function(self) return ipairs(self.l) end})) do
- ipairs_mt_supported = true
-end
-
-if not ipairs_mt_supported then
- local raw_ipairs = ipairs
- ipairs = function(t)
- local metatable = getmetatable(t)
- if metatable and metatable.__ipairs then
- return metatable.__ipairs(t)
- end
- return raw_ipairs(t)
- 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
- self:_set_data(tostring(_ffi.cast("uintptr_t", data)))
- end
- local width = self.width
- local height = self.height
-
- local size = width * height * 4
- if size > canvas_ffi_buffer_size then
- canvas_ffi_buffer = _ffi.new(
- string.format("unsigned char[%s]", size)
- )
- canvas_ffi_buffer_size = size
- end
- for i=0, size - 1 do
- canvas_ffi_buffer[i] = data[i + 1]
- end
- self:_set_data(tostring(_ffi.cast("uintptr_t", canvas_ffi_buffer)))
-end
+local _debug_getinfo = debug.getinfo
function crc32(bytes, chksum)
- local chksum = chksum or 0
+ chksum = chksum or 0
local length = #bytes
if type(bytes) == "table" then
@@ -231,20 +38,59 @@ function parse_path(path)
return string.sub(path, 1, index-1), string.sub(path, index+1, -1)
end
-function pack.is_installed(packid)
- return file.isfile(packid..":package.json")
+-- Lua has no parallelizm, also _set_data does not call any lua functions so
+-- may be reused one global ffi buffer per lua_State
+local canvas_ffi_buffer
+local canvas_ffi_buffer_size = 0
+local _ffi = ffi
+function __vc_Canvas_set_data(self, data)
+ if type(data) == "cdata" then
+ self:_set_data(tostring(_ffi.cast("uintptr_t", data)))
+ end
+ local width = self.width
+ local height = self.height
+
+ local size = width * height * 4
+ if size > canvas_ffi_buffer_size then
+ canvas_ffi_buffer = _ffi.new(
+ string.format("unsigned char[%s]", size)
+ )
+ canvas_ffi_buffer_size = size
+ end
+ for i=0, size - 1 do
+ canvas_ffi_buffer[i] = data[i + 1]
+ end
+ self:_set_data(tostring(_ffi.cast("uintptr_t", canvas_ffi_buffer)))
end
-function pack.data_file(packid, name)
- file.mkdirs("world:data/"..packid)
- return "world:data/"..packid.."/"..name
+local ipairs_mt_supported = false
+for i, _ in ipairs(setmetatable({l={1}}, {
+ __ipairs=function(self) return ipairs(self.l) end})) do
+ ipairs_mt_supported = true
end
-function pack.shared_file(packid, name)
- file.mkdirs("config:"..packid)
- return "config:"..packid.."/"..name
+if not ipairs_mt_supported then
+ local raw_ipairs = ipairs
+ ipairs = function(t)
+ local metatable = getmetatable(t)
+ if metatable and metatable.__ipairs then
+ return metatable.__ipairs(t)
+ end
+ return raw_ipairs(t)
+ 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
function timeit(iters, func, ...)
local tm = os.clock()
@@ -256,364 +102,6 @@ end
----------------------------------------------
-function math.clamp(_in, low, high)
- return math.min(math.max(_in, low), high)
-end
-
-function math.rand(low, high)
- return low + (high - low) * math.random()
-end
-
-function math.normalize(num, conf)
- conf = conf or 1
-
- return (num / conf) % 1
-end
-
-function math.round(num, places)
- places = places or 0
-
- local mult = 10 ^ places
- return math.floor(num * mult + 0.5) / mult
-end
-
-function math.sum(...)
- local numbers = nil
- local sum = 0
-
- if type(...) == "table" then
- numbers = ...
- else
- numbers = {...}
- end
-
- for _, v in ipairs(numbers) do
- sum = sum + v
- end
-
- return sum
-end
-
-----------------------------------------------
-
-function table.copy(t)
- local copied = {}
-
- for k, v in pairs(t) do
- copied[k] = v
- end
-
- return copied
-end
-
-function table.deep_copy(t)
- local copied = {}
-
- for k, v in pairs(t) do
- if type(v) == "table" then
- copied[k] = table.deep_copy(v)
- else
- copied[k] = v
- end
- end
-
- return setmetatable(copied, getmetatable(t))
-end
-
-function table.count_pairs(t)
- local count = 0
-
- for k, v in pairs(t) do
- count = count + 1
- end
-
- return count
-end
-
-function table.random(t)
- return t[math.random(1, #t)]
-end
-
-function table.shuffle(t)
- for i = #t, 2, -1 do
- local j = math.random(i)
- t[i], t[j] = t[j], t[i]
- end
-
- return t
-end
-
-function table.merge(t1, t2)
- for i, v in pairs(t2) do
- if type(i) == "number" then
- t1[#t1 + 1] = v
- elseif t1[i] == nil then
- t1[i] = v
- end
- end
-
- return t1
-end
-
-function table.map(t, func)
- for i, v in pairs(t) do
- t[i] = func(i, v)
- end
-
- return t
-end
-
-function table.filter(t, func)
-
- for i = #t, 1, -1 do
- if not func(i, t[i]) then
- table.remove(t, i)
- end
- end
-
- local size = #t
-
- for i, v in pairs(t) do
- local i_type = type(i)
- if i_type == "number" then
- if i < 1 or i > size then
- if not func(i, v) then
- t[i] = nil
- end
- end
- else
- if not func(i, v) then
- t[i] = nil
- end
- end
- end
-
- return t
-end
-
-function table.set_default(t, key, default)
- if t[key] == nil then
- t[key] = default
- return default
- end
-
- return t[key]
-end
-
-function table.flat(t)
- local flat = {}
-
- for _, v in pairs(t) do
- if type(v) == "table" then
- table.merge(flat, v)
- else
- table.insert(flat, v)
- end
- end
-
- return flat
-end
-
-function table.deep_flat(t)
- local flat = {}
-
- for _, v in pairs(t) do
- if type(v) == "table" then
- table.merge(flat, table.deep_flat(v))
- else
- table.insert(flat, v)
- end
- end
-
- return flat
-end
-
-function table.sub(arr, start, stop)
- local res = {}
- start = start or 1
- stop = stop or #arr
-
- for i = start, stop do
- table.insert(res, arr[i])
- end
-
- return res
-end
-
-----------------------------------------------
-
-local pattern_escape_replacements = {
- ["("] = "%(",
- [")"] = "%)",
- ["."] = "%.",
- ["%"] = "%%",
- ["+"] = "%+",
- ["-"] = "%-",
- ["*"] = "%*",
- ["?"] = "%?",
- ["["] = "%[",
- ["]"] = "%]",
- ["^"] = "%^",
- ["$"] = "%$",
- ["\0"] = "%z"
-}
-
-function string.pattern_safe(str)
- return string.gsub(str, ".", pattern_escape_replacements)
-end
-
-local string_sub = string.sub
-local string_find = string.find
-local string_len = string.len
-function string.explode(separator, str, withpattern)
- if (withpattern == nil) then withpattern = false end
-
- local ret = {}
- local current_pos = 1
-
- for i = 1, string_len(str) do
- local start_pos, end_pos = string_find(
- str, separator, current_pos, not withpattern)
- if (not start_pos) then break end
- ret[i] = string_sub(str, current_pos, start_pos - 1)
- current_pos = end_pos + 1
- end
-
- ret[#ret + 1] = string_sub(str, current_pos)
-
- return ret
-end
-
-function string.split(str, delimiter)
- return string.explode(delimiter, str)
-end
-
-function string.formatted_time(seconds, format)
- if (not seconds) then seconds = 0 end
- local hours = math.floor(seconds / 3600)
- local minutes = math.floor((seconds / 60) % 60)
- local millisecs = (seconds - math.floor(seconds)) * 1000
- seconds = math.floor(seconds % 60)
-
- if (format) then
- return string.format(format, minutes, seconds, millisecs)
- else
- return { h = hours, m = minutes, s = seconds, ms = millisecs }
- end
-end
-
-function string.replace(str, tofind, toreplace)
- local tbl = string.explode(tofind, str)
- if (tbl[1]) then return table.concat(tbl, toreplace) end
- return str
-end
-
-function string.trim(s, char)
- if char then char = string.pattern_safe(char) else char = "%s" end
- return string.match(s, "^" .. char .. "*(.-)" .. char .. "*$") or s
-end
-
-function string.trim_right(s, char)
- if char then char = string.pattern_safe(char) else char = "%s" end
- return string.match(s, "^(.-)" .. char .. "*$") or s
-end
-
-function string.trim_left(s, char)
- if char then char = string.pattern_safe(char) else char = "%s" end
- return string.match(s, "^" .. char .. "*(.+)$") or s
-end
-
-function string.pad(str, size, char)
- char = char == nil and " " or char
-
- local padding = math.floor((size - #str) / 2)
- local extra_padding = (size - #str) % 2
-
- return string.rep(char, padding) .. str .. string.rep(char, padding + extra_padding)
-end
-
-function string.left_pad(str, size, char)
- char = char == nil and " " or char
-
- local left_padding = size - #str
- return string.rep(char, left_padding) .. str
-end
-
-function string.right_pad(str, size, char)
- char = char == nil and " " or char
-
- local right_padding = size - #str
- return str .. string.rep(char, right_padding)
-end
-
-string.lower = utf8.lower
-string.upper = utf8.upper
-string.escape = utf8.escape
-
-local meta = getmetatable("")
-
-function meta:__index(key)
- local val = string[key]
- if (val ~= nil) then
- return val
- elseif (tonumber(key)) then
- return string.sub(self, key, key)
- end
-end
-
-function string.starts_with(str, start)
- return string.sub(str, 1, string.len(start)) == start
-end
-
-function string.ends_with(str, endStr)
- return endStr == "" or string.sub(str, -string.len(endStr)) == endStr
-end
-
-function table.has(t, x)
- for i,v in ipairs(t) do
- if v == x then
- return true
- end
- end
- return false
-end
-
-function table.index(t, x)
- for i,v in ipairs(t) do
- if v == x then
- return i
- end
- end
- return -1
-end
-
-function table.remove_value(t, x)
- local index = table.index(t, x)
- if index ~= -1 then
- table.remove(t, index)
- end
-end
-
-function table.tostring(t)
- local s = '['
- for i,v in ipairs(t) do
- s = s..tostring(v)
- if i < #t then
- s = s..', '
- end
- end
- return s..']'
-end
-
-function file.readlines(path)
- local str = file.read(path)
- local lines = {}
- for s in str:gmatch("[^\r\n]+") do
- table.insert(lines, s)
- end
- return lines
-end
-
function debug.count_frames()
local frames = 1
while true do
@@ -768,7 +256,7 @@ function __vc__error(msg, frame, n, lastn)
if events then
local frames = debug.get_traceback(1)
events.emit(
- "core:error", msg,
+ "core:error", msg,
table.sub(frames, 1 + (n or 0), lastn and #frames-lastn)
)
end
@@ -782,42 +270,20 @@ function __vc_warning(msg, detail, n)
end
end
-function file.name(path)
- return path:match("([^:/\\]+)$")
-end
+require "core:internal/extensions/pack"
+require "core:internal/extensions/math"
+require "core:internal/extensions/file"
+require "core:internal/extensions/table"
+require "core:internal/extensions/string"
-function file.stem(path)
- local name = file.name(path)
- return name:match("(.+)%.[^%.]+$") or name
-end
-
-function file.ext(path)
- return path:match("%.([^:/\\]+)$")
-end
-
-function file.prefix(path)
- return path:match("^([^:]+)")
-end
-
-function file.parent(path)
- local dir = path:match("(.*)/")
- if not dir then
- return file.prefix(path)..":"
- end
- return dir
-end
-
-function file.path(path)
- local pos = path:find(':')
- return path:sub(pos + 1)
-end
-
-function file.join(a, b)
- if a[#a] == ':' then
- return a .. b
- end
- return a .. "/" .. b
-end
+local bytearray = require "core:internal/bytearray"
+Bytearray = bytearray.FFIBytearray
+Bytearray_as_string = bytearray.FFIBytearray_as_string
+U16view = bytearray.FFIU16view
+I16view = bytearray.FFII16view
+U32view = bytearray.FFIU32view
+I32view = bytearray.FFII32view
+Bytearray_construct = function(...) return Bytearray(...) end
bit.compile = require "core:bitwise/compiler"
bit.execute = require "core:bitwise/executor"
diff --git a/res/texts/ru_RU.txt b/res/texts/ru_RU.txt
index 5f772a33..82bece38 100644
--- a/res/texts/ru_RU.txt
+++ b/res/texts/ru_RU.txt
@@ -25,7 +25,8 @@ Grant %{0} pack modification permission?=Выдать разрешение на
Error at line %{0}=Ошибка на строке %{0}
Run=Запустить
Filter=Фильтр
-Are you sure you want to open the link: =Вы уверены, что хотите открыть ссылку:
+Are you sure you want to open the link: =Вы уверены, что хотите открыть ссылку:
+Grant '%{0}' pack audio recording permission?=Выдать паку '%{0}' разрешение на запись звука?
editor.info.tooltip=CTRL+S - Сохранить\nCTRL+R - Запустить\nCTRL+Z - Отменить\nCTRL+Y - Повторить
devtools.traceback=Стек вызовов (от последнего)
@@ -107,6 +108,7 @@ settings.Shadows quality=Качество теней
settings.Conflict=Найдены возможные конфликты
settings.Windowed=Оконный
settings.Borderless=Безрамочный
+settings.Microphone=Микрофон
# Управление
chunks.reload=Перезагрузить Чанки
diff --git a/src/audio/AL/ALAudio.cpp b/src/audio/AL/ALAudio.cpp
index 7f28882b..e38c0f16 100644
--- a/src/audio/AL/ALAudio.cpp
+++ b/src/audio/AL/ALAudio.cpp
@@ -5,11 +5,41 @@
#include "debug/Logger.hpp"
#include "alutil.hpp"
+#include "../MemoryPCMStream.hpp"
static debug::Logger logger("al-audio");
using namespace audio;
+const char* alc_error_to_string(ALCenum error) {
+ switch (error) {
+ case ALC_NO_ERROR:
+ return "no error";
+ case ALC_INVALID_DEVICE:
+ return "invalid device handle";
+ case ALC_INVALID_CONTEXT:
+ return "invalid context handle";
+ case ALC_INVALID_ENUM:
+ return "invalid enum parameter passed to an ALC call";
+ case ALC_INVALID_VALUE:
+ return "invalid value parameter passed to an ALC call";
+ case ALC_OUT_OF_MEMORY:
+ return "out of memory";
+ default:
+ return "unknown ALC error";
+ }
+}
+
+static bool check_alc_errors(ALCdevice* device, const char* context) {
+ ALCenum error = alcGetError(device);
+ if (error == ALC_NO_ERROR) {
+ return false;
+ }
+ logger.error() << context << ": " << alc_error_to_string(error) << "("
+ << error << ")";
+ return true;
+}
+
ALSound::ALSound(
ALAudio* al, uint buffer, const std::shared_ptr& pcm, bool keepPCM
)
@@ -37,6 +67,70 @@ std::unique_ptr ALSound::newInstance(int priority, int channel) const {
return speaker;
}
+ALInputDevice::ALInputDevice(
+ ALAudio* al,
+ ALCdevice* device,
+ uint channels,
+ uint bitsPerSample,
+ uint sampleRate
+)
+ : al(al),
+ device(device),
+ channels(channels),
+ bitsPerSample(bitsPerSample),
+ sampleRate(sampleRate) {
+ const ALCchar* deviceName = alcGetString(device, ALC_CAPTURE_DEVICE_SPECIFIER);
+
+ if (deviceName) {
+ deviceSpecifier = std::string(deviceName);
+ } else {
+ logger.warning() << "could not retrieve input device specifier";
+ }
+}
+
+ALInputDevice::~ALInputDevice() {
+ alcCaptureCloseDevice(device);
+ check_alc_errors(device, "alcCaptureCloseDevice");
+}
+
+void ALInputDevice::startCapture() {
+ alcCaptureStart(device);
+ check_alc_errors(device, "alcCaptureStart");
+}
+
+void ALInputDevice::stopCapture() {
+ alcCaptureStop(device);
+ check_alc_errors(device, "alcCaptureStop");
+}
+
+uint ALInputDevice::getChannels() const {
+ return channels;
+}
+
+uint ALInputDevice::getSampleRate() const {
+ return sampleRate;
+}
+
+uint ALInputDevice::getBitsPerSample() const {
+ return bitsPerSample;
+}
+
+const std::string& ALInputDevice::getDeviceSpecifier() const {
+ return deviceSpecifier;
+}
+
+size_t ALInputDevice::read(char* buffer, size_t bufferSize) {
+ ALCint samplesCount = 0;
+ alcGetIntegerv(device, ALC_CAPTURE_SAMPLES, sizeof(samplesCount), &samplesCount);
+ check_alc_errors(device, "alcGetIntegerv(ALC_CAPTURE_SAMPLES)");
+ size_t samplesRead = std::min(
+ samplesCount, bufferSize / channels / (bitsPerSample >> 3)
+ );
+ alcCaptureSamples(device, buffer, samplesRead);
+ check_alc_errors(device, "alcCaptureSamples");
+ return samplesRead * channels * (bitsPerSample >> 3);
+}
+
ALStream::ALStream(
ALAudio* al, std::shared_ptr source, bool keepSource
)
@@ -81,9 +175,10 @@ std::unique_ptr ALStream::createSpeaker(bool loop, int channel) {
for (uint i = 0; i < ALStream::STREAM_BUFFERS; i++) {
uint free_buffer = al->getFreeBuffer();
if (!preloadBuffer(free_buffer, loop)) {
- break;
+ unusedBuffers.push(free_buffer);
+ } else {
+ AL_CHECK(alSourceQueueBuffers(free_source, 1, &free_buffer));
}
- AL_CHECK(alSourceQueueBuffers(free_source, 1, &free_buffer));
}
return std::make_unique(al, free_source, PRIORITY_HIGH, channel);
}
@@ -130,11 +225,11 @@ void ALStream::unqueueBuffers(uint alsource) {
uint ALStream::enqueueBuffers(uint alsource) {
uint preloaded = 0;
if (!unusedBuffers.empty()) {
- uint first_buffer = unusedBuffers.front();
- if (preloadBuffer(first_buffer, loop)) {
+ uint firstBuffer = unusedBuffers.front();
+ if (preloadBuffer(firstBuffer, loop)) {
preloaded++;
unusedBuffers.pop();
- AL_CHECK(alSourceQueueBuffers(alsource, 1, &first_buffer));
+ AL_CHECK(alSourceQueueBuffers(alsource, 1, &firstBuffer));
}
}
return preloaded;
@@ -144,14 +239,14 @@ void ALStream::update(double delta) {
if (this->speaker == 0) {
return;
}
- auto p_speaker = audio::get_speaker(this->speaker);
- if (p_speaker == nullptr) {
+ auto speaker = audio::get_speaker(this->speaker);
+ if (speaker == nullptr) {
this->speaker = 0;
return;
}
- ALSpeaker* alspeaker = dynamic_cast(p_speaker);
+ ALSpeaker* alspeaker = dynamic_cast(speaker);
assert(alspeaker != nullptr);
- if (alspeaker->stopped) {
+ if (alspeaker->manuallyStopped) {
this->speaker = 0;
return;
}
@@ -162,11 +257,11 @@ void ALStream::update(double delta) {
uint preloaded = enqueueBuffers(alsource);
// alspeaker->stopped is assigned to false at ALSpeaker::play(...)
- if (p_speaker->isStopped() && !alspeaker->stopped) { //TODO: -V560 false-positive?
+ if (speaker->isStopped() && !alspeaker->manuallyStopped) { //TODO: -V560 false-positive?
if (preloaded) {
- p_speaker->play();
- } else {
- p_speaker->stop();
+ speaker->play();
+ } else if (isStopOnEnd()){
+ speaker->stop();
}
}
}
@@ -207,6 +302,14 @@ void ALStream::setTime(duration_t time) {
}
}
+bool ALStream::isStopOnEnd() const {
+ return stopOnEnd;
+}
+
+void ALStream::setStopOnEnd(bool flag) {
+ stopOnEnd = flag;
+}
+
ALSpeaker::ALSpeaker(ALAudio* al, uint source, int priority, int channel)
: al(al), priority(priority), channel(channel), source(source) {
}
@@ -273,7 +376,7 @@ void ALSpeaker::setLoop(bool loop) {
void ALSpeaker::play() {
paused = false;
- stopped = false;
+ manuallyStopped = false;
auto p_channel = get_channel(this->channel);
AL_CHECK(alSourcef(
source,
@@ -289,7 +392,7 @@ void ALSpeaker::pause() {
}
void ALSpeaker::stop() {
- stopped = true;
+ manuallyStopped = true;
if (source) {
AL_CHECK(alSourceStop(source));
@@ -353,6 +456,13 @@ int ALSpeaker::getPriority() const {
return priority;
}
+
+bool ALSpeaker::isManuallyStopped() const {
+ return manuallyStopped;
+}
+
+static bool alc_enumeration_ext = false;
+
ALAudio::ALAudio(ALCdevice* device, ALCcontext* context)
: device(device), context(context) {
ALCint size;
@@ -365,9 +475,15 @@ ALAudio::ALAudio(ALCdevice* device, ALCcontext* context)
maxSources = attrs[i + 1];
}
}
- auto devices = getAvailableDevices();
- logger.info() << "devices:";
- for (auto& name : devices) {
+ auto outputDevices = getOutputDeviceNames();
+ logger.info() << "output devices:";
+ for (auto& name : outputDevices) {
+ logger.info() << " " << name;
+ }
+
+ auto inputDevices = getInputDeviceNames();
+ logger.info() << "input devices:";
+ for (auto& name : inputDevices) {
logger.info() << " " << name;
}
}
@@ -385,8 +501,10 @@ ALAudio::~ALAudio() {
AL_CHECK(alDeleteBuffers(1, &buffer));
}
- AL_CHECK(alcMakeContextCurrent(context));
+ alcMakeContextCurrent(nullptr);
+ check_alc_errors(device, "alcMakeContextCurrent");
alcDestroyContext(context);
+ check_alc_errors(device, "alcDestroyContext");
if (!alcCloseDevice(device)) {
logger.error() << "device not closed!";
}
@@ -411,7 +529,71 @@ std::unique_ptr ALAudio::openStream(
return std::make_unique(this, stream, keepSource);
}
+std::vector ALAudio::getInputDeviceNames() {
+ std::vector devices;
+
+ if (!alc_enumeration_ext) {
+ logger.warning() << "enumeration extension is not available";
+ return devices;
+ }
+
+ auto deviceList = alcGetString(nullptr, ALC_CAPTURE_DEVICE_SPECIFIER);
+ if (deviceList == nullptr) {
+ logger.warning() << "no input devices found";
+ return devices;
+ }
+ while (*deviceList) {
+ std::string deviceName(deviceList);
+ devices.push_back(deviceName);
+ deviceList += deviceName.length() + 1;
+ }
+
+ return devices;
+}
+
+std::vector ALAudio::getOutputDeviceNames() {
+ std::vector devices;
+
+ if (!alc_enumeration_ext) {
+ logger.warning() << "enumeration extension is not available";
+ return devices;
+ }
+
+ auto deviceList = alcGetString(nullptr, ALC_ALL_DEVICES_SPECIFIER);
+ if (deviceList == nullptr) {
+ logger.warning() << "no input devices found";
+ return devices;
+ }
+ while (*deviceList) {
+ std::string deviceName(deviceList);
+ devices.push_back(deviceName);
+ deviceList += deviceName.length() + 1;
+ }
+
+ return devices;
+}
+
+std::unique_ptr ALAudio::openInputDevice(
+ const std::string& deviceName, uint sampleRate, uint channels, uint bitsPerSample
+) {
+ uint bps = bitsPerSample >> 3;
+ ALCdevice* device = alcCaptureOpenDevice(
+ deviceName.empty() ? nullptr : deviceName.c_str(),
+ sampleRate,
+ AL::to_al_format(channels, bitsPerSample),
+ sampleRate * channels * bps / 8
+ );
+ if (check_alc_errors(device, "alcCaptureOpenDevice"))
+ return nullptr;
+
+ return std::make_unique(
+ this, device, channels, bitsPerSample, sampleRate
+ );
+}
+
std::unique_ptr ALAudio::create() {
+ alc_enumeration_ext = alcIsExtensionPresent(nullptr, "ALC_ENUMERATION_EXT");
+
ALCdevice* device = alcOpenDevice(nullptr);
if (device == nullptr) return nullptr;
ALCcontext* context = alcCreateContext(device, nullptr);
@@ -468,24 +650,6 @@ void ALAudio::freeBuffer(uint buffer) {
freebuffers.push_back(buffer);
}
-std::vector ALAudio::getAvailableDevices() const {
- std::vector devicesVec;
-
- const ALCchar* devices;
- devices = alcGetString(device, ALC_DEVICE_SPECIFIER);
- if (!AL_GET_ERROR()) {
- return devicesVec;
- }
-
- const char* ptr = devices;
- do {
- devicesVec.emplace_back(ptr);
- ptr += devicesVec.back().size() + 1;
- } while (ptr[0]);
-
- return devicesVec;
-}
-
void ALAudio::setListener(
glm::vec3 position, glm::vec3 velocity, glm::vec3 at, glm::vec3 up
) {
diff --git a/src/audio/AL/ALAudio.hpp b/src/audio/AL/ALAudio.hpp
index 7e0f0c29..f154741f 100644
--- a/src/audio/AL/ALAudio.hpp
+++ b/src/audio/AL/ALAudio.hpp
@@ -58,6 +58,7 @@ namespace audio {
bool keepSource;
char buffer[BUFFER_SIZE];
bool loop = false;
+ bool stopOnEnd = false;
bool preloadBuffer(uint buffer, bool loop);
void unqueueBuffers(uint alsource);
@@ -80,6 +81,39 @@ namespace audio {
void setTime(duration_t time) override;
static inline constexpr uint STREAM_BUFFERS = 3;
+
+ bool isStopOnEnd() const override;
+
+ void setStopOnEnd(bool stopOnEnd) override;
+ };
+
+ class ALInputDevice : public InputDevice {
+ public:
+ ALInputDevice(
+ ALAudio* al,
+ ALCdevice* device,
+ uint channels,
+ uint bitsPerSample,
+ uint sampleRate
+ );
+ ~ALInputDevice() override;
+
+ void startCapture() override;
+ void stopCapture() override;
+
+ uint getChannels() const override;
+ uint getSampleRate() const override;
+ uint getBitsPerSample() const override;
+ const std::string& getDeviceSpecifier() const override;
+
+ size_t read(char* buffer, size_t bufferSize) override;
+ private:
+ ALAudio* al;
+ ALCdevice* device;
+ uint channels;
+ uint bitsPerSample;
+ uint sampleRate;
+ std::string deviceSpecifier;
};
/// @brief AL source adapter
@@ -90,7 +124,7 @@ namespace audio {
float volume = 0.0f;
public:
ALStream* stream = nullptr;
- bool stopped = true;
+ bool manuallyStopped = true;
bool paused = false;
uint source;
duration_t duration = 0.0f;
@@ -130,6 +164,8 @@ namespace audio {
bool isRelative() const override;
int getPriority() const override;
+
+ bool isManuallyStopped() const override;
};
class ALAudio : public Backend {
@@ -152,15 +188,24 @@ namespace audio {
void freeSource(uint source);
void freeBuffer(uint buffer);
- std::vector getAvailableDevices() const;
-
std::unique_ptr createSound(
std::shared_ptr pcm, bool keepPCM
) override;
+
std::unique_ptr openStream(
std::shared_ptr stream, bool keepSource
) override;
+ std::unique_ptr openInputDevice(
+ const std::string& deviceName,
+ uint sampleRate,
+ uint channels,
+ uint bitsPerSample
+ ) override;
+
+ std::vector getOutputDeviceNames() override;
+ std::vector getInputDeviceNames() override;
+
void setListener(
glm::vec3 position,
glm::vec3 velocity,
diff --git a/src/audio/MemoryPCMStream.cpp b/src/audio/MemoryPCMStream.cpp
new file mode 100644
index 00000000..e0f40d5e
--- /dev/null
+++ b/src/audio/MemoryPCMStream.cpp
@@ -0,0 +1,67 @@
+#include "MemoryPCMStream.hpp"
+
+#include
+
+using namespace audio;
+
+MemoryPCMStream::MemoryPCMStream(
+ uint sampleRate, uint channels, uint bitsPerSample
+)
+ : sampleRate(sampleRate), channels(channels), bitsPerSample(bitsPerSample) {
+}
+
+void MemoryPCMStream::feed(util::span bytes) {
+ buffer.insert(buffer.end(), bytes.begin(), bytes.end());
+}
+
+bool MemoryPCMStream::isOpen() const {
+ return open;
+}
+
+void MemoryPCMStream::close() {
+ open = false;
+ buffer = {};
+}
+
+size_t MemoryPCMStream::read(char* dst, size_t bufferSize) {
+ if (!open) {
+ return PCMStream::ERROR;
+ }
+ if (buffer.empty()) {
+ return 0;
+ }
+ size_t count = std::min(bufferSize, buffer.size());
+ std::memcpy(dst, buffer.data(), count);
+ buffer.erase(buffer.begin(), buffer.begin() + count);
+ return count;
+}
+
+size_t MemoryPCMStream::getTotalSamples() const {
+ return 0;
+}
+
+duration_t MemoryPCMStream::getTotalDuration() const {
+ return 0.0;
+}
+
+uint MemoryPCMStream::getChannels() const {
+ return channels;
+}
+
+uint MemoryPCMStream::getSampleRate() const {
+ return sampleRate;
+}
+
+uint MemoryPCMStream::getBitsPerSample() const {
+ return bitsPerSample;
+}
+
+bool MemoryPCMStream::isSeekable() const {
+ return false;
+}
+
+void MemoryPCMStream::seek(size_t position) {}
+
+size_t MemoryPCMStream::available() const {
+ return buffer.size();
+}
diff --git a/src/audio/MemoryPCMStream.hpp b/src/audio/MemoryPCMStream.hpp
new file mode 100644
index 00000000..bef2a05b
--- /dev/null
+++ b/src/audio/MemoryPCMStream.hpp
@@ -0,0 +1,44 @@
+#pragma once
+
+#include
+
+#include "audio.hpp"
+#include "util/span.hpp"
+
+namespace audio {
+ class MemoryPCMStream : public PCMStream {
+ public:
+ MemoryPCMStream(uint sampleRate, uint channels, uint bitsPerSample);
+
+ void feed(util::span bytes);
+
+ bool isOpen() const override;
+
+ void close() override;
+
+ size_t read(char* buffer, size_t bufferSize) override;
+
+ size_t getTotalSamples() const override;
+
+ duration_t getTotalDuration() const override;
+
+ uint getChannels() const override;
+
+ uint getSampleRate() const override;
+
+ uint getBitsPerSample() const override;
+
+ bool isSeekable() const override;
+
+ void seek(size_t position) override;
+
+ size_t available() const;
+ private:
+ uint sampleRate;
+ uint channels;
+ uint bitsPerSample;
+ bool open = true;
+
+ std::vector buffer;
+ };
+}
diff --git a/src/audio/NoAudio.hpp b/src/audio/NoAudio.hpp
index edfc3319..0da1b4d6 100644
--- a/src/audio/NoAudio.hpp
+++ b/src/audio/NoAudio.hpp
@@ -61,6 +61,13 @@ namespace audio {
void setTime(duration_t time) override {
}
+
+ bool isStopOnEnd() const override {
+ return false;
+ }
+
+ void setStopOnEnd(bool stopOnEnd) override {
+ }
};
class NoAudio : public Backend {
@@ -71,10 +78,24 @@ namespace audio {
std::unique_ptr createSound(
std::shared_ptr pcm, bool keepPCM
) override;
+
std::unique_ptr openStream(
std::shared_ptr stream, bool keepSource
) override;
+ std::unique_ptr openInputDevice(
+ const std::string& deviceName, uint sampleRate, uint channels, uint bitsPerSample
+ ) override {
+ return nullptr;
+ }
+
+ std::vector getInputDeviceNames() override {
+ return {};
+ }
+ std::vector getOutputDeviceNames() override {
+ return {};
+ }
+
void setListener(
glm::vec3 position, glm::vec3 velocity, glm::vec3 at, glm::vec3 up
) override {
diff --git a/src/audio/audio.cpp b/src/audio/audio.cpp
index f98c436e..0069ff12 100644
--- a/src/audio/audio.cpp
+++ b/src/audio/audio.cpp
@@ -151,6 +151,8 @@ public:
}
};
+static std::unique_ptr input_device = nullptr;
+
void audio::initialize(bool enabled, AudioSettings& settings) {
enabled = enabled && settings.enabled.get();
if (enabled) {
@@ -180,6 +182,15 @@ void audio::initialize(bool enabled, AudioSettings& settings) {
audio::get_channel(channel.name)->setVolume(value * value);
}, true));
}
+
+ input_device = backend->openInputDevice("", 44100, 1, 16);
+ if (input_device) {
+ input_device->startCapture();
+ }
+}
+
+InputDevice* audio::get_input_device() {
+ return input_device.get();
}
std::unique_ptr audio::load_PCM(const io::path& file, bool headerOnly) {
@@ -242,6 +253,38 @@ std::unique_ptr audio::open_stream(
return backend->openStream(std::move(stream), keepSource);
}
+std::unique_ptr audio::open_input_device(
+ const std::string& deviceName, uint sampleRate, uint channels, uint bitsPerSample
+) {
+ return backend->openInputDevice(
+ deviceName, sampleRate, channels, bitsPerSample
+ );
+}
+
+std::vector audio::get_input_devices_names() {
+ return backend->getInputDeviceNames();
+}
+
+std::vector audio::get_output_devices_names() {
+ return backend->getOutputDeviceNames();
+}
+
+void audio::set_input_device(const std::string& deviceName) {
+ auto newDevice = backend->openInputDevice(deviceName, 44100, 1, 16);
+ if (newDevice == nullptr) {
+ logger.error() << "could not open input device: " << deviceName;
+ return;
+ }
+
+ if (input_device) {
+ input_device->stopCapture();
+ }
+ input_device = std::move(newDevice);
+ if (input_device) {
+ input_device->startCapture();
+ }
+}
+
void audio::set_listener(
glm::vec3 position, glm::vec3 velocity, glm::vec3 lookAt, glm::vec3 up
) {
@@ -421,8 +464,15 @@ void audio::update(double delta) {
speaker->update(channel);
}
if (speaker->isStopped()) {
- streams.erase(it->first);
- it = speakers.erase(it);
+ auto foundStream = streams.find(it->first);
+ if (foundStream == streams.end() ||
+ (!speaker->isManuallyStopped() &&
+ foundStream->second->isStopOnEnd())) {
+ streams.erase(it->first);
+ it = speakers.erase(it);
+ } else {
+ it++;
+ }
} else {
it++;
}
@@ -458,6 +508,9 @@ void audio::reset_channel(int index) {
}
void audio::close() {
+ if (input_device) {
+ input_device->stopCapture();
+ }
speakers.clear();
delete backend;
backend = nullptr;
diff --git a/src/audio/audio.hpp b/src/audio/audio.hpp
index 5893c334..2cd8c127 100644
--- a/src/audio/audio.hpp
+++ b/src/audio/audio.hpp
@@ -24,6 +24,8 @@ namespace audio {
/// @brief streams and important sounds
constexpr inline int PRIORITY_HIGH = 10;
+ constexpr inline size_t MAX_INPUT_SAMPLES = 22050;
+
class Speaker;
/// @brief Audio speaker states
@@ -108,6 +110,31 @@ namespace audio {
}
};
+ class InputDevice {
+ public:
+ virtual ~InputDevice() {};
+
+ virtual void startCapture() = 0;
+ virtual void stopCapture() = 0;
+
+ /// @brief Get number of audio channels
+ /// @return 1 if mono, 2 if stereo
+ virtual uint getChannels() const = 0;
+ /// @brief Get audio sampling frequency
+ /// @return number of mono samples per second
+ virtual uint getSampleRate() const = 0;
+ /// @brief Get number of bits per mono sample
+ /// @return 8 or 16
+ virtual uint getBitsPerSample() const = 0;
+
+ /// @brief Read available data to buffer.
+ /// @return size of data received or PCMStream::ERROR in case of error
+ virtual size_t read(char* buffer, size_t bufferSize) = 0;
+
+ /// @brief Get device specifier string
+ virtual const std::string& getDeviceSpecifier() const = 0;
+ };
+
/// @brief audio::PCMStream is a data source for audio::Stream
class PCMStream {
public:
@@ -121,6 +148,10 @@ namespace audio {
/// (always equals bufferSize if seekable and looped)
virtual size_t readFully(char* buffer, size_t bufferSize, bool loop);
+ /// @brief Read available data to buffer
+ /// @param buffer destination buffer
+ /// @param bufferSize destination buffer size
+ /// @return count of received bytes or PCMStream::ERROR
virtual size_t read(char* buffer, size_t bufferSize) = 0;
/// @brief Close stream
@@ -195,6 +226,9 @@ namespace audio {
/// @brief Set playhead to the selected time
/// @param time selected time
virtual void setTime(duration_t time) = 0;
+
+ virtual bool isStopOnEnd() const = 0;
+ virtual void setStopOnEnd(bool stopOnEnd) = 0;
};
/// @brief Sound is an audio asset that supposed to support many
@@ -329,6 +363,8 @@ namespace audio {
inline bool isStopped() const {
return getState() == State::stopped;
}
+
+ virtual bool isManuallyStopped() const = 0;
};
class Backend {
@@ -341,12 +377,20 @@ namespace audio {
virtual std::unique_ptr openStream(
std::shared_ptr stream, bool keepSource
) = 0;
+ virtual std::unique_ptr openInputDevice(
+ const std::string& deviceName,
+ uint sampleRate,
+ uint channels,
+ uint bitsPerSample
+ ) = 0;
virtual void setListener(
glm::vec3 position,
glm::vec3 velocity,
glm::vec3 lookAt,
glm::vec3 up
) = 0;
+ virtual std::vector getInputDeviceNames() = 0;
+ virtual std::vector getOutputDeviceNames() = 0;
virtual void update(double delta) = 0;
/// @brief Check if backend is an abstraction that does not internally
@@ -402,6 +446,28 @@ namespace audio {
std::shared_ptr stream, bool keepSource
);
+ /// @brief Open audio input device
+ /// @param sampleRate sample rate
+ /// @param channels channels count (1 - mono, 2 - stereo)
+ /// @param bitsPerSample number of bits per sample (8 or 16)
+ /// @return new InputDevice instance or nullptr
+ std::unique_ptr open_input_device(
+ const std::string& deviceName,
+ uint sampleRate,
+ uint channels,
+ uint bitsPerSample
+ );
+
+ /// @brief Retrieve names of available audio input devices
+ /// @return list of device names
+ std::vector get_input_devices_names();
+
+ /// @brief Retrieve names of available audio output devices
+ /// @return list of device names
+ std::vector get_output_devices_names();
+
+ void set_input_device(const std::string& deviceName);
+
/// @brief Configure 3D listener
/// @param position listener position
/// @param velocity listener velocity (used for Doppler effect)
@@ -515,6 +581,8 @@ namespace audio {
/// @brief Stop all playing audio in channel, reset channel state
void reset_channel(int channel);
+ InputDevice* get_input_device();
+
/// @brief Finalize audio system
void close();
};
diff --git a/src/logic/scripting/lua/libs/libassets.cpp b/src/logic/scripting/lua/libs/libassets.cpp
index 76e824e2..35720195 100644
--- a/src/logic/scripting/lua/libs/libassets.cpp
+++ b/src/logic/scripting/lua/libs/libassets.cpp
@@ -9,7 +9,7 @@
#include "graphics/core/Texture.hpp"
#include "graphics/core/Atlas.hpp"
#include "util/Buffer.hpp"
-#include "../lua_custom_types.hpp"
+#include "../usertypes/lua_type_canvas.hpp"
using namespace scripting;
diff --git a/src/logic/scripting/lua/libs/libaudio.cpp b/src/logic/scripting/lua/libs/libaudio.cpp
index 606c4351..b80676c1 100644
--- a/src/logic/scripting/lua/libs/libaudio.cpp
+++ b/src/logic/scripting/lua/libs/libaudio.cpp
@@ -68,6 +68,26 @@ inline audio::speakerid_t play_stream(
if (channel == -1) {
return 0;
}
+ if (!scripting::engine->isHeadless()) {
+ auto assets = scripting::engine->getAssets();
+
+ auto stream = assets->getShared(filename);
+ if (stream) {
+ return audio::play(
+ audio::open_stream(std::move(stream), true),
+ glm::vec3(
+ static_cast(x),
+ static_cast(y),
+ static_cast(z)
+ ),
+ relative,
+ volume,
+ pitch,
+ loop,
+ channel
+ );
+ }
+ }
io::path file;
if (std::strchr(filename, ':')) {
file = std::string(filename);
@@ -360,16 +380,80 @@ static int l_audio_get_velocity(lua::State* L) {
return 0;
}
-// @brief audio.count_speakers() -> integer
+/// @brief audio.count_speakers() -> integer
static int l_audio_count_speakers(lua::State* L) {
return lua::pushinteger(L, audio::count_speakers());
}
-// @brief audio.count_streams() -> integer
+/// @brief audio.count_streams() -> integer
static int l_audio_count_streams(lua::State* L) {
return lua::pushinteger(L, audio::count_streams());
}
+/// @brief audio.input.fetch(size) -> Bytearray
+static int l_audio_fetch_input(lua::State* L) {
+ auto device = audio::get_input_device();
+ if (device == nullptr) {
+ return 0;
+ }
+ size_t size = lua::touinteger(L, 1);
+ const size_t MAX_BUFFER_SIZE = audio::MAX_INPUT_SAMPLES * 4;
+ if (size == 0) {
+ size = MAX_BUFFER_SIZE;
+ }
+ size = std::min(size, MAX_BUFFER_SIZE);
+ ubyte buffer[MAX_BUFFER_SIZE];
+ size = device->read(reinterpret_cast(buffer), size);
+
+ std::vector bytes(buffer, buffer + size);
+ return lua::create_bytearray(L, std::move(bytes));
+}
+
+static int l_audio_get_input_devices_names(lua::State* L) {
+ auto device_names = audio::get_input_devices_names();
+ lua::createtable(L, device_names.size(), 0);
+ int index = 1;
+ for (const auto& name : device_names) {
+ lua::pushstring(L, name.c_str());
+ lua::rawseti(L, index++);
+ }
+ return 1;
+}
+
+static int l_audio_get_output_devices_names(lua::State* L) {
+ auto device_names = audio::get_output_devices_names();
+ lua::createtable(L, device_names.size(), 0);
+ int index = 1;
+ for (const auto& name : device_names) {
+ lua::pushstring(L, name.c_str());
+ lua::rawseti(L, index++);
+ }
+ return 1;
+}
+
+static int l_audio_set_input_device(lua::State* L) {
+ auto device_name = lua::tostring(L, 1);
+ audio::set_input_device(device_name);
+ return 0;
+}
+
+static int l_audio_get_input_info(lua::State* L) {
+ auto device = audio::get_input_device();
+ if (device == nullptr) {
+ return 0;
+ }
+ lua::createtable(L, 0, 4);
+ lua::pushlstring(L, device->getDeviceSpecifier());
+ lua::setfield(L, "device_specifier");
+ lua::pushinteger(L, device->getChannels());
+ lua::setfield(L, "channels");
+ lua::pushinteger(L, device->getSampleRate());
+ lua::setfield(L, "sample_rate");
+ lua::pushinteger(L, device->getBitsPerSample());
+ lua::setfield(L, "bits_per_sample");
+ return 1;
+}
+
const luaL_Reg audiolib[] = {
{"play_sound", lua::wrap},
{"play_sound_2d", lua::wrap},
@@ -395,5 +479,10 @@ const luaL_Reg audiolib[] = {
{"get_velocity", lua::wrap},
{"count_speakers", lua::wrap},
{"count_streams", lua::wrap},
+ {"__fetch_input", lua::wrap},
+ {"get_input_devices_names", lua::wrap},
+ {"get_output_devices_names", lua::wrap},
+ {"set_input_device", lua::wrap},
+ {"get_input_info", lua::wrap},
{nullptr, nullptr}
};
diff --git a/src/logic/scripting/lua/libs/libbjson.cpp b/src/logic/scripting/lua/libs/libbjson.cpp
index 71498dbc..8c28f04e 100644
--- a/src/logic/scripting/lua/libs/libbjson.cpp
+++ b/src/logic/scripting/lua/libs/libbjson.cpp
@@ -1,7 +1,6 @@
#include "coders/binary_json.hpp"
#include "api_lua.hpp"
#include "util/Buffer.hpp"
-#include "../lua_custom_types.hpp"
static int l_tobytes(lua::State* L) {
auto value = lua::tovalue(L, 1);
diff --git a/src/logic/scripting/lua/libs/libgeneration.cpp b/src/logic/scripting/lua/libs/libgeneration.cpp
index 49407022..2e872ec7 100644
--- a/src/logic/scripting/lua/libs/libgeneration.cpp
+++ b/src/logic/scripting/lua/libs/libgeneration.cpp
@@ -10,7 +10,7 @@
#include "content/ContentControl.hpp"
#include "engine/Engine.hpp"
#include "engine/EnginePaths.hpp"
-#include "../lua_custom_types.hpp"
+#include "../usertypes/lua_type_voxelfragment.hpp"
using namespace scripting;
diff --git a/src/logic/scripting/lua/libs/libgui.cpp b/src/logic/scripting/lua/libs/libgui.cpp
index 032cc168..3697d39c 100644
--- a/src/logic/scripting/lua/libs/libgui.cpp
+++ b/src/logic/scripting/lua/libs/libgui.cpp
@@ -24,6 +24,7 @@
#include "items/Inventories.hpp"
#include "util/stringutil.hpp"
#include "world/Level.hpp"
+#include "../usertypes/lua_type_canvas.hpp"
using namespace gui;
using namespace scripting;
diff --git a/src/logic/scripting/lua/libs/libutf8.cpp b/src/logic/scripting/lua/libs/libutf8.cpp
index ec746caa..e6538c2e 100644
--- a/src/logic/scripting/lua/libs/libutf8.cpp
+++ b/src/logic/scripting/lua/libs/libutf8.cpp
@@ -3,7 +3,6 @@
#include
#include
-#include "../lua_custom_types.hpp"
#include "util/stringutil.hpp"
static int l_tobytes(lua::State* L) {
diff --git a/src/logic/scripting/lua/lua_commons.hpp b/src/logic/scripting/lua/lua_commons.hpp
index f272dbf0..bb3163c8 100644
--- a/src/logic/scripting/lua/lua_commons.hpp
+++ b/src/logic/scripting/lua/lua_commons.hpp
@@ -48,4 +48,9 @@ namespace lua {
void log_error(const std::string& text);
+ class Userdata {
+ public:
+ virtual ~Userdata() {};
+ virtual const std::string& getTypeName() const = 0;
+ };
}
diff --git a/src/logic/scripting/lua/lua_custom_types.hpp b/src/logic/scripting/lua/lua_custom_types.hpp
deleted file mode 100644
index e1e9c3b6..00000000
--- a/src/logic/scripting/lua/lua_custom_types.hpp
+++ /dev/null
@@ -1,140 +0,0 @@
-#pragma once
-
-#include
-#include
-#include
-#include
-
-#include "lua_commons.hpp"
-#include "constants.hpp"
-#include "maths/UVRegion.hpp"
-
-struct fnl_state;
-class Heightmap;
-class VoxelFragment;
-class Texture;
-class ImageData;
-
-namespace lua {
- class Userdata {
- public:
- virtual ~Userdata() {};
- virtual const std::string& getTypeName() const = 0;
- };
-
- class LuaHeightmap : public Userdata {
- std::shared_ptr map;
- std::unique_ptr noise;
- public:
- LuaHeightmap(const std::shared_ptr& map);
- LuaHeightmap(uint width, uint height);
-
- virtual ~LuaHeightmap();
-
- uint getWidth() const;
-
- uint getHeight() const;
-
- float* getValues();
-
- const float* getValues() const;
-
- const std::string& getTypeName() const override {
- return TYPENAME;
- }
-
- const std::shared_ptr& getHeightmap() const {
- return map;
- }
-
- fnl_state* getNoise() {
- return noise.get();
- }
-
- void setSeed(int64_t seed);
-
- static int createMetatable(lua::State*);
- inline static std::string TYPENAME = "Heightmap";
- };
- static_assert(!std::is_abstract());
-
- class LuaVoxelFragment : public Userdata {
- std::array, 4> fragmentVariants;
- public:
- LuaVoxelFragment(
- std::array, 4> fragmentVariants
- );
-
- virtual ~LuaVoxelFragment();
-
- std::shared_ptr getFragment(size_t rotation) const {
- return fragmentVariants.at(rotation & 0b11);
- }
-
- const std::string& getTypeName() const override {
- return TYPENAME;
- }
-
- static int createMetatable(lua::State*);
- inline static std::string TYPENAME = "VoxelFragment";
- };
- static_assert(!std::is_abstract());
-
- class LuaCanvas : public Userdata {
- public:
- explicit LuaCanvas(
- std::shared_ptr texture,
- std::shared_ptr data,
- UVRegion region = UVRegion(0, 0, 1, 1)
- );
- ~LuaCanvas() override = default;
-
- const std::string& getTypeName() const override {
- return TYPENAME;
- }
-
- [[nodiscard]] auto& getTexture() const {
- return *texture;
- }
-
- [[nodiscard]] auto& getData() const {
- return *data;
- }
-
- [[nodiscard]] bool hasTexture() const {
- return texture != nullptr;
- }
-
- auto shareTexture() const {
- return texture;
- }
-
- void update(int extrusion = ATLAS_EXTRUSION);
-
- void createTexture();
-
- static int createMetatable(lua::State*);
- inline static std::string TYPENAME = "Canvas";
- private:
- std::shared_ptr texture; // nullable
- std::shared_ptr data;
- UVRegion region;
- };
- static_assert(!std::is_abstract());
-
- class LuaRandom : public Userdata {
- public:
- std::mt19937 rng;
-
- explicit LuaRandom(uint64_t seed) : rng(seed) {}
- virtual ~LuaRandom() override = default;
-
- const std::string& getTypeName() const override {
- return TYPENAME;
- }
-
- static int createMetatable(lua::State*);
- inline static std::string TYPENAME = "__vc_Random";
- };
- static_assert(!std::is_abstract());
-}
diff --git a/src/logic/scripting/lua/lua_engine.cpp b/src/logic/scripting/lua/lua_engine.cpp
index 324b3d3b..eef391b5 100644
--- a/src/logic/scripting/lua/lua_engine.cpp
+++ b/src/logic/scripting/lua/lua_engine.cpp
@@ -8,7 +8,11 @@
#include "debug/Logger.hpp"
#include "util/stringutil.hpp"
#include "libs/api_lua.hpp"
-#include "lua_custom_types.hpp"
+#include "usertypes/lua_type_heightmap.hpp"
+#include "usertypes/lua_type_voxelfragment.hpp"
+#include "usertypes/lua_type_canvas.hpp"
+#include "usertypes/lua_type_random.hpp"
+#include "usertypes/lua_type_pcmstream.hpp"
#include "engine/Engine.hpp"
static debug::Logger logger("lua-state");
@@ -181,5 +185,12 @@ State* lua::create_state(const EnginePaths& paths, StateType stateType) {
}
pop(L);
}
+ newusertype(L);
+ if (getglobal(L, "audio")) {
+ if (getglobal(L, "__vc_PCMStream")) {
+ setfield(L, "PCMStream");
+ }
+ pop(L);
+ }
return L;
}
diff --git a/src/logic/scripting/lua/lua_util.cpp b/src/logic/scripting/lua/lua_util.cpp
index 1f0bfd99..6fdbe91e 100644
--- a/src/logic/scripting/lua/lua_util.cpp
+++ b/src/logic/scripting/lua/lua_util.cpp
@@ -155,18 +155,18 @@ static int l_error_handler(lua_State* L) {
}
int lua::call(State* L, int argc, int nresults) {
- int handler_pos = gettop(L) - argc;
+ int handlerPos = gettop(L) - argc;
pushcfunction(L, l_error_handler);
- insert(L, handler_pos);
+ insert(L, handlerPos);
int top = gettop(L);
- if (lua_pcall(L, argc, nresults, handler_pos)) {
+ if (lua_pcall(L, argc, nresults, handlerPos)) {
std::string log = tostring(L, -1);
pop(L);
- remove(L, handler_pos);
+ remove(L, handlerPos);
throw luaerror(log);
}
int added = gettop(L) - (top - argc - 1);
- remove(L, handler_pos);
+ remove(L, handlerPos);
return added;
}
diff --git a/src/logic/scripting/lua/lua_util.hpp b/src/logic/scripting/lua/lua_util.hpp
index 9e543a0a..f64cdbf7 100644
--- a/src/logic/scripting/lua/lua_util.hpp
+++ b/src/logic/scripting/lua/lua_util.hpp
@@ -6,7 +6,6 @@
#include
#include "data/dv.hpp"
-#include "lua_custom_types.hpp"
#include "lua_wrapper.hpp"
#define GLM_ENABLE_EXPERIMENTAL
#include
diff --git a/src/logic/scripting/lua/usertypes/lua_type_canvas.cpp b/src/logic/scripting/lua/usertypes/lua_type_canvas.cpp
index 8f5d3e00..325f916f 100644
--- a/src/logic/scripting/lua/usertypes/lua_type_canvas.cpp
+++ b/src/logic/scripting/lua/usertypes/lua_type_canvas.cpp
@@ -1,12 +1,13 @@
-#include
+#include "lua_type_canvas.hpp"
#include "graphics/core/ImageData.hpp"
#include "graphics/core/Texture.hpp"
-#include "logic/scripting/lua/lua_custom_types.hpp"
#include "logic/scripting/lua/lua_util.hpp"
#include "engine/Engine.hpp"
#include "assets/Assets.hpp"
+#include
+
using namespace lua;
LuaCanvas::LuaCanvas(
diff --git a/src/logic/scripting/lua/usertypes/lua_type_canvas.hpp b/src/logic/scripting/lua/usertypes/lua_type_canvas.hpp
new file mode 100644
index 00000000..198313cb
--- /dev/null
+++ b/src/logic/scripting/lua/usertypes/lua_type_canvas.hpp
@@ -0,0 +1,52 @@
+#pragma once
+
+#include "../lua_commons.hpp"
+#include "maths/UVRegion.hpp"
+#include "constants.hpp"
+
+class Texture;
+class ImageData;
+
+namespace lua {
+ class LuaCanvas : public Userdata {
+ public:
+ explicit LuaCanvas(
+ std::shared_ptr texture,
+ std::shared_ptr data,
+ UVRegion region = UVRegion(0, 0, 1, 1)
+ );
+ ~LuaCanvas() override = default;
+
+ const std::string& getTypeName() const override {
+ return TYPENAME;
+ }
+
+ [[nodiscard]] auto& getTexture() const {
+ return *texture;
+ }
+
+ [[nodiscard]] auto& getData() const {
+ return *data;
+ }
+
+ [[nodiscard]] bool hasTexture() const {
+ return texture != nullptr;
+ }
+
+ auto shareTexture() const {
+ return texture;
+ }
+
+ void update(int extrusion = ATLAS_EXTRUSION);
+
+ void createTexture();
+
+ static int createMetatable(lua::State*);
+ inline static std::string TYPENAME = "Canvas";
+ private:
+ std::shared_ptr texture; // nullable
+ std::shared_ptr data;
+ UVRegion region;
+ };
+ static_assert(!std::is_abstract());
+}
diff --git a/src/logic/scripting/lua/usertypes/lua_type_heightmap.cpp b/src/logic/scripting/lua/usertypes/lua_type_heightmap.cpp
index 2800035a..d6528298 100644
--- a/src/logic/scripting/lua/usertypes/lua_type_heightmap.cpp
+++ b/src/logic/scripting/lua/usertypes/lua_type_heightmap.cpp
@@ -1,9 +1,4 @@
-#include "../lua_custom_types.hpp"
-
-#include
-#include
-#include
-#include
+#include "lua_type_heightmap.hpp"
#include "util/functional_util.hpp"
#define FNL_IMPL
@@ -15,6 +10,12 @@
#include "engine/Engine.hpp"
#include "engine/EnginePaths.hpp"
#include "../lua_util.hpp"
+#include "lua_type_heightmap.hpp"
+
+#include
+#include
+#include
+#include
using namespace lua;
diff --git a/src/logic/scripting/lua/usertypes/lua_type_heightmap.hpp b/src/logic/scripting/lua/usertypes/lua_type_heightmap.hpp
new file mode 100644
index 00000000..4109be67
--- /dev/null
+++ b/src/logic/scripting/lua/usertypes/lua_type_heightmap.hpp
@@ -0,0 +1,44 @@
+#pragma once
+
+#include "../lua_commons.hpp"
+
+struct fnl_state;
+class Heightmap;
+
+namespace lua {
+ class LuaHeightmap : public Userdata {
+ std::shared_ptr map;
+ std::unique_ptr noise;
+ public:
+ LuaHeightmap(const std::shared_ptr& map);
+ LuaHeightmap(uint width, uint height);
+
+ virtual ~LuaHeightmap();
+
+ uint getWidth() const;
+
+ uint getHeight() const;
+
+ float* getValues();
+
+ const float* getValues() const;
+
+ const std::string& getTypeName() const override {
+ return TYPENAME;
+ }
+
+ const std::shared_ptr& getHeightmap() const {
+ return map;
+ }
+
+ fnl_state* getNoise() {
+ return noise.get();
+ }
+
+ void setSeed(int64_t seed);
+
+ static int createMetatable(lua::State*);
+ inline static std::string TYPENAME = "Heightmap";
+ };
+ static_assert(!std::is_abstract());
+}
diff --git a/src/logic/scripting/lua/usertypes/lua_type_pcmstream.cpp b/src/logic/scripting/lua/usertypes/lua_type_pcmstream.cpp
new file mode 100644
index 00000000..a4a13ea4
--- /dev/null
+++ b/src/logic/scripting/lua/usertypes/lua_type_pcmstream.cpp
@@ -0,0 +1,117 @@
+#include "../lua_util.hpp"
+#include "lua_type_pcmstream.hpp"
+#include "assets/Assets.hpp"
+#include "audio/MemoryPCMStream.hpp"
+#include "engine/Engine.hpp"
+
+using namespace lua;
+using namespace audio;
+using namespace scripting;
+
+LuaPCMStream::LuaPCMStream(std::shared_ptr&& stream)
+ : stream(std::move(stream)) {
+}
+
+LuaPCMStream::~LuaPCMStream() = default;
+
+const std::shared_ptr& LuaPCMStream::getStream() const {
+ return stream;
+}
+
+static int l_feed(lua::State* L) {
+ auto stream = touserdata(L, 1);
+ if (stream == nullptr) {
+ return 0;
+ }
+ auto bytes = bytearray_as_string(L, 2);
+ stream->getStream()->feed(
+ {reinterpret_cast(bytes.data()), bytes.size()}
+ );
+ return 0;
+}
+
+static int l_share(lua::State* L) {
+ auto stream = touserdata(L, 1);
+ if (stream == nullptr) {
+ return 0;
+ }
+ auto alias = require_lstring(L, 2);
+ if (engine->isHeadless()) {
+ return 0;
+ }
+ auto assets = engine->getAssets();
+ assets->store(stream->getStream(), std::string(alias));
+ return 0;
+}
+
+static int l_create_sound(lua::State* L) {
+ auto stream = touserdata(L, 1);
+ if (stream == nullptr) {
+ return 0;
+ }
+ auto alias = require_lstring(L, 2);
+ auto memoryStream = stream->getStream();
+
+ std::vector buffer(memoryStream->available());
+ memoryStream->readFully(buffer.data(), buffer.size(), true);
+
+ auto pcm = std::make_shared(
+ std::move(buffer),
+ 0,
+ memoryStream->getChannels(),
+ static_cast(memoryStream->getBitsPerSample()),
+ memoryStream->getSampleRate(),
+ memoryStream->isSeekable()
+ );
+ auto sound = audio::create_sound(std::move(pcm), true);
+ auto assets = engine->getAssets();
+ assets->store(std::move(sound), std::string(alias));
+ return 0;
+}
+
+static std::unordered_map methods {
+ {"feed", lua::wrap},
+ {"share", lua::wrap},
+ {"create_sound", lua::wrap},
+};
+
+static int l_meta_meta_call(lua::State* L) {
+ auto sampleRate = touinteger(L, 2);
+ auto channels = touinteger(L, 3);
+ auto bitsPerSample = touinteger(L, 4);
+ auto stream =
+ std::make_shared(sampleRate, channels, bitsPerSample);
+ return newuserdata(L, std::move(stream));
+}
+
+static int l_meta_tostring(lua::State* L) {
+ return pushstring(L, "PCMStream");
+}
+
+static int l_meta_index(lua::State* L) {
+ auto stream = touserdata(L, 1);
+ if (stream == nullptr) {
+ return 0;
+ }
+ if (isstring(L, 2)) {
+ auto found = methods.find(tostring(L, 2));
+ if (found != methods.end()) {
+ return pushcfunction(L, found->second);
+ }
+ }
+ return 0;
+}
+
+int LuaPCMStream::createMetatable(lua::State* L) {
+ createtable(L, 0, 3);
+ pushcfunction(L, lua::wrap);
+ setfield(L, "__tostring");
+ pushcfunction(L, lua::wrap);
+ setfield(L, "__index");
+
+ createtable(L, 0, 1);
+ pushcfunction(L, lua::wrap);
+ setfield(L, "__call");
+ setmetatable(L);
+ return 1;
+}
diff --git a/src/logic/scripting/lua/usertypes/lua_type_pcmstream.hpp b/src/logic/scripting/lua/usertypes/lua_type_pcmstream.hpp
new file mode 100644
index 00000000..524c7169
--- /dev/null
+++ b/src/logic/scripting/lua/usertypes/lua_type_pcmstream.hpp
@@ -0,0 +1,26 @@
+#pragma once
+
+#include "../lua_commons.hpp"
+
+namespace audio {
+ class MemoryPCMStream;
+}
+
+namespace lua {
+ class LuaPCMStream : public Userdata {
+ public:
+ explicit LuaPCMStream(std::shared_ptr&& stream);
+ virtual ~LuaPCMStream() override;
+
+ const std::shared_ptr& getStream() const;
+
+ const std::string& getTypeName() const override {
+ return TYPENAME;
+ }
+ static int createMetatable(lua::State*);
+ inline static std::string TYPENAME = "__vc_PCMStream";
+ private:
+ std::shared_ptr stream;
+ };
+ static_assert(!std::is_abstract());
+}
diff --git a/src/logic/scripting/lua/usertypes/lua_type_random.cpp b/src/logic/scripting/lua/usertypes/lua_type_random.cpp
index 99177056..bdbe4a1a 100644
--- a/src/logic/scripting/lua/usertypes/lua_type_random.cpp
+++ b/src/logic/scripting/lua/usertypes/lua_type_random.cpp
@@ -1,5 +1,5 @@
-#include "../lua_custom_types.hpp"
#include "../lua_util.hpp"
+#include "lua_type_random.hpp"
#include
diff --git a/src/logic/scripting/lua/usertypes/lua_type_random.hpp b/src/logic/scripting/lua/usertypes/lua_type_random.hpp
new file mode 100644
index 00000000..06ab24f8
--- /dev/null
+++ b/src/logic/scripting/lua/usertypes/lua_type_random.hpp
@@ -0,0 +1,23 @@
+#pragma once
+
+#include "../lua_commons.hpp"
+
+#include
+
+namespace lua {
+ class LuaRandom : public Userdata {
+ public:
+ std::mt19937 rng;
+
+ explicit LuaRandom(uint64_t seed) : rng(seed) {}
+ virtual ~LuaRandom() override = default;
+
+ const std::string& getTypeName() const override {
+ return TYPENAME;
+ }
+
+ static int createMetatable(lua::State*);
+ inline static std::string TYPENAME = "__vc_Random";
+ };
+ static_assert(!std::is_abstract());
+}
diff --git a/src/logic/scripting/lua/usertypes/lua_type_voxelfragment.cpp b/src/logic/scripting/lua/usertypes/lua_type_voxelfragment.cpp
index 271a2a5a..17cf37d1 100644
--- a/src/logic/scripting/lua/usertypes/lua_type_voxelfragment.cpp
+++ b/src/logic/scripting/lua/usertypes/lua_type_voxelfragment.cpp
@@ -1,7 +1,6 @@
-#include "../lua_custom_types.hpp"
+#include "lua_type_voxelfragment.hpp"
#include "../lua_util.hpp"
-
#include "world/generator/VoxelFragment.hpp"
#include "util/stringutil.hpp"
#include "world/Level.hpp"
diff --git a/src/logic/scripting/lua/usertypes/lua_type_voxelfragment.hpp b/src/logic/scripting/lua/usertypes/lua_type_voxelfragment.hpp
new file mode 100644
index 00000000..21c41c04
--- /dev/null
+++ b/src/logic/scripting/lua/usertypes/lua_type_voxelfragment.hpp
@@ -0,0 +1,31 @@
+#pragma once
+
+#include
+
+#include "../lua_commons.hpp"
+
+class VoxelFragment;
+
+namespace lua {
+ class LuaVoxelFragment : public Userdata {
+ std::array, 4> fragmentVariants;
+ public:
+ LuaVoxelFragment(
+ std::array, 4> fragmentVariants
+ );
+
+ virtual ~LuaVoxelFragment();
+
+ std::shared_ptr getFragment(size_t rotation) const {
+ return fragmentVariants.at(rotation & 0b11);
+ }
+
+ const std::string& getTypeName() const override {
+ return TYPENAME;
+ }
+
+ static int createMetatable(lua::State*);
+ inline static std::string TYPENAME = "VoxelFragment";
+ };
+ static_assert(!std::is_abstract());
+}
diff --git a/src/logic/scripting/scripting.cpp b/src/logic/scripting/scripting.cpp
index 8fe88d39..19accc89 100644
--- a/src/logic/scripting/scripting.cpp
+++ b/src/logic/scripting/scripting.cpp
@@ -17,7 +17,6 @@
#include "logic/BlocksController.hpp"
#include "logic/LevelController.hpp"
#include "lua/lua_engine.hpp"
-#include "lua/lua_custom_types.hpp"
#include "maths/Heightmap.hpp"
#include "objects/Player.hpp"
#include "util/stringutil.hpp"
diff --git a/src/logic/scripting/scripting_world_generation.cpp b/src/logic/scripting/scripting_world_generation.cpp
index bf6c1dd1..3199b46c 100644
--- a/src/logic/scripting/scripting_world_generation.cpp
+++ b/src/logic/scripting/scripting_world_generation.cpp
@@ -6,7 +6,7 @@
#include "scripting_commons.hpp"
#include "typedefs.hpp"
#include "lua/lua_engine.hpp"
-#include "lua/lua_custom_types.hpp"
+#include "lua/usertypes/lua_type_heightmap.hpp"
#include "content/Content.hpp"
#include "voxels/Block.hpp"
#include "voxels/Chunk.hpp"
diff --git a/src/util/span.hpp b/src/util/span.hpp
new file mode 100644
index 00000000..7c97f5ac
--- /dev/null
+++ b/src/util/span.hpp
@@ -0,0 +1,42 @@
+#pragma once
+
+#include
+
+namespace util {
+ template
+ class span {
+ public:
+ constexpr span(const T* ptr, size_t length)
+ : ptr(ptr), length(length) {}
+
+ const T& operator[](size_t index) const {
+ return ptr[index];
+ }
+
+ const T& at(size_t index) const {
+ if (index >= length) {
+ throw std::out_of_range("index is out of range");
+ }
+ return ptr[index];
+ }
+
+ auto begin() const {
+ return ptr;
+ }
+
+ auto end() const {
+ return ptr + length;
+ }
+
+ const T* data() const {
+ return ptr;
+ }
+
+ size_t size() const {
+ return length;
+ }
+ private:
+ const T* ptr;
+ size_t length;
+ };
+}