Merge pull request #483 from MihailRis/update-console

Update console
This commit is contained in:
MihailRis 2025-03-16 22:45:07 +03:00 committed by GitHub
commit 06b9cc2ec6
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
86 changed files with 1709 additions and 272 deletions

View File

@ -79,6 +79,7 @@ Properties:
| hint | string | yes | yes | text to display when nothing is entered |
| caret | int | yes | yes | carriage position. `textbox.caret = -1` will set the position to the end of the text |
| editable | bool | yes | yes | text mutability |
| edited | bool | yes | yes\* | is text edited since the last set / edited status reset |
| multiline | bool | yes | yes | multiline support |
| lineNumbers | bool | yes | yes | display line numbers |
| textWrap | bool | yes | yes | automatic text wrapping (only with multiline: "true") |
@ -87,6 +88,8 @@ Properties:
| syntax | string | yes | yes | syntax highlighting ("lua" - Lua) |
| markup | string | yes | yes | text markup language ("md" - Markdown) |
\* - false only
Methods:
| Method | Description |

View File

@ -110,6 +110,8 @@ Inner text - initially entered text
- `supplier` - text supplier (called every frame)
- `consumer` - lua function that receives the entered text. Called only when input is complete
- `sub-consumer` - lua function-receiver of the input text. Called during text input or deletion.
- `oncontrolkey` - lua function called for combinations of the form (Ctrl + ?). The codepoint of the second key is given as the first argument.
The key code for comparison can be obtained via `input.keycode("key_name")`
- `autoresize` - automatic change of element size (default - false). Does not affect font size.
- `multiline` - allows display of multiline text.
- `text-wrap` - allows automatic text wrapping (works only with multiline: "true")

View File

@ -79,6 +79,7 @@ document["worlds-panel"]:clear()
| hint | string | да | да | текст, отображаемый, когда ничего не введено |
| caret | int | да | да | позиция каретки. `textbox.caret = -1` установит позицию в конец текста |
| editable | bool | да | да | изменяемость текста |
| edited | bool | да | да\* | был ли изменён текст с последней установки/сброса свойства |
| multiline | bool | да | да | поддержка многострочности |
| lineNumbers | bool | да | да | отображение номеров строк |
| textWrap | bool | да | да | автоматический перенос текста (только при multiline: "true") |
@ -87,6 +88,8 @@ document["worlds-panel"]:clear()
| syntax | string | да | да | подсветка синтаксиса ("lua" - Lua) |
| markup | string | да | да | язык разметки текста ("md" - Markdown) |
\* - только false
Методы:
| Метод | Описание |

View File

@ -111,6 +111,8 @@
- `supplier` - поставщик текста (вызывается каждый кадр)
- `consumer` - lua функция-приемник введенного текста. Вызывается только при завершении ввода
- `sub-consumer` - lua функция-приемник вводимого текста. Вызывается во время ввода или удаления текста.
- `oncontrolkey` - lua функция вызываемая для сочетаний вида (Ctrl + ?). На вход подаётся числовой код второй клавиши.
Код клавиши для сравнения можно получить через `input.keycode("имя_клавиши")`
- `autoresize` - автоматическое изменение размера элемента (по-умолчанию - false). Не влияет на размер шрифта.
- `multiline` - разрешает отображение многострочного текста.
- `text-wrap` - разрешает автоматический перенос текста (работает только при multiline: "true")

View File

@ -2,17 +2,14 @@
<panel interval="0"
orientation="horizontal"
color="#00000010"
size-func="gui.get_viewport()[1]-350,30">
size-func="gui.get_viewport()[1],30">
<button id="s_chat" size="110,30" onclick="modes:set('chat')">@Chat</button>
<button id="s_console" size="110,30" onclick="modes:set('console')">@Console</button>
<button id="s_debug" size="110,30" onclick="modes:set('debug')">@Debug</button>
</panel>
<container pos="0,30" size-func="gui.get_viewport()[1]-350,30" color="#00000020">
<label id="title" pos="8,8"></label>
</container>
<container id="logContainer" pos="0,60"
size-func="unpack(vec2.add(gui.get_viewport(), {-350,-100}))">
size-func="unpack(vec2.add(gui.get_viewport(), {-450,-100}))">
<textbox
id='log'
color='0'
@ -20,38 +17,77 @@
margin='0'
editable='false'
multiline='true'
size-func="gui.get_viewport()[1]-350,40"
size-func="-1,40"
gravity="bottom-left"
markup="md"
></textbox>
</container>
<container id="editorContainer" pos="0,60" color="#00000080"
size-func="unpack(vec2.add(gui.get_viewport(), {-350,-230}))">
<textbox
id='editor'
color='0'
autoresize='true'
margin='0'
padding='5'
multiline='true'
line-numbers='true'
syntax='lua'
size-func="gui.get_viewport()[1]-350,40"
gravity="top-left"
text-wrap='false'
scroll-step='50'
></textbox>
</container>
<panel id="traceback" gravity="bottom-left" padding="4" color="#000000A0"
max-length="170" size-func="gui.get_viewport()[1]-350,170">
</panel>
<panel id="problemsLog"
color="#00000010"
position-func="gui.get_viewport()[1]-350,0"
size-func="350,gui.get_viewport()[2]-40"
padding="5,15,5,15">
<label margin="0,0,0,5">@Problems</label>
</panel>
<splitbox id="editorRoot" pos="0,30" size-func="-1,gui.get_viewport()[2]-30"
orientation="horizontal" split-pos="0.3">
<splitbox split-pos="0.75">
<panel interval="2" color="0" padding="2">
<textbox pos="2" sub-consumer="filter_files"></textbox>
<panel id="filesList" color="#00000010" interval="6" padding="4"
size-func="-1,-45" pos="2,38">
<!-- content is generated in script -->
</panel>
</panel>
<panel id="problemsLog"
color="#00000010"
padding="5,15,5,15">
<label margin="0,0,0,5">@Problems</label>
</panel>
</splitbox>
<splitbox id="editorContainer" split-pos="0.8">
<container color="#00000080"
onclick="document.editor.focused = true document.editor.caret = -1">
<container size-func="-1,30" color="#00000020">
<image id="lockIcon" src="gui/lock" tooltip="@Read only"
interactive="true" onclick="unlock_access()"
color="#FFFFFF80" size="16" pos="4,6"
hover-color="#1080FF"></image>
<panel orientation="horizontal" gravity="top-right"
size="60,16" padding="8" interval="8" color="0">
<image id="saveIcon" src="gui/save" tooltip="@Save"
enabled="false" interactive="true"
hover-color="#1080FF"
onclick="save_current_file()"
color="#FFFFFF80" size="16"></image>
<image id="infoIcon" src="gui/info" tooltip="@editor.info.tooltip"
enabled="true" interactive="true"
color="#FFFFFF80" size="16"></image>
<image id="syncIcon" src="gui/play" tooltip="@Run"
enabled="true" interactive="true"
hover-color="#1080FF"
onclick="run_current_file()"
color="#FFFFFF80" size="16"></image>
</panel>
<label id="title" pos="26,8"></label>
</container>
<textbox
id='editor'
pos='0,30'
color='0'
autoresize='true'
margin='0'
padding='5'
multiline='true'
line-numbers='true'
oncontrolkey='on_control_combination'
syntax='lua'
size-func="-1,40"
text-wrap='false'
scroll-step='50'
></textbox>
</container>
<splitbox orientation="horizontal" split-pos="0.4">
<panel id="traceback" padding="4" color="#000000A0">
</panel>
<panel id="output" padding="4" color="#000000A0">
</panel>
</splitbox>
</splitbox>
</splitbox>
<textbox id='prompt'
consumer='submit'
margin='0'

View File

@ -9,6 +9,15 @@ local errors_all = {}
local warning_id = 0
local error_id = 0
local writeables = {}
local registry = require "core:internal/scripts_registry"
local filenames
local current_file = {
filename = "",
mutable = nil
}
events.on("core:warning", function (wtype, text, traceback)
local full = wtype..": "..text
if table.has(warnings_all, full) then
@ -45,14 +54,184 @@ events.on("core:error", function (msg, traceback)
table.insert(errors_all, full)
end)
local function find_mutable(filename)
local packid = file.prefix(filename)
if packid == "core" then
return
end
local saved = writeables[packid]
if saved then
return saved..":"..file.path(filename)
end
local packinfo = pack.get_info(packid)
if not packinfo then
return
end
local path = packinfo.path
if file.is_writeable(path) then
return file.join(path, file.path(filename))
end
end
local function refresh_file_title()
if current_file.filename == "" then
document.title.text = ""
return
end
local edited = document.editor.edited
current_file.modified = edited
document.saveIcon.enabled = edited
document.title.text = gui.str('File')..' - '..current_file.filename
..(edited and ' *' or '')
end
function build_files_list(filenames, selected)
local files_list = document.filesList
files_list:clear()
for _, actual_filename in ipairs(filenames) do
local filename = actual_filename
if selected then
filename = filename:gsub(selected, "**"..selected.."**")
end
local parent = file.parent(filename)
local info = registry.get_info(actual_filename)
local icon = "file"
if info then
icon = info.type == "component" and "entity" or info.type
end
files_list:add(gui.template("script_file", {
path = parent .. (parent[#parent] == ':' and '' or '/'),
name = file.name(filename),
icon = icon,
unit = info and info.unit or '',
filename = actual_filename
}))
end
end
function filter_files(text)
local filtered = {}
for _, filename in ipairs(filenames) do
if filename:find(text) then
table.insert(filtered, filename)
end
end
build_files_list(filtered, text)
end
function on_control_combination(keycode)
if keycode == input.keycode("s") then
save_current_file()
elseif keycode == input.keycode("r") then
run_current_file()
end
end
function unlock_access()
if current_file.filename == "" then
return
end
pack.request_writeable(file.prefix(current_file.filename),
function(token)
writeables[file.prefix(current_file.filename)] = token
current_file.mutable = token..":"..file.path(current_file.filename)
open_file_in_editor(current_file.filename, 0, current_file.mutable)
end
)
end
function run_current_file()
if not current_file.filename then
return
end
local chunk, err = loadstring(document.editor.text, current_file.filename)
clear_output()
if not chunk then
local line, message = err:match(".*:(%d*): (.*)")
document.output:add(
string.format(
"<label color='#FF3030' enabled='false' margin='2'>%s: %s</label>",
gui.str("Error at line %{0}"):gsub("%%{0}", line), message)
)
return
end
local info = registry.get_info(current_file.filename)
local script_type = info and info.type or "file"
local unit = info and info.unit
save_current_file()
local func = function()
local stack_size = debug.count_frames()
xpcall(chunk, function(msg) __vc__error(msg, 1, 1, stack_size) end)
end
local funcs = {
block = block.reload_script,
item = item.reload_script,
world = world.reload_script,
hud = hud.reload_script,
component = entities.reload_component,
module = reload_module,
}
func = funcs[script_type] or func
local output = core.capture_output(function() func(unit) end)
document.output:add(
string.format(
"<label enabled='false' multiline='true' margin='2'>%s</label>",
output)
)
end
function save_current_file()
if not current_file.mutable then
return
end
file.write(current_file.mutable, document.editor.text)
current_file.modified = false
document.saveIcon.enabled = false
document.title.text = gui.str('File')..' - '..current_file.filename
document.editor.edited = false
end
function open_file_in_editor(filename, line, mutable)
local editor = document.editor
local source = file.read(filename):gsub('\t', ' ')
editor.text = source
editor.focused = true
if line then
time.post_runnable(function()
editor.caret = editor:linePos(line)
end)
end
document.title.text = gui.str('File') .. ' - ' .. filename
current_file.filename = filename
current_file.mutable = mutable or find_mutable(filename)
document.lockIcon.visible = current_file.mutable == nil
document.editor.editable = current_file.mutable ~= nil
document.saveIcon.enabled = current_file.modified
end
function clear_traceback()
local tb_list = document.traceback
tb_list:clear()
tb_list:add("<label enabled='false' margin='2'>@devtools.traceback</label>")
end
function clear_output()
local output = document.output
output:clear()
output:add("<label enabled='false' margin='2'>@devtools.output</label>")
end
events.on("core:open_traceback", function(traceback_b64)
local traceback = bjson.frombytes(base64.decode(traceback_b64))
modes:set('debug')
clear_traceback()
local tb_list = document.traceback
local srcsize = tb_list.size
tb_list:clear()
tb_list:add("<label enabled='false' margin='2'>@devtools.traceback</label>")
for _, frame in ipairs(traceback.frames) do
local callback = ""
local framestr = ""
@ -62,23 +241,12 @@ events.on("core:open_traceback", function(traceback_b64)
framestr = frame.source..":"..tostring(frame.currentline).." "
if file.exists(frame.source) then
callback = string.format(
"local editor = document.editor "..
"local source = file.read('%s'):gsub('\t', ' ') "..
"editor.text = source "..
"editor.focused = true "..
"time.post_runnable(function()"..
"editor.caret = editor:linePos(%s) "..
"end)",
"open_file_in_editor('%s', %s)",
frame.source, frame.currentline-1
)
else
callback = "document.editor.text = 'Could not open source file'"
end
callback = string.format(
"%s document.title.text = gui.str('File')..' - %s'",
callback,
frame.source
)
end
if frame.name then
framestr = framestr.."("..tostring(frame.name)..")"
@ -184,7 +352,8 @@ end
function set_mode(mode)
local show_prompt = mode == 'chat' or mode == 'console'
document.title.text = ""
document.lockIcon.visible = false
document.editorRoot.visible = mode == 'debug'
document.editorContainer.visible = mode == 'debug'
document.logContainer.visible = mode ~= 'debug'
@ -194,7 +363,6 @@ function set_mode(mode)
document.root.color = {0, 0, 0, 128}
end
document.traceback.visible = mode == 'debug'
document.prompt.visible = show_prompt
if show_prompt then
document.prompt.focused = true
@ -211,6 +379,17 @@ function on_open(mode)
}, function (mode)
set_mode(mode)
end, mode or "console")
local files_list = document.filesList
filenames = registry.filenames
table.sort(filenames)
build_files_list(filenames)
document.editorContainer:setInterval(200, refresh_file_title)
clear_traceback()
clear_output()
elseif mode then
modes:set(mode)
end

View File

@ -1,9 +1,7 @@
<container id="%{id}" size="32" tooltip="%{text}"
onclick="events.emit('core:open_traceback', '%{traceback}')">
<image src="gui/%{type}" size="32"/>
<container pos="36,2" size="280,32" interactive="false">
<label>%{text}</label>
</container>
<label pos="36,2" sizefunc="-1,-1">%{text}</label>
<image src="gui/cross" interactive="true" size="16" gravity="top-right"
onclick="document['%{id}']:destruct()"></image>
</container>

View File

@ -0,0 +1,12 @@
<container size="18">
<image src="gui/%{icon}" size="16"></image>
<label hover-color='#30A0FF'
pos="20,2"
interactive="true"
onclick='open_file_in_editor("%{filename}")'
markup='md'
tooltip='%{unit}'
sizefunc="-1,-1">
[#FFFFFF80]%{path}[#FFFFFFFF]%{name}
</label>
</container>

View File

@ -0,0 +1,74 @@
local export = {}
local function collect_components(dirname, dest)
if file.isdir(dirname) then
local files = file.list(dirname)
for i, filename in ipairs(files) do
if file.ext(filename) == "lua" then
table.insert(dest, filename)
export.classification[filename] = {
type="component",
unit=file.prefix(filename)..":"..file.stem(filename)
}
end
end
end
end
local function collect_scripts(dirname, dest, ismodule)
if file.isdir(dirname) then
local files = file.list(dirname)
for i, filename in ipairs(files) do
if file.name(filename) == "components" and not ismodule then
collect_components(filename, dest)
elseif file.isdir(filename) then
collect_scripts(filename, dest)
elseif file.ext(filename) == "lua" then
table.insert(dest, filename)
end
end
end
end
local function load_scripts_list()
local packs = pack.get_installed()
for _, packid in ipairs(packs) do
collect_scripts(packid..":modules", export.filenames, true)
end
for _, filename in ipairs(export.filenames) do
export.classification[filename] = {
type="module",
unit=file.join(file.parent(file.prefix(filename)..":"..
filename:sub(filename:find("/")+1)),
file.stem(filename))
}
end
for _, packid in ipairs(packs) do
collect_scripts(packid..":scripts", export.filenames, false)
end
end
function export.build_classification()
local classification = {}
for id, props in pairs(block.properties) do
classification[props["script-file"]] = {type="block", unit=block.name(id)}
end
for id, props in pairs(item.properties) do
classification[props["script-file"]] = {type="item", unit=item.name(id)}
end
local packs = pack.get_installed()
for _, packid in ipairs(packs) do
classification[packid..":scripts/world.lua"] = {type="world", unit=packid}
classification[packid..":scripts/hud.lua"] = {type="hud", unit=packid}
end
export.classification = classification
export.filenames = {}
load_scripts_list()
end
function export.get_info(filename)
return export.classification[filename]
end
return export

View File

@ -24,7 +24,18 @@
"misc/snow",
"gui/check_mark",
"gui/left_arrow",
"gui/right_arrow"
"gui/right_arrow",
"gui/lock",
"gui/save",
"gui/block",
"gui/item",
"gui/file",
"gui/module",
"gui/play",
"gui/info",
"gui/world",
"gui/hud",
"gui/entity"
],
"fonts": [
{

View File

@ -7,7 +7,7 @@ local names = {
"hidden", "draw-group", "picking-item", "surface-replacement", "script-name",
"ui-layout", "inventory-size", "tick-interval", "overlay-texture",
"translucent", "fields", "particles", "icon-type", "icon", "placing-block",
"stack-size", "name"
"stack-size", "name", "script-file"
}
for name, _ in pairs(user_props) do
table.insert(names, name)
@ -61,3 +61,6 @@ end
cache_names(block)
cache_names(item)
local scripts_registry = require "core:internal/scripts_registry"
scripts_registry.build_classification()

View File

@ -429,6 +429,18 @@ function file.readlines(path)
return lines
end
function debug.count_frames()
local frames = 1
while true do
local info = debug.getinfo(frames)
if info then
frames = frames + 1
else
return frames - 1
end
end
end
function debug.get_traceback(start)
local frames = {}
local n = 2 + (start or 0)
@ -463,6 +475,35 @@ function on_deprecated_call(name, alternatives)
end
end
function reload_module(name)
local prefix, name = parse_path(name)
local path = prefix..":modules/"..name..".lua"
local previous = package.loaded[path]
if not previous then
debug.log("attempt to reload non-loaded module "..name.." ("..path..")")
return
end
local script, err = load(file.read(path), path)
if script == nil then
error(err)
end
local result = script()
if not result then
return
end
for i, value in ipairs(result) do
previous[i] = value
end
local copy = table.copy(result)
for key, value in pairs(result) do
result[key] = nil
end
for key, value in pairs(copy) do
previous[key] = value
end
end
-- Load script with caching
--
-- path - script path `contentpack:filename`.
@ -513,9 +554,13 @@ function __scripts_cleanup()
end
end
function __vc__error(msg, frame)
function __vc__error(msg, frame, n, lastn)
if events then
events.emit("core:error", msg, debug.get_traceback(1))
local frames = debug.get_traceback(1)
events.emit(
"core:error", msg,
table.sub(frames, 1 + (n or 0), lastn and #frames-lastn)
)
end
return debug.traceback(msg, frame)
end
@ -543,3 +588,23 @@ 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

View File

@ -11,7 +11,9 @@ world.delete-confirm=Do you want to delete world forever?
world.generators.default=Default
world.generators.flat=Flat
editor.info.tooltip=CTRL+S - Save\nCTRL+R - Run\nCTRL+Z - Undo\nCTRL+Y - Redo
devtools.traceback=Traceback (most recent call first)
devtools.output=Output
# Tooltips
graphics.gamma.tooltip=Lighting brightness curve

View File

@ -19,8 +19,16 @@ Problems=Проблемы
Monitor=Мониторинг
Debug=Отладка
File=Файл
Read only=Только для чтения
Save=Сохранить
Grant %{0} pack modification permission?=Выдать разрешение на модификацию пака %{0}?
Error at line %{0}=Ошибка на строке %{0}
Run=Запустить
editor.info.tooltip=CTRL+S - Сохранить\nCTRL+R - Запустить\nCTRL+Z - Отменить\nCTRL+Y - Повторить
devtools.traceback=Стек вызовов (от последнего)
devtools.output=Вывод
error.pack-not-found=Не удалось найти пакет
error.dependency-not-found=Используемая зависимость не найдена
pack.remove-confirm=Удалить весь поставляемый паком/паками контент из мира (безвозвратно)?

BIN
res/textures/gui/block.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 114 B

BIN
res/textures/gui/entity.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 150 B

BIN
res/textures/gui/file.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 120 B

BIN
res/textures/gui/hud.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 160 B

BIN
res/textures/gui/info.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 129 B

BIN
res/textures/gui/item.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.7 KiB

BIN
res/textures/gui/lock.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 120 B

BIN
res/textures/gui/module.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 150 B

BIN
res/textures/gui/play.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 130 B

BIN
res/textures/gui/save.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 123 B

BIN
res/textures/gui/world.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 178 B

View File

@ -68,3 +68,5 @@ inline const std::string LAYOUTS_FOLDER = "layouts";
inline const std::string SOUNDS_FOLDER = "sounds";
inline const std::string MODELS_FOLDER = "models";
inline const std::string SKELETONS_FOLDER = "skeletons";
inline const std::string FONT_DEFAULT = "normal";

View File

@ -79,6 +79,14 @@ const ContentPackRuntime* Content::getPackRuntime(const std::string& id) const {
return found->second.get();
}
ContentPackRuntime* Content::getPackRuntime(const std::string& id) {
auto found = packs.find(id);
if (found == packs.end()) {
return nullptr;
}
return found->second.get();
}
const UptrsMap<std::string, BlockMaterial>& Content::getBlockMaterials() const {
return blockMaterials;
}

View File

@ -121,6 +121,14 @@ public:
return *found->second;
}
T& require(const std::string& id) {
const auto& found = defs.find(id);
if (found == defs.end()) {
throw std::runtime_error("missing content unit " + id);
}
return *found->second;
}
const auto& getDefs() const {
return defs;
}
@ -240,6 +248,7 @@ public:
const rigging::SkeletonConfig* getSkeleton(const std::string& id) const;
const BlockMaterial* findBlockMaterial(const std::string& id) const;
const ContentPackRuntime* getPackRuntime(const std::string& id) const;
ContentPackRuntime* getPackRuntime(const std::string& id);
const UptrsMap<std::string, BlockMaterial>& getBlockMaterials() const;
const UptrsMap<std::string, ContentPackRuntime>& getPacks() const;

View File

@ -94,8 +94,9 @@ std::unique_ptr<Content> ContentBuilder::build() {
def->rt.surfaceReplacement = content->blocks.require(def->surfaceReplacement).rt.id;
if (def->properties == nullptr) {
def->properties = dv::object();
def->properties["name"] = def->name;
}
def->properties["name"] = def->name;
def->properties["script-file"] = def->scriptFile;
}
for (ItemDef* def : itemDefsIndices) {
@ -104,6 +105,7 @@ std::unique_ptr<Content> ContentBuilder::build() {
def->properties = dv::object();
}
def->properties["name"] = def->name;
def->properties["script-file"] = def->scriptFile;
}
for (auto& [name, def] : content->generators.getDefs()) {

View File

@ -394,6 +394,7 @@ void ContentLoader::loadBlock(
if (def.hidden && def.pickingItem == def.name + BLOCK_ITEM_SUFFIX) {
def.pickingItem = CORE_EMPTY;
}
def.scriptFile = pack->id + ":scripts/" + def.scriptName + ".lua";
}
void ContentLoader::loadItem(
@ -452,6 +453,8 @@ void ContentLoader::loadItem(
def.emission[1] = emissionarr[1].asNumber();
def.emission[2] = emissionarr[2].asNumber();
}
def.scriptFile = pack->id + ":scripts/" + def.scriptName + ".lua";
}
void ContentLoader::loadEntity(
@ -850,25 +853,54 @@ void ContentLoader::load() {
}
template <class T>
static void load_scripts(Content& content, ContentUnitDefs<T>& units) {
for (const auto& [name, def] : units.getDefs()) {
size_t pos = name.find(':');
if (pos == std::string::npos) {
throw std::runtime_error("invalid content unit name");
}
const auto runtime = content.getPackRuntime(name.substr(0, pos));
const auto& pack = runtime->getInfo();
const auto& folder = pack.folder;
auto scriptfile = folder / ("scripts/" + def->scriptName + ".lua");
if (io::is_regular_file(scriptfile)) {
scripting::load_content_script(
runtime->getEnvironment(),
name,
scriptfile,
pack.id + ":scripts/" + def->scriptName + ".lua",
def->rt.funcsset
);
}
static void load_script(const Content& content, T& def) {
const auto& name = def.name;
size_t pos = name.find(':');
if (pos == std::string::npos) {
throw std::runtime_error("invalid content unit name");
}
const auto runtime = content.getPackRuntime(name.substr(0, pos));
const auto& pack = runtime->getInfo();
const auto& folder = pack.folder;
auto scriptfile = folder / ("scripts/" + def.scriptName + ".lua");
if (io::is_regular_file(scriptfile)) {
scripting::load_content_script(
runtime->getEnvironment(),
name,
scriptfile,
def.scriptFile,
def.rt.funcsset
);
}
}
template <class T>
static void load_scripts(const Content& content, ContentUnitDefs<T>& units) {
for (const auto& [_, def] : units.getDefs()) {
load_script(content, *def);
}
}
void ContentLoader::reloadScript(const Content& content, Block& block) {
load_script(content, block);
}
void ContentLoader::reloadScript(const Content& content, ItemDef& item) {
load_script(content, item);
}
void ContentLoader::loadWorldScript(ContentPackRuntime& runtime) {
const auto& pack = runtime.getInfo();
const auto& folder = pack.folder;
io::path scriptFile = folder / "scripts/world.lua";
if (io::is_regular_file(scriptFile)) {
scripting::load_world_script(
runtime.getEnvironment(),
pack.id,
scriptFile,
pack.id + ":scripts/world.lua",
runtime.worldfuncsset
);
}
}
@ -881,16 +913,8 @@ void ContentLoader::loadScripts(Content& content) {
const auto& folder = pack.folder;
// Load main world script
io::path scriptFile = folder / "scripts/world.lua";
if (io::is_regular_file(scriptFile)) {
scripting::load_world_script(
runtime->getEnvironment(),
pack.id,
scriptFile,
pack.id + ":scripts/world.lua",
runtime->worldfuncsset
);
}
loadWorldScript(*runtime);
// Load entity components
io::path componentsDir = folder / "scripts/components";
foreach_file(componentsDir, [&pack](const io::path& file) {

View File

@ -77,4 +77,7 @@ public:
void load();
static void loadScripts(Content& content);
static void loadWorldScript(ContentPackRuntime& pack);
static void reloadScript(const Content& content, Block& block);
static void reloadScript(const Content& content, ItemDef& item);
};

View File

@ -15,6 +15,7 @@ using wstringsupplier = std::function<std::wstring()>;
using doublesupplier = std::function<double()>;
using boolsupplier = std::function<bool()>;
using vec2supplier = std::function<glm::vec2()>;
using key_handler = std::function<bool(int)>;
using stringconsumer = std::function<void(const std::string&)>;
using wstringconsumer = std::function<void(const std::wstring&)>;

164
src/devtools/actions.hpp Normal file
View File

@ -0,0 +1,164 @@
#pragma once
#include <vector>
#include <memory>
class Action {
public:
virtual ~Action() = default;
virtual void apply() = 0;
virtual void revert() = 0;
};
class InversedAction : public Action {
public:
InversedAction(std::unique_ptr<Action> action) : action(std::move(action)) {}
void apply() override {
action->revert();
}
void revert() override {
action->apply();
}
private:
std::unique_ptr<Action> action;
};
class CombinedAction : public Action {
public:
CombinedAction(std::vector<std::unique_ptr<Action>> actions)
: actions(std::move(actions)) {
}
void apply() override {
for (auto& action : actions) {
action->apply();
}
}
void revert() override {
for (int i = actions.size() - 1; i >= 0; i--) {
actions[i]->revert();
}
}
private:
std::vector<std::unique_ptr<Action>> actions;
};
class ActionsHistory {
public:
ActionsHistory() {};
/// @brief Remove all actions available to redo
void clearRedo() {
if (actionPtr < actions.size()) {
actions.erase(actions.begin() + actionPtr, actions.end());
}
}
/// @brief Store action without applying
void store(std::unique_ptr<Action> action, bool reverse=false) {
if (lock) {
return;
}
if (reverse) {
action = std::make_unique<InversedAction>(std::move(action));
}
clearRedo();
actions.emplace_back(std::move(action));
actionPtr++;
}
/// @brief Apply action and store it
void apply(std::unique_ptr<Action> action) {
if (lock) {
return;
}
clearRedo();
lock = true;
action->apply();
lock = false;
actions.emplace_back(std::move(action));
actionPtr++;
}
/// @brief Revert the last action
/// @return true if any action reverted
bool undo() {
if (lock || actionPtr == 0) {
return false;
}
auto& action = actions[--actionPtr];
lock = true;
action->revert();
lock = false;
return true;
}
/// @brief Revert the last action
/// @return true if any action reapplied
bool redo() {
if (lock || actionPtr == actions.size()) {
return false;
}
auto& action = actions[actionPtr++];
lock = true;
action->apply();
lock = false;
return true;
}
/// @brief Clear history without reverting actions
void clear() {
actionPtr = 0;
actions.clear();
}
/// @brief Squash last n actions into one CombinedAction
/// @param n number of actions to squash
void squash(ptrdiff_t n) {
if (n < 2) {
return;
}
n = std::min(n, static_cast<ptrdiff_t>(actionPtr));
std::vector<std::unique_ptr<Action>> squashing;
for (size_t i = actionPtr - n; i < actionPtr; i++) {
squashing.emplace_back(std::move(actions[i]));
}
actions.erase(actions.begin() + actionPtr - n, actions.end());
actionPtr -= n;
store(std::make_unique<CombinedAction>(std::move(squashing)));
}
size_t size() const {
return actionPtr;
}
/// @brief On destruction squashing actions stored since initialization
struct Combination {
ActionsHistory& history;
size_t historySize;
Combination(ActionsHistory& history)
: history(history), historySize(history.size()) {
}
Combination(const Combination&) = delete;
Combination(Combination&&) = default;
~Combination() {
history.squash(history.size() - historySize);
}
};
Combination beginCombination() {
return Combination(*this);
}
private:
std::vector<std::unique_ptr<Action>> actions;
size_t actionPtr = 0;
bool lock = false;
};

View File

@ -163,6 +163,9 @@ void Engine::updateHotkeys() {
if (Events::jpressed(keycode::F2)) {
saveScreenshot();
}
if (Events::jpressed(keycode::F8)) {
gui->toggleDebug();
}
if (Events::jpressed(keycode::F11)) {
settings.display.fullscreen.toggle();
}
@ -481,6 +484,10 @@ const Content* Engine::getContent() const {
return content.get();
}
Content* Engine::getWriteableContent() {
return content.get();
}
std::vector<ContentPack> Engine::getAllContentPacks() {
auto packs = getContentPacks();
packs.insert(packs.begin(), ContentPack::createCore(paths));

View File

@ -151,6 +151,8 @@ public:
/// @brief Get current Content instance
const Content* getContent() const;
Content* getWriteableContent();
/// @brief Get selected content packs
std::vector<ContentPack>& getContentPacks();

View File

@ -262,7 +262,7 @@ void Hud::updateHotbarControl() {
}
}
void Hud::updateWorldGenDebugVisualization() {
void Hud::updateWorldGenDebug() {
auto& level = frontend.getLevel();
const auto& chunks = *player.chunks;
auto generator =
@ -314,7 +314,9 @@ void Hud::update(bool visible) {
const auto& chunks = *player.chunks;
const auto& menu = gui.getMenu();
debugPanel->setVisible(debug && visible);
debugPanel->setVisible(
debug && visible && !(inventoryOpen && inventoryView == nullptr)
);
if (!visible && inventoryOpen) {
closeInventory();
@ -358,7 +360,7 @@ void Hud::update(bool visible) {
debugMinimap->setVisible(debug && showGeneratorMinimap);
if (debug && showGeneratorMinimap) {
updateWorldGenDebugVisualization();
updateWorldGenDebug();
}
}

View File

@ -135,7 +135,7 @@ class Hud : public util::ObjectsKeeper {
void dropExchangeSlot();
void showExchangeSlot();
void updateWorldGenDebugVisualization();
void updateWorldGenDebug();
public:
Hud(Engine& engine, LevelFrontend& frontend, Player& player);
~Hud();

View File

@ -142,7 +142,7 @@ void Batch2D::rect(
bool flippedY,
glm::vec4 tint
) {
if (index + 6*B2D_VERTEX_SIZE >= capacity) {
if (index + 6 * B2D_VERTEX_SIZE >= capacity) {
flush();
}
setPrimitive(DrawPrimitive::triangle);
@ -230,6 +230,11 @@ void Batch2D::rect(
}
void Batch2D::lineRect(float x, float y, float w, float h) {
if (index + 8 * B2D_VERTEX_SIZE >= capacity) {
flush();
}
setPrimitive(DrawPrimitive::line);
vertex(x, y, 0.0f, 0.0f, color.r, color.g, color.b, color.a);
vertex(x, y+h, 0.0f, 1.0f, color.r, color.g, color.b, color.a);

View File

@ -48,10 +48,14 @@ public:
void sprite(float x, float y, float w, float h, float skew, int atlasRes, int index, glm::vec4 tint);
void point(float x, float y, float r, float g, float b, float a);
void setColor(glm::vec4 color) {
void setColor(const glm::vec4& color) {
this->color = color;
}
void setColor(int r, int g, int b, int a=255) {
this->color = glm::vec4(r / 255.0f, g / 255.0f, b / 255.0f, a / 255.0f);
}
void resetColor() {
this->color = glm::vec4(1.0f);
}

View File

@ -38,11 +38,11 @@ bool Font::isPrintableChar(uint codepoint) const {
}
}
int Font::calcWidth(const std::wstring& text, size_t length) const {
int Font::calcWidth(std::wstring_view text, size_t length) const {
return calcWidth(text, 0, length);
}
int Font::calcWidth(const std::wstring& text, size_t offset, size_t length) const {
int Font::calcWidth(std::wstring_view text, size_t offset, size_t length) const {
return std::min(text.length()-offset, length) * glyphInterval;
}

View File

@ -56,14 +56,14 @@ public:
/// @param text selected text
/// @param length max substring length (default: no limit)
/// @return pixel width of the substring
int calcWidth(const std::wstring& text, size_t length=-1) const;
int calcWidth(std::wstring_view text, size_t length=-1) const;
/// @brief Calculate text width in pixels
/// @param text selected text
/// @param offset start of the substring
/// @param length max substring length
/// @return pixel width of the substring
int calcWidth(const std::wstring& text, size_t offset, size_t length) const;
int calcWidth(std::wstring_view text, size_t offset, size_t length) const;
/// @brief Check if character is visible (non-whitespace)
/// @param codepoint character unicode codepoint

View File

@ -10,6 +10,7 @@
#include "graphics/core/Batch3D.hpp"
#include "graphics/core/Shader.hpp"
#include "presets/NotePreset.hpp"
#include "constants.hpp"
TextsRenderer::TextsRenderer(
Batch3D& batch, const Assets& assets, const Frustum& frustum
@ -44,7 +45,7 @@ void TextsRenderer::renderNote(
}
opacity = preset.xrayOpacity;
}
const auto& font = assets.require<Font>("normal");
const auto& font = assets.require<Font>(FONT_DEFAULT);
glm::vec3 xvec = note.getAxisX();
glm::vec3 yvec = note.getAxisY();

View File

@ -11,14 +11,15 @@
#include "frontend/UiDocument.hpp"
#include "frontend/locale.hpp"
#include "graphics/core/Batch2D.hpp"
#include "graphics/core/LineBatch.hpp"
#include "graphics/core/Shader.hpp"
#include "graphics/core/Font.hpp"
#include "graphics/core/DrawContext.hpp"
#include "window/Events.hpp"
#include "window/Window.hpp"
#include "window/input.hpp"
#include "window/Camera.hpp"
#include <iostream>
#include <algorithm>
#include <utility>
@ -34,12 +35,13 @@ GUI::GUI()
menu = std::make_shared<Menu>();
menu->setId("menu");
menu->setZIndex(10);
container->add(menu);
container->setScrollable(false);
tooltip = guiutil::create(
"<container color='#000000A0' interactive='false' z-index='999'>"
"<label id='tooltip.label' pos='2' autoresize='true'></label>"
"<label id='tooltip.label' pos='2' autoresize='true' multiline='true' text-wrap='false'></label>"
"</container>"
);
store("tooltip", tooltip);
@ -107,7 +109,7 @@ void GUI::actMouse(float delta) {
doubleClicked = false;
doubleClickTimer += delta + mouseDelta * 0.1f;
auto hover = container->getAt(Events::cursor, nullptr);
auto hover = container->getAt(Events::cursor);
if (this->hover && this->hover != hover) {
this->hover->setHover(false);
}
@ -238,6 +240,39 @@ void GUI::draw(const DrawContext& pctx, const Assets& assets) {
if (hover) {
Window::setCursor(hover->getCursor());
}
if (hover && debug) {
auto pos = hover->calcPos();
const auto& id = hover->getId();
if (!id.empty()) {
auto& font = assets.require<Font>(FONT_DEFAULT);
auto text = util::str2wstr_utf8(id);
int width = font.calcWidth(text);
int height = font.getLineHeight();
batch2D->untexture();
batch2D->setColor(0, 0, 0);
batch2D->rect(pos.x, pos.y, width, height);
batch2D->resetColor();
font.draw(*batch2D, text, pos.x, pos.y, nullptr, 0);
}
batch2D->untexture();
auto node = hover->getParent();
while (node) {
auto pos = node->calcPos();
auto size = node->getSize();
batch2D->setColor(0, 255, 255);
batch2D->lineRect(pos.x, pos.y, size.x-1, size.y-1);
node = node->getParent();
}
// debug draw
auto size = hover->getSize();
batch2D->setColor(0, 255, 0);
batch2D->lineRect(pos.x, pos.y, size.x-1, size.y-1);
}
}
std::shared_ptr<UINode> GUI::getFocused() const {
@ -252,8 +287,8 @@ void GUI::add(std::shared_ptr<UINode> node) {
container->add(std::move(node));
}
void GUI::remove(std::shared_ptr<UINode> node) noexcept {
container->remove(std::move(node));
void GUI::remove(UINode* node) noexcept {
container->remove(node);
}
void GUI::store(const std::string& name, std::shared_ptr<UINode> node) {
@ -297,3 +332,7 @@ void GUI::setDoubleClickDelay(float delay) {
float GUI::getDoubleClickDelay() const {
return doubleClickDelay;
}
void GUI::toggleDebug() {
debug = !debug;
}

View File

@ -14,6 +14,7 @@ class DrawContext;
class Assets;
class Camera;
class Batch2D;
class LineBatch;
/*
Some info about padding and margin.
@ -73,6 +74,7 @@ namespace gui {
float doubleClickTimer = 0.0f;
float doubleClickDelay = 0.5f;
bool doubleClicked = false;
bool debug = false;
void actMouse(float delta);
void actFocused();
@ -113,7 +115,11 @@ namespace gui {
void add(std::shared_ptr<UINode> node);
/// @brief Remove node from the main container
void remove(std::shared_ptr<UINode> node) noexcept;
void remove(UINode* node) noexcept;
void remove(const std::shared_ptr<UINode>& node) noexcept {
return remove(node.get());
}
/// @brief Store node in the GUI nodes dictionary
/// (does not add node to the main container)
@ -144,5 +150,7 @@ namespace gui {
void setDoubleClickDelay(float delay);
float getDoubleClickDelay() const;
void toggleDebug();
};
}

View File

@ -0,0 +1,43 @@
#pragma once
#include "Container.hpp"
namespace gui {
class BasePanel : public Container {
public:
virtual ~BasePanel() = default;
virtual void setOrientation(Orientation orientation) {
this->orientation = orientation;
refresh();
}
Orientation getOrientation() const {
return orientation;
}
virtual void setPadding(glm::vec4 padding) {
this->padding = padding;
refresh();
}
glm::vec4 getPadding() const {
return padding;
}
protected:
BasePanel(
glm::vec2 size,
glm::vec4 padding = glm::vec4(0.0f),
float interval = 2.0f,
Orientation orientation = Orientation::vertical
)
: Container(std::move(size)),
padding(std::move(padding)),
interval(interval) {
}
Orientation orientation = Orientation::vertical;
glm::vec4 padding;
float interval = 2.0f;
};
}

View File

@ -17,9 +17,7 @@ Container::~Container() {
Container::clear();
}
std::shared_ptr<UINode> Container::getAt(
const glm::vec2& pos, const std::shared_ptr<UINode>& self
) {
std::shared_ptr<UINode> Container::getAt(const glm::vec2& pos) {
if (!isInteractive() || !isEnabled()) {
return nullptr;
}
@ -28,19 +26,19 @@ std::shared_ptr<UINode> Container::getAt(
}
int diff = (actualLength-size.y);
if (scrollable && diff > 0 && pos.x > calcPos().x + getSize().x - scrollBarWidth) {
return UINode::getAt(pos, self);
return UINode::getAt(pos);
}
for (int i = nodes.size()-1; i >= 0; i--) {
auto& node = nodes[i];
if (!node->isVisible())
continue;
auto hover = node->getAt(pos, node);
auto hover = node->getAt(pos);
if (hover != nullptr) {
return hover;
}
}
return UINode::getAt(pos, self);
return UINode::getAt(pos);
}
void Container::mouseMove(GUI* gui, int x, int y) {
@ -172,11 +170,11 @@ void Container::add(const std::shared_ptr<UINode>& node, glm::vec2 pos) {
add(node);
}
void Container::remove(const std::shared_ptr<UINode>& selected) {
void Container::remove(UINode* selected) {
selected->setParent(nullptr);
nodes.erase(std::remove_if(nodes.begin(), nodes.end(),
[selected](const std::shared_ptr<UINode>& node) {
return node == selected;
return node.get() == selected;
}
), nodes.end());
refresh();
@ -185,7 +183,7 @@ void Container::remove(const std::shared_ptr<UINode>& selected) {
void Container::remove(const std::string& id) {
for (auto& node : nodes) {
if (node->getId() == id) {
return remove(node);
return remove(node.get());
}
}
}

View File

@ -28,11 +28,11 @@ namespace gui {
virtual void act(float delta) override;
virtual void drawBackground(const DrawContext& pctx, const Assets& assets);
virtual void draw(const DrawContext& pctx, const Assets& assets) override;
virtual std::shared_ptr<UINode> getAt(const glm::vec2& pos, const std::shared_ptr<UINode>& self) override;
virtual std::shared_ptr<UINode> getAt(const glm::vec2& pos) override;
virtual void add(const std::shared_ptr<UINode>& node);
virtual void add(const std::shared_ptr<UINode>& node, glm::vec2 pos);
virtual void clear();
virtual void remove(const std::shared_ptr<UINode>& node);
virtual void remove(UINode* node);
virtual void remove(const std::string& id);
virtual void scrolled(int value) override;
virtual void setScrollable(bool flag);

View File

@ -207,7 +207,7 @@ void SlotView::draw(const DrawContext& pctx, const Assets& assets) {
drawItemIcon(batch, stack, item, assets, tint, pos);
if (stack.getCount() > 1 || stack.getFields() != nullptr) {
const auto& font = assets.require<Font>("normal");
const auto& font = assets.require<Font>(FONT_DEFAULT);
drawItemInfo(batch, stack, item, font, pos);
}
}

View File

@ -35,7 +35,7 @@ uint LabelCache::getLineByTextIndex(size_t index) const {
return lines.size()-1;
}
void LabelCache::update(const std::wstring& text, bool multiline, bool wrap) {
void LabelCache::update(std::wstring_view text, bool multiline, bool wrap) {
resetFlag = false;
lines.clear();
lines.push_back(LineScheme {0, false});
@ -59,6 +59,27 @@ void LabelCache::update(const std::wstring& text, bool multiline, bool wrap) {
}
}
}
if (font != nullptr) {
int lineHeight = font->getLineHeight();
int maxWidth = 0;
for (int i = 0; i < lines.size() - 1; i++) {
const auto& next = lines[i + 1];
const auto& cur = lines[i];
maxWidth = std::max(
font->calcWidth(
text.substr(cur.offset, next.offset - cur.offset)
),
maxWidth
);
}
maxWidth = std::max(
font->calcWidth(
text.substr(lines[lines.size() - 1].offset)
),
maxWidth
);
multilineWidth = maxWidth;
}
}
}
@ -89,8 +110,15 @@ glm::vec2 Label::calcSize() {
if (cache.lines.size() > 1) {
lineHeight *= lineInterval;
}
auto view = std::wstring_view(text);
if (multiline) {
return glm::vec2(
cache.multilineWidth,
lineHeight * cache.lines.size() + font->getYOffset()
);
}
return glm::vec2 (
cache.font->calcWidth(text),
cache.font->calcWidth(view),
lineHeight * cache.lines.size() + font->getYOffset()
);
}

View File

@ -1,6 +1,7 @@
#pragma once
#include "UINode.hpp"
#include "constants.hpp"
class Font;
struct FontStylesScheme;
@ -17,9 +18,10 @@ namespace gui {
/// @brief Reset cache flag
bool resetFlag = true;
size_t wrapWidth = -1;
int multilineWidth = 0;
void prepare(Font* font, size_t wrapWidth);
void update(const std::wstring& text, bool multiline, bool wrap);
void update(std::wstring_view text, bool multiline, bool wrap);
size_t getTextLineOffset(size_t line) const;
uint getLineByTextIndex(size_t index) const;
@ -61,8 +63,8 @@ namespace gui {
std::unique_ptr<FontStylesScheme> styles;
public:
Label(const std::string& text, std::string fontName="normal");
Label(const std::wstring& text, std::string fontName="normal");
Label(const std::string& text, std::string fontName=FONT_DEFAULT);
Label(const std::wstring& text, std::string fontName=FONT_DEFAULT);
virtual ~Label();

View File

@ -55,7 +55,7 @@ void Menu::setPage(const std::string &name, bool history) {
void Menu::setPage(Page page, bool history) {
if (current.panel) {
Container::remove(current.panel);
Container::remove(current.panel.get());
if (history && !current.temporal) {
pageStack.push(current);
}
@ -104,7 +104,7 @@ void Menu::clearHistory() {
void Menu::reset() {
clearHistory();
if (current.panel) {
Container::remove(current.panel);
Container::remove(current.panel.get());
current = Page {"", nullptr};
}
}

View File

@ -5,9 +5,7 @@
using namespace gui;
Panel::Panel(glm::vec2 size, glm::vec4 padding, float interval)
: Container(size),
padding(padding),
interval(interval)
: BasePanel(size, padding, interval, Orientation::vertical)
{
setColor(glm::vec4(0.0f, 0.0f, 0.0f, 0.75f));
}
@ -31,15 +29,6 @@ int Panel::getMinLength() const {
return minLength;
}
void Panel::setPadding(glm::vec4 padding) {
this->padding = padding;
refresh();
}
glm::vec4 Panel::getPadding() const {
return padding;
}
void Panel::cropToContent() {
if (maxLength > 0.0f) {
setSize(glm::vec2(
@ -63,7 +52,7 @@ void Panel::add(const std::shared_ptr<UINode> &node) {
fullRefresh();
}
void Panel::remove(const std::shared_ptr<UINode> &node) {
void Panel::remove(UINode* node) {
Container::remove(node);
fullRefresh();
}
@ -109,11 +98,3 @@ void Panel::refresh() {
actualLength = size.y;
}
}
void Panel::setOrientation(Orientation orientation) {
this->orientation = orientation;
}
Orientation Panel::getOrientation() const {
return orientation;
}

View File

@ -1,31 +1,22 @@
#pragma once
#include "commons.hpp"
#include "Container.hpp"
#include "BasePanel.hpp"
namespace gui {
class Panel : public Container {
protected:
Orientation orientation = Orientation::vertical;
glm::vec4 padding {2.0f};
float interval = 2.0f;
int minLength = 0;
int maxLength = 0;
class Panel : public BasePanel {
public:
Panel(
glm::vec2 size,
glm::vec4 padding=glm::vec4(2.0f),
glm::vec4 padding=glm::vec4(0.0f),
float interval=2.0f
);
virtual ~Panel();
virtual void cropToContent();
virtual void setOrientation(Orientation orientation);
Orientation getOrientation() const;
virtual void add(const std::shared_ptr<UINode>& node) override;
virtual void remove(const std::shared_ptr<UINode>& node) override;
virtual void remove(UINode* node) override;
virtual void refresh() override;
virtual void fullRefresh() override;
@ -35,8 +26,8 @@ namespace gui {
virtual void setMinLength(int value);
int getMinLength() const;
virtual void setPadding(glm::vec4 padding);
glm::vec4 getPadding() const;
protected:
int minLength = 0;
int maxLength = 0;
};
}

View File

@ -5,6 +5,7 @@
#include "graphics/core/DrawContext.hpp"
#include "assets/Assets.hpp"
#include "util/stringutil.hpp"
#include "constants.hpp"
using namespace gui;
@ -37,7 +38,7 @@ void Plotter::draw(const DrawContext& pctx, const Assets& assets) {
}
int current_point = static_cast<int>(points[index % dmwidth]);
auto font = assets.get<Font>("normal");
auto font = assets.get<Font>(FONT_DEFAULT);
for (int y = 0; y < dmheight; y += labelsInterval) {
std::wstring string;
if (current_point/16 == y/labelsInterval) {

View File

@ -4,7 +4,6 @@
#include "typedefs.hpp"
#include <memory>
#include <glm/glm.hpp>
class Assets;
class DrawContext;

View File

@ -0,0 +1,74 @@
#include "SplitBox.hpp"
using namespace gui;
SplitBox::SplitBox(const glm::vec2& size, float splitPos, Orientation orientation)
: BasePanel(size, glm::vec4(), 4.0f, orientation), splitPos(splitPos) {
setCursor(
orientation == Orientation::vertical ? CursorShape::NS_RESIZE
: CursorShape::EW_RESIZE
);
}
void SplitBox::mouseMove(GUI*, int x, int y) {
auto pos = calcPos();
auto size = getSize();
glm::ivec2 cursor(x - pos.x, y - pos.y);
int axis = orientation == Orientation::vertical;
int v = cursor[axis];
v = std::max(std::min(static_cast<int>(size[axis]) - 10, v), 10);
float t = v / size[axis];
splitPos = t;
refresh();
}
void SplitBox::refresh() {
Container::refresh();
if (nodes.empty()) {
return;
}
glm::vec2 size = getSize();
if (nodes.size() == 1) {
auto node = nodes.at(0);
node->setPos(glm::vec2());
node->setSize(size);
return;
}
auto nodeA = nodes.at(0);
auto nodeB = nodes.at(1);
float sepRadius = interval / 2.0f;
nodeA->setPos(glm::vec2(padding));
const auto& p = padding;
if (orientation == Orientation::vertical) {
float splitPos = this->splitPos * size.y;
nodeA->setSize({size.x-p.x-p.z, splitPos - sepRadius - p.y});
nodeB->setSize({size.x-p.x-p.z, size.y - splitPos - sepRadius - p.w});
nodeB->setPos({p.x, splitPos + sepRadius});
} else {
float splitPos = this->splitPos * size.x;
nodeA->setSize({splitPos - sepRadius - p.x, size.y - p.y - p.w});
nodeB->setSize({size.x - splitPos - sepRadius - p.z, size.y - p.y - p.w});
nodeB->setPos({splitPos + sepRadius, p.y});
}
}
void SplitBox::doubleClick(GUI*, int x, int y) {
if (nodes.size() < 2) {
return;
}
std::swap(nodes[0], nodes[1]);
refresh();
}
void SplitBox::fullRefresh() {
refresh();
reposition();
Container::fullRefresh();
}

View File

@ -0,0 +1,17 @@
#pragma once
#include "BasePanel.hpp"
namespace gui {
class SplitBox : public BasePanel {
public:
SplitBox(const glm::vec2& size, float splitPos, Orientation orientation);
virtual void mouseMove(GUI*, int x, int y) override;
virtual void refresh() override;
virtual void fullRefresh() override;
virtual void doubleClick(GUI*, int x, int y) override;
private:
float splitPos;
};
}

View File

@ -13,18 +13,185 @@
#include "util/stringutil.hpp"
#include "window/Events.hpp"
#include "window/Window.hpp"
#include "devtools/actions.hpp"
#include "../markdown.hpp"
using namespace gui;
inline constexpr int LINE_NUMBERS_PANE_WIDTH = 40;
TextBox::TextBox(std::wstring placeholder, glm::vec4 padding)
: Container(glm::vec2(200,32)),
padding(padding),
input(L""),
placeholder(std::move(placeholder))
{
class InputAction : public Action {
std::weak_ptr<TextBox> textbox;
size_t position;
std::wstring string;
public:
InputAction(
std::weak_ptr<TextBox> textbox, size_t position, std::wstring string
)
: textbox(std::move(textbox)),
position(position),
string(std::move(string)) {
}
void apply() override {
if (auto box = textbox.lock()) {
box->select(position, position);
box->paste(string);
}
}
void revert() override {
if (auto box = textbox.lock()) {
box->select(position, position);
box->erase(position, string.length());
}
}
};
class SelectionAction : public Action {
std::weak_ptr<TextBox> textbox;
size_t start;
size_t end;
public:
SelectionAction(std::weak_ptr<TextBox> textbox, size_t start, size_t end)
: textbox(std::move(textbox)), start(start), end(end) {}
void apply() override {
if (auto box = textbox.lock()) {
box->select(start, end);
}
}
void revert() override {
if (auto box = textbox.lock()) {
box->select(0, 0);
}
}
};
namespace gui {
/// @brief Accumulates small changes into words for InputAction creation
class TextBoxHistorian {
public:
TextBoxHistorian(TextBox& textBox, ActionsHistory& history)
: textBox(textBox), history(history) {
}
void onPaste(size_t pos, std::wstring_view text) {
if (locked) {
return;
}
if (erasing) {
sync();
}
if (this->pos == static_cast<size_t>(-1)) {
this->pos = pos;
}
if (this->pos + length != pos || text == L" " || text == L"\n") {
sync();
this->pos = pos;
}
ss << text;
length += text.length();
}
void onErase(size_t pos, std::wstring_view text, bool selection=false) {
if (locked) {
return;
}
if (!erasing) {
sync();
erasing = true;
}
if (selection) {
history.store(
std::make_unique<SelectionAction>(
getTextBoxWeakptr(),
textBox.getSelectionStart(),
textBox.getSelectionEnd()
),
true
);
}
if (this->pos == static_cast<size_t>(-1)) {
this->pos = pos;
} else if (this->pos - text.length() != pos) {
sync();
erasing = true;
this->pos = pos;
}
if (text == L" " || text == L"\n") {
sync();
erasing = true;
this->pos = pos;
}
auto str = ss.str();
ss.seekp(0);
ss << text << str;
this->pos = pos;
length += text.length();
}
/// @brief Flush buffer and push all changes to the ActionsHistory
void sync() {
auto string = ss.str();
if (string.empty()) {
return;
}
auto action =
std::make_unique<InputAction>(getTextBoxWeakptr(), pos, string);
history.store(std::move(action), erasing);
reset();
}
void undo() {
sync();
locked = true;
history.undo();
locked = false;
}
void redo() {
sync();
locked = true;
history.redo();
locked = false;
}
void reset() {
pos = -1;
length = 0;
erasing = false;
ss = {};
}
bool isSynced() const {
return length == 0;
}
private:
TextBox& textBox;
ActionsHistory& history;
std::wstringstream ss;
size_t pos = -1;
size_t length = 0;
bool erasing = false;
bool locked = false;
std::weak_ptr<TextBox> getTextBoxWeakptr() {
return std::weak_ptr<TextBox>(std::dynamic_pointer_cast<TextBox>(
textBox.shared_from_this()
));
}
};
}
TextBox::TextBox(std::wstring placeholder, glm::vec4 padding)
: Container(glm::vec2(200, 32)),
history(std::make_shared<ActionsHistory>()),
historian(std::make_unique<TextBoxHistorian>(*this, *history)),
padding(padding),
input(L""),
placeholder(std::move(placeholder)) {
setCursor(CursorShape::TEXT);
setOnUpPressed(nullptr);
setOnDownPressed(nullptr);
@ -49,6 +216,8 @@ TextBox::TextBox(std::wstring placeholder, glm::vec4 padding)
scrollStep = 0;
}
TextBox::~TextBox() = default;
void TextBox::draw(const DrawContext& pctx, const Assets& assets) {
Container::draw(pctx, assets);
@ -71,6 +240,7 @@ void TextBox::draw(const DrawContext& pctx, const Assets& assets) {
auto batch = pctx.getBatch2D();
batch->texture(nullptr);
batch->setColor(glm::vec4(1.0f));
if (editable && int((Window::time() - caretLastMove) * 2) % 2 == 0) {
uint line = rawTextCache.getLineByTextIndex(caret);
uint lcaret = caret - rawTextCache.getTextLineOffset(line);
@ -138,7 +308,6 @@ void TextBox::draw(const DrawContext& pctx, const Assets& assets) {
}
do {
int lineY = label->getLineYOffset(line);
int lineHeight = font->getLineHeight() * label->getLineInterval();
batch->setColor(glm::vec4(1, 1, 1, 0.05f));
if (showLineNumbers) {
@ -260,18 +429,22 @@ void TextBox::refreshLabel() {
/// @brief Insert text at the caret. Also selected text will be erased
/// @param text Inserting text
void TextBox::paste(const std::wstring& text) {
void TextBox::paste(const std::wstring& text, bool history) {
eraseSelected();
auto inputText = text;
inputText.erase(
std::remove(inputText.begin(), inputText.end(), '\r'), inputText.end()
);
historian->onPaste(caret, inputText);
if (caret >= input.length()) {
input += text;
input += inputText;
} else {
auto left = input.substr(0, caret);
auto right = input.substr(caret);
input = left + text + right;
input = left + inputText + right;
}
input.erase(std::remove(input.begin(), input.end(), '\r'), input.end());
refreshLabel();
setCaret(caret + text.length());
setCaret(caret + inputText.length());
if (validate()) {
onInput();
}
@ -296,6 +469,11 @@ bool TextBox::eraseSelected() {
if (selectionStart == selectionEnd) {
return false;
}
historian->onErase(
selectionStart,
input.substr(selectionStart, selectionEnd - selectionStart),
true
);
erase(selectionStart, selectionEnd-selectionStart);
resetSelection();
onInput();
@ -336,7 +514,9 @@ void TextBox::setTextOffset(uint x) {
void TextBox::typed(unsigned int codepoint) {
if (editable) {
paste(std::wstring({(wchar_t)codepoint}));
// Combine deleting selected text and inserting a symbol
auto combination = history->beginCombination();
paste(std::wstring({static_cast<wchar_t>(codepoint)}));
}
}
@ -383,6 +563,23 @@ bool TextBox::isEditable() const {
return editable;
}
bool TextBox::isEdited() const {
return history->size() != editedHistorySize || !historian->isSynced();
}
void TextBox::setUnedited() {
historian->sync();
editedHistorySize = history->size();
}
size_t TextBox::getSelectionStart() const {
return selectionStart;
}
size_t TextBox::getSelectionEnd() const {
return selectionEnd;
}
void TextBox::setOnEditStart(runnable oneditstart) {
onEditStart = oneditstart;
}
@ -404,6 +601,12 @@ void TextBox::onFocus(GUI* gui) {
}
}
void TextBox::reposition() {
auto size = getSize();
UINode::reposition();
refreshLabel();
}
void TextBox::refresh() {
Container::refresh();
label->setSize(size-glm::vec2(padding.z+padding.x, padding.w+padding.y));
@ -609,6 +812,7 @@ void TextBox::performEditingKeyboardEvents(keycode key) {
if (caret > input.length()) {
caret = input.length();
}
historian->onErase(caret - 1, input.substr(caret - 1, 1));
input = input.substr(0, caret-1) + input.substr(caret);
setCaret(caret-1);
if (validate()) {
@ -617,6 +821,7 @@ void TextBox::performEditingKeyboardEvents(keycode key) {
}
} else if (key == keycode::DELETE) {
if (!eraseSelected() && caret < input.length()) {
historian->onErase(caret, input.substr(caret, 1));
input = input.substr(0, caret) + input.substr(caret + 1);
if (validate()) {
onInput();
@ -648,7 +853,12 @@ void TextBox::keyPressed(keycode key) {
if (editable) {
performEditingKeyboardEvents(key);
}
if (Events::pressed(keycode::LEFT_CONTROL)) {
if (Events::pressed(keycode::LEFT_CONTROL) && key != keycode::LEFT_CONTROL) {
if (controlCombinationsHandler) {
if (controlCombinationsHandler(static_cast<int>(key))) {
return;
}
}
// Copy selected text to clipboard
if (key == keycode::C || key == keycode::X) {
std::string text = util::wstr2str_utf8(getSelection());
@ -663,7 +873,11 @@ void TextBox::keyPressed(keycode key) {
if (key == keycode::V && editable) {
const char* text = Window::getClipboardText();
if (text) {
historian->sync(); // flush buffer before combination
// Combine deleting selected text and pasing a clipboard content
auto combination = history->beginCombination();
paste(util::str2wstr_utf8(text));
historian->sync();
}
}
// Select/deselect all
@ -674,6 +888,14 @@ void TextBox::keyPressed(keycode key) {
resetSelection();
}
}
if (key == keycode::Z) {
historian->undo();
refreshSyntax();
}
if (key == keycode::Y) {
historian->redo();
refreshSyntax();
}
}
}
@ -698,10 +920,8 @@ size_t TextBox::getLinePos(uint line) const {
return label->getTextLineOffset(line);
}
std::shared_ptr<UINode> TextBox::getAt(
const glm::vec2& pos, const std::shared_ptr<UINode>& self
) {
return UINode::getAt(pos, self);
std::shared_ptr<UINode> TextBox::getAt(const glm::vec2& pos) {
return UINode::getAt(pos);
}
void TextBox::setOnUpPressed(const runnable &callback) {
@ -752,6 +972,10 @@ void TextBox::setTextValidator(wstringchecker validator) {
this->validator = std::move(validator);
}
void TextBox::setOnControlCombination(key_handler handler) {
this->controlCombinationsHandler = std::move(handler);
}
void TextBox::setFocusedColor(glm::vec4 color) {
this->focusedColor = color;
}
@ -786,6 +1010,9 @@ const std::wstring& TextBox::getText() const {
void TextBox::setText(const std::wstring& value) {
this->input = value;
input.erase(std::remove(input.begin(), input.end(), '\r'), input.end());
historian->reset();
history->clear();
editedHistorySize = 0;
refreshSyntax();
}

View File

@ -4,10 +4,15 @@
#include "Label.hpp"
class Font;
class ActionsHistory;
namespace gui {
class TextBoxHistorian;
class TextBox : public Container {
LabelCache rawTextCache;
std::shared_ptr<ActionsHistory> history;
std::unique_ptr<TextBoxHistorian> historian;
int editedHistorySize = 0;
protected:
glm::vec4 focusedColor {0.0f, 0.0f, 0.0f, 1.0f};
glm::vec4 invalidColor {0.1f, 0.05f, 0.03f, 1.0f};
@ -29,6 +34,7 @@ namespace gui {
wstringconsumer subconsumer = nullptr;
/// @brief Text validator returning boolean value
wstringchecker validator = nullptr;
key_handler controlCombinationsHandler = nullptr;
/// @brief Function called on focus
runnable onEditStart = nullptr;
/// @brief Function called on up arrow pressed
@ -68,7 +74,6 @@ namespace gui {
int calcIndexAt(int x, int y) const;
void setTextOffset(uint x);
void erase(size_t start, size_t length);
bool eraseSelected();
void resetSelection();
void extendSelection(int index);
@ -93,8 +98,11 @@ namespace gui {
std::wstring placeholder,
glm::vec4 padding=glm::vec4(4.0f)
);
virtual ~TextBox();
void paste(const std::wstring& text);
void paste(const std::wstring& text, bool history=true);
void erase(size_t start, size_t length);
virtual void setTextSupplier(wstringsupplier supplier);
@ -111,6 +119,8 @@ namespace gui {
/// @param validator std::wstring consumer returning boolean
virtual void setTextValidator(wstringchecker validator);
virtual void setOnControlCombination(key_handler handler);
virtual void setFocusedColor(glm::vec4 color);
virtual glm::vec4 getFocusedColor() const;
@ -198,9 +208,15 @@ namespace gui {
/// @brief Check if text editing feature is enabled
virtual bool isEditable() const;
virtual bool isEdited() const;
virtual void setUnedited();
virtual void setPadding(glm::vec4 padding);
glm::vec4 getPadding() const;
size_t getSelectionStart() const;
size_t getSelectionEnd() const;
/// @brief Set runnable called on textbox focus
virtual void setOnEditStart(runnable oneditstart);
@ -210,6 +226,7 @@ namespace gui {
virtual void setShowLineNumbers(bool flag);
virtual bool isShowLineNumbers() const;
virtual void reposition() override;
virtual void onFocus(GUI*) override;
virtual void refresh() override;
virtual void doubleClick(GUI*, int x, int y) override;
@ -220,9 +237,7 @@ namespace gui {
virtual void drawBackground(const DrawContext& pctx, const Assets& assets) override;
virtual void typed(unsigned int codepoint) override;
virtual void keyPressed(keycode key) override;
virtual std::shared_ptr<UINode> getAt(
const glm::vec2& pos, const std::shared_ptr<UINode>& self
) override;
virtual std::shared_ptr<UINode> getAt(const glm::vec2& pos) override;
virtual void setOnUpPressed(const runnable &callback);
virtual void setOnDownPressed(const runnable &callback);

View File

@ -111,11 +111,11 @@ bool UINode::isInside(glm::vec2 point) {
point.x < pos.x + size.x && point.y < pos.y + size.y);
}
std::shared_ptr<UINode> UINode::getAt(const glm::vec2& point, const std::shared_ptr<UINode>& self) {
std::shared_ptr<UINode> UINode::getAt(const glm::vec2& point) {
if (!isInteractive() || !enabled) {
return nullptr;
}
return isInside(point) ? self : nullptr;
return isInside(point) ? shared_from_this() : nullptr;
}
bool UINode::isInteractive() const {
@ -266,7 +266,7 @@ void UINode::moveInto(
) {
auto parent = node->getParent();
if (auto container = dynamic_cast<Container*>(parent)) {
container->remove(node);
container->remove(node.get());
}
if (parent) {
parent->scrolled(0);
@ -301,9 +301,13 @@ const std::string& UINode::getId() const {
void UINode::reposition() {
if (sizefunc) {
auto newSize = sizefunc();
auto defsize = newSize;
if (parent) {
defsize = parent->getSize();
}
setSize(
{newSize.x < 0 ? size.x : newSize.x,
newSize.y < 0 ? size.y : newSize.y}
{newSize.x < 0 ? defsize.x + (newSize.x + 1) : newSize.x,
newSize.y < 0 ? defsize.y + (newSize.y + 1) : newSize.y}
);
}
if (positionfunc) {

View File

@ -63,7 +63,7 @@ namespace gui {
};
/// @brief Base abstract class for all UI elements
class UINode {
class UINode : public std::enable_shared_from_this<UINode> {
/// @brief element identifier used for direct access in UiDocument
std::string id = "";
/// @brief element enabled state
@ -195,7 +195,7 @@ namespace gui {
/// @param pos cursor screen position
/// @param self shared pointer to element
/// @return self, sub-element or nullptr if element is not interractive
virtual std::shared_ptr<UINode> getAt(const glm::vec2& pos, const std::shared_ptr<UINode>& self);
virtual std::shared_ptr<UINode> getAt(const glm::vec2& pos);
/// @brief Check if element is opaque for cursor
virtual bool isInteractive() const;
@ -250,7 +250,7 @@ namespace gui {
const std::string& getId() const;
/// @brief Fetch pos from positionfunc if assigned
void reposition();
virtual void reposition();
virtual void setGravity(Gravity gravity);

View File

@ -91,7 +91,11 @@ void guiutil::confirm(
if (yestext.empty()) yestext = langs::get(L"Yes");
if (notext.empty()) notext = langs::get(L"No");
auto container = std::make_shared<Container>(glm::vec2(5000, 5000));
container->setColor(glm::vec4(0.05f, 0.05f, 0.05f, 0.7f));
auto panel = std::make_shared<Panel>(glm::vec2(600, 200), glm::vec4(8.0f), 8.0f);
panel->setGravity(Gravity::center_center);
container->add(panel);
panel->setColor(glm::vec4(0.0f, 0.0f, 0.0f, 0.5f));
panel->add(std::make_shared<Label>(text));
auto subpanel = std::make_shared<Panel>(glm::vec2(600, 53));
@ -103,8 +107,8 @@ void guiutil::confirm(
menu->removePage("<confirm>");
if (on_confirm) {
on_confirm();
} else {
menu->back();
} else if (!menu->back()) {
menu->reset();
}
};
@ -112,8 +116,8 @@ void guiutil::confirm(
menu->removePage("<confirm>");
if (on_deny) {
on_deny();
} else {
menu->back();
} else if (!menu->back()) {
menu->reset();
}
};
@ -136,7 +140,7 @@ void guiutil::confirm(
}));
panel->refresh();
menu->addPage("<confirm>", panel, true);
menu->addPage("<confirm>", container, true);
menu->setPage("<confirm>");
}

View File

@ -7,6 +7,7 @@
#include "elements/Canvas.hpp"
#include "elements/CheckBox.hpp"
#include "elements/TextBox.hpp"
#include "elements/SplitBox.hpp"
#include "elements/TrackBar.hpp"
#include "elements/InputBindBox.hpp"
#include "elements/InventoryView.hpp"
@ -157,7 +158,13 @@ static void read_uinode(
}
if (element.has("tooltip")) {
node.setTooltip(util::str2wstr_utf8(element.attr("tooltip").getText()));
auto tooltip = util::str2wstr_utf8(element.attr("tooltip").getText());
if (!tooltip.empty() && tooltip[0] == '@') {
tooltip = langs::get(
tooltip.substr(1), util::str2wstr_utf8(reader.getContext())
);
}
node.setTooltip(tooltip);
}
if (element.has("tooltip-delay")) {
node.setTooltipDelay(element.attr("tooltip-delay").asFloat());
@ -206,11 +213,10 @@ void UiXmlReader::readUINode(
read_uinode(reader, element, node);
}
static void read_panel_impl(
static void read_base_panel_impl(
UiXmlReader& reader,
const xml::xmlelement& element,
Panel& panel,
bool subnodes = true
BasePanel& panel
) {
read_uinode(reader, element, panel);
@ -223,6 +229,22 @@ static void read_panel_impl(
size.y + padding.y + padding.w
));
}
if (element.has("orientation")) {
auto &oname = element.attr("orientation").getText();
if (oname == "horizontal") {
panel.setOrientation(Orientation::horizontal);
}
}
}
static void read_panel_impl(
UiXmlReader& reader,
const xml::xmlelement& element,
Panel& panel,
bool subnodes = true
) {
read_base_panel_impl(reader, element, panel);
if (element.has("size")) {
panel.setResizing(false);
}
@ -232,12 +254,6 @@ static void read_panel_impl(
if (element.has("min-length")) {
panel.setMinLength(element.attr("min-length").asInt());
}
if (element.has("orientation")) {
auto &oname = element.attr("orientation").getText();
if (oname == "horizontal") {
panel.setOrientation(Orientation::horizontal);
}
}
if (subnodes) {
for (auto& sub : element.getElements()) {
if (sub->isText())
@ -313,6 +329,28 @@ static std::shared_ptr<UINode> read_container(
return container;
}
static std::shared_ptr<UINode> read_split_box(
UiXmlReader& reader, const xml::xmlelement& element
) {
float splitPos = element.attr("split-pos", "0.5").asFloat();
Orientation orientation =
element.attr("orientation", "vertical").getText() == "horizontal"
? Orientation::horizontal
: Orientation::vertical;
auto splitBox =
std::make_shared<SplitBox>(glm::vec2(), splitPos, orientation);
read_base_panel_impl(reader, element, *splitBox);
for (auto& sub : element.getElements()) {
if (sub->isText())
continue;
auto subnode = reader.readUINode(*sub);
if (subnode) {
splitBox->add(subnode);
}
}
return splitBox;
}
static std::shared_ptr<UINode> read_panel(
UiXmlReader& reader, const xml::xmlelement& element
) {
@ -455,6 +493,13 @@ static std::shared_ptr<UINode> read_text_box(
reader.getFilename()
));
}
if (element.has("oncontrolkey")) {
textbox->setOnControlCombination(scripting::create_key_handler(
reader.getEnvironment(),
element.attr("oncontrolkey").getText(),
reader.getFilename()
));
}
if (auto onUpPressed = create_runnable(reader, element, "onup")) {
textbox->setOnUpPressed(onUpPressed);
}
@ -677,6 +722,7 @@ UiXmlReader::UiXmlReader(const scriptenv& env) : env(env) {
add("button", read_button);
add("textbox", read_text_box);
add("pagebox", read_page_box);
add("splitbox", read_split_box);
add("checkbox", read_check_box);
add("trackbar", read_track_bar);
add("container", read_container);

View File

@ -23,10 +23,6 @@ static inline void emit_md(
template <typename CharT>
static glm::vec4 parse_color(const std::basic_string_view<CharT>& color_code) {
if (color_code.size() != 8 || color_code[0] != '#') {
return glm::vec4(1, 1, 1, 1); // default to white
}
auto hex_to_float = [](char high, char low) {
int high_val = hexchar2int(high);
int low_val = hexchar2int(low);
@ -36,11 +32,18 @@ static glm::vec4 parse_color(const std::basic_string_view<CharT>& color_code) {
return (high_val * 16 + low_val) / 255.0f;
};
if (color_code[0] != '#') {
return glm::vec4(1, 1, 1, 1);
}
if (color_code.size() < 8) {
return glm::vec4(1, 1, 1, 1);
}
return glm::vec4(
hex_to_float(color_code[1], color_code[2]),
hex_to_float(color_code[3], color_code[4]),
hex_to_float(color_code[5], color_code[6]),
1
color_code.size() == 10 ? hex_to_float(color_code[7], color_code[8]) : 1
);
}
@ -99,18 +102,24 @@ Result<CharT> process_markdown(
pos++;
continue;
case '[':
if (pos + 9 < source.size() && source[pos + 1] == '#' &&
source[pos + 8] == ']') {
if (!eraseMarkdown) {
emit_md(source[pos - 1], styles, ss);
if (source[pos + 1] == '#') {
int closingPos = -1;
if (pos + 8 < source.size() && source[pos + 8] == ']') {
closingPos = 8;
} else if (pos + 10 < source.size() && source[pos + 10] == ']') {
closingPos = 10;
}
for (int i = 0; i < 10; ++i) {
emit(source[pos + i], styles, ss);
if (closingPos != -1) {
if (!eraseMarkdown) {
emit_md(source[pos - 1], styles, ss);
}
int length = closingPos + 2;
for (int i = 0; i < length; ++i) {
emit(source[pos + i], styles, ss);
}
pos += length;
continue;
}
pos += 10;
continue;
}
}
pos--;
@ -125,6 +134,16 @@ Result<CharT> process_markdown(
}
pos += 9; // Skip past the color code
continue;
} else if (first == '[' && pos + 11 <= source.size() && source[pos + 1] == '#' && source[pos + 10] == ']') {
std::basic_string_view<CharT> color_code = source.substr(pos + 1, 10);
apply_color(color_code, styles, style);
if (!eraseMarkdown) {
for (int i = 0; i < 11; ++i) {
emit_md(source[pos + i], styles, ss);
}
}
pos += 11; // Skip past the color code
continue;
} else if (first == '*') {
if (pos + 1 < source.size() && source[pos + 1] == '*') {
pos++;

View File

@ -150,11 +150,45 @@ void EnginePaths::setCurrentWorldFolder(io::path folder) {
io::create_subdevice("world", "user", currentWorldFolder);
}
#include <chrono>
#include "maths/util.hpp"
std::string EnginePaths::createWriteablePackDevice(const std::string& name) {
const auto& found = writeablePacks.find(name);
if (found != writeablePacks.end()) {
return found->second;
}
io::path folder;
for (const auto& pack : *contentPacks) {
if (pack.id == name) {
folder = pack.folder;
break;
}
}
if (folder.emptyOrInvalid()) {
throw std::runtime_error("pack not found");
}
auto now = std::chrono::high_resolution_clock::now();
auto seed = now.time_since_epoch().count();
util::PseudoRandom random(seed); // fixme: replace with safe random
auto number = random.rand64();
auto entryPoint = std::string("W.") + util::base64_urlsafe_encode(reinterpret_cast<ubyte*>(&number), 6);
io::create_subdevice(entryPoint, folder.entryPoint(), folder.pathPart());
writeablePacks[name] = entryPoint;
return entryPoint;
}
void EnginePaths::setContentPacks(std::vector<ContentPack>* contentPacks) {
// Remove previous content entry-points
for (const auto& id : contentEntryPoints) {
io::remove_device(id);
}
for (const auto& [_, entryPoint] : writeablePacks) {
io::remove_device(entryPoint);
}
contentEntryPoints.clear();
this->contentPacks = contentPacks;
// Create content devices

View File

@ -1,5 +1,6 @@
#pragma once
#include <unordered_map>
#include <stdexcept>
#include <optional>
#include <string>
@ -33,6 +34,8 @@ public:
io::path getControlsFile() const;
io::path getSettingsFile() const;
std::string createWriteablePackDevice(const std::string& name);
void setContentPacks(std::vector<ContentPack>* contentPacks);
std::vector<io::path> scanForWorlds() const;
@ -47,6 +50,7 @@ private:
std::optional<std::filesystem::path> scriptFolder;
std::vector<ContentPack>* contentPacks = nullptr;
std::vector<std::string> contentEntryPoints;
std::unordered_map<std::string, std::string> writeablePacks;
};
struct PathsRoot {

View File

@ -59,6 +59,8 @@ struct ItemDef {
std::string modelName = name + ".model";
std::string scriptFile;
struct {
itemid_t id;
blockid_t placingBlock;

View File

@ -1,4 +1,5 @@
#include "content/Content.hpp"
#include "content/ContentLoader.hpp"
#include "lighting/Lighting.hpp"
#include "logic/BlocksController.hpp"
#include "logic/LevelController.hpp"
@ -12,6 +13,7 @@
#include "world/Level.hpp"
#include "maths/voxmaths.hpp"
#include "data/StructLayout.hpp"
#include "engine/Engine.hpp"
#include "api_lua.hpp"
using namespace scripting;
@ -617,6 +619,17 @@ static int l_set_field(lua::State* L) {
return set_field(L, dst, *field, index, dataStruct, value);
}
static int l_reload_script(lua::State* L) {
auto name = lua::require_string(L, 1);
if (content == nullptr) {
throw std::runtime_error("content is not initialized");
}
auto& writeableContent = *engine->getWriteableContent();
auto& def = writeableContent.blocks.require(name);
ContentLoader::reloadScript(writeableContent, def);
return 0;
}
const luaL_Reg blocklib[] = {
{"index", lua::wrap<l_index>},
{"name", lua::wrap<l_get_def>},
@ -652,5 +665,6 @@ const luaL_Reg blocklib[] = {
{"decompose_state", lua::wrap<l_decompose_state>},
{"get_field", lua::wrap<l_get_field>},
{"set_field", lua::wrap<l_set_field>},
{"reload_script", lua::wrap<l_reload_script>},
{NULL, NULL}
};

View File

@ -273,6 +273,33 @@ static int l_blank(lua::State*) {
return 0;
}
static int l_capture_output(lua::State* L) {
int argc = lua::gettop(L) - 1;
if (!lua::isfunction(L, 1)) {
throw std::runtime_error("function expected as argument 1");
}
for (int i = 0; i < argc; i++) {
lua::pushvalue(L, i + 2);
}
lua::pushvalue(L, 1);
auto prev_output = output_stream;
auto prev_error = error_stream;
std::stringstream captured_output;
output_stream = &captured_output;
error_stream = &captured_output;
lua::call_nothrow(L, argc, 0);
output_stream = prev_output;
error_stream = prev_error;
lua::pushstring(L, captured_output.str());
return 1;
}
const luaL_Reg corelib[] = {
{"blank", lua::wrap<l_blank>},
{"get_version", lua::wrap<l_get_version>},
@ -292,6 +319,7 @@ const luaL_Reg corelib[] = {
{"get_setting_info", lua::wrap<l_get_setting_info>},
{"open_folder", lua::wrap<l_open_folder>},
{"quit", lua::wrap<l_quit>},
{"capture_output", lua::wrap<l_capture_output>},
{"__load_texture", lua::wrap<l_load_texture>},
{NULL, NULL}
};

View File

@ -223,6 +223,18 @@ static int l_raycast(lua::State* L) {
return 0;
}
static int l_reload_component(lua::State* L) {
std::string name = lua::require_string(L, 1);
size_t pos = name.find(':');
if (pos == std::string::npos) {
throw std::runtime_error("missing entry point");
}
auto filename = name.substr(0, pos + 1) + "scripts/components/" +
name.substr(pos + 1) + ".lua";
scripting::load_entity_component(name, filename, filename);
return 0;
}
const luaL_Reg entitylib[] = {
{"exists", lua::wrap<l_exists>},
{"def_index", lua::wrap<l_def_index>},
@ -238,5 +250,6 @@ const luaL_Reg entitylib[] = {
{"get_all_in_box", lua::wrap<l_get_all_in_box>},
{"get_all_in_radius", lua::wrap<l_get_all_in_radius>},
{"raycast", lua::wrap<l_raycast>},
{"reload_component", lua::wrap<l_reload_component>},
{NULL, NULL}
};

View File

@ -41,10 +41,23 @@ static std::set<std::string> writeable_entry_points {
"world", "export", "config"
};
static bool is_writeable(const std::string& entryPoint) {
if (entryPoint.length() < 2) {
return false;
}
if (entryPoint.substr(0, 2) == "W.") {
return true;
}
if (writeable_entry_points.find(entryPoint) != writeable_entry_points.end()) {
return true;
}
return false;
}
static io::path get_writeable_path(lua::State* L) {
io::path path = lua::require_string(L, 1);
auto entryPoint = path.entryPoint();
if (writeable_entry_points.find(entryPoint) == writeable_entry_points.end()) {
if (!is_writeable(entryPoint)) {
throw std::runtime_error("access denied");
}
return path;
@ -60,7 +73,7 @@ static int l_write(lua::State* L) {
static int l_remove(lua::State* L) {
io::path path = lua::require_string(L, 1);
auto entryPoint = path.entryPoint();
if (writeable_entry_points.find(entryPoint) == writeable_entry_points.end()) {
if (!is_writeable(entryPoint)) {
throw std::runtime_error("access denied");
}
return lua::pushboolean(L, io::remove(path));
@ -69,7 +82,7 @@ static int l_remove(lua::State* L) {
static int l_remove_tree(lua::State* L) {
io::path path = lua::require_string(L, 1);
auto entryPoint = path.entryPoint();
if (writeable_entry_points.find(entryPoint) == writeable_entry_points.end()) {
if (!is_writeable(entryPoint)) {
throw std::runtime_error("access denied");
}
return lua::pushinteger(L, io::remove_all(path));
@ -222,10 +235,7 @@ static int l_read_combined_object(lua::State* L) {
static int l_is_writeable(lua::State* L) {
io::path path = lua::require_string(L, 1);
auto entryPoint = path.entryPoint();
if (writeable_entry_points.find(entryPoint) == writeable_entry_points.end()) {
return lua::pushboolean(L, false);
}
return lua::pushboolean(L, true);
return lua::pushboolean(L, is_writeable(entryPoint));
}
const luaL_Reg filelib[] = {
@ -249,4 +259,5 @@ const luaL_Reg filelib[] = {
{"read_combined_list", lua::wrap<l_read_combined_list>},
{"read_combined_object", lua::wrap<l_read_combined_object>},
{"is_writeable", lua::wrap<l_is_writeable>},
{NULL, NULL}};
{NULL, NULL}
};

View File

@ -96,7 +96,7 @@ static int l_node_destruct(lua::State* L) {
engine->getGUI()->postRunnable([node]() {
auto parent = node->getParent();
if (auto container = dynamic_cast<Container*>(parent)) {
container->remove(node);
container->remove(node.get());
}
});
return 0;
@ -294,6 +294,13 @@ static int p_get_editable(UINode* node, lua::State* L) {
return 0;
}
static int p_get_edited(UINode* node, lua::State* L) {
if (auto box = dynamic_cast<TextBox*>(node)) {
return lua::pushboolean(L, box->isEdited());
}
return 0;
}
static int p_get_line_numbers(UINode* node, lua::State* L) {
if (auto box = dynamic_cast<TextBox*>(node)) {
return lua::pushboolean(L, box->isShowLineNumbers());
@ -451,6 +458,7 @@ static int l_gui_getattr(lua::State* L) {
{"caret", p_get_caret},
{"text", p_get_text},
{"editable", p_get_editable},
{"edited", p_get_edited},
{"lineNumbers", p_get_line_numbers},
{"lineAt", p_get_line_at},
{"linePos", p_get_line_pos},
@ -545,6 +553,13 @@ static void p_set_editable(UINode* node, lua::State* L, int idx) {
box->setEditable(lua::toboolean(L, idx));
}
}
static void p_set_edited(UINode* node, lua::State* L, int idx) {
if (auto box = dynamic_cast<TextBox*>(node)) {
if (!lua::toboolean(L, idx)) {
box->setUnedited();
}
}
}
static void p_set_line_numbers(UINode* node, lua::State* L, int idx) {
if (auto box = dynamic_cast<TextBox*>(node)) {
box->setShowLineNumbers(lua::toboolean(L, idx));
@ -667,6 +682,7 @@ static int l_gui_setattr(lua::State* L) {
{"hint", p_set_hint},
{"text", p_set_text},
{"editable", p_set_editable},
{"edited", p_set_edited},
{"lineNumbers", p_set_line_numbers},
{"syntax", p_set_syntax},
{"markup", p_set_markup},

View File

@ -171,6 +171,23 @@ static int l_set_allow_pause(lua::State* L) {
return 0;
}
static int l_reload_script(lua::State* L) {
auto packid = lua::require_string(L, 1);
if (content == nullptr) {
throw std::runtime_error("content is not initialized");
}
auto& writeableContent = *engine->getWriteableContent();
auto pack = writeableContent.getPackRuntime(packid);
const auto& info = pack->getInfo();
scripting::load_hud_script(
pack->getEnvironment(),
packid,
info.folder / "scripts/hud.lua",
pack->getId() + ":scripts/hud.lua"
);
return 0;
}
const luaL_Reg hudlib[] = {
{"open_inventory", wrap_hud<l_open_inventory>},
{"close_inventory", wrap_hud<l_close_inventory>},
@ -189,5 +206,6 @@ const luaL_Reg hudlib[] = {
{"_set_content_access", wrap_hud<l_set_content_access>},
{"_set_debug_cheats", wrap_hud<l_set_debug_cheats>},
{"set_allow_pause", wrap_hud<l_set_allow_pause>},
{"reload_script", wrap_hud<l_reload_script>},
{NULL, NULL}
};

View File

@ -1,6 +1,8 @@
#include "content/Content.hpp"
#include "content/ContentLoader.hpp"
#include "items/ItemDef.hpp"
#include "api_lua.hpp"
#include "engine/Engine.hpp"
using namespace scripting;
@ -87,6 +89,17 @@ static int l_uses(lua::State* L) {
return 0;
}
static int l_reload_script(lua::State* L) {
auto name = lua::require_string(L, 1);
if (content == nullptr) {
throw std::runtime_error("content is not initialized");
}
auto& writeableContent = *engine->getWriteableContent();
auto& def = writeableContent.items.require(name);
ContentLoader::reloadScript(writeableContent, def);
return 0;
}
const luaL_Reg itemlib[] = {
{"index", lua::wrap<l_index>},
{"name", lua::wrap<l_name>},
@ -98,5 +111,6 @@ const luaL_Reg itemlib[] = {
{"model_name", lua::wrap<l_model_name>},
{"emission", lua::wrap<l_emission>},
{"uses", lua::wrap<l_uses>},
{"reload_script", lua::wrap<l_reload_script>},
{NULL, NULL}
};

View File

@ -7,6 +7,9 @@
#include "assets/AssetsLoader.hpp"
#include "content/Content.hpp"
#include "engine/Engine.hpp"
#include "graphics/ui/gui_util.hpp"
#include "graphics/ui/elements/Menu.hpp"
#include "frontend/locale.hpp"
#include "world/files/WorldFiles.hpp"
#include "io/engine_paths.hpp"
#include "world/Level.hpp"
@ -247,6 +250,20 @@ static int l_pack_assemble(lua::State* L) {
return 1;
}
static int l_pack_request_writeable(lua::State* L) {
auto packid = lua::require_string(L, 1);
lua::pushvalue(L, 2);
auto handler = lua::create_lambda_nothrow(L);
auto str = langs::get(L"Grant %{0} pack modification permission?");
util::replaceAll(str, L"%{0}", util::str2wstr_utf8(packid));
guiutil::confirm(*engine, str, [packid, handler]() {
handler({engine->getPaths().createWriteablePackDevice(packid)});
engine->getGUI()->getMenu()->reset();
});
return 0;
}
const luaL_Reg packlib[] = {
{"get_folder", lua::wrap<l_pack_get_folder>},
{"get_installed", lua::wrap<l_pack_get_installed>},
@ -254,4 +271,6 @@ const luaL_Reg packlib[] = {
{"get_info", lua::wrap<l_pack_get_info>},
{"get_base_packs", lua::wrap<l_pack_get_base_packs>},
{"assemble", lua::wrap<l_pack_assemble>},
{NULL, NULL}};
{"request_writeable", lua::wrap<l_pack_request_writeable>},
{NULL, NULL}
};

View File

@ -6,6 +6,7 @@
#include "assets/AssetsLoader.hpp"
#include "coders/json.hpp"
#include "content/Content.hpp"
#include "content/ContentLoader.hpp"
#include "engine/Engine.hpp"
#include "world/files/WorldFiles.hpp"
#include "io/engine_paths.hpp"
@ -213,6 +214,17 @@ static int l_count_chunks(lua::State* L) {
return lua::pushinteger(L, level->chunks->size());
}
static int l_reload_script(lua::State* L) {
auto packid = lua::require_string(L, 1);
if (content == nullptr) {
throw std::runtime_error("content is not initialized");
}
auto& writeableContent = *engine->getWriteableContent();
auto pack = writeableContent.getPackRuntime(packid);
ContentLoader::loadWorldScript(*pack);
return 0;
}
const luaL_Reg worldlib[] = {
{"is_open", lua::wrap<l_is_open>},
{"get_list", lua::wrap<l_get_list>},
@ -230,5 +242,6 @@ const luaL_Reg worldlib[] = {
{"set_chunk_data", lua::wrap<l_set_chunk_data>},
{"save_chunk_data", lua::wrap<l_save_chunk_data>},
{"count_chunks", lua::wrap<l_count_chunks>},
{"reload_script", lua::wrap<l_reload_script>},
{NULL, NULL}
};

View File

@ -2,6 +2,8 @@
#include "libs/api_lua.hpp"
using namespace scripting;
/// @brief Modified version of luaB_print from lbaselib.c
int l_print(lua::State* L) {
int n = lua::gettop(L); /* number of arguments */
@ -16,10 +18,10 @@ int l_print(lua::State* L) {
L,
LUA_QL("tostring") " must return a string to " LUA_QL("print")
);
if (i > 1) std::cout << "\t";
std::cout << s;
if (i > 1) *output_stream << "\t";
*output_stream << s;
lua::pop(L); /* pop result */
}
std::cout << std::endl;
*output_stream << std::endl;
return 0;
}

View File

@ -34,6 +34,8 @@ static debug::Logger logger("scripting");
static inline const std::string STDCOMP = "stdcomp";
std::ostream* scripting::output_stream = &std::cout;
std::ostream* scripting::error_stream = &std::cerr;
Engine* scripting::engine = nullptr;
Level* scripting::level = nullptr;
const Content* scripting::content = nullptr;
@ -804,21 +806,21 @@ bool scripting::register_event(
if (lua::pushenv(L, env) == 0) {
lua::pushglobals(L);
}
if (lua::getfield(L, name)) {
lua::pop(L);
lua::getglobal(L, "events");
lua::getfield(L, "reset");
lua::pushstring(L, id);
lua::getfield(L, name, -4);
lua::call_nothrow(L, 2);
lua::pop(L);
// remove previous name
bool success = true;
lua::getglobal(L, "events");
lua::getfield(L, "reset");
lua::pushstring(L, id);
if (!lua::getfield(L, name, -4)) {
success = false;
lua::pushnil(L);
lua::setfield(L, name);
return true;
}
return false;
lua::call_nothrow(L, 2);
lua::pop(L);
// remove previous name
lua::pushnil(L);
lua::setfield(L, name);
return success;
}
int scripting::get_values_on_stack() {
@ -834,6 +836,8 @@ void scripting::load_content_script(
) {
int env = *senv;
lua::pop(lua::get_main_state(), load_script(env, "block", file, fileName));
funcsset = {};
funcsset.init = register_event(env, "init", prefix + ".init");
funcsset.update = register_event(env, "on_update", prefix + ".update");
funcsset.randupdate =
@ -859,6 +863,8 @@ void scripting::load_content_script(
) {
int env = *senv;
lua::pop(lua::get_main_state(), load_script(env, "item", file, fileName));
funcsset = {};
funcsset.init = register_event(env, "init", prefix + ".init");
funcsset.on_use = register_event(env, "on_use", prefix + ".use");
funcsset.on_use_on_block =
@ -886,6 +892,8 @@ void scripting::load_world_script(
) {
int env = *senv;
lua::pop(lua::get_main_state(), load_script(env, "world", file, fileName));
funcsset = {};
register_event(env, "init", prefix + ".init");
register_event(env, "on_world_open", prefix + ":.worldopen");
register_event(env, "on_world_tick", prefix + ":.worldtick");

View File

@ -42,6 +42,8 @@ namespace scripting {
extern Level* level;
extern BlocksController* blocks;
extern LevelController* controller;
extern std::ostream* output_stream;
extern std::ostream* error_stream;
void initialize(Engine* engine);

View File

@ -36,6 +36,28 @@ static lua::State* process_callback(
return nullptr;
}
key_handler scripting::create_key_handler(
const scriptenv& env, const std::string& src, const std::string& file
) {
return [=](int code) {
if (auto L = process_callback(env, src, file)) {
int top = lua::gettop(L);
if (lua::isfunction(L, -1)) {
lua::pushinteger(L, code);
lua::call_nothrow(L, 1);
}
int returned = lua::gettop(L) - top + 1;
if (returned) {
bool x = lua::toboolean(L, -1);
lua::pop(L, returned);
return x;
}
return false;
}
return false;
};
}
wstringconsumer scripting::create_wstring_consumer(
const scriptenv& env, const std::string& src, const std::string& file
) {

View File

@ -17,6 +17,12 @@ namespace scripting {
const std::string& file = "[string]"
);
key_handler create_key_handler(
const scriptenv& env,
const std::string& src,
const std::string& file = "[string]"
);
wstringconsumer create_wstring_consumer(
const scriptenv& env,
const std::string& src,

View File

@ -306,6 +306,12 @@ const char B64ABC[] =
"0123456789"
"+/";
const char URLSAFE_B64ABC[] =
"ABCDEFGHIJKLMNOPQRSTUVWXYZ"
"abcdefghijklmnopqrstuvwxyz"
"0123456789"
"-_";
inline ubyte base64_decode_char(char c) {
if (c >= 'A' && c <= 'Z') return c - 'A';
if (c >= 'a' && c <= 'z') return c - 'a' + 26;
@ -315,16 +321,27 @@ inline ubyte base64_decode_char(char c) {
return 0;
}
inline void base64_encode_(const ubyte* segment, char* output) {
output[0] = B64ABC[(segment[0] & 0b11111100) >> 2];
output[1] =
B64ABC[((segment[0] & 0b11) << 4) | ((segment[1] & 0b11110000) >> 4)];
output[2] =
B64ABC[((segment[1] & 0b1111) << 2) | ((segment[2] & 0b11000000) >> 6)];
output[3] = B64ABC[segment[2] & 0b111111];
inline ubyte base64_urlsafe_decode_char(char c) {
if (c >= 'A' && c <= 'Z') return c - 'A';
if (c >= 'a' && c <= 'z') return c - 'a' + 26;
if (c >= '0' && c <= '9') return c - '0' + 52;
if (c == '-') return 62;
if (c == '_') return 63;
return 0;
}
std::string util::base64_encode(const ubyte* data, size_t size) {
template <const char* ABC>
static void base64_encode_(const ubyte* segment, char* output) {
output[0] = ABC[(segment[0] & 0b11111100) >> 2];
output[1] =
ABC[((segment[0] & 0b11) << 4) | ((segment[1] & 0b11110000) >> 4)];
output[2] =
ABC[((segment[1] & 0b1111) << 2) | ((segment[2] & 0b11000000) >> 6)];
output[3] = ABC[segment[2] & 0b111111];
}
template <const char* ABC>
static std::string base64_encode_impl(const ubyte* data, size_t size) {
std::stringstream ss;
size_t fullsegments = (size / 3) * 3;
@ -332,7 +349,7 @@ std::string util::base64_encode(const ubyte* data, size_t size) {
size_t i = 0;
for (; i < fullsegments; i += 3) {
char output[] = "====";
base64_encode_(data + i, output);
base64_encode_<ABC>(data + i, output);
ss << output;
}
@ -343,18 +360,27 @@ std::string util::base64_encode(const ubyte* data, size_t size) {
size_t trailing = size - fullsegments;
if (trailing) {
char output[] = "====";
output[0] = B64ABC[(ending[0] & 0b11111100) >> 2];
output[0] = ABC[(ending[0] & 0b11111100) >> 2];
output[1] =
B64ABC[((ending[0] & 0b11) << 4) | ((ending[1] & 0b11110000) >> 4)];
ABC[((ending[0] & 0b11) << 4) | ((ending[1] & 0b11110000) >> 4)];
if (trailing > 1)
output[2] = B64ABC
[((ending[1] & 0b1111) << 2) | ((ending[2] & 0b11000000) >> 6)];
if (trailing > 2) output[3] = B64ABC[ending[2] & 0b111111];
output[2] =
ABC[((ending[1] & 0b1111) << 2) |
((ending[2] & 0b11000000) >> 6)];
if (trailing > 2) output[3] = ABC[ending[2] & 0b111111];
ss << output;
}
return ss.str();
}
std::string util::base64_encode(const ubyte* data, size_t size) {
return base64_encode_impl<B64ABC>(data, size);
}
std::string util::base64_urlsafe_encode(const ubyte* data, size_t size) {
return base64_encode_impl<URLSAFE_B64ABC>(data, size);
}
std::string util::tohex(uint64_t value) {
std::stringstream ss;
ss << std::hex << value;
@ -366,7 +392,8 @@ std::string util::mangleid(uint64_t value) {
return tohex(value);
}
util::Buffer<ubyte> util::base64_decode(const char* str, size_t size) {
template <ubyte(base64_decode_char)(char)>
static util::Buffer<ubyte> base64_decode_impl(const char* str, size_t size) {
util::Buffer<ubyte> bytes((size / 4) * 3);
ubyte* dst = bytes.data();
for (size_t i = 0; i < (size / 4) * 4;) {
@ -387,18 +414,35 @@ util::Buffer<ubyte> util::base64_decode(const char* str, size_t size) {
return bytes;
}
util::Buffer<ubyte> util::base64_urlsafe_decode(const char* str, size_t size) {
return base64_decode_impl<base64_urlsafe_decode_char>(str, size);
}
util::Buffer<ubyte> util::base64_decode(const char* str, size_t size) {
return base64_decode_impl<base64_decode_char>(str, size);
}
util::Buffer<ubyte> util::base64_urlsafe_decode(std::string_view str) {
return base64_urlsafe_decode(str.data(), str.size());
}
util::Buffer<ubyte> util::base64_decode(std::string_view str) {
return base64_decode(str.data(), str.size());
}
int util::replaceAll(
std::string& str, const std::string& from, const std::string& to
template <typename CharT>
static int replace_all(
std::basic_string<CharT>& str,
const std::basic_string<CharT>& from,
const std::basic_string<CharT>& to
) {
int count = 0;
size_t offset = 0;
while (true) {
size_t start_pos = str.find(from, offset);
if (start_pos == std::string::npos) break;
if (start_pos == std::basic_string<CharT>::npos) {
break;
}
str.replace(start_pos, from.length(), to);
offset = start_pos + to.length();
count++;
@ -406,6 +450,18 @@ int util::replaceAll(
return count;
}
int util::replaceAll(
std::string& str, const std::string& from, const std::string& to
) {
return replace_all(str, from, to);
}
int util::replaceAll(
std::wstring& str, const std::wstring& from, const std::wstring& to
) {
return replace_all(str, from, to);
}
// replace it with std::from_chars in the far far future
double util::parse_double(const std::string& str) {
std::istringstream ss(str);

View File

@ -74,8 +74,12 @@ namespace util {
std::wstring to_wstring(double x, int precision);
std::string base64_encode(const ubyte* data, size_t size);
std::string base64_urlsafe_encode(const ubyte* data, size_t size);
util::Buffer<ubyte> base64_decode(const char* str, size_t size);
util::Buffer<ubyte> base64_urlsafe_decode(const char* str, size_t size);
util::Buffer<ubyte> base64_decode(std::string_view str);
util::Buffer<ubyte> base64_urlsafe_decode(std::string_view str);
std::string tohex(uint64_t value);
@ -85,6 +89,10 @@ namespace util {
std::string& str, const std::string& from, const std::string& to
);
int replaceAll(
std::wstring& str, const std::wstring& from, const std::wstring& to
);
double parse_double(const std::string& str);
double parse_double(const std::string& str, size_t offset, size_t len);

View File

@ -204,6 +204,8 @@ public:
/// @brief Block script name in blocks/ without extension
std::string scriptName = name.substr(name.find(':') + 1);
std::string scriptFile;
/// @brief Block will be used instead of this if generated on surface
std::string surfaceReplacement = name;

View File

@ -33,14 +33,15 @@ glm::mat4 Camera::getProjection() const {
constexpr float epsilon = 1e-6f; // 0.000001
float aspect_ratio = this->aspect;
if (std::fabs(aspect_ratio) < epsilon) {
aspect_ratio = (float)Window::width / (float)Window::height;
aspect_ratio = Window::width / static_cast<float>(Window::height);
}
if (perspective)
if (perspective) {
return glm::perspective(fov * zoom, aspect_ratio, near, far);
else if (flipped)
return glm::ortho(0.0f, fov * aspect_ratio, fov, 0.0f);
else
return glm::ortho(0.0f, fov * aspect_ratio, 0.0f, fov);
} else if (flipped) {
return glm::ortho(-0.5f, fov * aspect_ratio-0.5f, fov, 0.0f);
} else {
return glm::ortho(-0.5f, fov * aspect_ratio-0.5f, 0.0f, fov);
}
}
glm::mat4 Camera::getView(bool pos) const {

View File

@ -31,3 +31,19 @@ TEST(stringutil, base64) {
}
}
}
TEST(stringutil, base64_urlsafe) {
srand(2019);
for (size_t size = 0; size < 30; size++) {
auto bytes = std::make_unique<ubyte[]>(size);
for (int i = 0; i < size; i++) {
bytes[i] = rand();
}
auto base64 = util::base64_urlsafe_encode(bytes.get(), size);
auto decoded = util::base64_urlsafe_decode(base64);
ASSERT_EQ(size, decoded.size());
for (size_t i = 0; i < size; i++) {
ASSERT_EQ(bytes[i], decoded[i]);
}
}
}