Merge branch 'dev' into schedule

This commit is contained in:
MihailRis 2025-08-01 21:39:44 +03:00
commit 2c60e7c6ae
16 changed files with 1303 additions and 1 deletions

View File

@ -183,3 +183,26 @@ file.join(директория: str, путь: str) --> str
Соединяет путь. Пример: `file.join("world:data", "base/config.toml)` -> `world:data/base/config.toml`. Соединяет путь. Пример: `file.join("world:data", "base/config.toml)` -> `world:data/base/config.toml`.
Следует использовать данную функцию вместо конкатенации с `/`, так как `префикс:/путь` не является валидным. Следует использовать данную функцию вместо конкатенации с `/`, так как `префикс:/путь` не является валидным.
```lua
file.open(путь: str, режим: str) --> io_stream
```
Открывает поток для записи/чтения в файл по пути `путь`.
Аргумент `режим` это список отдельных режимов, в котором каждый обозначается одним символом
`r` - Чтение из файла
`w` - Запись в файл
`b` - Открыть поток в двоичном режиме (см. `../io_stream.md`)
`+` - Работает совместно с `w`. Добавляет к существующим данным новые (`append-mode`)
```lua
file.open_named_pipe(имя: str, режим: str) -> io_stream
```
Открывает поток для записи/чтения в Named Pipe по пути `путь`
`/tmp/` или `\\\\.\\pipe\\` добавлять не нужно - движок делает это автоматически.
Доступные режимы такие же, как и в `file.open`, за исключением `+`

View File

@ -11,3 +11,21 @@ time.delta() -> float
``` ```
Возвращает дельту времени (время прошедшее с предыдущего кадра) Возвращает дельту времени (время прошедшее с предыдущего кадра)
```python
time.utc_time() -> int
```
Возвращает время UTC в секундах
```python
time.local_time() -> int
```
Возвращает локальное (системное) время в секундах
```python
time.utc_offset() -> int
```
Возвращает смещение локального времени от UTC в секундах

View File

@ -265,3 +265,9 @@ function await(co: coroutine) -> result, error
Ожидает завершение переданной корутины, возвращая поток управления. Функция может быть использована только внутри корутины. Ожидает завершение переданной корутины, возвращая поток управления. Функция может быть использована только внутри корутины.
Возвращает значения аналогичные возвращаемым значениям *pcall*. Возвращает значения аналогичные возвращаемым значениям *pcall*.
```lua
os.pid -> number
```
Константа, в которой хранится PID текущего инстанса движка

View File

@ -0,0 +1,188 @@
# Класс *io_stream*
Класс, предназначенный для работы с потоками
## Режимы
Поток имеет три различных вида режима:
**general** - Общий режим работы I/O
**binary** - Формат записи и чтения I/O
**flush** - Режим работы flush
### general
Имеет три режима:
**default** - Дефолтный режим работы потока. При read может вернуть только часть от требуемых данных, при write сразу записывает данные в поток.
**yield** - Почти тоже самое, что и **default**, но всегда будет возвращать все требуемые данные. Пока они не будут прочитаны, будет вызывать `coroutine.yield()`. Предназначен для работы в корутинах.
**buffered** - Буферизирует записываемые и читаемые данные.
При вызове `available`/`read` обновляет буфер чтения.
После обновления в `read`, если буфер чтения переполнен, то бросает ошибку `buffer overflow`.
Если требуемого кол-ва байт недостаточно в буфере для чтения, то бросает ошибку `buffer-underflow`.
При вызове `write` записывает итоговые байты в буфер для записи. Если он переполнен, то бросает ошибку `buffer overflow`.
При вызове `flush` проталкивает данные из буфера для записи в напрямую в поток
### flush
**all** - Сначала проталкивает данные из буфера напрямую в поток (если используется **buffered** режим), а после вызывает `flush` напрямую из библиотеки
**buffer** - Только проталкивает данные из буфера в поток (если используется **buffered** режим)
## Методы
Методы, позволяющие изменить или получить различные режимы поведения потока
```lua
-- Возвращает true, если поток используется в двоичном режиме
io_stream:is_binary_mode() --> bool
-- Включает или выключает двоичный режим
io_stream:set_binary_mode(bool)
-- Возвращает режим работы потока
io_stream:get_mode() --> string
-- Задаёт режим работы потока. Выбрасывает ошибку, если передан неизвестный режим
io_stream:set_mode(string)
-- Возвращает режим работы flush
io_stream:get_flush_mode() --> string
-- Задаёт режим работы flush
io_stream:set_flush_mode(string)
```
I/O методы
```lua
--[[
Читает данные из потока
В двоичном режиме:
Если arg - int, то читает из потока arg байт и возвращает ввиде Bytearray или таблицы, если useTable = true
Если arg - string, то функция интерпретирует arg как шаблон для byteutil. Прочитает кол-во байт, которое определено шаблоном, передаст их в byteutil.unpack и вернёт результат
В текстовом режиме:
Если arg - int, то читает нужное кол-во строк с окончанием CRLF/LF из arg и возвращает ввиде таблицы. Также, если trimEmptyLines = true, то удаляет пустые строки с начала и конца из итоговой таблицы
Если arg не определён, то читает одну строку с окончанием CRLF/LF и возвращает её.
--]]
io_stream:read(
[опционально] arg: int | string,
[опционально] useTable | trimEmptyLines: bool
) --> Bytearray | table<int> | string | table<string> | ...
--[[
Записывает данные в поток
В двоичном режиме:
Если arg - string, то функция интерпретирует arg как шаблон для byteutil, передаст его и ... в byteutil.pack и результат запишет в поток
Если arg - Bytearray | table<int>, то записывает байты в поток
В текстовом режиме:
Если arg - string, то записывает строку в поток (вместе с окончанием LF)
Если arg - table<string>, то записывает каждую строку из таблицы отдельно
--]]
io_stream:write(
arg: Bytearray | table<int> | string | table<string>,
[опционально] ...
)
-- Читает одну строку с окончанием CRLF/LF из потока вне зависимости от двоичного режима
io_stream:read_line() --> string
-- Записывает одну строку с окончанием LF в поток вне зависимости от двоичного режима
io_stream:write_line(string)
--[[
В двоичном режиме:
Читает все доступные байты из потока и возвращает ввиде Bytearray или table<int>, если useTable = true
В текстовом режиме:
Читает все доступные строки из потока в table<string> если useTable = true, или в одну строку вместе с окончаниями, если нет
--]]
io_stream:read_fully(
[опционально] useTable: bool
) --> Bytearray | table<int> | table<string> | string
```
Методы, имеющие смысл в использовании только в buffered режиме
```lua
--[[
Если length определён, то возвращает true, если length байт доступно к чтению. Иначе возвращает false
Если не определён, то возвращает количество байт, которое можно прочитать
--]]
io_stream:available(
[опционально] length: int
) --> int | bool
-- Возвращает максимальный размер буферов
io_stream:get_max_buffer_size() --> int
-- Задаёт новый максимальный размер буферов
io_stream:set_max_buffer_size(max_size: int)
```
Методы, контролирующие состояние потока
```lua
-- Возвращает true, если поток открыт на данный момент
io_stream:is_alive() --> bool
-- Возвращает true, если поток закрыт на данный момент
io_stream:is_closed() --> bool
-- Закрывает поток
io_stream:close()
--[[
Записывает все данные из write-буфера в поток в buffer/all flush-режимах
Вызывает ioLib.flush() в all flush-режиме
--]]
io_stream:flush()
```
Создание нового потока
```lua
--[[
Создаёт новый поток с переданным дескриптором и использующим переданную I/O библиотеку. (Более подробно в core:io_stream.lua)
--]]
io_stream.new(
descriptor: int,
binaryMode: bool,
ioLib: table,
[опционально] mode: string = "default",
[опционально] flushMode: string = "all"
) -> io_stream
```

View File

@ -0,0 +1,17 @@
local io_stream = require "core:io_stream"
local lib = {
read = file.__read_descriptor,
write = file.__write_descriptor,
flush = file.__flush_descriptor,
is_alive = file.__has_descriptor,
close = file.__close_descriptor
}
return function(path, mode)
return io_stream.new(
file.__open_descriptor(path, mode),
mode:find('b') ~= nil,
lib
)
end

View File

@ -0,0 +1,7 @@
local FFI = ffi
if FFI.os == "Windows" then
return require "core:internal/stream_providers/named_pipe_windows"
else
return require "core:internal/stream_providers/named_pipe_unix"
end

View File

@ -0,0 +1,21 @@
local forbiddenPaths = {
"/..\\", "\\../",
"/../", "\\..\\"
}
return function(path)
local corrected = true
if path:starts_with("../") or path:starts_with("..\\") then
corrected = false
else
for _, forbiddenPath in ipairs(forbiddenPaths) do
if path:find(forbiddenPath) then
corrected = false
break
end
end
end
if not corrected then error "special path \"../\" is not allowed in path to named pipe" end
end

View File

@ -0,0 +1,104 @@
local path_validate = require "core:internal/stream_providers/named_pipe_path_validate"
local io_stream = require "core:io_stream"
local FFI = ffi
FFI.cdef[[
int open(const char *pathname, int flags);
int close(int fd);
ssize_t read(int fd, void *buf, size_t count);
ssize_t write(int fd, const void *buf, size_t count);
int fcntl(int fd, int cmd, ...);
const char *strerror(int errnum);
]]
local C = FFI.C
local O_RDONLY = 0x0
local O_WRONLY = 0x1
local O_RDWR = 0x2
local O_NONBLOCK = 0x800
local F_GETFL = 3
local function getError()
local err = ffi.errno()
return ffi.string(C.strerror(err)).." ("..err..")"
end
local lib = {}
function lib.read(fd, len)
local buffer = FFI.new("uint8_t[?]", len)
local result = C.read(fd, buffer, len)
local out = Bytearray()
if result <= 0 then
return out
end
for i = 0, result - 1 do
out[i+1] = buffer[i]
end
return out
end
function lib.write(fd, bytearray)
local len = #bytearray
local buffer = FFI.new("uint8_t[?]", len)
for i = 1, len do
buffer[i-1] = bytearray[i]
end
if C.write(fd, buffer, len) == -1 then
error("failed to write to named pipe: "..getError())
end
end
function lib.flush(fd)
-- no flush on unix
end
function lib.is_alive(fd)
if fd == nil or fd < 0 then return false end
return C.fcntl(fd, F_GETFL) ~= -1
end
function lib.close(fd)
C.close(fd)
end
return function(path, mode)
path_validate(path)
path = "/tmp/"..path
local read = mode:find('r') ~= nil
local write = mode:find('w') ~= nil
local flags
if read and write then
flags = O_RDWR
elseif read then
flags = O_RDONLY
elseif write then
flags = O_WRONLY
else
error "mode must contain read or write flag"
end
flags = bit.bor(flags, O_NONBLOCK)
local fd = C.open(path, flags)
if fd == -1 then
error("failed to open named pipe: "..getError())
end
return io_stream.new(fd, mode:find('b') ~= nil, lib)
end

View File

@ -0,0 +1,144 @@
local path_validate = require "core:internal/stream_providers/named_pipe_path_validate"
local io_stream = require "core:io_stream"
local FFI = ffi
FFI.cdef[[
typedef void* HANDLE;
typedef uint32_t DWORD;
typedef int BOOL;
typedef void* LPVOID;
typedef const char* LPCSTR;
BOOL CloseHandle(HANDLE hObject);
DWORD GetFileType(HANDLE hFile);
BOOL ReadFile(HANDLE hFile, void* lpBuffer, DWORD nNumberOfBytesToRead,
DWORD* lpNumberOfBytesRead, void* lpOverlapped);
BOOL WriteFile(HANDLE hFile, const void* lpBuffer, DWORD nNumberOfBytesToWrite,
DWORD* lpNumberOfBytesWritten, void* lpOverlapped);
HANDLE CreateFileA(LPCSTR lpFileName, DWORD dwDesiredAccess, DWORD dwShareMode,
void* lpSecurityAttributes, DWORD dwCreationDisposition,
DWORD dwFlagsAndAttributes, HANDLE hTemplateFile);
BOOL PeekNamedPipe(
HANDLE hNamedPipe,
LPVOID lpBuffer,
DWORD nBufferSize,
DWORD* lpBytesRead,
DWORD* lpTotalBytesAvail,
DWORD* lpBytesLeftThisMessage
);
DWORD GetLastError(void);
BOOL FlushFileBuffers(HANDLE hFile);
]]
local C = FFI.C
local GENERIC_READ = 0x80000000
local GENERIC_WRITE = 0x40000000
local OPEN_EXISTING = 3
local FILE_ATTRIBUTE_NORMAL = 0x00000080
local FILE_TYPE_UNKNOWN = 0x0000
local INVALID_HANDLE_VALUE = FFI.cast("HANDLE", -1)
local lib = {}
local function is_data_available(handle)
local bytes_available = FFI.new("DWORD[1]")
local success = FFI.C.PeekNamedPipe(handle, nil, 0, nil, bytes_available, nil)
if success == 0 then
return -1
end
return bytes_available[0] > 0
end
function lib.read(handle, len)
local out = Bytearray()
local has_data, err = is_data_available(handle)
if not has_data then
return out
elseif hasData == -1 then
error("failed to read from named pipe: "..tostring(C.GetLastError()))
end
local buffer = FFI.new("uint8_t[?]", len)
local read = FFI.new("DWORD[1]")
local ok = C.ReadFile(handle, buffer, len, read, nil)
if ok == 0 or read[0] == 0 then
return out
end
for i = 0, read[0] - 1 do
out[i+1] = buffer[i]
end
return out
end
function lib.write(handle, bytearray)
local len = #bytearray
local buffer = FFI.new("uint8_t[?]", len)
for i = 1, len do
buffer[i-1] = bytearray[i]
end
local written = FFI.new("DWORD[1]")
if C.WriteFile(handle, buffer, len, written, nil) == 0 then
error("failed to write to named pipe: "..tostring(C.GetLastError()))
end
end
function lib.flush(handle)
C.FlushFileBuffers(handle)
end
function lib.is_alive(handle)
if handle == nil or handle == INVALID_HANDLE_VALUE then
return false
else
return C.GetFileType(handle) ~= FILE_TYPE_UNKNOWN
end
end
function lib.close(handle)
C.CloseHandle(handle)
end
return function(path, mode)
path_validate(path)
path = "\\\\.\\pipe\\"..path
local read = mode:find('r') ~= nil
local write = mode:find('w') ~= nil
local flags
if read and write then
flags = bit.bor(GENERIC_READ, GENERIC_WRITE)
elseif read then
flags = GENERIC_READ
elseif write then
flags = GENERIC_WRITE
else
error("mode must contain read or write flag")
end
local handle = C.CreateFileA(path, flags, 0, nil, OPEN_EXISTING,
FILE_ATTRIBUTE_NORMAL, nil)
if handle == INVALID_HANDLE_VALUE then
error("failed to open named pipe: "..tostring(C.GetLastError()))
end
return io_stream.new(handle, mode:find('b') ~= nil, lib)
end

398
res/modules/io_stream.lua Normal file
View File

@ -0,0 +1,398 @@
local io_stream = { }
io_stream.__index = io_stream
local MAX_BUFFER_SIZE = 8192
local DEFAULT_MODE = "default"
local BUFFERED_MODE = "buffered"
local YIELD_MODE = "yield"
local ALL_MODES = {
DEFAULT_MODE,
BUFFERED_MODE,
YIELD_MODE
}
local FLUSH_MODE_ALL = "all"
local FLUSH_MODE_ONLY_BUFFER = "buffer"
local ALL_FLUSH_MODES = {
FLUSH_MODE_ALL,
FLUSH_MODE_ONLY_BUFFER
}
local CR = string.byte('\r')
local LF = string.byte('\n')
local function readFully(result, readFunc)
local isTable = type(result) == "table"
local buf
repeat
buf = readFunc(MAX_BUFFER_SIZE)
if isTable then
for i = 1, #buf do
result[#result + 1] = buf[i]
end
else result:append(buf) end
until #buf == 0
end
--[[
descriptor - descriptor of stream for provided I/O library
binaryMode - if enabled, most methods will expect bytes instead of strings
ioLib - I/O library. Should include the following functions:
read(descriptor: int, length: int) -> Bytearray
May return bytearray with a smaller size if bytes have not arrived yet or have run out
write(descriptor: int, data: Bytearray)
flush(descriptor: int)
is_alive(descriptor: int) -> bool
close(descriptor: int)
--]]
function io_stream.new(descriptor, binaryMode, ioLib, mode, flushMode)
mode = mode or DEFAULT_MODE
flushMode = flushMode or FLUSH_MODE_ALL
local self = setmetatable({}, io_stream)
self.descriptor = descriptor
self.binaryMode = binaryMode
self.maxBufferSize = MAX_BUFFER_SIZE
self.ioLib = ioLib
self:set_mode(mode)
self:set_flush_mode(flushMode)
return self
end
function io_stream:is_binary_mode()
return self.binaryMode
end
function io_stream:set_binary_mode(binaryMode)
self.binaryMode = binaryMode ~= nil
end
function io_stream:get_mode()
return self.mode
end
function io_stream:set_mode(mode)
if not table.has(ALL_MODES, mode) then
error("invalid stream mode: "..mode)
end
if self.mode == BUFFERED_MODE then
self.writeBuffer:clear()
self.readBuffer:clear()
end
if mode == BUFFERED_MODE and not self.writeBuffer then
self.writeBuffer = Bytearray()
self.readBuffer = Bytearray()
end
self.mode = mode
end
function io_stream:get_flush_mode()
return self.flushMode
end
function io_stream:set_flush_mode(flushMode)
if not table.has(ALL_FLUSH_MODES, flushMode) then
error("invalid flush mode: "..flushMode)
end
self.flushMode = flushMode
end
function io_stream:get_max_buffer_size()
return self.maxBufferSize
end
function io_stream:set_max_buffer_size(maxBufferSize)
self.maxBufferSize = maxBufferSize
end
function io_stream:available(length)
if self.mode == BUFFERED_MODE then
self:__update_read_buffer()
if not length then
return #self.readBuffer
else
return #self.readBuffer >= length
end
end
end
function io_stream:__update_read_buffer()
local readed = Bytearray()
readFully(readed, function(length) return self.ioLib.read(self.descriptor, length) end)
self.readBuffer:append(readed)
if #self.readBuffer > self.maxBufferSize then
error "buffer overflow"
end
end
function io_stream:__read(length)
if self.mode == YIELD_MODE then
local buffer = Bytearray()
while #buffer < length do
buffer:append(self.ioLib.read(self.descriptor, length - #buffer))
if #buffer < length then coroutine.yield() end
end
return buffer
elseif self.mode == BUFFERED_MODE then
self:__update_read_buffer()
if #self.readBuffer < length then
error "buffer underflow"
end
local copy
if #self.readBuffer == length then
copy = Bytearray()
copy:append(self.readBuffer)
self.readBuffer:clear()
else
copy = Bytearray()
for i = 1, length do
copy[i] = self.readBuffer[i]
end
self.readBuffer:remove(1, length)
end
return copy
elseif self.mode == DEFAULT_MODE then
return self.ioLib.read(self.descriptor, length)
end
end
function io_stream:__write(data)
if self.mode == BUFFERED_MODE then
self.writeBuffer:append(data)
if #self.writeBuffer > self.maxBufferSize then
error "buffer overflow"
end
elseif self.mode == DEFAULT_MODE or self.mode == YIELD_MODE then
return self.ioLib.write(self.descriptor, data)
end
end
function io_stream:read_fully(useTable)
if self.binaryMode then
local result = useTable and Bytearray() or { }
readFully(result, function() return self:__read(self.maxBufferSize) end)
else
if useTable then
local lines = { }
local line
repeat
line = self:read_line()
lines[#lines + 1] = line
until not line
return lines
else
local result = Bytearray()
readFully(result, function() return self:__read(self.maxBufferSize) end)
return utf8.tostring(result)
end
end
end
function io_stream:read_line()
local result = Bytearray()
local first = true
while true do
local char = self:__read(1)
if #char == 0 then
if first then return else break end
end
char = char[1]
if char == LF then break
elseif char == CR then
char = self:__read(1)
if char[1] == LF then break
else
result:append(CR)
result:append(char[1])
end
else result:append(char) end
first = false
end
return utf8.tostring(result)
end
function io_stream:write_line(str)
self:__write(utf8.tobytes(str .. LF))
end
function io_stream:read(arg, useTable)
local argType = type(arg)
if self.binaryMode then
local byteArr
if argType == "number" then
-- using 'arg' as length
byteArr = self:__read(arg)
if useTable == true then
local t = { }
for i = 1, #byteArr do
t[i] = byteArr[i]
end
return t
else
return byteArr
end
elseif argType == "string" then
return byteutil.unpack(
arg,
self:__read(byteutil.get_size(arg))
)
elseif argType == nil then
error(
"in binary mode the first argument must be a string data format"..
" for the library \"byteutil\" or the number of bytes to read"
)
else
error("unknown argument type: "..argType)
end
else
if not arg then
return self:read_line()
else
local linesCount = arg
local trimLastEmptyLines = useTable or true
if linesCount < 0 then error "count of lines to read must be positive" end
local result = { }
for i = 1, linesCount do
result[i] = self:read_line()
end
if trimLastEmptyLines then
local i = #result
while i >= 0 do
local length = utf8.length(result[i])
if length > 0 then break
else result[i] = nil end
i = i - 1
end
local i = 1
while #result > 0 do
local length = utf8.length(result[i])
if length > 0 then break
else table.remove(result, i) end
end
end
return result
end
end
end
function io_stream:write(arg, ...)
local argType = type(arg)
if self.binaryMode then
local byteArr
if argType ~= "string" then
-- using arg as bytes table/bytearray
if argType == "table" then
byteArr = Bytearray(arg)
else
byteArr = arg
end
else
byteArr = byteutil.pack(arg, ...)
end
self:__write(byteArr)
else
if argType == "string" then
self:write_line(arg)
elseif argType == "table" then
for i = 1, #arg do
self:write_line(arg[i])
end
else error("unknown argument type: "..argType) end
end
end
function io_stream:is_alive()
return self.ioLib.is_alive(self.descriptor)
end
function io_stream:is_closed()
return not self:is_alive()
end
function io_stream:close()
if self.mode == BUFFERED_MODE then
self.readBuffer:clear()
self.writeBuffer:clear()
end
return self.ioLib.close(self.descriptor)
end
function io_stream:flush()
if self.mode == BUFFERED_MODE and #self.writeBuffer > 0 then
self.ioLib.write(self.descriptor, self.writeBuffer)
self.writeBuffer:clear()
end
if self.flushMode ~= FLUSH_MODE_ONLY_BUFFER then self.ioLib.flush(self.descriptor) end
end
return io_stream

View File

@ -317,10 +317,30 @@ entities.get_all = function(uids)
return stdcomp.get_all(uids) return stdcomp.get_all(uids)
end end
end end
local bytearray = require "core:internal/bytearray" local bytearray = require "core:internal/bytearray"
Bytearray = bytearray.FFIBytearray Bytearray = bytearray.FFIBytearray
Bytearray_as_string = bytearray.FFIBytearray_as_string Bytearray_as_string = bytearray.FFIBytearray_as_string
Bytearray_construct = function(...) return Bytearray(...) end Bytearray_construct = function(...) return Bytearray(...) end
file.open = require "core:internal/stream_providers/file"
file.open_named_pipe = require "core:internal/stream_providers/named_pipe"
if ffi.os == "Windows" then
ffi.cdef[[
unsigned long GetCurrentProcessId();
]]
os.pid = ffi.C.GetCurrentProcessId()
else
ffi.cdef[[
int getpid(void);
]]
os.pid = ffi.C.getpid()
end
ffi = nil ffi = nil
math.randomseed(time.uptime() * 1536227939) math.randomseed(time.uptime() * 1536227939)
@ -506,6 +526,7 @@ function __vc_on_world_quit()
_rules.clear() _rules.clear()
gui_util:__reset_local() gui_util:__reset_local()
stdcomp.__reset() stdcomp.__reset()
file.__close_all_descriptors()
end end
local __vc_coroutines = {} local __vc_coroutines = {}

View File

@ -0,0 +1,104 @@
#include "logic/scripting/descriptors_manager.hpp"
#include "debug/Logger.hpp"
static debug::Logger logger("descriptors-manager");
namespace scripting {
std::vector<std::optional<StreamDescriptor>> descriptors_manager::descriptors;
std::istream* descriptors_manager::get_input(int descriptor) {
if (!is_readable(descriptor))
return nullptr;
return descriptors[descriptor]->in.get();
}
std::ostream* descriptors_manager::get_output(int descriptor) {
if (!is_writeable(descriptor))
return nullptr;
return descriptors[descriptor]->out.get();
}
void descriptors_manager::flush(int descriptor) {
if (is_writeable(descriptor)) {
descriptors[descriptor]->out->flush();
}
}
bool descriptors_manager::has_descriptor(int descriptor) {
return is_readable(descriptor) || is_writeable(descriptor);
}
bool descriptors_manager::is_readable(int descriptor) {
return descriptor >= 0 && descriptor < static_cast<int>(descriptors.size())
&& descriptors[descriptor].has_value()
&& descriptors[descriptor]->in != nullptr;
}
bool descriptors_manager::is_writeable(int descriptor) {
return descriptor >= 0 && descriptor < static_cast<int>(descriptors.size())
&& descriptors[descriptor].has_value()
&& descriptors[descriptor]->out != nullptr;
}
void descriptors_manager::close(int descriptor) {
if (descriptor >= 0 && descriptor < static_cast<int>(descriptors.size())) {
if (descriptors[descriptor].has_value()) {
auto& desc = descriptors[descriptor].value();
if (desc.out)
desc.out->flush();
desc.in.reset();
desc.out.reset();
}
descriptors[descriptor].reset();
descriptors[descriptor] = std::nullopt;
}
}
int descriptors_manager::open_descriptor(const io::path& path, bool write, bool read) {
std::unique_ptr<std::istream> in;
std::unique_ptr<std::ostream> out;
try {
if (read)
in = io::read(path);
if (write)
out = io::write(path);
} catch (const std::exception& e) {
logger.error() << "failed to open descriptor for " << path.string()
<< ": " << e.what();
return -1;
}
for (int i = 0; i < static_cast<int>(descriptors.size()); ++i) {
if (!descriptors[i].has_value()) {
descriptors[i] = StreamDescriptor{ std::move(in), std::move(out) };
return i;
}
}
descriptors.emplace_back(StreamDescriptor{ std::move(in), std::move(out) });
return static_cast<int>(descriptors.size() - 1);
}
void descriptors_manager::close_all_descriptors() {
for (int i = 0; i < static_cast<int>(descriptors.size()); ++i) {
if (descriptors[i].has_value()) {
close(i);
}
}
descriptors.clear();
}
}

View File

@ -0,0 +1,39 @@
#pragma once
#include <memory>
#include <vector>
#include <optional>
#include <string>
#include <istream>
#include <ostream>
#include "io/io.hpp"
namespace scripting {
struct StreamDescriptor {
std::unique_ptr<std::istream> in;
std::unique_ptr<std::ostream> out;
};
class descriptors_manager {
private:
static std::vector<std::optional<StreamDescriptor>> descriptors;
public:
static std::istream* get_input(int descriptor);
static std::ostream* get_output(int descriptor);
static void flush(int descriptor);
static bool has_descriptor(int descriptor);
static bool is_readable(int descriptor);
static bool is_writeable(int descriptor);
static void close(int descriptor);
static int open_descriptor(const io::path& path, bool write, bool read);
static void close_all_descriptors();
};
}

View File

@ -197,9 +197,16 @@ static int l_tpack(lua::State* L) {
return pack(L, format, true); return pack(L, format, true);
} }
static int l_get_size(lua::State* L) {
return lua::pushinteger(
L, static_cast<int>(calc_size(lua::require_string(L, 1)))
);
}
const luaL_Reg byteutillib[] = { const luaL_Reg byteutillib[] = {
{"pack", lua::wrap<l_pack>}, {"pack", lua::wrap<l_pack>},
{"tpack", lua::wrap<l_tpack>}, {"tpack", lua::wrap<l_tpack>},
{"unpack", lua::wrap<l_unpack>}, {"unpack", lua::wrap<l_unpack>},
{"get_size", lua::wrap<l_get_size>},
{NULL, NULL} {NULL, NULL}
}; };

View File

@ -9,6 +9,7 @@
#include "util/stringutil.hpp" #include "util/stringutil.hpp"
#include "api_lua.hpp" #include "api_lua.hpp"
#include "../lua_engine.hpp" #include "../lua_engine.hpp"
#include "logic/scripting/descriptors_manager.hpp"
namespace fs = std::filesystem; namespace fs = std::filesystem;
using namespace scripting; using namespace scripting;
@ -258,6 +259,149 @@ static int l_create_zip(lua::State* L) {
return 0; return 0;
} }
static int l_open_descriptor(lua::State* L) {
io::path path = lua::require_string(L, 1);
auto mode = lua::require_lstring(L, 2);
bool write = mode.find('w') != std::string::npos;
bool read = mode.find('r') != std::string::npos;
if (write && !is_writeable(path.entryPoint())) {
throw std::runtime_error("access denied");
}
if(!write && !read) {
throw std::runtime_error("mode must contain read or write flag");
}
if(write && read) {
throw std::runtime_error("random access file i/o is not supported");
}
bool wplusMode = write && mode.find('+') != std::string::npos;
std::vector<char> buffer;
if(wplusMode) {
int temp_descriptor = scripting::descriptors_manager::open_descriptor(path, false, true);
if (temp_descriptor == -1) {
throw std::runtime_error("failed to open descriptor for initial reading");
}
auto* in_stream = scripting::descriptors_manager::get_input(temp_descriptor);
in_stream->seekg(0, std::ios::end);
std::streamsize size = in_stream->tellg();
in_stream->seekg(0, std::ios::beg);
buffer.resize(size);
in_stream->read(buffer.data(), size);
scripting::descriptors_manager::close(temp_descriptor);
}
int descriptor = scripting::descriptors_manager::open_descriptor(path, write, read);
if(descriptor == -1) {
throw std::runtime_error("failed to open descriptor");
}
if(wplusMode) {
auto* out_stream = scripting::descriptors_manager::get_output(descriptor);
out_stream->write(buffer.data(), buffer.size());
out_stream->flush();
}
return lua::pushinteger(L, descriptor);
}
static int l_has_descriptor(lua::State* L) {
return lua::pushboolean(L, scripting::descriptors_manager::has_descriptor(lua::tointeger(L, 1)));
}
static int l_read_descriptor(lua::State* L) {
int descriptor = lua::tointeger(L, 1);
if (!scripting::descriptors_manager::has_descriptor(descriptor)) {
throw std::runtime_error("unknown descriptor");
}
if (!scripting::descriptors_manager::is_readable(descriptor)) {
throw std::runtime_error("descriptor is not readable");
}
int maxlen = lua::tointeger(L, 2);
auto* stream = scripting::descriptors_manager::get_input(descriptor);
util::Buffer<char> buffer(maxlen);
stream->read(buffer.data(), maxlen);
std::streamsize read_len = stream->gcount();
return lua::create_bytearray(L, buffer.data(), read_len);
}
static int l_write_descriptor(lua::State* L) {
int descriptor = lua::tointeger(L, 1);
if (!scripting::descriptors_manager::has_descriptor(descriptor)) {
throw std::runtime_error("unknown descriptor");
}
if (!scripting::descriptors_manager::is_writeable(descriptor)) {
throw std::runtime_error("descriptor is not writeable");
}
auto data = lua::bytearray_as_string(L, 2);
auto* stream = scripting::descriptors_manager::get_output(descriptor);
stream->write(data.data(), static_cast<std::streamsize>(data.size()));
if (!stream->good()) {
throw std::runtime_error("failed to write to stream");
}
return 0;
}
static int l_flush_descriptor(lua::State* L) {
int descriptor = lua::tointeger(L, 1);
if (!scripting::descriptors_manager::has_descriptor(descriptor)) {
throw std::runtime_error("unknown descriptor");
}
if (!scripting::descriptors_manager::is_writeable(descriptor)) {
throw std::runtime_error("descriptor is not writeable");
}
scripting::descriptors_manager::flush(descriptor);
return 0;
}
static int l_close_descriptor(lua::State* L) {
int descriptor = lua::tointeger(L, 1);
if (!scripting::descriptors_manager::has_descriptor(descriptor)) {
throw std::runtime_error("unknown descriptor");
}
scripting::descriptors_manager::close(descriptor);
return 0;
}
static int l_close_all_descriptors(lua::State* L) {
scripting::descriptors_manager::close_all_descriptors();
return 0;
}
const luaL_Reg filelib[] = { const luaL_Reg filelib[] = {
{"exists", lua::wrap<l_exists>}, {"exists", lua::wrap<l_exists>},
{"find", lua::wrap<l_find>}, {"find", lua::wrap<l_find>},
@ -283,5 +427,12 @@ const luaL_Reg filelib[] = {
{"mount", lua::wrap<l_mount>}, {"mount", lua::wrap<l_mount>},
{"unmount", lua::wrap<l_unmount>}, {"unmount", lua::wrap<l_unmount>},
{"create_zip", lua::wrap<l_create_zip>}, {"create_zip", lua::wrap<l_create_zip>},
{"__open_descriptor", lua::wrap<l_open_descriptor>},
{"__has_descriptor", lua::wrap<l_has_descriptor>},
{"__read_descriptor", lua::wrap<l_read_descriptor>},
{"__write_descriptor", lua::wrap<l_write_descriptor>},
{"__flush_descriptor", lua::wrap<l_flush_descriptor>},
{"__close_descriptor", lua::wrap<l_close_descriptor>},
{"__close_all_descriptors", lua::wrap<l_close_all_descriptors>},
{NULL, NULL} {NULL, NULL}
}; };

View File

@ -1,8 +1,13 @@
#include "engine/Engine.hpp" #include "engine/Engine.hpp"
#include "api_lua.hpp" #include "api_lua.hpp"
#include <ctime>
using namespace scripting; using namespace scripting;
#if defined(_WIN32) || defined(_WIN64)
#define USE_MSVC_TIME_SAFE
#endif
static int l_uptime(lua::State* L) { static int l_uptime(lua::State* L) {
return lua::pushnumber(L, engine->getTime().getTime()); return lua::pushnumber(L, engine->getTime().getTime());
} }
@ -11,8 +16,57 @@ static int l_delta(lua::State* L) {
return lua::pushnumber(L, engine->getTime().getDelta()); return lua::pushnumber(L, engine->getTime().getDelta());
} }
static int l_utc_time(lua::State* L) {
return lua::pushnumber(L, std::time(nullptr));
}
static int l_local_time(lua::State* L) {
std::time_t t = std::time(nullptr);
std::tm gmt_tm{};
std::tm local_tm{};
#if defined(USE_MSVC_TIME_SAFE)
gmtime_s(&gmt_tm, &t);
localtime_s(&local_tm, &t);
#else
gmtime_r(&t, &gmt_tm);
localtime_r(&t, &local_tm);
#endif
std::time_t utc_time = std::mktime(&gmt_tm);
std::time_t local_time = std::mktime(&local_tm);
std::time_t offset = local_time - utc_time;
return lua::pushnumber(L, t + offset);
}
static int l_utc_offset(lua::State* L) {
std::time_t t = std::time(nullptr);
std::tm gmt_tm{};
std::tm local_tm{};
#if defined(USE_MSVC_TIME_SAFE)
gmtime_s(&gmt_tm, &t);
localtime_s(&local_tm, &t);
#else
gmtime_r(&t, &gmt_tm);
localtime_r(&t, &local_tm);
#endif
std::time_t utc_time = std::mktime(&gmt_tm);
std::time_t local_time = std::mktime(&local_tm);
std::time_t offset = local_time - utc_time;
return lua::pushnumber(L, offset);
}
const luaL_Reg timelib[] = { const luaL_Reg timelib[] = {
{"uptime", lua::wrap<l_uptime>}, {"uptime", lua::wrap<l_uptime>},
{"delta", lua::wrap<l_delta>}, {"delta", lua::wrap<l_delta>},
{"utc_time", lua::wrap<l_utc_time>},
{"utc_offset", lua::wrap<l_utc_offset>},
{"local_time", lua::wrap<l_local_time>},
{NULL, NULL} {NULL, NULL}
}; };