diff --git a/.github/workflows/appimage.yml b/.github/workflows/appimage.yml index f5224c8f..5a01a618 100644 --- a/.github/workflows/appimage.yml +++ b/.github/workflows/appimage.yml @@ -2,9 +2,9 @@ name: x86-64 AppImage on: push: - branches: [ "main", "release-**"] + branches: [ "main", "dev", "release-**"] pull_request: - branches: [ "main" ] + branches: [ "main", "dev" ] jobs: build-appimage: diff --git a/.github/workflows/macos.yml b/.github/workflows/macos.yml index 3af2ee8b..f8fe3dfa 100644 --- a/.github/workflows/macos.yml +++ b/.github/workflows/macos.yml @@ -2,9 +2,9 @@ name: Macos Build on: push: - branches: [ "main", "release-**"] + branches: [ "main", "dev", "release-**"] pull_request: - branches: [ "main" ] + branches: [ "main", "dev" ] jobs: build-dmg: diff --git a/.github/workflows/windows-clang.yml b/.github/workflows/windows-clang.yml index 1356149f..cdf354dd 100644 --- a/.github/workflows/windows-clang.yml +++ b/.github/workflows/windows-clang.yml @@ -2,9 +2,9 @@ name: Windows Build (CLang) on: push: - branches: [ "main", "release-**"] + branches: [ "main", "dev", "release-**"] pull_request: - branches: [ "main" ] + branches: [ "main", "dev" ] jobs: build-windows: diff --git a/.github/workflows/windows.yml b/.github/workflows/windows.yml index 3f7efc9c..e5ee5a69 100644 --- a/.github/workflows/windows.yml +++ b/.github/workflows/windows.yml @@ -2,9 +2,9 @@ name: MSVC Build on: push: - branches: [ "main", "release-**"] + branches: [ "main", "dev", "release-**"] pull_request: - branches: [ "main" ] + branches: [ "main", "dev" ] jobs: build-windows: diff --git a/.gitignore b/.gitignore index 9056cce5..c0b0e1c9 100644 --- a/.gitignore +++ b/.gitignore @@ -9,6 +9,7 @@ Debug/voxel_engine /export /config /out +/projects /misc /world @@ -47,4 +48,4 @@ appimage-build/ # libs /libs/ -/vcpkg_installed/ \ No newline at end of file +/vcpkg_installed/ diff --git a/CHANGELOG.md b/CHANGELOG.md index b41f15b2..8fe8f20e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,74 +1,131 @@ -# 0.28 - 2025.07.18 +# 0.29 - 2025.09.20 -[Documentation](https://github.com/MihailRis/VoxelEngine-Cpp/tree/release-0.28/doc/en/main-page.md) for 0.28 +[Documentation](https://github.com/MihailRis/VoxelEngine-Cpp/tree/release-0.29/doc/en/main-page.md) for 0.29 Table of contents: - [Added](#added) - - [Changes](#changes) - [Functions](#functions) + - [Changes](#changes) - [Fixes](#fixes) ## Added -- advanced graphics mode -- state bits based models -- post-effects -- ui elements: - - iframe - - select - - modelviewer -- vcm models format -- bit.compile -- yaml encoder/decoder -- error handler argument in http.get, http.post -- ui properties: - - image.region -- rotation profiles: - - stairs -- libraries - - gfx.posteffects - - yaml -- stairs rotation profile -- models editing in console -- syntax highlighting: xml, glsl, vcm -- beginning of projects system +- pathfinding +- components: + - core:pathfinding + - core:player + - core:mob +- libraries: + - random + - gfx.skeletons + - (documented) assets +- udp support +- schedules +- events: + - on_physics_update (components) + - on_block_tick(x, y, z, tps) (blocks) +- custom hand controller +- http headers +- named pipes +- optimizations: + - speed up block.set + - speed up vectors +- items description +- item properties methods +- tab + shift+tab +- blocks, items tags +- pack dependencies versions +- ~~allow to disable autospawn position~~ use player.set_spawnpoint +- entity.spawn command +- project script +- gui.root document +- time.schedules.world.common: Schedule ### Changes -- reserved 'project', 'pack', 'packid', 'root' entry points -- Bytearray optimized with FFI -- chunks non-unloading zone limited with circle +- app.sleep_until - added 'timeout argument' +- network.get / post - added 'data' argument to error callback +- autorefresh model preview +- move player controls to lua +- move hand control to lua ### Functions -- yaml.tostring -- yaml.parse -- gfx.posteffects.index -- gfx.posteffects.set_effect -- gfx.posteffects.get_intensity -- gfx.posteffects.set_intensity -- gfx.posteffects.is_active -- gfx.posteffects.set_params -- gfx.posteffects.set_array -- block.get_variant -- block.set_variant -- bit.compile -- Bytearray_as_string +- block.model_name +- block.has_tag +- item.has_tag +- item.description +- base64.encode_urlsafe +- base64.decode_urlsafe +- vec2.rotate +- vecn.distance +- vecn.mix +- rigidbody:get_vdamping +- rigidbody:set_vdamping +- entity:require_component +- network.udp_connect +- random.random +- random.bytes +- random.uuid +- Random:random +- Random:seed +- hud.hand_controller +- inventory.get_caption +- inventory.set_caption +- inventory.get_description +- inventory.set_description +- pathfinding.create_agent +- pathfinding.remove_agent +- pathfinding.set_enabled +- pathfinding.is_enabled +- pathfinding.make_route +- pathfinding.make_route_async +- pathfinding.pull_route +- pathfinding.set_max_visited +- pathfinding.avoid_tag +- gfx.skeletons.get +- Skeleton:index +- Skeleton:get_model +- Skeleton:set_model +- Skeleton:get_matrix +- Skeleton:set_matrix +- Skeleton:get_texture +- Skeleton:set_texture +- Skeleton:is_visible +- Skeleton:set_visible +- Skeleton:get_color +- Skeleton:set_color +- Schedule:set_timeout(time_ms, callback) +- Schedule:set_interval(interval_ms, callback, [optional] repetions): int +- Schedule:remove_interval(id) +- ScheduleGroup:publish(schedule: Schedule) ## Fixes -- [fix: "unknown argument --memcheck" in vctest](https://github.com/MihailRis/voxelcore/commit/281d5e09e6f1c016646af6000f6b111695c994b3) -- [fix "upgrade square is not fully inside of area" error](https://github.com/MihailRis/voxelcore/commit/bf79f6bc75a7686d59fdd0dba8b9018d6191e980 ) -- [fix generator area centering](https://github.com/MihailRis/voxelcore/commit/98813472a8c25b1de93dd5d843af38c5aec9b1d8 "fix generator area centering") -- [fix incomplete content reset](https://github.com/MihailRis/voxelcore/commit/61af8ba943a24f6544c6482def2e244cf0af4d18) -- [fix stack traces](https://github.com/MihailRis/voxelcore/commit/05ddffb5c9902e237c73cdea55d4ac1e303c6a8e) -- [fix containers refreshing](https://github.com/MihailRis/voxelcore/commit/34295faca276b55c6e3c0ddd98b867a0aab3eb2a) -- [fix toml encoder](https://github.com/MihailRis/voxelcore/commit/9cd95bb0eb73521bef07f6f0d5e8b78f3e309ebf) -- [fix InputBindBox](https://github.com/MihailRis/voxelcore/commit/7c976a573b01e3fb6f43bacaab22e34037b55b73 "fix InputBindBox") -- [fix inventory.* functions error messages](https://github.com/MihailRis/voxelcore/commit/af3c315c04959eea6c11f5ae2854a6f253e3450f) -- [fix: validator not called after backspace](https://github.com/MihailRis/voxelcore/commit/df3640978d279b85653d647facb26ef15c509848) -- [fix: missing pack.has_indices if content is not loaded](https://github.com/MihailRis/voxelcore/commit/b02b45457322e1ce8f6b9735caeb5b58b1e2ffb4) -- [fix: entities despawn on F5](https://github.com/MihailRis/voxelcore/commit/6ab48fda935f3f1d97d76a833c8511522857ba6a) -- [bug fix [#549]](https://github.com/MihailRis/voxelcore/commit/49727ec02647e48323266fbf814c15f6d5632ee9) -- [fix player camera zoom with fov-effects disabled](https://github.com/MihailRis/voxelcore/commit/014ffab183687ed9acbb93ab90e43d8f82ed826a) +- fix 3d text position / culling +- fix fragment:place rotation (#593) +- fix server socket creation in macos +- fix: base packs not scanned for app scripts +- fix lua::getfield and events registering +- fix UIDocument::rebuildIndices +- fix input library in headless mode +- fix rigidbody:set_gravity_scale +- fix extended blocks destruction particles spawn spread, offset +- fix shaders recompiling +- fix: C++ vecn functions precision loss +- fix coroutines errors handling +- fix: viewport size on toggle fullscreen +- fix: fullscreen monitor refresh rate +- fix: content menu panel height +- fix generation.create_fragment (#596) +- fix bytearray:insert (#594) +- fix: script overriding +- fix: hud.close after hud.show_overlay bug +- fix: 'cannot resume dead coroutine' (#569) +- fix: skybox is not visible behind translucent blocks +- fix: sampler arrays inbdexed with non-constant / uniform-based expressions are forbidden +- fix initial weather intensity +- fix drop count (560) +- fix BasicParser::parseNumber() out of range (560) +- fix rotation interpolation (#557) diff --git a/dev/tests/network_http.lua b/dev/tests/network_http.lua new file mode 100644 index 00000000..6bd9ba79 --- /dev/null +++ b/dev/tests/network_http.lua @@ -0,0 +1,11 @@ +local response_received = false + +network.get("https://api.github.com/repos/MihailRis/VoxelEngine-Cpp/releases/latest", function (s) + print(json.parse(s).name) + response_received = true +end, function (code) + print("repond with code", code) + response_received = true +end) + +app.sleep_until(function () return response_received end, nil, 10) diff --git a/dev/tests/network_tcp.lua b/dev/tests/network_tcp.lua new file mode 100644 index 00000000..eb6b1880 --- /dev/null +++ b/dev/tests/network_tcp.lua @@ -0,0 +1,45 @@ +for i=1,3 do + print(string.format("iteration %s", i + 1)) + local text = "" + local complete = false + + for j=1,100 do + text = text .. math.random(0, 9) + end + + local server = network.tcp_open(7645, function (client) + print("client connected") + start_coroutine(function() + print("client-listener started") + local received_text = "" + while client:is_alive() and #received_text < #text do + local received = client:recv(512) + if received then + received_text = received_text .. utf8.tostring(received) + print(string.format("received %s byte(s) from client", #received)) + end + coroutine.yield() + end + asserts.equals (text, received_text) + complete = true + end, "client-listener") + end) + + network.tcp_connect("localhost", 7645, function (socket) + print("connected to server") + start_coroutine(function() + print("data-sender started") + local ptr = 1 + while ptr <= #text do + local n = math.random(1, 20) + socket:send(string.sub(text, ptr, ptr + n - 1)) + print(string.format("sent %s byte(s) to server", n)) + ptr = ptr + n + end + socket:close() + end, "data-sender") + end) + + app.sleep_until(function () return complete end, nil, 5) + server:close() +end diff --git a/doc/en/block-properties.md b/doc/en/block-properties.md index 60f87722..1244da99 100644 --- a/doc/en/block-properties.md +++ b/doc/en/block-properties.md @@ -297,3 +297,29 @@ Methods are used to manage the overwriting of properties when extending a block ### `property_name@append` Adds elements to the end of the list instead of completely overwriting it. + +## Tags + +Tags allow you to designate general properties of blocks. Names should be formatted as `prefix:tag_name`. +The prefix is ​​optional, but helps avoid unwanted logical collisions. Example: + +```json +{ + "tags": [ + "core:ore", + "base_survival:food", + ] +} +``` + +Block tags can also be added from other packs using the `your_pack:tags.toml` file. Example: + +```toml +"prefix:tag_name" = [ + "random_pack:some_block", + "another_pack:item", +] +"other_prefix:other_tag_name" = [ + # ... +] +`` diff --git a/doc/en/content-packs.md b/doc/en/content-packs.md index c32d1b03..a41f95f9 100644 --- a/doc/en/content-packs.md +++ b/doc/en/content-packs.md @@ -30,6 +30,11 @@ If prefix is not specified, '!' level will be used. Example: '~randutil' - weak dependency 'randutil'. +Dependency version is indicated after '@' symbol and have operators to restrict acceptable versions. +If version is not specified, '\*' (any) version will be used. + +Example: 'randutil@>=1.0' - dependency 'randutil' which requires version 1.0 or newer. + Example: ```json { diff --git a/doc/en/entity-properties.md b/doc/en/entity-properties.md index f9ff2aa0..a6530191 100644 --- a/doc/en/entity-properties.md +++ b/doc/en/entity-properties.md @@ -20,6 +20,22 @@ Example: ] ``` +You can pass values ​​in ARGS from the entity configuration. +They will be passed both when creating a new entity and when loading a saved one. +The `args` list is used for this: + +```json +"components": [ + { + "name": "base:drop", + "args": { + "item": "base:stone.item", + "count": 1 + } + } +] +``` + The components code should be in `scripts/components`. ## Physics diff --git a/doc/en/item-properties.md b/doc/en/item-properties.md index 0240676b..7bef0766 100644 --- a/doc/en/item-properties.md +++ b/doc/en/item-properties.md @@ -18,6 +18,14 @@ Name of the item model. The model will be loaded automatically. Default value is `packid:itemname.model`. If the model is not specified, an automatic one will be generated. +### Caption and Description +`caption` - name of item in inventory +`description` - item description in inventory + +this props allow to use `md` + +*see [Text Styles](/doc/en/text-styles.md)* + ## Behaviour ### *placing-block* @@ -58,3 +66,29 @@ Property status is displayed in the inventory interface. Display method is defin - `number` - number - `relation` - current value to initial value (x/y) - `vbar` - vertical scale (used by default) + +## Tags + +Tags allow you to designate general properties of items. Names should be formatted as `prefix:tag_name`. +The prefix is ​​optional, but helps avoid unwanted logical collisions. Example: + +```json +{ + "tags": [ + "core:fuel", + "base_survival:poison", + ] +} +``` + +Tags can also be added to items from other packs using the `your_pack:tags.toml` file. Example + +```toml +"prefix:tag_name" = [ + "random_pack:item", + "another_pack:some_block", +] +"other_prefix:other_tag_name" = [ + # ... +] +``` diff --git a/doc/en/main-page.md b/doc/en/main-page.md index 2443cd9a..a5e866bf 100644 --- a/doc/en/main-page.md +++ b/doc/en/main-page.md @@ -1,6 +1,6 @@ # Documentation -Documentation for release 0.28. +Documentation for 0.29. ## Sections diff --git a/doc/en/scripting.md b/doc/en/scripting.md index b9661cd3..9b2a0297 100644 --- a/doc/en/scripting.md +++ b/doc/en/scripting.md @@ -10,6 +10,7 @@ Subsections: - [Entities and components](scripting/ecs.md) - [Libraries](#) - [app](scripting/builtins/libapp.md) + - [assets](scripting/builtins/libassets.md) - [base64](scripting/builtins/libbase64.md) - [bjson, json, toml, yaml](scripting/filesystem.md) - [block](scripting/builtins/libblock.md) @@ -20,6 +21,7 @@ Subsections: - [gfx.blockwraps](scripting/builtins/libgfx-blockwraps.md) - [gfx.particles](particles.md#gfxparticles-library) - [gfx.posteffects](scripting/builtins/libgfx-posteffects.md) + - [gfx.skeletons](scripting/builtins/libgfx-skeletons.md) - [gfx.text3d](3d-text.md#gfxtext3d-library) - [gfx.weather](scripting/builtins/libgfx-weather.md) - [gui](scripting/builtins/libgui.md) @@ -30,8 +32,10 @@ Subsections: - [mat4](scripting/builtins/libmat4.md) - [network](scripting/builtins/libnetwork.md) - [pack](scripting/builtins/libpack.md) + - [pathfinding](scripting/builtins/libpathfinding.md) - [player](scripting/builtins/libplayer.md) - [quat](scripting/builtins/libquat.md) + - [random](scripting/builtins/librandom.md) - [rules](scripting/builtins/librules.md) - [time](scripting/builtins/libtime.md) - [utf8](scripting/builtins/libutf8.md) diff --git a/doc/en/scripting/builtins/libapp.md b/doc/en/scripting/builtins/libapp.md index 3a8712c7..69a472ee 100644 --- a/doc/en/scripting/builtins/libapp.md +++ b/doc/en/scripting/builtins/libapp.md @@ -25,9 +25,12 @@ Waits for the specified time in seconds, performing the main engine loop. app.sleep_until( -- function that checks the condition for ending the wait predicate: function() -> bool, - -- the maximum number of engine loop ticks after which + -- maximum number of engine loop ticks after which -- a "max ticks exceed" exception will be thrown - [optional] max_ticks = 1e9 + [optional] max_ticks = 1e9, + -- maximum wait time in seconds. + -- (works with system time, including test mode) + [optional] timeout = 1e9 ) ``` diff --git a/doc/en/scripting/builtins/libassets.md b/doc/en/scripting/builtins/libassets.md new file mode 100644 index 00000000..bd095435 --- /dev/null +++ b/doc/en/scripting/builtins/libassets.md @@ -0,0 +1,28 @@ +# *assets* library + +A library for working with audio/visual assets. + +## Functions + +```lua +-- Loads a texture +assets.load_texture( + -- Array of bytes of an image file + data: table | Bytearray, + -- Texture name after loading + name: str, + -- Image file format (only png is supported) + [optional] + format: str = "png" +) + +-- Parses and loads a 3D model +assets.parse_model( + -- Model file format (xml / vcm) + format: str, + -- Contents of the model file + content: str, + -- Model name after loading + name: str +) +``` diff --git a/doc/en/scripting/builtins/libbase64.md b/doc/en/scripting/builtins/libbase64.md index fdfd467e..3f2ee2a5 100644 --- a/doc/en/scripting/builtins/libbase64.md +++ b/doc/en/scripting/builtins/libbase64.md @@ -8,4 +8,10 @@ base64.encode(bytes: table|ByteArray) -> str -- Decode base64 string to ByteArray or lua table if second argument is set to true base64.decode(base64string: str, [optional]usetable: bool=false) -> table|ByteArray + +-- Encode bytes to urlsafe-base64 string ('-', '_' instead of '+', '/') +base64.encode_urlsafe(bytes: table|ByteArray) -> str + +-- Decodes urlsafe-base64 string to a ByteArray or a table of numbers if the second argument is set to true +base64.decode_urlsafe(base64string: str, [optional]usetable: bool=false) -> table|ByteArray ``` diff --git a/doc/en/scripting/builtins/libblock.md b/doc/en/scripting/builtins/libblock.md index 6d657899..044555c4 100644 --- a/doc/en/scripting/builtins/libblock.md +++ b/doc/en/scripting/builtins/libblock.md @@ -68,6 +68,9 @@ block.get_variant(x: int, y: int, z: int) -> int -- Sets the block variant by index block.set_variant(x: int, y: int, z: int, index: int) -> int + +-- Checks if an block has specified tag +block.has_tag(id: int, tag: str) -> bool ``` ## Rotation @@ -119,13 +122,13 @@ block.seek_origin(x: int, y: int, z: int) -> int, int, int Part of a voxel data used for scripting. Size: 8 bit. -```python +```lua block.get_user_bits(x: int, y: int, z: int, offset: int, bits: int) -> int ``` Get specified bits as an unsigned integer. -```python +```lua block.set_user_bits(x: int, y: int, z: int, offset: int, bits: int, value: int) -> int ``` Set specified bits. @@ -151,6 +154,21 @@ The function returns a table with the results or nil if the ray does not hit any The result will use the destination table instead of creating a new one if the optional argument specified. +## Model + +Block model information. + +```lua +-- returns block model type (block/aabb/custom/...) +block.get_model(id: int) -> str + +-- returns block model name +block.model_name(id: int) -> str + +-- returns array of 6 textures assigned to sides of block +block.get_textures(id: int) -> string table +``` + ## Data fields ```lua diff --git a/doc/en/scripting/builtins/libgfx-skeletons.md b/doc/en/scripting/builtins/libgfx-skeletons.md new file mode 100644 index 00000000..4146b4be --- /dev/null +++ b/doc/en/scripting/builtins/libgfx-skeletons.md @@ -0,0 +1,48 @@ +# gfx.skeletons library + +A library for working with named skeletons, such as 'hand', +used to control the hand and the carried item displayed in first-person view. +The set of functions is similar to the skeleton component of entities. + +The first argument to the function is the name of the skeleton. + +```lua +-- Returns an object wrapper over the skeleton +local skeleton = gfx.skeletons.get(name: str) + +-- Returns the index of the bone by name or nil +skeleton:index(name: str) -> int + +-- Returns the name of the model assigned to the bone with the specified index +skeleton:get_model(index: int) -> str + +-- Reassigns the model of the bone with the specified index +-- Resets to the original if you do not specify a name +skeleton:set_model(index: int, name: str) + +-- Returns the transformation matrix of the bone with the specified index +skeleton:get_matrix(index: int) -> mat4 + +-- Sets the transformation matrix of the bone with the specified index +skeleton:set_matrix(index: int, matrix: mat4) + +-- Returns the texture by key (dynamically assigned textures - '$name') +skeleton:get_texture(key: str) -> str + +-- Assigns a texture by key +skeleton:set_texture(key: str, value: str) + +-- Checks the visibility status of a bone by index +-- or the entire skeleton if index is not specified +skeleton:is_visible([optional] index: int) -> bool + +-- Sets the visibility status of a bone by index +-- or the entire skeleton if index is not specified +skeleton:set_visible([optional] index: int, status: bool) + +-- Returns the color of the entity +skeleton:get_color() -> vec3 + +-- Sets the color of the entity +skeleton:set_color(color: vec3) +``` diff --git a/doc/en/scripting/builtins/libgui.md b/doc/en/scripting/builtins/libgui.md index cf6839db..cbcca4ce 100644 --- a/doc/en/scripting/builtins/libgui.md +++ b/doc/en/scripting/builtins/libgui.md @@ -103,3 +103,9 @@ gui.load_document( ``` Loads a UI document with its script, returns the name of the document if successfully loaded. + +```lua +gui.root: Document +``` + +Root UI document diff --git a/doc/en/scripting/builtins/libhud.md b/doc/en/scripting/builtins/libhud.md index 06766422..58d78302 100644 --- a/doc/en/scripting/builtins/libhud.md +++ b/doc/en/scripting/builtins/libhud.md @@ -65,4 +65,7 @@ hud.is_inventory_open() -> bool -- Sets whether to allow pausing. If false, the pause menu will not pause the game. hud.set_allow_pause(flag: bool) + +-- Function that controls the named skeleton 'hand' (see gfx.skeletons) +hud.hand_controller: function() ``` diff --git a/doc/en/scripting/builtins/libinventory.md b/doc/en/scripting/builtins/libinventory.md index 428761d5..8cf2d7c3 100644 --- a/doc/en/scripting/builtins/libinventory.md +++ b/doc/en/scripting/builtins/libinventory.md @@ -97,6 +97,40 @@ inventory.set(...) inventory.set_all_data(...) ``` for moving is inefficient, use inventory.move or inventory.move_range. +```lua +-- Get item caption +inventory.get_caption( + -- id of inventory + invid: int, + -- slot id + slot: int +) +-- Set item caption +inventory.set_caption( + -- id of inventory + invid: int, + -- slot id + slot: int, + -- Item Caption + caption: string +) +-- Get item description +inventory.get_description( + -- id of inventory + invid: int, + -- slot id + slot: int +) +-- Set item description +inventory.set_description( + -- id of inventory + invid: int, + -- slot id + slot: int, + -- Item Description + description: string +) +``` ```lua -- Returns a copy of value of a local property of an item by name or nil. diff --git a/doc/en/scripting/builtins/libitem.md b/doc/en/scripting/builtins/libitem.md index a2d01632..d8b42f5c 100644 --- a/doc/en/scripting/builtins/libitem.md +++ b/doc/en/scripting/builtins/libitem.md @@ -10,6 +10,9 @@ item.index(name: str) -> int -- Returns the item display name. block.caption(blockid: int) -> str +-- Returns the item display description. +item.description(itemid: int) -> str + -- Returns max stack size for the item item.stack_size(itemid: int) -> int @@ -30,4 +33,7 @@ item.emission(itemid: int) -> str -- Returns the value of the `uses` property item.uses(itemid: int) -> int + +-- Checks if an item has specified tag +item.has_tag(itemid: int, tag: str) -> bool ``` diff --git a/doc/en/scripting/builtins/libnetwork.md b/doc/en/scripting/builtins/libnetwork.md index a93eaefa..0d32d849 100644 --- a/doc/en/scripting/builtins/libnetwork.md +++ b/doc/en/scripting/builtins/libnetwork.md @@ -6,9 +6,15 @@ A library for working with the network. ```lua -- Performs a GET request to the specified URL. --- After receiving the response, passes the text to the callback function. --- In case of an error, the HTTP response code will be passed to onfailure. -network.get(url: str, callback: function(str), [optional] onfailure: function(int)) +network.get( + url: str, + -- Function to call when response is received + callback: function(str), + -- Error handler + [optional] onfailure: function(int, str), + -- List of additional request headers + [optional] headers: table +) -- Example: network.get("https://api.github.com/repos/MihailRis/VoxelEngine-Cpp/releases/latest", function (s) @@ -16,13 +22,28 @@ network.get("https://api.github.com/repos/MihailRis/VoxelEngine-Cpp/releases/lat end) -- A variant for binary files, with a byte array instead of a string in the response. -network.get_binary(url: str, callback: function(table|ByteArray), [optional] onfailure: function(int)) +network.get_binary( + url: str, + callback: function(ByteArray), + [optional] onfailure: function(int, str), + [optional] headers: table +) -- Performs a POST request to the specified URL. -- Currently, only `Content-Type: application/json` is supported -- After receiving the response, passes the text to the callback function. -- In case of an error, the HTTP response code will be passed to onfailure. -network.post(url: str, data: table, callback: function(str), [optional] onfailure: function(int)) +network.post( + url: str, + -- Request body as a table (will be converted to JSON) or string + body: table|str, + -- Function called when response is received + callback: function(str), + -- Error handler + [optional] onfailure: function(int, str), + -- List of additional request headers + [optional] headers: table +) ``` ## TCP Connections diff --git a/doc/en/scripting/builtins/libpathfinding.md b/doc/en/scripting/builtins/libpathfinding.md new file mode 100644 index 00000000..19562177 --- /dev/null +++ b/doc/en/scripting/builtins/libpathfinding.md @@ -0,0 +1,66 @@ +# *pathfinding* library + +The *pathfinding* library provides functions for working with the pathfinding system in the game world. It allows you to create and manage agents finding routes between points in the world. + +When used in entity logic, the `core:pathfinding` component should be used. + +## `core:pathfinding` component + +```lua +local pf = entity:get_component("core:pathfinding") + +--- ... +local x = ... +local y = ... +local z = ... + +--- Set the target for the agent +pf.set_target({x, y, z}) + +--- Get the current target of the agent +local target = pf.get_target() --> vec3 or nil +--- ... + +--- Get the current route of the agent +local route = pf.get_route() --> table or nil +--- ... +``` + +## Library functions + +```lua +--- Create a new agent. Returns the ID of the created agent +local agent = pathfinding.create_agent() --> int + +--- Delete an agent by ID. Returns true if the agent existed, otherwise false +pathfinding.remove_agent(agent: int) --> bool + +--- Set the agent state (enabled/disabled) +pathfinding.set_enabled(agent: int, enabled: bool) + +--- Check the agent state. Returns true if the agent is enabled, otherwise false +pathfinding.is_enabled(agent: int) --> bool + +--- Create a route based on the given points. Returns an array of route points +pathfinding.make_route(start: vec3, target: vec3) --> table + +--- Asynchronously create a route based on the given points. +--- This function allows to perform pathfinding in the background without blocking the main thread of execution +pathfinding.make_route_async(agent: int, start: vec3, target: vec3) + +--- Get the route that the agent has already found. Used to get the route after an asynchronous search. +--- If the search has not yet completed, returns nil. If the route is not found, returns an empty table. +pathfinding.pull_route(agent: int) --> table or nil + +--- Set the maximum number of visited blocks for the agent. Used to limit the amount of work of the pathfinding algorithm. +pathfinding.set_max_visited(agent: int, max_visited: int) + +--- Adding an avoided blocks tag +pathfinding.avoid_tag( + agent: int, + -- tag for avoided blocks + tag: string, [optional], + -- cost of crossing a block + cost: int = 10 +) +``` diff --git a/doc/en/scripting/builtins/librandom.md b/doc/en/scripting/builtins/librandom.md new file mode 100644 index 00000000..0ae52155 --- /dev/null +++ b/doc/en/scripting/builtins/librandom.md @@ -0,0 +1,38 @@ +# *random* library + +A library of functions for generating random numbers. + +## Non-deterministic numbers + +```lua +-- Generates a random number in the range [0..1) +random.random() --> number + +-- Generates a random integer in the range [0..n] +random.random(n) --> number + +-- Generates a random integer in the range [a..b] +random.random(a, b) --> number + +-- Generates a random byte array of length n +random.bytes(n: number) -> Bytearray + +-- Generates a UUID version 4 +random.uuid() -> str +``` + +## Pseudorandom numbers + +The library provides the Random class - a generator with its own isolated state. + +```lua +local rng = random.Random() + +-- Used similarly to math.random +local a = rng:random() --> [0..1) +local b = rng:random(10) --> [0..10] +local c = rng:random(5, 20) --> [5..20] + +-- Sets the generator state to generate a reproducible sequence of random numbers +rng:seed(42) +``` diff --git a/doc/en/scripting/builtins/libvecn.md b/doc/en/scripting/builtins/libvecn.md index 22552eb7..f330b2c4 100644 --- a/doc/en/scripting/builtins/libvecn.md +++ b/doc/en/scripting/builtins/libvecn.md @@ -100,6 +100,13 @@ vecn.length(a: vector) ``` +#### Distance - *vecn.distance(...)* + +```lua +-- returns the distance between two vectors +vecn.distance(a: vector, b: vector) +``` + #### Absolute value - *vecn.abs(...)* ```lua @@ -136,6 +143,16 @@ vecn.pow(v: vector, exponent: number, dst: vector) vecn.dot(a: vector, b: vector) ``` +#### Mixing - *vecn.mix(...)* + +```lua +-- returns vector a * (1.0 - t) + b * t +vecn.mix(a: vector, b: vector, t: number) + +-- writes to dst vector a * (1.0 - t) + b * t +vecn.mix(a: vector, b: vector, t: number, dst: vector) +``` + #### Convert to string - *vecn.tostring(...)* > [!WARNING] > Returns only if the content is a vector @@ -160,6 +177,12 @@ vec2.angle(v: vec2) -- returns the direction angle of the vector {x, y} in degrees [0, 360] vec2.angle(x: number, y: number) + +-- returns the vector rotated by an angle in degrees counterclockwise +vec2.rotate(v: vec2, angle: number) -> vec2 + +-- writes the vector rotated by an angle in degrees counterclockwise to dst +vec2.rotate(v: vec2, angle: number, dst: vec2) -> vec2 ``` @@ -188,6 +211,10 @@ print("mul: " .. vec3.tostring(result_mul)) -- {10, 40, 80} local result_mul_scal = vec3.mul(v1_3d, scal) print("mul_scal: " .. vec3.tostring(result_mul_scal)) -- {6, 12, 12} +-- calculating distance between vectors +local result_distance = vec3.distance(v1_3d, v2_3d) +print("distance: " .. result_distance) -- 43 + -- vector normalization local result_norm = vec3.normalize(v1_3d) print("norm: " .. vec3.tostring(result_norm)) -- {0.333, 0.667, 0.667} @@ -211,3 +238,7 @@ print("pow: " .. vec3.tostring(result_pow)) -- {1, 4, 4} -- scalar product of vectors local result_dot = vec3.dot(v1_3d, v2_3d) print("dot: " ..result_dot) -- 250 + +-- mixing vectors +local result_mix = vec3.mix(v1_3d, v2_3d, 0.25) +print("mix: " .. vec3.tostring(result_mix)) -- {3.25, 6.5, 11.5} diff --git a/doc/en/scripting/ecs.md b/doc/en/scripting/ecs.md index f0a33a97..e72723af 100644 --- a/doc/en/scripting/ecs.md +++ b/doc/en/scripting/ecs.md @@ -26,6 +26,8 @@ entity:get_uid() -> int entity:get_component(name: str) -> component or nil -- Checks for the presence of a component by name entity:has_component(name: str) -> bool +-- Retrieves a component by name. Throws an exception if it does not exist +entity:require_component(name: str) -> component -- Enables/disables the component entity:set_enabled(name: str, enable: bool) @@ -93,10 +95,12 @@ body:get_linear_damping() -> number -- Sets the linear velocity attenuation multiplier body:set_linear_damping(value: number) --- Checks if vertical velocity attenuation is enabled +-- Checks if vertical damping is enabled body:is_vdamping() -> bool --- Enables/disables vertical velocity attenuation -body:set_vdamping(enabled: bool) +-- Returns the vertical damping multiplier +body:get_vdamping() -> number +-- Enables/disables vertical damping / sets vertical damping multiplier +body:set_vdamping(enabled: bool | number) -- Checks if the entity is on the ground body:is_grounded() -> bool @@ -188,6 +192,12 @@ function on_update(tps: int) Called every entities tick (currently 20 times per second). +```lua +function on_physics_update(delta: number) +``` + +Called after each physics step + ```lua function on_render(delta: number) ``` diff --git a/doc/en/scripting/events.md b/doc/en/scripting/events.md index 21e1cee5..712dcb8c 100644 --- a/doc/en/scripting/events.md +++ b/doc/en/scripting/events.md @@ -46,6 +46,13 @@ function on_blocks_tick(tps: int) Called tps (20) times per second. Use 1/tps instead of `time.delta()`. +```lua +function on_block_tick(x, y, z, tps: number) +``` + +Called tps (20 / tick-interval) times per second for a block. +Use 1/tps instead of `time.delta()`. + ```lua function on_player_tick(playerid: int, tps: int) ``` diff --git a/doc/ru/block-properties.md b/doc/ru/block-properties.md index 601a0dba..0003cb92 100644 --- a/doc/ru/block-properties.md +++ b/doc/ru/block-properties.md @@ -306,3 +306,29 @@ ### `имя_свойства@append` Добавляет элементы в конец списка, вместо его полной перезаписи. + +## Теги - *tags* + +Теги позволяют обозначать обобщённые свойства блоков. Названия следует формировать как `префикс:имя_тега`. +Префикс не является обязательным, но позволяет избегать нежелательных логических коллизий. Пример: + +```json +{ + "tags": [ + "core:ore", + "base_survival:food", + ] +} +``` + +Теги блокам можно добавлять и из других паков, с помощью файла `ваш_пак:tags.toml`. Пример + +```toml +"префикс:имя_тега" = [ + "рандомный_пак:какой_то_блок", + "ещё_один_пак:предмет", +] +"другой_префикс:другое_имя_тега" = [ + # ... +] +``` diff --git a/doc/ru/content-packs.md b/doc/ru/content-packs.md index 77a78636..4245555f 100644 --- a/doc/ru/content-packs.md +++ b/doc/ru/content-packs.md @@ -32,6 +32,11 @@ Пример: '~randutil' - слабая зависимость 'randutil'. +Версии зависимостей указываются после '@' и имеют операторы для ограничения допустимых версий. +Отсутствие версии зависимости интерпретируется как '\*', т.е. любая версия. + +Пример: 'randutil@>=1.0' - зависимость 'randutil' версии 1.0 и старше. + Пример: ```json { diff --git a/doc/ru/entity-properties.md b/doc/ru/entity-properties.md index ec11306c..3489e3aa 100644 --- a/doc/ru/entity-properties.md +++ b/doc/ru/entity-properties.md @@ -20,6 +20,22 @@ ] ``` +Из конфигурации сущности можно передавать значения в ARGS. +Они будут передаваться как при создании новой сущности, так и при загрузке сохранённой. +Для этого используется список `args`: + +```json +"components": [ + { + "name": "base:drop", + "args": { + "item": "base:stone.item", + "count": 1 + } + } +] +``` + Код компонентов должен находиться в `scripts/components`. ## Физика diff --git a/doc/ru/item-properties.md b/doc/ru/item-properties.md index 68803296..cb45827a 100644 --- a/doc/ru/item-properties.md +++ b/doc/ru/item-properties.md @@ -17,6 +17,14 @@ Значение по-умолчанию - `packid:itemname.model`. Если модель не указана, будет сгенерирована автоматическию +### Имя и Описание +`caption` - имя предмета в инвентаре +`description` - описание предмета в инвентаре + +Можно использовать `md` + +*см. [Text Styles](/doc/en/text-styles.md)* + ## Поведение ### Устанавливаемый блок - `placing-block` @@ -57,3 +65,30 @@ - `number` - число - `relation` - отношение текущего значения к изначальному (x/y) - `vbar` - вертикальная шкала (используется по-умолчанию) + + +## Теги - *tags* + +Теги позволяют обозначать обобщённые свойства предметов. Названия следует формировать как `префикс:имя_тега`. +Префикс не является обязательным, но позволяет избегать нежелательных логических коллизий. Пример: + +```json +{ + "tags": [ + "core:fuel", + "base_survival:poison", + ] +} +``` + +Теги предметам можно добавлять и из других паков, с помощью файла `ваш_пак:tags.toml`. Пример + +```toml +"префикс:имя_тега" = [ + "рандомный_пак:предмет", + "ещё_один_пак:какой_то_блок", +] +"другой_префикс:другое_имя_тега" = [ + # ... +] +``` diff --git a/doc/ru/main-page.md b/doc/ru/main-page.md index eda864e8..c98d893d 100644 --- a/doc/ru/main-page.md +++ b/doc/ru/main-page.md @@ -1,6 +1,6 @@ # Документация -Документация версии 0.28. +Документация версии 0.29. ## Разделы diff --git a/doc/ru/scripting.md b/doc/ru/scripting.md index 6fb86dce..d8f1b3be 100644 --- a/doc/ru/scripting.md +++ b/doc/ru/scripting.md @@ -10,6 +10,7 @@ - [Сущности и компоненты](scripting/ecs.md) - [Библиотеки](#) - [app](scripting/builtins/libapp.md) + - [assets](scripting/builtins/libassets.md) - [base64](scripting/builtins/libbase64.md) - [bjson, json, toml, yaml](scripting/filesystem.md) - [block](scripting/builtins/libblock.md) @@ -20,6 +21,7 @@ - [gfx.blockwraps](scripting/builtins/libgfx-blockwraps.md) - [gfx.particles](particles.md#библиотека-gfxparticles) - [gfx.posteffects](scripting/builtins/libgfx-posteffects.md) + - [gfx.skeletons](scripting/builtins/libgfx-skeletons.md) - [gfx.text3d](3d-text.md#библиотека-gfxtext3d) - [gfx.weather](scripting/builtins/libgfx-weather.md) - [gui](scripting/builtins/libgui.md) @@ -30,8 +32,10 @@ - [mat4](scripting/builtins/libmat4.md) - [network](scripting/builtins/libnetwork.md) - [pack](scripting/builtins/libpack.md) + - [pathfinding](scripting/builtins/libpathfinding.md) - [player](scripting/builtins/libplayer.md) - [quat](scripting/builtins/libquat.md) + - [random](scripting/builtins/librandom.md) - [rules](scripting/builtins/librules.md) - [time](scripting/builtins/libtime.md) - [utf8](scripting/builtins/libutf8.md) diff --git a/doc/ru/scripting/builtins/libapp.md b/doc/ru/scripting/builtins/libapp.md index eb504d25..4e9493c4 100644 --- a/doc/ru/scripting/builtins/libapp.md +++ b/doc/ru/scripting/builtins/libapp.md @@ -27,7 +27,10 @@ app.sleep_until( predicate: function() -> bool, -- максимальное количество тактов цикла движка, после истечения которых -- будет брошено исключение "max ticks exceed" - [опционально] max_ticks = 1e9 + [опционально] max_ticks = 1e9, + -- максимальное длительность ожидания в секундах. + -- (работает с системным временем, включая test-режим) + [опционально] timeout = 1e9 ) ``` diff --git a/doc/ru/scripting/builtins/libassets.md b/doc/ru/scripting/builtins/libassets.md new file mode 100644 index 00000000..2c510948 --- /dev/null +++ b/doc/ru/scripting/builtins/libassets.md @@ -0,0 +1,28 @@ +# Библиотека *assets* + +Библиотека для работы с аудио/визуальными загружаемыми ресурсами. + +## Функции + +```lua +-- Загружает текстуру +assets.load_texture( + -- Массив байт файла изображения + data: table | Bytearray, + -- Имя текстуры после загрузки + name: str, + -- Формат файла изображения (поддерживается только png) + [опционально] + format: str = "png" +) + +-- Парсит и загружает 3D модель +assets.parse_model( + -- Формат файла модели (xml / vcm) + format: str, + -- Содержимое файла модели + content: str, + -- Имя модели после загрузки + name: str +) +``` diff --git a/doc/ru/scripting/builtins/libbase64.md b/doc/ru/scripting/builtins/libbase64.md index 7078fa0b..33bac975 100644 --- a/doc/ru/scripting/builtins/libbase64.md +++ b/doc/ru/scripting/builtins/libbase64.md @@ -8,4 +8,10 @@ base64.encode(bytes: table|ByteArray) -> str -- Декодирует base64 строку в ByteArray или таблицу чисел, если второй аргумент установлен на true base64.decode(base64string: str, [опционально]usetable: bool=false) -> table|ByteArray + +-- Кодирует массив байт в urlsafe-base64 строку ('-', '_' вместо '+', '/') +base64.encode_urlsafe(bytes: table|ByteArray) -> str + +-- Декодирует urlsafe-base64 строку в ByteArray или таблицу чисел, если второй аргумент установлен на true +base64.decode_urlsafe(base64string: str, [опционально]usetable: bool=false) -> table|ByteArray ``` diff --git a/doc/ru/scripting/builtins/libblock.md b/doc/ru/scripting/builtins/libblock.md index e5bdbb44..1466d1c8 100644 --- a/doc/ru/scripting/builtins/libblock.md +++ b/doc/ru/scripting/builtins/libblock.md @@ -67,6 +67,9 @@ block.get_variant(x: int, y: int, z: int) -> int -- Устанавливает вариант блока по индексу block.set_variant(x: int, y: int, z: int, index: int) -> int + +-- Проверяет наличие тега у блока +block.has_tag(id: int, tag: str) -> bool ``` ### Raycast @@ -171,6 +174,9 @@ block.get_hitbox(id: int, rotation_index: int) -> {vec3, vec3} -- возвращает тип модели блока (block/aabb/custom/...) block.get_model(id: int) -> str +-- возвращает имя модели блока +block.model_name(id: int) -> str + -- возвращает массив из 6 текстур, назначенных на стороны блока block.get_textures(id: int) -> таблица строк ``` diff --git a/doc/ru/scripting/builtins/libfile.md b/doc/ru/scripting/builtins/libfile.md index 6f35306d..e06e420c 100644 --- a/doc/ru/scripting/builtins/libfile.md +++ b/doc/ru/scripting/builtins/libfile.md @@ -183,3 +183,26 @@ file.join(директория: str, путь: str) --> str Соединяет путь. Пример: `file.join("world:data", "base/config.toml)` -> `world:data/base/config.toml`. Следует использовать данную функцию вместо конкатенации с `/`, так как `префикс:/путь` не является валидным. + +```lua +file.open(путь: str, режим: str) --> io_stream +``` + +Открывает поток для записи/чтения в файл по пути `путь`. + +Аргумент `режим` это список отдельных режимов, в котором каждый обозначается одним символом + +`r` - Чтение из файла +`w` - Запись в файл +`b` - Открыть поток в двоичном режиме (см. `../io_stream.md`) +`+` - Работает совместно с `w`. Добавляет к существующим данным новые (`append-mode`) + +```lua +file.open_named_pipe(имя: str, режим: str) -> io_stream +``` + +Открывает поток для записи/чтения в Named Pipe по пути `путь` + +`/tmp/` или `\\\\.\\pipe\\` добавлять не нужно - движок делает это автоматически. + +Доступные режимы такие же, как и в `file.open`, за исключением `+` \ No newline at end of file diff --git a/doc/ru/scripting/builtins/libgfx-skeletons.md b/doc/ru/scripting/builtins/libgfx-skeletons.md new file mode 100644 index 00000000..0e80b8f9 --- /dev/null +++ b/doc/ru/scripting/builtins/libgfx-skeletons.md @@ -0,0 +1,49 @@ +# Библиотека gfx.skeletons + +Библиотека для работы с именованными скелетами, такими как 'hand', +использующийся для управления, отображаемыми при виде от первого лица, +рукой и переносимым предметом. Набор функций аналогичен компоненту skeleton +у сущностей. + +Первым аргументом в функции передаётся имя скелета. + +```lua +-- Возвращает объектную обёртку над скелетом +local skeleton = gfx.skeletons.get(name: str) + +-- Возвращает индекс кости по имени или nil +skeleton:index(name: str) -> int + +-- Возвращает имя модели, назначенной на кость с указанным индексом +skeleton:get_model(index: int) -> str + +-- Переназначает модель кости с указанным индексом +-- Сбрасывает до изначальной, если не указывать имя +skeleton:set_model(index: int, name: str) + +-- Возвращает матрицу трансформации кости с указанным индексом +skeleton:get_matrix(index: int) -> mat4 + +-- Устанавливает матрицу трансформации кости с указанным индексом +skeleton:set_matrix(index: int, matrix: mat4) + +-- Возвращает текстуру по ключу (динамически назначаемые текстуры - '$имя') +skeleton:get_texture(key: str) -> str + +-- Назначает текстуру по ключу +skeleton:set_texture(key: str, value: str) + +-- Проверяет статус видимости кости по индесу +-- или всего скелета, если индекс не указан +skeleton:is_visible([опционально] index: int) -> bool + +-- Устанавливает статус видимости кости по индексу +-- или всего скелета, если индекс не указан +skeleton:set_visible([опционально] index: int, status: bool) + +-- Возвращает цвет сущности +skeleton:get_color() -> vec3 + +-- Устанавливает цвет сущности +skeleton:set_color(color: vec3) +``` diff --git a/doc/ru/scripting/builtins/libgui.md b/doc/ru/scripting/builtins/libgui.md index aea2f44a..a9bffe48 100644 --- a/doc/ru/scripting/builtins/libgui.md +++ b/doc/ru/scripting/builtins/libgui.md @@ -100,3 +100,9 @@ gui.load_document( ``` Загружает UI документ с его скриптом, возвращает имя документа, если успешно загружен. + +```lua +gui.root: Document +``` + +Корневой UI документ diff --git a/doc/ru/scripting/builtins/libhud.md b/doc/ru/scripting/builtins/libhud.md index 311542cb..5f0856b3 100644 --- a/doc/ru/scripting/builtins/libhud.md +++ b/doc/ru/scripting/builtins/libhud.md @@ -68,4 +68,7 @@ hud.is_inventory_open() -> bool -- Устанавливает разрешение на паузу. При значении false меню паузы не приостанавливает игру. hud.set_allow_pause(flag: bool) + +-- Функция, управляющая именованным скелетом 'hand' (см. gfx.skeletons) +hud.hand_controller: function() ``` diff --git a/doc/ru/scripting/builtins/libinventory.md b/doc/ru/scripting/builtins/libinventory.md index 7e939ddd..97fa56ff 100644 --- a/doc/ru/scripting/builtins/libinventory.md +++ b/doc/ru/scripting/builtins/libinventory.md @@ -94,6 +94,40 @@ inventory.set(...) inventory.set_all_data(...) ``` для перемещения вляется неэффективным, используйте inventory.move или inventory.move_range. +```lua +-- Получает имя предмета в слоте +inventory.get_caption( + -- id инвентаря + invid: int, + -- индекс слота + slot: int +) +-- Задает имя предмету в слоте +inventory.set_caption( + -- id инвентаря + invid: int, + -- индекс слота + slot: int, + -- Имя предмета + caption: string +) +-- Получает описание предмета в слоте +inventory.get_description( + -- id инвентаря + invid: int, + -- индекс слота + slot: int +) +-- Задает описание предмету в слоте +inventory.set_description( + -- id инвентаря + invid: int, + -- индекс слота + slot: int, + -- Описание предмета + description: string +) +``` ```lua -- Проверяет наличие локального свойства по имени без копирования его значения. diff --git a/doc/ru/scripting/builtins/libitem.md b/doc/ru/scripting/builtins/libitem.md index 5baaed68..2c312dc4 100644 --- a/doc/ru/scripting/builtins/libitem.md +++ b/doc/ru/scripting/builtins/libitem.md @@ -10,6 +10,9 @@ item.index(name: str) -> int -- Возвращает название предмета, отображаемое в интерфейсе. item.caption(itemid: int) -> str +-- Возвращает описание предмета, отображаемое в интерфейсе. +item.description(itemid: int) -> str + -- Возвращает максимальный размер стопки для предмета. item.stack_size(itemid: int) -> int @@ -30,6 +33,9 @@ item.emission(itemid: int) -> str -- Возвращает значение свойства `uses` item.uses(itemid: int) -> int + +-- Проверяет наличие тега у предмета +item.has_tag(itemid: int, tag: str) -> bool ``` diff --git a/doc/ru/scripting/builtins/libnetwork.md b/doc/ru/scripting/builtins/libnetwork.md index 0d33acf1..c880240c 100644 --- a/doc/ru/scripting/builtins/libnetwork.md +++ b/doc/ru/scripting/builtins/libnetwork.md @@ -2,13 +2,19 @@ Библиотека для работы с сетью. -## HTTP-запросы +## HTTP-Запросы ```lua -- Выполняет GET запрос к указанному URL. --- После получения ответа, передаёт текст в функцию callback. --- В случае ошибки в onfailure будет передан HTTP-код ответа. -network.get(url: str, callback: function(str), [опционально] onfailure: function(int)) +network.get( + url: str, + -- Функция, вызываемая при получении ответа + callback: function(str), + -- Обработчик ошибок + [опционально] onfailure: function(int, str), + -- Список дополнительных заголовков запроса + [опционально] headers: table +) -- Пример: network.get("https://api.github.com/repos/MihailRis/VoxelEngine-Cpp/releases/latest", function (s) @@ -16,13 +22,28 @@ network.get("https://api.github.com/repos/MihailRis/VoxelEngine-Cpp/releases/lat end) -- Вариант для двоичных файлов, с массивом байт вместо строки в ответе. -network.get_binary(url: str, callback: function(table|ByteArray), [опционально] onfailure: function(int)) +network.get_binary( + url: str, + callback: function(ByteArray), + [опционально] onfailure: function(int, Bytearray), + [опционально] headers: table +) -- Выполняет POST запрос к указанному URL. -- На данный момент реализована поддержка только `Content-Type: application/json` -- После получения ответа, передаёт текст в функцию callback. -- В случае ошибки в onfailure будет передан HTTP-код ответа. -network.post(url: str, data: table, callback: function(str), [опционально] onfailure: function(int)) +network.post( + url: str, + -- Тело запроса в виде таблицы, конвертируемой в JSON или строки + body: table|str, + -- Функция, вызываемая при получении ответа + callback: function(str), + -- Обработчик ошибок + [опционально] onfailure: function(int, str), + -- Список дополнительных заголовков запроса + [опционально] headers: table +) ``` ## TCP-Соединения @@ -98,6 +119,65 @@ server:is_open() --> bool server:get_port() --> int ``` +## UDP-Датаграммы + +```lua +network.udp_connect( + address: str, + port: int, + -- Функция, вызываемая при получении датаграммы с указанного при открытии сокета адреса и порта + datagramHandler: function(Bytearray), + -- Функция, вызываемая после открытия сокета + -- Опциональна, так как в UDP нет handshake + [опционально] openCallback: function(WriteableSocket), +) --> WriteableSocket +``` + +Открывает UDP-сокет с привязкой к удалённому адресу и порту + +Класс WriteableSocket имеет следующие методы: + +```lua +-- Отправляет датаграмму на адрес и порт, заданные при открытии сокета +socket:send(table|Bytearray|str) + +-- Закрывает сокет +socket:close() + +-- Проверяет открыт ли сокет +socket:is_open() --> bool + +-- Возвращает адрес и порт, на которые привязан сокет +socket:get_address() --> str, int +``` + +```lua +network.udp_open( + port: int, + -- Функция, вызываемая при получении датаграмы + -- В параметры передаётся адрес и порт отправителя, а также сами данные + datagramHandler: function(address: str, port: int, data: Bytearray, server: DatagramServerSocket) +) --> DatagramServerSocket +``` + +Открывает UDP-сервер на указанном порту + +Класс DatagramServerSocket имеет следующие методы: + +```lua +-- Отправляет датаграмму на переданный адрес и порт +server:send(address: str, port: int, data: table|Bytearray|str) + +-- Завершает принятие датаграмм +server:stop() + +-- Проверяет возможность принятия датаграмм +server:is_open() --> bool + +-- Возвращает порт, который слушает сервер +server:get_port() --> int +``` + ## Аналитика ```lua diff --git a/doc/ru/scripting/builtins/libpathfinding.md b/doc/ru/scripting/builtins/libpathfinding.md new file mode 100644 index 00000000..fe31c18e --- /dev/null +++ b/doc/ru/scripting/builtins/libpathfinding.md @@ -0,0 +1,66 @@ +# Библиотека *pathfinding* + +Библиотека *pathfinding* предоставляет функции для работы с системой поиска пути в игровом мире. Она позволяет создавать и управлять агентами, которые могут находить маршруты между точками в мире. + +При использовании в логике сущностей следует использовать компонент `core:pathfinding`. + +## Компонент `core:pathfinding` + +```lua +local pf = entity:get_component("core:pathfinding") + +--- ... +local x = ... +local y = ... +local z = ... + +--- Установка цели для агента +pf.set_target({x, y, z}) + +--- Получение текущей цели агента +local target = pf.get_target() --> vec3 или nil +--- ... + +--- Получение текущего маршрута агента +local route = pf.get_route() --> table или nil +--- ... +``` + +## Функции библиотеки + +```lua +--- Создание нового агента. Возвращает идентификатор созданного агента +local agent = pathfinding.create_agent() --> int + +--- Удаление агента по идентификатору. Возвращает true, если агент существовал, иначе false +pathfinding.remove_agent(agent: int) --> bool + +--- Установка состояния агента (включен/выключен) +pathfinding.set_enabled(agent: int, enabled: bool) + +--- Проверка состояния агента. Возвращает true, если агент включен, иначе false +pathfinding.is_enabled(agent: int) --> bool + +--- Создание маршрута на основе заданных точек. Возвращает массив точек маршрута +pathfinding.make_route(start: vec3, target: vec3) --> table + +--- Асинхронное создание маршрута на основе заданных точек. +--- Функция позволяет выполнять поиск пути в фоновом режиме, не блокируя основной поток выполнения +pathfinding.make_route_async(agent: int, start: vec3, target: vec3) + +--- Получение маршрута, который агент уже нашел. Используется для получения маршрута после асинхронного поиска. +--- Если поиск ещё не завершён, возвращает nil. Если маршрут не найден, возвращает пустую таблицу. +pathfinding.pull_route(agent: int) --> table или nil + +--- Установка максимального количества посещенных блоков для агента. Используется для ограничения объема работы алгоритма поиска пути. +pathfinding.set_max_visited(agent: int, max_visited: int) + +--- Добавление тега избегаемых блоков +pathfinding.avoid_tag( + agent: int, + -- тег избегаемых блоков + tag: string, [опционально], + -- стоимость пересечения блока + cost: int = 10 +) +``` diff --git a/doc/ru/scripting/builtins/librandom.md b/doc/ru/scripting/builtins/librandom.md new file mode 100644 index 00000000..8aa58592 --- /dev/null +++ b/doc/ru/scripting/builtins/librandom.md @@ -0,0 +1,38 @@ +# Библиотека *random* + +Библиотека функций для генерации случайный чисел. + +## Недетерминированные числа + +```lua +-- Генерирует случайное число в диапазоне [0..1) +random.random() --> number + +-- Генерирует случайное целое число в диапазоне [0..n] +random.random(n) --> number + +-- Генерирует случайное целое число в диапазоне [a..b] +random.random(a, b) --> number + +-- Генерирует случайный массив байт длиной n +random.bytes(n: number) -> Bytearray + +-- Генерирует UUID версии 4 +random.uuid() -> str +``` + +## Псевдослучайные числа + +Библиотека предоставляет класс Random - генератор с собственным изолированным состоянием. + +```lua +local rng = random.Random() + +-- Используется аналогично math.random +local a = rng:random() --> [0..1) +local b = rng:random(10) --> [0..10] +local c = rng:random(5, 20) --> [5..20] + +-- Устанавливает состояние генератора для генерации воспроизводимой последовательности случайных чисел +rng:seed(42) +``` diff --git a/doc/ru/scripting/builtins/libtime.md b/doc/ru/scripting/builtins/libtime.md index d62a7af2..5bf9ad01 100644 --- a/doc/ru/scripting/builtins/libtime.md +++ b/doc/ru/scripting/builtins/libtime.md @@ -11,3 +11,21 @@ time.delta() -> float ``` Возвращает дельту времени (время прошедшее с предыдущего кадра) + +```python +time.utc_time() -> int +``` + +Возвращает время UTC в секундах + +```python +time.local_time() -> int +``` + +Возвращает локальное (системное) время в секундах + +```python +time.utc_offset() -> int +``` + +Возвращает смещение локального времени от UTC в секундах \ No newline at end of file diff --git a/doc/ru/scripting/builtins/libvecn.md b/doc/ru/scripting/builtins/libvecn.md index 44cfa994..b3791bad 100644 --- a/doc/ru/scripting/builtins/libvecn.md +++ b/doc/ru/scripting/builtins/libvecn.md @@ -100,6 +100,13 @@ vecn.length(a: vector) ``` +#### Дистанция - *vecn.distance(...)* + +```lua +-- возвращает расстояние между двумя векторами +vecn.distance(a: vector, b: vector) +``` + #### Абсолютное значение - *vecn.abs(...)* ```lua @@ -136,6 +143,16 @@ vecn.pow(v: vector, exponent: number, dst: vector) vecn.dot(a: vector, b: vector) ``` +#### Смешивание - *vecn.mix(...)* + +```lua +-- возвращает вектор a * (1.0 - t) + b * t +vecn.mix(a: vector, b: vector, t: number) + +-- записывает в dst вектор a * (1.0 - t) + b * t +vecn.mix(a: vector, b: vector, t: number, dst: vector) +``` + #### Перевод в строку - *vecn.tostring(...)* > [!WARNING] > Возвращает только тогда, когда содержимым является вектор @@ -160,6 +177,12 @@ vec2.angle(v: vec2) -- возвращает угол направления вектора {x, y} в градусах [0, 360] vec2.angle(x: number, y: number) + +-- возвращает повернутый вектор на угол в градусах против часовой стрелки +vec2.rotate(v: vec2, angle: number) -> vec2 + +-- записывает повернутый вектор на угол в градусах против часовой стрелки в dst +vec2.rotate(v: vec2, angle: number, dst: vec2) -> vec2 ``` @@ -192,6 +215,10 @@ print("mul_scal: " .. vec3.tostring(result_mul_scal)) -- {6, 12, 12} local result_norm = vec3.normalize(v1_3d) print("norm: " .. vec3.tostring(result_norm)) -- {0.333, 0.667, 0.667} +-- дистанция между векторами +local result_distance = vec3.distance(v1_3d, v2_3d) +print("distance: " .. result_distance) -- 43 + -- длина вектора local result_len = vec3.length(v1_3d) print("len: " .. result_len) -- 3 @@ -211,4 +238,9 @@ print("pow: " .. vec3.tostring(result_pow)) -- {1, 4, 4} -- скалярное произведение векторов local result_dot = vec3.dot(v1_3d, v2_3d) print("dot: " .. result_dot) -- 250 + +-- смешивание векторов +local result_mix = vec3.mix(v1_3d, v2_3d, 0.25) +print("mix: " .. vec3.tostring(result_mix)) -- {3.25, 6.5, 11.5} + ``` diff --git a/doc/ru/scripting/ecs.md b/doc/ru/scripting/ecs.md index 89af90fa..ea217b00 100644 --- a/doc/ru/scripting/ecs.md +++ b/doc/ru/scripting/ecs.md @@ -26,6 +26,8 @@ entity:get_uid() -> int entity:get_component(name: str) -> компонент или nil -- Проверяет наличие компонента по имени entity:has_component(name: str) -> bool +-- Запрашивает компонент по имени. Бросает исключение при отсутствии +entity:require_component(name: str) -> компонент -- Включает/выключает компонент по имени entity:set_enabled(name: str, enable: bool) @@ -95,8 +97,10 @@ body:set_linear_damping(value: number) -- Проверяет, включено ли вертикальное затухание скорости body:is_vdamping() -> bool --- Включает/выключает вертикальное затухание скорости -body:set_vdamping(enabled: bool) +-- Возвращает множитель вертикального затухания скорости +body:get_vdamping() -> number +-- Включает/выключает вертикальное затухание скорости / устанавливает значение множителя +body:set_vdamping(enabled: bool | number) -- Проверяет, находится ли сущность на земле (приземлена) body:is_grounded() -> bool @@ -188,6 +192,12 @@ function on_update(tps: int) Вызывается каждый такт сущностей (на данный момент - 20 раз в секунду). +```lua +function on_physics_update(delta: number) +``` + +Вызывается после каждого шага физики + ```lua function on_render(delta: number) ``` diff --git a/doc/ru/scripting/events.md b/doc/ru/scripting/events.md index c7781879..4ca3fda5 100644 --- a/doc/ru/scripting/events.md +++ b/doc/ru/scripting/events.md @@ -46,6 +46,13 @@ function on_blocks_tick(tps: int) Вызывается tps (20) раз в секунду. Используйте 1/tps вместо `time.delta()`. +```lua +function on_block_tick(x, y, z, tps: number) +``` + +Вызывается tps (20 / tick-interval) раз в секунду для конкретного блока. +Используйте 1/tps вместо `time.delta()`. + ```lua function on_player_tick(playerid: int, tps: int) ``` diff --git a/doc/ru/scripting/extensions.md b/doc/ru/scripting/extensions.md index 32798e43..7ef835f4 100644 --- a/doc/ru/scripting/extensions.md +++ b/doc/ru/scripting/extensions.md @@ -258,3 +258,16 @@ function sleep(timesec: number) ``` Вызывает остановку корутины до тех пор, пока не пройдёт количество секунд, указанное в **timesec**. Функция может быть использована только внутри корутины. + +```lua +function await(co: coroutine) -> result, error +``` + +Ожидает завершение переданной корутины, возвращая поток управления. Функция может быть использована только внутри корутины. +Возвращает значения аналогичные возвращаемым значениям *pcall*. + +```lua +os.pid -> number +``` + +Константа, в которой хранится PID текущего инстанса движка diff --git a/doc/ru/scripting/io_stream.md b/doc/ru/scripting/io_stream.md new file mode 100644 index 00000000..16a978db --- /dev/null +++ b/doc/ru/scripting/io_stream.md @@ -0,0 +1,188 @@ +# Класс *io_stream* + +Класс, предназначенный для работы с потоками + +## Режимы + +Поток имеет три различных вида режима: + +**general** - Общий режим работы I/O +**binary** - Формат записи и чтения I/O +**flush** - Режим работы flush + +### general + +Имеет три режима: + +**default** - Дефолтный режим работы потока. При read может вернуть только часть от требуемых данных, при write сразу записывает данные в поток. + +**yield** - Почти тоже самое, что и **default**, но всегда будет возвращать все требуемые данные. Пока они не будут прочитаны, будет вызывать `coroutine.yield()`. Предназначен для работы в корутинах. + +**buffered** - Буферизирует записываемые и читаемые данные. + +При вызове `available`/`read` обновляет буфер чтения. + +После обновления в `read`, если буфер чтения переполнен, то бросает ошибку `buffer overflow`. + +Если требуемого кол-ва байт недостаточно в буфере для чтения, то бросает ошибку `buffer-underflow`. + +При вызове `write` записывает итоговые байты в буфер для записи. Если он переполнен, то бросает ошибку `buffer overflow`. + +При вызове `flush` проталкивает данные из буфера для записи в напрямую в поток + +### flush + +**all** - Сначала проталкивает данные из буфера напрямую в поток (если используется **buffered** режим), а после вызывает `flush` напрямую из библиотеки + +**buffer** - Только проталкивает данные из буфера в поток (если используется **buffered** режим) + +## Методы + +Методы, позволяющие изменить или получить различные режимы поведения потока + +```lua +-- Возвращает true, если поток используется в двоичном режиме +io_stream:is_binary_mode() --> bool + +-- Включает или выключает двоичный режим +io_stream:set_binary_mode(bool) + +-- Возвращает режим работы потока +io_stream:get_mode() --> string + +-- Задаёт режим работы потока. Выбрасывает ошибку, если передан неизвестный режим +io_stream:set_mode(string) + +-- Возвращает режим работы flush +io_stream:get_flush_mode() --> string + +-- Задаёт режим работы flush +io_stream:set_flush_mode(string) +``` + +I/O методы + +```lua + +--[[ +Читает данные из потока + +В двоичном режиме: + +Если arg - int, то читает из потока arg байт и возвращает ввиде Bytearray или таблицы, если useTable = true + +Если arg - string, то функция интерпретирует arg как шаблон для byteutil. Прочитает кол-во байт, которое определено шаблоном, передаст их в byteutil.unpack и вернёт результат + + +В текстовом режиме: + +Если arg - int, то читает нужное кол-во строк с окончанием CRLF/LF из arg и возвращает ввиде таблицы. Также, если trimEmptyLines = true, то удаляет пустые строки с начала и конца из итоговой таблицы + +Если arg не определён, то читает одну строку с окончанием CRLF/LF и возвращает её. +--]] +io_stream:read( + [опционально] arg: int | string, + [опционально] useTable | trimEmptyLines: bool +) --> Bytearray | table | string | table | ... + +--[[ +Записывает данные в поток + +В двоичном режиме: + +Если arg - string, то функция интерпретирует arg как шаблон для byteutil, передаст его и ... в byteutil.pack и результат запишет в поток + +Если arg - Bytearray | table, то записывает байты в поток + +В текстовом режиме: + +Если arg - string, то записывает строку в поток (вместе с окончанием LF) + +Если arg - table, то записывает каждую строку из таблицы отдельно +--]] +io_stream:write( + arg: Bytearray | table | string | table, + [опционально] ... +) + +-- Читает одну строку с окончанием CRLF/LF из потока вне зависимости от двоичного режима +io_stream:read_line() --> string + +-- Записывает одну строку с окончанием LF в поток вне зависимости от двоичного режима +io_stream:write_line(string) + +--[[ + +В двоичном режиме: + +Читает все доступные байты из потока и возвращает ввиде Bytearray или table, если useTable = true + +В текстовом режиме: + +Читает все доступные строки из потока в table если useTable = true, или в одну строку вместе с окончаниями, если нет + +--]] +io_stream:read_fully( + [опционально] useTable: bool +) --> Bytearray | table | table | string +``` + +Методы, имеющие смысл в использовании только в buffered режиме + +```lua +--[[ + +Если length определён, то возвращает true, если length байт доступно к чтению. Иначе возвращает false + +Если не определён, то возвращает количество байт, которое можно прочитать + +--]] +io_stream:available( + [опционально] length: int +) --> int | bool + +-- Возвращает максимальный размер буферов +io_stream:get_max_buffer_size() --> int + +-- Задаёт новый максимальный размер буферов +io_stream:set_max_buffer_size(max_size: int) +``` + +Методы, контролирующие состояние потока + +```lua + +-- Возвращает true, если поток открыт на данный момент +io_stream:is_alive() --> bool + +-- Возвращает true, если поток закрыт на данный момент +io_stream:is_closed() --> bool + +-- Закрывает поток +io_stream:close() + +--[[ + +Записывает все данные из write-буфера в поток в buffer/all flush-режимах +Вызывает ioLib.flush() в all flush-режиме + +--]] +io_stream:flush() +``` + +Создание нового потока + +```lua +--[[ + +Создаёт новый поток с переданным дескриптором и использующим переданную I/O библиотеку. (Более подробно в core:io_stream.lua) + +--]] +io_stream.new( + descriptor: int, + binaryMode: bool, + ioLib: table, + [опционально] mode: string = "default", + [опционально] flushMode: string = "all" +) -> io_stream +``` \ No newline at end of file diff --git a/res/content/base/blocks/coal_ore.json b/res/content/base/blocks/coal_ore.json index c73998ec..ae2bd25b 100644 --- a/res/content/base/blocks/coal_ore.json +++ b/res/content/base/blocks/coal_ore.json @@ -1,4 +1,5 @@ { "texture": "coal_ore", + "tags": ["base:ore"], "base:durability": 16.0 } diff --git a/res/content/base/blocks/water.json b/res/content/base/blocks/water.json index f7785044..3bbc014b 100644 --- a/res/content/base/blocks/water.json +++ b/res/content/base/blocks/water.json @@ -7,5 +7,6 @@ "obstacle": false, "selectable": false, "replaceable": true, - "translucent": true + "translucent": true, + "tags": ["core:liquid"] } diff --git a/res/content/base/config/defaults.toml b/res/content/base/config/defaults.toml index a099c198..650c24d7 100644 --- a/res/content/base/config/defaults.toml +++ b/res/content/base/config/defaults.toml @@ -1,2 +1,3 @@ generator = "base:demo" player-entity = "base:player" +hand-skeleton = "base:hand" diff --git a/res/content/base/entities/drop.json b/res/content/base/entities/drop.json index 4c5e064f..951ea5e6 100644 --- a/res/content/base/entities/drop.json +++ b/res/content/base/entities/drop.json @@ -1,6 +1,13 @@ { "components": [ - "base:drop" + { + "name": "base:drop", + "args": { + "item": "base:stone.item", + "count": 1 + } + } + ], "hitbox": [0.4, 0.25, 0.4], "sensors": [ diff --git a/res/content/base/entities/player.json b/res/content/base/entities/player.json index b20a7aed..b6656914 100644 --- a/res/content/base/entities/player.json +++ b/res/content/base/entities/player.json @@ -1,5 +1,12 @@ { "components": [ + { + "name": "core:mob", + "args": { + "jump_force": 8.0 + } + }, + "core:player", "base:player_animator" ], "hitbox": [0.6, 1.8, 0.6] diff --git a/res/content/base/package.json b/res/content/base/package.json index 73b8dcd0..4aeca4d9 100644 --- a/res/content/base/package.json +++ b/res/content/base/package.json @@ -1,6 +1,6 @@ { "id": "base", "title": "Base", - "version": "0.28", + "version": "0.29", "description": "basic content package" } diff --git a/res/content/base/scripts/components/drop.lua b/res/content/base/scripts/components/drop.lua index a979c72d..2b798a38 100644 --- a/res/content/base/scripts/components/drop.lua +++ b/res/content/base/scripts/components/drop.lua @@ -8,6 +8,9 @@ timer = 0.3 local def_index = entity:def_index() dropitem = ARGS +if dropitem.item then + dropitem.id = item.index(dropitem.item) +end if dropitem then timer = dropitem.pickup_delay or timer end diff --git a/res/content/base/scripts/components/player_animator.lua b/res/content/base/scripts/components/player_animator.lua index 78049a2c..f82927cc 100644 --- a/res/content/base/scripts/components/player_animator.lua +++ b/res/content/base/scripts/components/player_animator.lua @@ -1,11 +1,10 @@ local tsf = entity.transform local body = entity.rigidbody local rig = entity.skeleton +local mob = entity:require_component("core:mob") local itemid = 0 -local headIndex = rig:index("head") local itemIndex = rig:index("item") -local bodyIndex = rig:index("body") local function refresh_model(id) itemid = id @@ -18,10 +17,11 @@ function on_render() if pid == -1 then return end - - local rx, ry, rz = player.get_rot(pid, pid ~= hud.get_player()) - rig:set_matrix(headIndex, mat4.rotate({1, 0, 0}, ry)) - rig:set_matrix(bodyIndex, mat4.rotate({0, 1, 0}, rx)) + + local rx, _, _ = player.get_rot(pid, pid ~= hud.get_player()) + + local dir = vec2.rotate({0, -1}, -rx) + mob.set_dir({dir[1], 0, dir[2]}) local invid, slotid = player.get_inventory(pid) local id, _ = inventory.get(invid, slotid) diff --git a/res/content/base/scripts/hud.lua b/res/content/base/scripts/hud.lua index 00f6bdc6..9fc10f2a 100644 --- a/res/content/base/scripts/hud.lua +++ b/res/content/base/scripts/hud.lua @@ -21,6 +21,9 @@ function on_hud_open() local ppos = vec3.add({player.get_pos(pid)}, {0, 0.7, 0}) local throw_force = vec3.mul(player.get_dir(pid), DROP_FORCE) local drop = base_util.drop(ppos, itemid, 1, data, 1.5) + if not drop then + return + end local velocity = vec3.add(throw_force, vec3.add(pvel, DROP_INIT_VEL)) drop.rigidbody:set_vel(velocity) end) diff --git a/res/content/base/scripts/world.lua b/res/content/base/scripts/world.lua index 4b78a3f1..7f468c29 100644 --- a/res/content/base/scripts/world.lua +++ b/res/content/base/scripts/world.lua @@ -1,6 +1,11 @@ function on_block_broken(id, x, y, z, playerid) if gfx then - gfx.particles.emit({x+0.5, y+0.5, z+0.5}, 64, { + local size = {block.get_size(id)} + gfx.particles.emit({ + x + size[1] * 0.5, + y + size[1] * 0.5, + z + size[1] * 0.5 + }, 64, { lifetime=1.0, spawn_interval=0.0001, explosion={4, 4, 4}, @@ -8,7 +13,7 @@ function on_block_broken(id, x, y, z, playerid) random_sub_uv=0.1, size={0.1, 0.1, 0.1}, spawn_shape="box", - spawn_spread={0.4, 0.4, 0.4} + spawn_spread=vec3.mul(size, 0.4) }) end diff --git a/res/content/base/skeletons/hand.json b/res/content/base/skeletons/hand.json new file mode 100644 index 00000000..8ff9d841 --- /dev/null +++ b/res/content/base/skeletons/hand.json @@ -0,0 +1,9 @@ +{ + "root": { + "nodes": [ + { + "name": "item" + } + ] + } +} diff --git a/res/layouts/code_editor.xml.lua b/res/layouts/code_editor.xml.lua index de460f2e..4e6c68d4 100644 --- a/res/layouts/code_editor.xml.lua +++ b/res/layouts/code_editor.xml.lua @@ -81,6 +81,11 @@ local function refresh_file_title() document.saveIcon.enabled = edited document.title.text = gui.str('File')..' - '..current_file.filename ..(edited and ' *' or '') + + local info = registry.get_info(current_file.filename) + if info and info.type == "model" then + pcall(run_current_file) + end end function on_control_combination(keycode) @@ -118,7 +123,6 @@ function run_current_file() local unit = info and info.unit if script_type == "model" then - print(current_file.filename) clear_output() local _, err = pcall(reload_model, current_file.filename, unit) if err then @@ -256,7 +260,7 @@ function open_file_in_editor(filename, line, mutable) end function on_open(mode) - registry = require "core:internal/scripts_registry" + registry = __vc_scripts_registry document.codePanel:setInterval(200, refresh_file_title) diff --git a/res/layouts/console.xml.lua b/res/layouts/console.xml.lua index 94cbda8b..f7e47d9b 100644 --- a/res/layouts/console.xml.lua +++ b/res/layouts/console.xml.lua @@ -4,7 +4,7 @@ history = session.get_entry("commands_history") history_pointer = #history events.on("core:open_traceback", function() - if modes then + if modes and modes.current ~= 'debug' then modes:set('debug') end end) diff --git a/res/layouts/files_panel.xml.lua b/res/layouts/files_panel.xml.lua index a4d6ca39..30b0ddc8 100644 --- a/res/layouts/files_panel.xml.lua +++ b/res/layouts/files_panel.xml.lua @@ -43,11 +43,8 @@ function build_files_list(filenames, highlighted_part) end end -function on_open(mode) - registry = require "core:internal/scripts_registry" - - local files_list = document.filesList - +function on_open() + registry = __vc_scripts_registry filenames = registry.filenames table.sort(filenames) build_files_list(filenames) diff --git a/res/layouts/pages/content.xml.lua b/res/layouts/pages/content.xml.lua index 3f1903f3..8ed37630 100644 --- a/res/layouts/pages/content.xml.lua +++ b/res/layouts/pages/content.xml.lua @@ -176,18 +176,105 @@ function place_pack(panel, packinfo, callback, position_func) end end +local Version = {}; + +function Version.matches_pattern(version) + for _, letter in string.gmatch(version, "%.+") do + if type(letter) ~= "number" or letter ~= "." then + return false; + end + + local t = string.split(version, "."); + + return #t == 2 or #t == 3; + end +end + +function Version.__equal(ver1, ver2) + return ver1[1] == ver2[1] and ver1[2] == ver2[2] and ver1[3] == ver2[3]; +end + +function Version.__more(ver1, ver2) + if ver1[1] ~= ver2[1] then return ver1[1] > ver2[1] end; + if ver1[2] ~= ver2[2] then return ver1[2] > ver2[2] end; + return ver1[3] > ver2[3]; +end + +function Version.__less(ver1, ver2) + return Version.__more(ver2, ver1); +end + +function Version.__more_or_equal(ver1, ver2) + return not Version.__less(ver1, ver2); +end + +function Version.__less_or_equal(ver1, ver2) + return not Version.__more(ver1, ver2); +end + +function Version.compare(op, ver1, ver2) + ver1 = string.split(ver1, "."); + ver2 = string.split(ver2, "."); + + if op == "=" then return Version.__equal(ver1, ver2); + elseif op == ">" then return Version.__more(ver1, ver2); + elseif op == "<" then return Version.__less(ver1, ver2); + elseif op == ">=" then return Version.__more_or_equal(ver1, ver2); + elseif op == "<=" then return Version.__less_or_equal(ver1, ver2); + else return false; end +end + +function Version.parse(version) + local op = string.sub(version, 1, 2); + if op == ">=" or op == "=>" then + return ">=", string.sub(version, #op + 1); + elseif op == "<=" or op == "=<" then + return "<=", string.sub(version, #op + 1); + end + + op = string.sub(version, 1, 1); + if op == ">" or op == "<" then + return op, string.sub(version, #op + 1); + end + + return "=", version; +end + +local function compare_version(dependent_version, actual_version) + if Version.matches_pattern(dependent_version) and Version.matches_pattern(actual_version) then + local op, dep_ver = Version.parse_version(dependent_version); + Version.compare(op, dep_ver, actual_version); + elseif dependent_version == "*" or dependent_version == actual_version then + return true; + else + return false; + end +end + function check_dependencies(packinfo) if packinfo.dependencies == nil then return end for i,dep in ipairs(packinfo.dependencies) do - local depid = dep:sub(2,-1) - if dep:sub(1,1) == '!' then + local depid, depver = unpack(string.split(dep:sub(2,-1), "@")) + + if dep:sub(1,1) == '!' then if not table.has(packs_all, depid) then return string.format( "%s (%s)", gui.str("error.dependency-not-found"), depid ) end + + + local dep_pack = pack.get_info(depid); + + if not compare_version(depver, dep_pack.version) then + local op, ver = Version.parse(depver); + + print(string.format("%s: %s !%s %s (%s)", gui.str("error.dependency-version-not-met"), dep_pack.version, op, ver, depid)); + return string.format("%s: %s != %s (%s)", gui.str("error.dependency-version-not-met"), dep_pack.version, ver, depid); + end + if table.has(packs_installed, packinfo.id) then table.insert(required, depid) end diff --git a/res/layouts/pages/scripts.xml.lua b/res/layouts/pages/scripts.xml.lua index 3529b3d2..dad0d1ff 100644 --- a/res/layouts/pages/scripts.xml.lua +++ b/res/layouts/pages/scripts.xml.lua @@ -13,9 +13,9 @@ end function refresh() document.list:clear() - local available = pack.get_available() - local infos = pack.get_info(available) - for _, name in ipairs(available) do + local allpacks = table.merge(pack.get_available(), pack.get_installed()) + local infos = pack.get_info(allpacks) + for _, name in ipairs(allpacks) do local info = infos[name] local scripts_dir = info.path.."/scripts/app" if not file.exists(scripts_dir) then diff --git a/res/layouts/pages/settings_controls.xml.lua b/res/layouts/pages/settings_controls.xml.lua index ad8491ad..34631929 100644 --- a/res/layouts/pages/settings_controls.xml.lua +++ b/res/layouts/pages/settings_controls.xml.lua @@ -1,3 +1,12 @@ +local WARNING_COLORS = { + {208, 104, 107, 255}, + {250, 75, 139, 255}, + {250, 151, 75, 255}, + {246, 233, 44, 255}, + {252, 200, 149, 255} +} + +local GENERAL_WARNING_COLOR = {208, 138, 0, 255} function refresh_search() local search_text = document.search_textbox.text @@ -40,6 +49,49 @@ function change_sensitivity(val) refresh_sensitivity() end +function refresh_binding_marks() + local prev_bindings = {} + local conflicts_colors = {} + local available_colors = table.copy(WARNING_COLORS) + + local bindings = input.get_bindings() + table.sort(bindings, function(a, b) return a > b end) + + for _, bind_name in ipairs(bindings) do + local key = input.get_binding_text(bind_name) + local prev = prev_bindings[key] + if prev then + local color = GENERAL_WARNING_COLOR + local conflict_color = conflicts_colors[key] + local available_colors_len = #available_colors + if conflict_color then + color = conflict_color + elseif available_colors_len > 0 then + color = available_colors[available_colors_len] + conflicts_colors[key] = color + table.remove(available_colors, available_colors_len) + end + + local tooltip = gui.str("settings.Conflict", "settings") + + local prev_bindmark = "bindmark_" .. prev + local cur_bindmark = "bindmark_" .. bind_name + document[prev_bindmark].visible = true + document[cur_bindmark].visible = true + + document[prev_bindmark].color = color + document[cur_bindmark].color = color + + document["bind_" .. prev].tooltip = tooltip + document["bind_" .. bind_name].tooltip = tooltip + else + document["bindmark_" .. bind_name].visible = false + document["bind_" .. bind_name].tooltip = '' + prev_bindings[key] = bind_name + end + end +end + function on_open() document.sensitivity_track.value = core.get_setting("camera.sensitivity") refresh_sensitivity() @@ -52,4 +104,8 @@ function on_open() id=name, name=gui.str(name) })) end + + document.bindings_panel:setInterval(100, function () + refresh_binding_marks() + end) end diff --git a/res/layouts/templates/binding.xml b/res/layouts/templates/binding.xml index 643bd5aa..92843749 100644 --- a/res/layouts/templates/binding.xml +++ b/res/layouts/templates/binding.xml @@ -1,4 +1,5 @@ - + - + + diff --git a/res/modules/internal/asserts.lua b/res/modules/internal/asserts.lua new file mode 100644 index 00000000..2317bb7c --- /dev/null +++ b/res/modules/internal/asserts.lua @@ -0,0 +1,10 @@ +local this = {} + +function this.equals(expected, fact) + assert(fact == expected, string.format( + "(fact == expected) assertion failed\n Expected: %s\n Fact: %s", + expected, fact + )) +end + +return this diff --git a/res/modules/internal/events.lua b/res/modules/internal/events.lua new file mode 100644 index 00000000..4820ddd5 --- /dev/null +++ b/res/modules/internal/events.lua @@ -0,0 +1,48 @@ +local events = { + handlers = {} +} + +function events.on(event, func) + if events.handlers[event] == nil then + events.handlers[event] = {} + end + table.insert(events.handlers[event], func) +end + +function events.reset(event, func) + if func == nil then + events.handlers[event] = nil + else + events.handlers[event] = {func} + end +end + +function events.remove_by_prefix(prefix) + for name, handlers in pairs(events.handlers) do + local actualname = name + if type(name) == 'table' then + actualname = name[1] + end + if actualname:sub(1, #prefix+1) == prefix..':' then + events.handlers[actualname] = nil + end + end +end + +function events.emit(event, ...) + local result = nil + local handlers = events.handlers[event] + if handlers == nil then + return nil + end + for _, func in ipairs(handlers) do + local status, newres = xpcall(func, __vc__error, ...) + if not status then + debug.error("error in event ("..event..") handler: "..newres) + else + result = result or newres + end + end + return result +end +return events diff --git a/res/modules/internal/maths_inline.lua b/res/modules/internal/maths_inline.lua new file mode 100644 index 00000000..36c0aecf --- /dev/null +++ b/res/modules/internal/maths_inline.lua @@ -0,0 +1,240 @@ +-- =================================================== -- +-- ====================== vec3 ======================= -- +-- =================================================== -- +function vec3.add(a, b, dst) + local btype = type(b) + if dst then + if btype == "table" then + dst[1] = a[1] + b[1] + dst[2] = a[2] + b[2] + dst[3] = a[3] + b[3] + else + dst[1] = a[1] + b + dst[2] = a[2] + b + dst[3] = a[3] + b + end + return dst + else + if btype == "table" then + return {a[1] + b[1], a[2] + b[2], a[3] + b[3]} + else + return {a[1] + b, a[2] + b, a[3] + b} + end + end +end + +function vec3.sub(a, b, dst) + local btype = type(b) + if dst then + if btype == "table" then + dst[1] = a[1] - b[1] + dst[2] = a[2] - b[2] + dst[3] = a[3] - b[3] + else + dst[1] = a[1] - b + dst[2] = a[2] - b + dst[3] = a[3] - b + end + return dst + else + if btype == "table" then + return {a[1] - b[1], a[2] - b[2], a[3] - b[3]} + else + return {a[1] - b, a[2] - b, a[3] - b} + end + end +end + +function vec3.mul(a, b, dst) + local btype = type(b) + if dst then + if btype == "table" then + dst[1] = a[1] * b[1] + dst[2] = a[2] * b[2] + dst[3] = a[3] * b[3] + else + dst[1] = a[1] * b + dst[2] = a[2] * b + dst[3] = a[3] * b + end + return dst + else + if btype == "table" then + return {a[1] * b[1], a[2] * b[2], a[3] * b[3]} + else + return {a[1] * b, a[2] * b, a[3] * b} + end + end +end + +function vec3.div(a, b, dst) + local btype = type(b) + if dst then + if btype == "table" then + dst[1] = a[1] / b[1] + dst[2] = a[2] / b[2] + dst[3] = a[3] / b[3] + else + dst[1] = a[1] / b + dst[2] = a[2] / b + dst[3] = a[3] / b + end + return dst + else + if btype == "table" then + return {a[1] / b[1], a[2] / b[2], a[3] / b[3]} + else + return {a[1] / b, a[2] / b, a[3] / b} + end + end +end + +function vec3.abs(a, dst) + local x = a[1] + local y = a[2] + local z = a[3] + if dst then + dst[1] = x < 0.0 and -x or x + dst[2] = y < 0.0 and -y or y + dst[3] = z < 0.0 and -z or z + else + return { + x < 0.0 and -x or x, + y < 0.0 and -y or y, + z < 0.0 and -z or z, + } + end +end + +function vec3.dot(a, b) + return a[1] * b[1] + a[2] * b[2] + a[3] * b[3] +end + +function vec3.mix(a, b, t, dest) + if dest then + dest[1] = a[1] * (1.0 - t) + b[1] * t + dest[2] = a[2] * (1.0 - t) + b[2] * t + dest[3] = a[3] * (1.0 - t) + b[3] * t + return dest + else + return { + a[1] * (1.0 - t) + b[1] * t, + a[2] * (1.0 - t) + b[2] * t, + a[3] * (1.0 - t) + b[3] * t, + } + end +end + +-- =================================================== -- +-- ====================== vec2 ======================= -- +-- =================================================== -- +function vec2.add(a, b, dst) + local btype = type(b) + if dst then + if btype == "table" then + dst[1] = a[1] + b[1] + dst[2] = a[2] + b[2] + else + dst[1] = a[1] + b + dst[2] = a[2] + b + end + return dst + else + if btype == "table" then + return {a[1] + b[1], a[2] + b[2]} + else + return {a[1] + b, a[2] + b} + end + end +end + +function vec2.sub(a, b, dst) + local btype = type(b) + if dst then + if btype == "table" then + dst[1] = a[1] - b[1] + dst[2] = a[2] - b[2] + else + dst[1] = a[1] - b + dst[2] = a[2] - b + end + return dst + else + if btype == "table" then + return {a[1] - b[1], a[2] - b[2]} + else + return {a[1] - b, a[2] - b} + end + end +end + +function vec2.mul(a, b, dst) + local btype = type(b) + if dst then + if btype == "table" then + dst[1] = a[1] * b[1] + dst[2] = a[2] * b[2] + else + dst[1] = a[1] * b + dst[2] = a[2] * b + end + return dst + else + if btype == "table" then + return {a[1] * b[1], a[2] * b[2]} + else + return {a[1] * b, a[2] * b} + end + end +end + +function vec2.div(a, b, dst) + local btype = type(b) + if dst then + if btype == "table" then + dst[1] = a[1] / b[1] + dst[2] = a[2] / b[2] + else + dst[1] = a[1] / b + dst[2] = a[2] / b + end + return dst + else + if btype == "table" then + return {a[1] / b[1], a[2] / b[2]} + else + return {a[1] / b, a[2] / b} + end + end +end + +function vec2.abs(a, dst) + local x = a[1] + local y = a[2] + if dst then + dst[1] = x < 0.0 and -x or x + dst[2] = y < 0.0 and -y or y + else + return { + x < 0.0 and -x or x, + y < 0.0 and -y or y, + } + end +end + +function vec2.dot(a, b) + return a[1] * b[1] + a[2] * b[2] +end + +function vec2.mix(a, b, t, dest) + if dest then + dest[1] = a[1] * (1.0 - t) + b[1] * t + dest[2] = a[2] * (1.0 - t) + b[2] * t + return dest + else + return { + a[1] * (1.0 - t) + b[1] * t, + a[2] * (1.0 - t) + b[2] * t, + } + end +end diff --git a/res/modules/internal/random_generator.lua b/res/modules/internal/random_generator.lua new file mode 100644 index 00000000..0c9073cc --- /dev/null +++ b/res/modules/internal/random_generator.lua @@ -0,0 +1,35 @@ +local Random = {} + +local M = 2 ^ 31 +local A = 1103515245 +local C = 12345 + +function Random.randint(self) + self._seed = (A * self._seed + C) % M + return self._seed +end + +function Random.random(self, a, b) + local num = self:randint() % M / M + if b then + return math.floor(num * (b - a + 1) + a) + elseif a then + return math.floor(num * a + 1) + else + return num + end +end + +function Random.seed(self, number) + if type(number) ~= "number" then + error("number expected") + end + self._seed = number +end + +return function(seed) + if seed and type(seed) ~= "number" then + error("number expected") + end + return setmetatable({_seed = seed or random.random(M)}, {__index = Random}) +end diff --git a/res/modules/internal/stdcomp.lua b/res/modules/internal/stdcomp.lua index a62989a9..7cedd188 100644 --- a/res/modules/internal/stdcomp.lua +++ b/res/modules/internal/stdcomp.lua @@ -25,6 +25,7 @@ local Rigidbody = {__index={ get_linear_damping=function(self) return __rigidbody.get_linear_damping(self.eid) end, set_linear_damping=function(self, f) return __rigidbody.set_linear_damping(self.eid, f) end, is_vdamping=function(self) return __rigidbody.is_vdamping(self.eid) end, + get_vdamping=function(self) return __rigidbody.get_vdamping(self.eid) end, set_vdamping=function(self, b) return __rigidbody.set_vdamping(self.eid, b) end, is_grounded=function(self) return __rigidbody.is_grounded(self.eid) end, is_crouching=function(self) return __rigidbody.is_crouching(self.eid) end, @@ -63,6 +64,13 @@ local Entity = {__index={ get_skeleton=function(self) return entities.get_skeleton(self.eid) end, set_skeleton=function(self, s) return entities.set_skeleton(self.eid, s) end, get_component=function(self, name) return self.components[name] end, + require_component=function(self, name) + local component = self.components[name] + if not component then + error(("entity has no required component '%s'"):format(name)) + end + return component + end, has_component=function(self, name) return self.components[name] ~= nil end, get_uid=function(self) return self.eid end, def_index=function(self) return entities.get_def(self.eid) end, @@ -125,6 +133,19 @@ return { ::continue:: end end, + physics_update = function(delta) + for uid, entity in pairs(entities) do + for _, component in pairs(entity.components) do + local callback = component.on_physics_update + if not component.__disabled and callback then + local result, err = pcall(callback, delta) + if err then + debug.error(err) + end + end + end + end + end, render = function(delta) for _,entity in pairs(entities) do for _, component in pairs(entity.components) do diff --git a/res/modules/internal/stream_providers/file.lua b/res/modules/internal/stream_providers/file.lua new file mode 100644 index 00000000..80bc8c5b --- /dev/null +++ b/res/modules/internal/stream_providers/file.lua @@ -0,0 +1,17 @@ +local io_stream = require "core:io_stream" + +local lib = { + read = file.__read_descriptor, + write = file.__write_descriptor, + flush = file.__flush_descriptor, + is_alive = file.__has_descriptor, + close = file.__close_descriptor +} + +return function(path, mode) + return io_stream.new( + file.__open_descriptor(path, mode), + mode:find('b') ~= nil, + lib + ) +end \ No newline at end of file diff --git a/res/modules/internal/stream_providers/named_pipe.lua b/res/modules/internal/stream_providers/named_pipe.lua new file mode 100644 index 00000000..bdce841c --- /dev/null +++ b/res/modules/internal/stream_providers/named_pipe.lua @@ -0,0 +1,7 @@ +local FFI = ffi + +if FFI.os == "Windows" then + return require "core:internal/stream_providers/named_pipe_windows" +else + return require "core:internal/stream_providers/named_pipe_unix" +end \ No newline at end of file diff --git a/res/modules/internal/stream_providers/named_pipe_path_validate.lua b/res/modules/internal/stream_providers/named_pipe_path_validate.lua new file mode 100644 index 00000000..d2ad446d --- /dev/null +++ b/res/modules/internal/stream_providers/named_pipe_path_validate.lua @@ -0,0 +1,21 @@ +local forbiddenPaths = { + "/..\\", "\\../", + "/../", "\\..\\" +} + +return function(path) + local corrected = true + + if path:starts_with("../") or path:starts_with("..\\") then + corrected = false + else + for _, forbiddenPath in ipairs(forbiddenPaths) do + if path:find(forbiddenPath) then + corrected = false + break + end + end + end + + if not corrected then error "special path \"../\" is not allowed in path to named pipe" end +end \ No newline at end of file diff --git a/res/modules/internal/stream_providers/named_pipe_unix.lua b/res/modules/internal/stream_providers/named_pipe_unix.lua new file mode 100644 index 00000000..1843daba --- /dev/null +++ b/res/modules/internal/stream_providers/named_pipe_unix.lua @@ -0,0 +1,104 @@ +local path_validate = require "core:internal/stream_providers/named_pipe_path_validate" +local io_stream = require "core:io_stream" + +local FFI = ffi + +FFI.cdef[[ +int open(const char *pathname, int flags); +int close(int fd); +ssize_t read(int fd, void *buf, size_t count); +ssize_t write(int fd, const void *buf, size_t count); +int fcntl(int fd, int cmd, ...); + +const char *strerror(int errnum); +]] + +local C = FFI.C + +local O_RDONLY = 0x0 +local O_WRONLY = 0x1 +local O_RDWR = 0x2 +local O_NONBLOCK = 0x800 +local F_GETFL = 3 + +local function getError() + local err = FFI.errno() + + return FFI.string(C.strerror(err)).." ("..err..")" +end + +local lib = {} + +function lib.read(fd, len) + local buffer = FFI.new("uint8_t[?]", len) + local result = tonumber(C.read(fd, buffer, len)) + + local out = Bytearray() + + if result <= 0 then + return out + end + + for i = 0, result - 1 do + out[i+1] = buffer[i] + end + + return out +end + +function lib.write(fd, bytearray) + local len = #bytearray + local buffer = FFI.new("uint8_t[?]", len) + for i = 1, len do + buffer[i-1] = bytearray[i] + end + + if C.write(fd, buffer, len) == -1 then + error("failed to write to named pipe: "..getError()) + end +end + +function lib.flush(fd) + -- no flush on unix +end + +function lib.is_alive(fd) + if fd == nil or fd < 0 then return false end + + return C.fcntl(fd, F_GETFL) ~= -1 +end + +function lib.close(fd) + C.close(fd) +end + +return function(path, mode) + path_validate(path) + + path = "/tmp/"..path + + local read = mode:find('r') ~= nil + local write = mode:find('w') ~= nil + + local flags + + if read and write then + flags = O_RDWR + elseif read then + flags = O_RDONLY + elseif write then + flags = O_WRONLY + else + error "mode must contain read or write flag" + end + + flags = bit.bor(flags, O_NONBLOCK) + + local fd = C.open(path, flags) + + if fd == -1 then + error("failed to open named pipe: "..getError()) + end + + return io_stream.new(fd, mode:find('b') ~= nil, lib) +end diff --git a/res/modules/internal/stream_providers/named_pipe_windows.lua b/res/modules/internal/stream_providers/named_pipe_windows.lua new file mode 100644 index 00000000..83691aa5 --- /dev/null +++ b/res/modules/internal/stream_providers/named_pipe_windows.lua @@ -0,0 +1,144 @@ +local path_validate = require "core:internal/stream_providers/named_pipe_path_validate" +local io_stream = require "core:io_stream" + +local FFI = ffi + +FFI.cdef[[ +typedef void* HANDLE; +typedef uint32_t DWORD; +typedef int BOOL; +typedef void* LPVOID; +typedef const char* LPCSTR; + +BOOL CloseHandle(HANDLE hObject); +DWORD GetFileType(HANDLE hFile); +BOOL ReadFile(HANDLE hFile, void* lpBuffer, DWORD nNumberOfBytesToRead, + DWORD* lpNumberOfBytesRead, void* lpOverlapped); +BOOL WriteFile(HANDLE hFile, const void* lpBuffer, DWORD nNumberOfBytesToWrite, + DWORD* lpNumberOfBytesWritten, void* lpOverlapped); +HANDLE CreateFileA(LPCSTR lpFileName, DWORD dwDesiredAccess, DWORD dwShareMode, + void* lpSecurityAttributes, DWORD dwCreationDisposition, + DWORD dwFlagsAndAttributes, HANDLE hTemplateFile); + +BOOL PeekNamedPipe( + HANDLE hNamedPipe, + LPVOID lpBuffer, + DWORD nBufferSize, + DWORD* lpBytesRead, + DWORD* lpTotalBytesAvail, + DWORD* lpBytesLeftThisMessage +); + +DWORD GetLastError(void); +BOOL FlushFileBuffers(HANDLE hFile); +]] + +local C = FFI.C + +local GENERIC_READ = 0x80000000 +local GENERIC_WRITE = 0x40000000 +local OPEN_EXISTING = 3 +local FILE_ATTRIBUTE_NORMAL = 0x00000080 +local FILE_TYPE_UNKNOWN = 0x0000 +local INVALID_HANDLE_VALUE = FFI.cast("HANDLE", -1) + +local lib = {} + +local function is_data_available(handle) + local bytes_available = FFI.new("DWORD[1]") + local success = FFI.C.PeekNamedPipe(handle, nil, 0, nil, bytes_available, nil) + + if success == 0 then + return -1 + end + + return bytes_available[0] > 0 +end + +function lib.read(handle, len) + local out = Bytearray() + + local has_data, err = is_data_available(handle) + + if not has_data then + return out + elseif hasData == -1 then + error("failed to read from named pipe: "..tostring(C.GetLastError())) + end + + local buffer = FFI.new("uint8_t[?]", len) + local read = FFI.new("DWORD[1]") + + local ok = C.ReadFile(handle, buffer, len, read, nil) + + if ok == 0 or read[0] == 0 then + return out + end + + for i = 0, read[0] - 1 do + out[i+1] = buffer[i] + end + + return out +end + +function lib.write(handle, bytearray) + local len = #bytearray + + local buffer = FFI.new("uint8_t[?]", len) + for i = 1, len do + buffer[i-1] = bytearray[i] + end + + local written = FFI.new("DWORD[1]") + + if C.WriteFile(handle, buffer, len, written, nil) == 0 then + error("failed to write to named pipe: "..tostring(C.GetLastError())) + end +end + +function lib.flush(handle) + C.FlushFileBuffers(handle) +end + +function lib.is_alive(handle) + if handle == nil or handle == INVALID_HANDLE_VALUE then + return false + else + return C.GetFileType(handle) ~= FILE_TYPE_UNKNOWN + end +end + +function lib.close(handle) + C.CloseHandle(handle) +end + +return function(path, mode) + path_validate(path) + + path = "\\\\.\\pipe\\"..path + + local read = mode:find('r') ~= nil + local write = mode:find('w') ~= nil + + local flags + + if read and write then + flags = bit.bor(GENERIC_READ, GENERIC_WRITE) + elseif read then + flags = GENERIC_READ + elseif write then + flags = GENERIC_WRITE + else + error("mode must contain read or write flag") + end + + local handle = C.CreateFileA(path, flags, 0, nil, OPEN_EXISTING, + FILE_ATTRIBUTE_NORMAL, nil) + + if handle == INVALID_HANDLE_VALUE then + error("failed to open named pipe: "..tostring(C.GetLastError())) + end + + return io_stream.new(handle, mode:find('b') ~= nil, lib) +end \ No newline at end of file diff --git a/res/modules/io_stream.lua b/res/modules/io_stream.lua new file mode 100644 index 00000000..185a29c2 --- /dev/null +++ b/res/modules/io_stream.lua @@ -0,0 +1,398 @@ +local io_stream = { } + +io_stream.__index = io_stream + +local MAX_BUFFER_SIZE = 8192 + +local DEFAULT_MODE = "default" +local BUFFERED_MODE = "buffered" +local YIELD_MODE = "yield" + +local ALL_MODES = { + DEFAULT_MODE, + BUFFERED_MODE, + YIELD_MODE +} + +local FLUSH_MODE_ALL = "all" +local FLUSH_MODE_ONLY_BUFFER = "buffer" + +local ALL_FLUSH_MODES = { + FLUSH_MODE_ALL, + FLUSH_MODE_ONLY_BUFFER +} + +local CR = string.byte('\r') +local LF = string.byte('\n') + +local function readFully(result, readFunc) + local isTable = type(result) == "table" + + local buf + + repeat + buf = readFunc(MAX_BUFFER_SIZE) + + if isTable then + for i = 1, #buf do + result[#result + 1] = buf[i] + end + else result:append(buf) end + until #buf == 0 +end + +--[[ + +descriptor - descriptor of stream for provided I/O library +binaryMode - if enabled, most methods will expect bytes instead of strings +ioLib - I/O library. Should include the following functions: + read(descriptor: int, length: int) -> Bytearray + May return bytearray with a smaller size if bytes have not arrived yet or have run out + write(descriptor: int, data: Bytearray) + flush(descriptor: int) + is_alive(descriptor: int) -> bool + close(descriptor: int) +--]] + +function io_stream.new(descriptor, binaryMode, ioLib, mode, flushMode) + mode = mode or DEFAULT_MODE + flushMode = flushMode or FLUSH_MODE_ALL + + local self = setmetatable({}, io_stream) + + self.descriptor = descriptor + self.binaryMode = binaryMode + self.maxBufferSize = MAX_BUFFER_SIZE + self.ioLib = ioLib + + self:set_mode(mode) + self:set_flush_mode(flushMode) + + return self +end + +function io_stream:is_binary_mode() + return self.binaryMode +end + +function io_stream:set_binary_mode(binaryMode) + self.binaryMode = binaryMode ~= nil +end + +function io_stream:get_mode() + return self.mode +end + +function io_stream:set_mode(mode) + if not table.has(ALL_MODES, mode) then + error("invalid stream mode: "..mode) + end + + if self.mode == BUFFERED_MODE then + self.writeBuffer:clear() + self.readBuffer:clear() + end + + if mode == BUFFERED_MODE and not self.writeBuffer then + self.writeBuffer = Bytearray() + self.readBuffer = Bytearray() + end + + self.mode = mode +end + +function io_stream:get_flush_mode() + return self.flushMode +end + +function io_stream:set_flush_mode(flushMode) + if not table.has(ALL_FLUSH_MODES, flushMode) then + error("invalid flush mode: "..flushMode) + end + + self.flushMode = flushMode +end + +function io_stream:get_max_buffer_size() + return self.maxBufferSize +end + +function io_stream:set_max_buffer_size(maxBufferSize) + self.maxBufferSize = maxBufferSize +end + +function io_stream:available(length) + if self.mode == BUFFERED_MODE then + self:__update_read_buffer() + + if not length then + return #self.readBuffer + else + return #self.readBuffer >= length + end + end +end + +function io_stream:__update_read_buffer() + local readed = Bytearray() + + readFully(readed, function(length) return self.ioLib.read(self.descriptor, length) end) + + self.readBuffer:append(readed) + + if #self.readBuffer > self.maxBufferSize then + error "buffer overflow" + end +end + +function io_stream:__read(length) + if self.mode == YIELD_MODE then + local buffer = Bytearray() + + while #buffer < length do + buffer:append(self.ioLib.read(self.descriptor, length - #buffer)) + + if #buffer < length then coroutine.yield() end + end + + return buffer + elseif self.mode == BUFFERED_MODE then + self:__update_read_buffer() + + if #self.readBuffer < length then + error "buffer underflow" + end + + local copy + + if #self.readBuffer == length then + copy = Bytearray() + + copy:append(self.readBuffer) + + self.readBuffer:clear() + else + copy = Bytearray() + + for i = 1, length do + copy[i] = self.readBuffer[i] + end + + self.readBuffer:remove(1, length) + end + + return copy + elseif self.mode == DEFAULT_MODE then + return self.ioLib.read(self.descriptor, length) + end +end + +function io_stream:__write(data) + if self.mode == BUFFERED_MODE then + self.writeBuffer:append(data) + + if #self.writeBuffer > self.maxBufferSize then + error "buffer overflow" + end + elseif self.mode == DEFAULT_MODE or self.mode == YIELD_MODE then + return self.ioLib.write(self.descriptor, data) + end +end + +function io_stream:read_fully(useTable) + if self.binaryMode then + local result = useTable and Bytearray() or { } + + readFully(result, function() return self:__read(self.maxBufferSize) end) + else + if useTable then + local lines = { } + + local line + + repeat + line = self:read_line() + + lines[#lines + 1] = line + until not line + + return lines + else + local result = Bytearray() + + readFully(result, function() return self:__read(self.maxBufferSize) end) + + return utf8.tostring(result) + end + end +end + +function io_stream:read_line() + local result = Bytearray() + + local first = true + + while true do + local char = self:__read(1) + + if #char == 0 then + if first then return else break end + end + + char = char[1] + + if char == LF then break + elseif char == CR then + char = self:__read(1) + + if char[1] == LF then break + else + result:append(CR) + result:append(char[1]) + end + else result:append(char) end + + first = false + end + + return utf8.tostring(result) +end + +function io_stream:write_line(str) + self:__write(utf8.tobytes(str .. LF)) +end + +function io_stream:read(arg, useTable) + local argType = type(arg) + + if self.binaryMode then + local byteArr + + if argType == "number" then + -- using 'arg' as length + + byteArr = self:__read(arg) + + if useTable == true then + local t = { } + + for i = 1, #byteArr do + t[i] = byteArr[i] + end + + return t + else + return byteArr + end + elseif argType == "string" then + return byteutil.unpack( + arg, + self:__read(byteutil.get_size(arg)) + ) + elseif argType == nil then + error( + "in binary mode the first argument must be a string data format".. + " for the library \"byteutil\" or the number of bytes to read" + ) + else + error("unknown argument type: "..argType) + end + else + if not arg then + return self:read_line() + else + local linesCount = arg + local trimLastEmptyLines = useTable or true + + if linesCount < 0 then error "count of lines to read must be positive" end + + local result = { } + + for i = 1, linesCount do + result[i] = self:read_line() + end + + if trimLastEmptyLines then + local i = #result + + while i >= 0 do + local length = utf8.length(result[i]) + + if length > 0 then break + else result[i] = nil end + + i = i - 1 + end + + local i = 1 + + while #result > 0 do + local length = utf8.length(result[i]) + + if length > 0 then break + else table.remove(result, i) end + end + end + + return result + end + end +end + +function io_stream:write(arg, ...) + local argType = type(arg) + + if self.binaryMode then + local byteArr + + if argType ~= "string" then + -- using arg as bytes table/bytearray + + if argType == "table" then + byteArr = Bytearray(arg) + else + byteArr = arg + end + else + byteArr = byteutil.pack(arg, ...) + end + + self:__write(byteArr) + else + if argType == "string" then + self:write_line(arg) + elseif argType == "table" then + for i = 1, #arg do + self:write_line(arg[i]) + end + else error("unknown argument type: "..argType) end + end +end + +function io_stream:is_alive() + return self.ioLib.is_alive(self.descriptor) +end + +function io_stream:is_closed() + return not self:is_alive() +end + +function io_stream:close() + if self.mode == BUFFERED_MODE then + self.readBuffer:clear() + self.writeBuffer:clear() + end + + return self.ioLib.close(self.descriptor) +end + +function io_stream:flush() + if self.mode == BUFFERED_MODE and #self.writeBuffer > 0 then + self.ioLib.write(self.descriptor, self.writeBuffer) + self.writeBuffer:clear() + end + + if self.flushMode ~= FLUSH_MODE_ONLY_BUFFER then self.ioLib.flush(self.descriptor) end +end + +return io_stream \ No newline at end of file diff --git a/res/modules/schedule.lua b/res/modules/schedule.lua new file mode 100644 index 00000000..457cbf90 --- /dev/null +++ b/res/modules/schedule.lua @@ -0,0 +1,50 @@ +local Schedule = { + __index = { + set_interval = function(self, ms, callback, repetions) + local id = self._next_interval + self._intervals[id] = { + last_called = 0.0, + delay = ms / 1000.0, + callback = callback, + repetions = repetions, + } + self._next_interval = id + 1 + return id + end, + set_timeout = function(self, ms, callback) + self:set_interval(ms, callback, 1) + end, + tick = function(self, dt) + local timer = self._timer + dt + for id, interval in pairs(self._intervals) do + if timer - interval.last_called >= interval.delay then + local stack_size = debug.count_frames() + xpcall(interval.callback, function(msg) + __vc__error(msg, 1, 1, stack_size) + end) + interval.last_called = timer + local repetions = interval.repetions + if repetions then + if repetions <= 1 then + self:remove_interval(id) + else + interval.repetions = repetions - 1 + end + end + end + end + self._timer = timer + end, + remove_interval = function (self, id) + self._intervals[id] = nil + end + } +} + +return function () + return setmetatable({ + _next_interval = 1, + _timer = 0.0, + _intervals = {}, + }, Schedule) +end diff --git a/res/preload.json b/res/preload.json index 887b0e26..0cd11b5b 100644 --- a/res/preload.json +++ b/res/preload.json @@ -47,7 +47,8 @@ "gui/info", "gui/world", "gui/hud", - "gui/entity" + "gui/entity", + "gui/half_block" ], "fonts": [ { diff --git a/res/project_client.lua b/res/project_client.lua new file mode 100644 index 00000000..03c43a4b --- /dev/null +++ b/res/project_client.lua @@ -0,0 +1,27 @@ +local menubg + +function on_menu_clear() + if menubg then + menubg:destruct() + menubg = nil + end +end + +function on_menu_setup() + local controller = {} + function controller.resize_menu_bg() + local w, h = unpack(gui.get_viewport()) + if menubg then + menubg.region = {0, math.floor(h / 48), math.floor(w / 48), 0} + menubg.pos = {0, 0} + end + return w, h + end + gui.root.root:add( + "", controller) + menubg = gui.root.menubg + controller.resize_menu_bg() + menu.page = "main" + menu.visible = true +end diff --git a/res/scripts/classes.lua b/res/scripts/classes.lua index 41406892..e950aa0d 100644 --- a/res/scripts/classes.lua +++ b/res/scripts/classes.lua @@ -46,18 +46,67 @@ local Socket = {__index={ get_address=function(self) return network.__get_address(self.id) end, }} +local WriteableSocket = {__index={ + send=function(self, ...) return network.__send(self.id, ...) end, + close=function(self) return network.__close(self.id) end, + is_open=function(self) return network.__is_alive(self.id) end, + get_address=function(self) return network.__get_address(self.id) end, +}} + local ServerSocket = {__index={ close=function(self) return network.__closeserver(self.id) end, is_open=function(self) return network.__is_serveropen(self.id) end, get_port=function(self) return network.__get_serverport(self.id) end, }} +local DatagramServerSocket = {__index={ + close=function(self) return network.__closeserver(self.id) end, + is_open=function(self) return network.__is_serveropen(self.id) end, + get_port=function(self) return network.__get_serverport(self.id) end, + send=function(self, ...) return network.__udp_server_send_to(self.id, ...) end +}} local _tcp_server_callbacks = {} local _tcp_client_callbacks = {} +local _udp_server_callbacks = {} +local _udp_client_datagram_callbacks = {} +local _udp_client_open_callbacks = {} +local _http_response_callbacks = {} +local _http_error_callbacks = {} + +network.get = function(url, callback, errorCallback, headers) + local id = network.__get(url, headers) + if callback then + _http_response_callbacks[id] = callback + end + if errorCallback then + _http_error_callbacks[id] = errorCallback + end +end + +network.get_binary = function(url, callback, errorCallback, headers) + local id = network.__get_binary(url, headers) + if callback then + _http_response_callbacks[id] = callback + end + if errorCallback then + _http_error_callbacks[id] = errorCallback + end +end + +network.post = function(url, data, callback, errorCallback, headers) + local id = network.__post(url, data, headers) + if callback then + _http_response_callbacks[id] = callback + end + if errorCallback then + _http_error_callbacks[id] = errorCallback + end +end + network.tcp_open = function (port, handler) - local socket = setmetatable({id=network.__open(port)}, ServerSocket) + local socket = setmetatable({id=network.__open_tcp(port)}, ServerSocket) _tcp_server_callbacks[socket.id] = function(id) handler(setmetatable({id=id}, Socket)) @@ -67,19 +116,133 @@ end network.tcp_connect = function(address, port, callback) local socket = setmetatable({id=0}, Socket) - socket.id = network.__connect(address, port) + socket.id = network.__connect_tcp(address, port) _tcp_client_callbacks[socket.id] = function() callback(socket) end return socket end +network.udp_open = function (port, datagramHandler) + if type(datagramHandler) ~= 'function' then + error "udp server cannot be opened without datagram handler" + end + + local socket = setmetatable({id=network.__open_udp(port)}, DatagramServerSocket) + + _udp_server_callbacks[socket.id] = function(address, port, data) + datagramHandler(address, port, data, socket) + end + + return socket +end + +network.udp_connect = function (address, port, datagramHandler, openCallback) + if type(datagramHandler) ~= 'function' then + error "udp client socket cannot be opened without datagram handler" + end + + local socket = setmetatable({id=0}, WriteableSocket) + socket.id = network.__connect_udp(address, port) + + _udp_client_datagram_callbacks[socket.id] = datagramHandler + _udp_client_open_callbacks[socket.id] = openCallback + + return socket +end + +local function clean(iterable, checkFun, ...) + local tables = { ... } + + for id, _ in pairs(iterable) do + if not checkFun(id) then + for i = 1, #tables do + tables[i][id] = nil + end + end + end +end + +local updating_blocks = {} +local TYPE_REGISTER = 0 +local TYPE_UNREGISTER = 1 + +block.__perform_ticks = function(delta) + for id, entry in pairs(updating_blocks) do + entry.timer = entry.timer + delta + local steps = math.floor(entry.timer / entry.delta * #entry / 3) + if steps == 0 then + goto continue + end + entry.timer = 0.0 + local event = entry.event + local tps = entry.tps + for i=1, steps do + local x = entry[entry.pointer + 1] + local y = entry[entry.pointer + 2] + local z = entry[entry.pointer + 3] + entry.pointer = (entry.pointer + 3) % #entry + events.emit(event, x, y, z, tps) + end + ::continue:: + end +end + +block.__process_register_events = function() + local register_events = block.__pull_register_events() + if not register_events then + return + end + for i=1, #register_events, 4 do + local header = register_events[i] + local type = bit.band(header, 0xFFFF) + local id = bit.rshift(header, 16) + local x = register_events[i + 1] + local y = register_events[i + 2] + local z = register_events[i + 3] + + local list = updating_blocks[id] + if type == TYPE_REGISTER then + if not list then + list = {} + list.event = block.name(id) .. ".blocktick" + list.tps = 20 / (block.properties[id]["tick-interval"] or 1) + list.delta = 1.0 / list.tps + list.timer = 0.0 + list.pointer = 0 + updating_blocks[id] = list + end + table.insert(list, x) + table.insert(list, y) + table.insert(list, z) + elseif type == TYPE_UNREGISTER then + if list then + for j=1, #list, 3 do + if list[j] == x and list[j + 1] == y and list[j + 2] == z then + for k=1,3 do + table.remove(list, j) + end + j = j - 3 + end + end + end + end + + print(type, id, x, y, z) + end +end + network.__process_events = function() local CLIENT_CONNECTED = 1 local CONNECTED_TO_SERVER = 2 + local DATAGRAM = 3 + local RESPONSE = 4 + + local ON_SERVER = 1 + local ON_CLIENT = 2 local cleaned = false local events = network.__pull_events() for i, event in ipairs(events) do - local etype, sid, cid = unpack(event) + local etype, sid, cid, addr, port, side, data = unpack(event) if etype == CLIENT_CONNECTED then local callback = _tcp_server_callbacks[sid] @@ -87,24 +250,42 @@ network.__process_events = function() callback(cid) end elseif etype == CONNECTED_TO_SERVER then - local callback = _tcp_client_callbacks[cid] + local callback = _tcp_client_callbacks[cid] or _udp_client_open_callbacks[cid] if callback then callback() end + elseif etype == DATAGRAM then + if side == ON_CLIENT then + _udp_client_datagram_callbacks[cid](data) + elseif side == ON_SERVER then + _udp_server_callbacks[sid](addr, port, data) + end + elseif etype == RESPONSE then + if event[2] / 100 == 2 then + local callback = _http_response_callbacks[event[3]] + _http_response_callbacks[event[3]] = nil + _http_error_callbacks[event[3]] = nil + if callback then + callback(event[4]) + end + else + local callback = _http_error_callbacks[event[3]] + _http_response_callbacks[event[3]] = nil + _http_error_callbacks[event[3]] = nil + if callback then + callback(event[2], event[4]) + end + end end -- remove dead servers if not cleaned then - for sid, _ in pairs(_tcp_server_callbacks) do - if not network.__is_serveropen(sid) then - _tcp_server_callbacks[sid] = nil - end - end - for cid, _ in pairs(_tcp_client_callbacks) do - if not network.__is_alive(cid) then - _tcp_client_callbacks[cid] = nil - end - end + clean(_tcp_server_callbacks, network.__is_serveropen, _tcp_server_callbacks) + clean(_tcp_client_callbacks, network.__is_alive, _tcp_client_callbacks) + + clean(_udp_server_callbacks, network.__is_serveropen, _udp_server_callbacks) + clean(_udp_client_datagram_callbacks, network.__is_alive, _udp_client_open_callbacks, _udp_client_datagram_callbacks) + cleaned = true end end diff --git a/res/scripts/components/mob.lua b/res/scripts/components/mob.lua new file mode 100644 index 00000000..4a9efaaa --- /dev/null +++ b/res/scripts/components/mob.lua @@ -0,0 +1,177 @@ +local body = entity.rigidbody +local tsf = entity.transform +local rig = entity.skeleton + +local props = {} + +local function def_prop(name, def_value) + props[name] = SAVED_DATA[name] or ARGS[name] or def_value + this["get_"..name] = function() return props[name] end + this["set_"..name] = function(value) + props[name] = value + if math.abs(value - def_value) < 1e-7 then + SAVED_DATA[name] = nil + else + SAVED_DATA[name] = value + end + end +end + +def_prop("jump_force", 0.0) +def_prop("air_damping", 1.0) +def_prop("ground_damping", 1.0) +def_prop("movement_speed", 3.0) +def_prop("run_speed_mul", 1.5) +def_prop("crouch_speed_mul", 0.35) +def_prop("flight_speed_mul", 2.0) +def_prop("gravity_scale", 1.0) + +local function normalize_angle(angle) + while angle > 180 do + angle = angle - 360 + end + while angle <= -180 do + angle = angle + 360 + end + return angle +end + +local function angle_delta(a, b) + return normalize_angle(a - b) +end + +local dir = mat4.mul(tsf:get_rot(), {0, 0, -1}) +local flight = false + +function jump(multiplier) + local vel = body:get_vel() + body:set_vel( + vec3.add(vel, {0, props.jump_force * (multiplier or 1.0), 0}, vel)) +end + +function move_vertical(speed, vel) + vel = vel or body:get_vel() + vel[2] = vel[2] * 0.2 + props.movement_speed * speed * 0.8 + body:set_vel(vel) +end + +local function move_horizontal(speed, dir, vel) + vel = vel or body:get_vel() + if vec2.length(dir) > 0.0 then + vec2.normalize(dir, dir) + + local magnitude = vec2.length({vel[1], vel[3]}) + + if magnitude <= 1e-4 or (magnitude < speed or vec2.dot( + {vel[1] / magnitude, vel[3] / magnitude}, dir) < 0.9) + then + vel[1] = vel[1] * 0.2 + dir[1] * speed * 0.8 + vel[3] = vel[3] * 0.2 + dir[2] * speed * 0.8 + end + magnitude = vec3.length({vel[1], 0, vel[3]}) + if vec2.dot({vel[1] / magnitude, vel[3] / magnitude}, dir) > 0.5 then + vel[1] = vel[1] / magnitude * speed + vel[3] = vel[3] / magnitude * speed + end + end + body:set_vel(vel) +end + +function go(dir, speed_multiplier, sprint, crouch, vel) + local speed = props.movement_speed * speed_multiplier + if flight then + speed = speed * props.flight_speed_mul + end + if sprint then + speed = speed * props.run_speed_mul + elseif crouch then + speed = speed * props.crouch_speed_mul + end + move_horizontal(speed, dir, vel) +end + +local headIndex = rig:index("head") + +function look_at(point, change_dir) + local pos = tsf:get_pos() + local viewdir = vec3.normalize(vec3.sub(point, pos)) + + local dot = vec3.dot(viewdir, dir) + if dot < 0.0 and not change_dir then + viewdir = mat4.mul(tsf:get_rot(), {0, 0, -1}) + else + dir[1] = dir[1] * 0.8 + viewdir[1] * 0.13 + dir[3] = dir[3] * 0.8 + viewdir[3] * 0.13 + end + + if not headIndex then + return + end + + local headrot = mat4.idt() + local curdir = mat4.mul(mat4.mul(tsf:get_rot(), + rig:get_matrix(headIndex)), {0, 0, -1}) + + vec3.mix(curdir, viewdir, 0.2, viewdir) + + headrot = mat4.inverse(mat4.look_at({0,0,0}, viewdir, {0, 1, 0})) + headrot = mat4.mul(mat4.inverse(tsf:get_rot()), headrot) + rig:set_matrix(headIndex, headrot) +end + +function follow_waypoints(pathfinding) + pathfinding = pathfinding or entity:require_component("core:pathfinding") + local pos = tsf:get_pos() + local waypoint = pathfinding.next_waypoint() + if not waypoint then + return + end + local speed = props.movement_speed + local vel = body:get_vel() + dir = vec3.sub( + vec3.add(waypoint, {0.5, 0, 0.5}), + {pos[1], math.floor(pos[2]), pos[3]} + ) + local upper = dir[2] > 0 + dir[2] = 0.0 + vec3.normalize(dir, dir) + move_horizontal(speed, {dir[1], dir[3]}, vel) + if upper and body:is_grounded() then + jump(1.0) + end +end + +function set_dir(new_dir) + dir = new_dir +end + +function is_flight() return flight end + +function set_flight(flag) flight = flag end + +local prev_angle = (vec2.angle({dir[3], dir[1]})) % 360 + +function on_physics_update(delta) + local grounded = body:is_grounded() + body:set_vdamping(flight) + body:set_gravity_scale({0, flight and 0.0 or props.gravity_scale, 0}) + body:set_linear_damping( + (flight or not grounded) and props.air_damping or props.ground_damping + ) + + local new_angle = (vec2.angle({dir[3], dir[1]})) % 360 + local angle = prev_angle + + local adelta = angle_delta( + normalize_angle(new_angle), + normalize_angle(prev_angle) + ) + local rotate_speed = entity:get_player() == -1 and 200 or 400 + + if math.abs(adelta) > 5 then + angle = angle + delta * rotate_speed * (adelta > 0 and 1 or -1) + end + + tsf:set_rot(mat4.rotate({0, 1, 0}, angle + 180)) + prev_angle = angle +end diff --git a/res/scripts/components/pathfinding.lua b/res/scripts/components/pathfinding.lua new file mode 100644 index 00000000..2a6e51aa --- /dev/null +++ b/res/scripts/components/pathfinding.lua @@ -0,0 +1,71 @@ +local target +local route +local started + +local tsf = entity.transform +local body = entity.rigidbody + +agent = pathfinding.create_agent() +pathfinding.set_max_visited(agent, 1e3) +pathfinding.avoid_tag(agent, "core:liquid", 8) + +function set_target(new_target) + target = new_target +end + +function set_jump_height(height) + pathfinding.set_jump_height(agent, height) +end + +function get_target() + return target +end + +function get_route() + return route +end + +function next_waypoint() + if not route or #route == 0 then + return + end + local waypoint = route[#route] + local pos = tsf:get_pos() + local dst = vec2.length({ + math.floor(waypoint[1] - math.floor(pos[1])), + math.floor(waypoint[3] - math.floor(pos[3])) + }) + if dst < 1.0 then + table.remove(route, #route) + end + return route[#route] +end + +local refresh_internal = 100 +local frameid = math.random(0, refresh_internal) + +function set_refresh_interval(interval) + refresh_internal = interval +end + +function on_update() + if not started then + frameid = frameid + 1 + if body:is_grounded() then + if target and (frameid % refresh_internal == 1 or not route) then + pathfinding.make_route_async(agent, tsf:get_pos(), target) + started = true + end + end + else + local new_route = pathfinding.pull_route(agent) + if new_route then + route = new_route + started = false + end + end +end + +function on_despawn() + pathfinding.remove_agent(agent) +end diff --git a/res/scripts/components/player.lua b/res/scripts/components/player.lua new file mode 100644 index 00000000..2a63f681 --- /dev/null +++ b/res/scripts/components/player.lua @@ -0,0 +1,62 @@ +local tsf = entity.transform +local body = entity.rigidbody +local mob = entity:require_component("core:mob") + +local cheat_speed_mul = 10.0 + +local function process_player_inputs(pid, delta) + if not hud or hud.is_inventory_open() or menu.page ~= "" then + return + end + local cam = cameras.get("core:first-person") + local front = cam:get_front() + local right = cam:get_right() + front[2] = 0.0 + vec3.normalize(front, front) + + local isjump = input.is_active('movement.jump') + local issprint = input.is_active('movement.sprint') + local iscrouch = input.is_active('movement.crouch') + local isforward = input.is_active('movement.forward') + local ischeat = input.is_active('movement.cheat') + local isback = input.is_active('movement.back') + local isleft = input.is_active('movement.left') + local isright = input.is_active('movement.right') + mob.set_flight(player.is_flight(pid)) + body:set_body_type(player.is_noclip(pid) and "kinematic" or "dynamic") + body:set_crouching(iscrouch) + + local vel = body:get_vel() + local speed = ischeat and cheat_speed_mul or 1.0 + + local dir = {0, 0, 0} + + if isforward then vec3.add(dir, front, dir) end + if isback then vec3.sub(dir, front, dir) end + if isright then vec3.add(dir, right, dir) end + if isleft then vec3.sub(dir, right, dir) end + + if vec3.length(dir) > 0.0 then + mob.go({dir[1], dir[3]}, speed, issprint, iscrouch, vel) + end + + if mob.is_flight() then + if isjump then + mob.move_vertical(speed * 3) + elseif iscrouch then + mob.move_vertical(-speed * 3) + end + elseif body:is_grounded() and isjump then + mob.jump() + end +end + +function on_physics_update(delta) + local pid = entity:get_player() + if pid ~= -1 then + local pos = tsf:get_pos() + local cam = cameras.get("core:first-person") + process_player_inputs(pid, delta) + mob.look_at(vec3.add(pos, cam:get_front())) + end +end diff --git a/res/scripts/hud.lua b/res/scripts/hud.lua index 3fd1375c..90883d94 100644 --- a/res/scripts/hud.lua +++ b/res/scripts/hud.lua @@ -22,6 +22,39 @@ local function configure_SSAO() -- for test purposes end +local function update_hand() + local skeleton = gfx.skeletons + local pid = hud.get_player() + local invid, slot = player.get_inventory(pid) + local itemid = inventory.get(invid, slot) + + local cam = cameras.get("core:first-person") + local bone = skeleton.index("hand", "item") + + local offset = vec3.mul(vec3.sub(cam:get_pos(), {player.get_pos(pid)}), -1) + + local rotation = cam:get_rot() + + local angle = player.get_rot(pid) - 90 + local cos = math.cos(angle / (180 / math.pi)) + local sin = math.sin(angle / (180 / math.pi)) + + local newX = offset[1] * cos - offset[3] * sin + local newZ = offset[1] * sin + offset[3] * cos + + offset[1] = newX + offset[3] = newZ + + local mat = mat4.translate(mat4.idt(), {0.06, 0.035, -0.1}) + mat4.scale(mat, {0.1, 0.1, 0.1}, mat) + mat4.mul(rotation, mat, mat) + mat4.rotate(mat, {0, 1, 0}, -90, mat) + mat4.translate(mat, offset, mat) + + skeleton.set_matrix("hand", bone, mat) + skeleton.set_model("hand", bone, item.model_name(itemid)) +end + function on_hud_open() input.add_callback("player.pick", function () if hud.is_paused() or hud.is_inventory_open() then @@ -63,7 +96,7 @@ function on_hud_open() player.set_noclip(pid, true) end end) - + input.add_callback("player.flight", function () if hud.is_paused() or hud.is_inventory_open() then return @@ -81,4 +114,14 @@ function on_hud_open() end) configure_SSAO() + + hud.default_hand_controller = update_hand +end + +function on_hud_render() + if hud.hand_controller then + hud.hand_controller() + else + update_hand() + end end diff --git a/res/scripts/hud_classes.lua b/res/scripts/hud_classes.lua index dc373c6e..e6259e22 100644 --- a/res/scripts/hud_classes.lua +++ b/res/scripts/hud_classes.lua @@ -12,7 +12,28 @@ local Text3D = {__index={ update_settings=function(self, t) return gfx.text3d.update_settings(self.id, t) end, }} +local Skeleton = {__index={ + index=function(self, s) return gfx.skeletons.index(self.name, s) end, + get_model=function(self, i) return gfx.skeletons.get_model(self.name, i) end, + set_model=function(self, i, s) return gfx.skeletons.set_model(self.name, i, s) end, + get_matrix=function(self, i) return gfx.skeletons.get_matrix(self.name, i) end, + set_matrix=function(self, i, m) return gfx.skeletons.set_matrix(self.name, i, m) end, + get_texture=function(self, i) return gfx.skeletons.get_texture(self.name, i) end, + set_texture=function(self, i, s) return gfx.skeletons.set_texture(self.name, i, s) end, + is_visible=function(self, i) return gfx.skeletons.is_visible(self.name, i) end, + set_visible=function(self, i, b) return gfx.skeletons.set_visible(self.name, i, b) end, + get_color=function(self, i) return gfx.skeletons.get_color(self.name, i) end, + set_color=function(self, i, c) return gfx.skeletons.set_color(self.name, i, c) end, +}} + gfx.text3d.new = function(pos, text, preset, extension) local id = gfx.text3d.show(pos, text, preset, extension) return setmetatable({id=id}, Text3D) end + +gfx.skeletons = __skeleton +gfx.skeletons.get = function(name) + if gfx.skeletons.exists(name) then + return setmetatable({name=name}, Skeleton) + end +end diff --git a/res/scripts/post_content.lua b/res/scripts/post_content.lua index 7b17876e..b512882d 100644 --- a/res/scripts/post_content.lua +++ b/res/scripts/post_content.lua @@ -6,28 +6,29 @@ local names = { "shadeless", "ambient-occlusion", "breakable", "selectable", "grounded", "hidden", "draw-group", "picking-item", "surface-replacement", "script-name", "ui-layout", "inventory-size", "tick-interval", "overlay-texture", - "translucent", "fields", "particles", "icon-type", "icon", "placing-block", + "translucent", "fields", "particles", "icon-type", "icon", "placing-block", "stack-size", "name", "script-file", "culling" } for name, _ in pairs(user_props) do table.insert(names, name) end --- remove undefined properties -for id, blockprops in pairs(block.properties) do - for propname, value in pairs(blockprops) do - if not table.has(names, propname) then - blockprops[propname] = nil - end - end -end -for id, itemprops in pairs(item.properties) do - for propname, value in pairs(itemprops) do - if not table.has(names, propname) then - itemprops[propname] = nil + +-- remove undefined properties and build tags set +local function process_properties(lib) + for id, props in pairs(lib.properties) do + for propname, _ in pairs(props) do + if not table.has(names, propname) then + props[propname] = nil + end end + + props.tags_set = lib.__get_tags(id) end end +process_properties(block) +process_properties(item) + local function make_read_only(t) setmetatable(t, { __newindex = function() @@ -57,10 +58,22 @@ local function cache_names(library) function library.index(name) return indices[name] end + + function library.has_tag(id, tag) + if id == nil then + error("id is nil") + end + local props = library.properties[id] + local tags_set = props.tags_set + if tags_set then + return tags_set[tag] + else + return false + end + end end cache_names(block) cache_names(item) -local scripts_registry = require "core:internal/scripts_registry" -scripts_registry.build_registry() +__vc_scripts_registry.build_registry() diff --git a/res/scripts/stdcmd.lua b/res/scripts/stdcmd.lua index 0308910f..73f4b059 100644 --- a/res/scripts/stdcmd.lua +++ b/res/scripts/stdcmd.lua @@ -157,6 +157,16 @@ console.add_command( end ) + +console.add_command( + "entity.spawn name:str x:num~pos.x y:num~pos.y z:num~pos.z", + "Spawn entity with default parameters", + function(args, kwargs) + local eid = entities.spawn(args[1], {args[2], args[3], args[4]}) + return string.format("spawned %s at %s, %s, %s", unpack(args)) + end +) + console.add_command( "entity.despawn entity:sel=$entity.selected", "Despawn entity", diff --git a/res/scripts/stdlib.lua b/res/scripts/stdlib.lua index 1ad98cac..b08e8c02 100644 --- a/res/scripts/stdlib.lua +++ b/res/scripts/stdlib.lua @@ -1,3 +1,5 @@ +local enable_experimental = core.get_setting("debug.enable-experimental") + ------------------------------------------------ ------ Extended kit of standard functions ------ ------------------------------------------------ @@ -77,13 +79,20 @@ local function complete_app_lib(app) coroutine.yield() end - function app.sleep_until(predicate, max_ticks) + function app.sleep_until(predicate, max_ticks, max_time) max_ticks = max_ticks or 1e9 + max_time = max_time or 1e9 local ticks = 0 - while ticks < max_ticks and not predicate() do + local start_time = os.clock() + while ticks < max_ticks and + os.clock() - start_time < max_time + and not predicate() do app.tick() ticks = ticks + 1 end + if os.clock() - start_time >= max_time then + error("timeout") + end if ticks == max_ticks then error("max ticks exceed") end @@ -130,61 +139,55 @@ function inventory.decrement(invid, slot, count) end end ------------------------------------------------- -------------------- Events --------------------- ------------------------------------------------- -events = { - handlers = {} -} +function inventory.get_caption(invid, slot) + local item_id, count = inventory.get(invid, slot) + local caption = inventory.get_data(invid, slot, "caption") + if not caption then return item.caption(item_id) end -function events.on(event, func) - if events.handlers[event] == nil then - events.handlers[event] = {} - end - table.insert(events.handlers[event], func) + return caption end -function events.reset(event, func) - if func == nil then - events.handlers[event] = nil - else - events.handlers[event] = {func} +function inventory.set_caption(invid, slot, caption) + local itemid, itemcount = inventory.get(invid, slot) + if itemid == 0 then + return end + if caption == nil or type(caption) ~= "string" then + caption = "" + end + inventory.set_data(invid, slot, "caption", caption) end -function events.remove_by_prefix(prefix) - for name, handlers in pairs(events.handlers) do - local actualname = name - if type(name) == 'table' then - actualname = name[1] - end - if actualname:sub(1, #prefix+1) == prefix..':' then - events.handlers[actualname] = nil - end - end +function inventory.get_description(invid, slot) + local item_id, count = inventory.get(invid, slot) + local description = inventory.get_data(invid, slot, "description") + if not description then return item.description(item_id) end + + return description end +function inventory.set_description(invid, slot, description) + local itemid, itemcount = inventory.get(invid, slot) + if itemid == 0 then + return + end + if description == nil or type(description) ~= "string" then + description = "" + end + inventory.set_data(invid, slot, "description", description) +end + +if enable_experimental then + require "core:internal/maths_inline" +end + +asserts = require "core:internal/asserts" +events = require "core:internal/events" + function pack.unload(prefix) events.remove_by_prefix(prefix) end -function events.emit(event, ...) - local result = nil - local handlers = events.handlers[event] - if handlers == nil then - return nil - end - for _, func in ipairs(handlers) do - local status, newres = xpcall(func, __vc__error, ...) - if not status then - debug.error("error in event ("..event..") handler: "..newres) - else - result = result or newres - end - end - return result -end - gui_util = require "core:internal/gui_util" Document = gui_util.Document @@ -199,6 +202,7 @@ end _GUI_ROOT = Document.new("core:root") _MENU = _GUI_ROOT.menu menu = _MENU +gui.root = _GUI_ROOT --- Console library extension --- console.cheats = {} @@ -278,11 +282,33 @@ entities.get_all = function(uids) return stdcomp.get_all(uids) end end + local bytearray = require "core:internal/bytearray" Bytearray = bytearray.FFIBytearray Bytearray_as_string = bytearray.FFIBytearray_as_string Bytearray_construct = function(...) return Bytearray(...) end + +__vc_scripts_registry = require "core:internal/scripts_registry" + +file.open = require "core:internal/stream_providers/file" +file.open_named_pipe = require "core:internal/stream_providers/named_pipe" + +if ffi.os == "Windows" then + ffi.cdef[[ + unsigned long GetCurrentProcessId(); + ]] + + os.pid = ffi.C.GetCurrentProcessId() +else + ffi.cdef[[ + int getpid(void); + ]] + + os.pid = ffi.C.getpid() +end + ffi = nil +__vc_lock_internal_modules() math.randomseed(time.uptime() * 1536227939) @@ -411,8 +437,41 @@ function __vc_on_hud_open() hud.open_permanent("core:ingame_chat") end +local Schedule = require "core:schedule" + +local ScheduleGroup_mt = { + __index = { + publish = function(self, schedule) + local id = self._next_schedule + self._schedules[id] = schedule + self._next_schedule = id + 1 + end, + tick = function(self, dt) + for id, schedule in pairs(self._schedules) do + schedule:tick(dt) + end + self.common:tick(dt) + end, + remove = function(self, id) + self._schedules[id] = nil + end, + } +} + +local function ScheduleGroup() + return setmetatable({ + _next_schedule = 1, + _schedules = {}, + common = Schedule() + }, ScheduleGroup_mt) +end + +time.schedules = {} + local RULES_FILE = "world:rules.toml" function __vc_on_world_open() + time.schedules.world = ScheduleGroup() + if not file.exists(RULES_FILE) then return end @@ -422,6 +481,10 @@ function __vc_on_world_open() end end +function __vc_on_world_tick(tps) + time.schedules.world:tick(1.0 / tps) +end + function __vc_on_world_save() local rule_values = {} for name, rule in pairs(rules.rules) do @@ -434,6 +497,7 @@ function __vc_on_world_quit() _rules.clear() gui_util:__reset_local() stdcomp.__reset() + file.__close_all_descriptors() end local __vc_coroutines = {} @@ -475,15 +539,18 @@ function start_coroutine(chunk, name) local co = coroutine.create(function() local status, error = xpcall(chunk, function(err) local fullmsg = "error: "..string.match(err, ": (.+)").."\n"..debug.traceback() - gui.alert(fullmsg, function() - if world.is_open() then - __vc_app.close_world() - else - __vc_app.reset_content() - menu:reset() - menu.page = "main" - end - end) + + if hud then + gui.alert(fullmsg, function() + if world.is_open() then + __vc_app.close_world() + else + __vc_app.reset_content() + menu:reset() + menu.page = "main" + end + end) + end return fullmsg end) if not status then @@ -521,6 +588,8 @@ function __process_post_runnables() end network.__process_events() + block.__process_register_events() + block.__perform_ticks(time.delta()) end function time.post_runnable(runnable) diff --git a/res/scripts/stdmin.lua b/res/scripts/stdmin.lua index 9f8c48c2..742c9506 100644 --- a/res/scripts/stdmin.lua +++ b/res/scripts/stdmin.lua @@ -20,6 +20,18 @@ if not ipairs_mt_supported then end end +function await(co) + local res, err + while coroutine.status(co) ~= 'dead' do + coroutine.yield() + res, err = coroutine.resume(co) + if err then + return res, err + end + end + return res, err +end + local _ffi = ffi function __vc_Canvas_set_data(self, data) if type(data) == "cdata" then @@ -538,6 +550,8 @@ function reload_module(name) end end +local internal_locked = false + -- Load script with caching -- -- path - script path `contentpack:filename`. @@ -547,6 +561,11 @@ end function __load_script(path, nocache) local packname, filename = parse_path(path) + if internal_locked and (packname == "res" or packname == "core") + and filename:starts_with("modules/internal") then + error("access to core:internal modules outside of [core]") + end + -- __cached_scripts used in condition because cached result may be nil if not nocache and __cached_scripts[path] ~= nil then return package.loaded[path] @@ -567,6 +586,10 @@ function __load_script(path, nocache) return result end +function __vc_lock_internal_modules() + internal_locked = true +end + function require(path) if not string.find(path, ':') then local prefix, _ = parse_path(_debug_getinfo(2).source) @@ -645,3 +668,5 @@ end bit.compile = require "core:bitwise/compiler" bit.execute = require "core:bitwise/executor" + +random.Random = require "core:internal/random_generator" diff --git a/res/shaders/lib/constants.glsl b/res/shaders/lib/constants.glsl index c7020584..74f29545 100644 --- a/res/shaders/lib/constants.glsl +++ b/res/shaders/lib/constants.glsl @@ -9,7 +9,7 @@ // lighting #define SKY_LIGHT_MUL 2.9 -#define SKY_LIGHT_TINT vec3(0.9, 0.8, 1.0) +#define SKY_LIGHT_TINT (vec3(1.0, 0.95, 0.9) * 2.0) #define MIN_SKY_LIGHT vec3(0.2, 0.25, 0.33) // fog diff --git a/res/shaders/lib/sky.glsl b/res/shaders/lib/sky.glsl index 771f37f2..028fb51a 100644 --- a/res/shaders/lib/sky.glsl +++ b/res/shaders/lib/sky.glsl @@ -4,7 +4,7 @@ #include vec3 pick_sky_color(samplerCube cubemap) { - vec3 skyLightColor = texture(cubemap, vec3(0.4f, 0.0f, 0.4f)).rgb; + vec3 skyLightColor = texture(cubemap, vec3(0.8f, 0.01f, 0.4f)).rgb; skyLightColor *= SKY_LIGHT_TINT; skyLightColor = min(vec3(1.0f), skyLightColor * SKY_LIGHT_MUL); skyLightColor = max(MIN_SKY_LIGHT, skyLightColor); diff --git a/res/texts/en_US.txt b/res/texts/en_US.txt index 57a6cb6b..e35ff99c 100644 --- a/res/texts/en_US.txt +++ b/res/texts/en_US.txt @@ -7,6 +7,7 @@ world.convert-block-layouts=Blocks fields have changes! Convert world files? pack.remove-confirm=Do you want to erase all pack(s) content from the world forever? error.pack-not-found=Could not to find pack error.dependency-not-found=Dependency pack is not found +error.dependency-version-not-met=Dependency pack version is not met. world.delete-confirm=Do you want to delete world forever? world.generators.default=Default world.generators.flat=Flat @@ -22,6 +23,7 @@ graphics.dense-render.tooltip=Enables transparency in blocks like leaves # settings settings.Controls Search Mode=Search by attached button name +settings.Conflict=Possible conflicts found # Bindings chunks.reload=Reload Chunks diff --git a/res/texts/ru_RU.txt b/res/texts/ru_RU.txt index d2d7c63e..084e11b3 100644 --- a/res/texts/ru_RU.txt +++ b/res/texts/ru_RU.txt @@ -32,6 +32,7 @@ devtools.output=Вывод error.pack-not-found=Не удалось найти пакет error.dependency-not-found=Используемая зависимость не найдена +error.dependency-version-not-met=Версия зависимости не соответствует необходимой pack.remove-confirm=Удалить весь поставляемый паком/паками контент из мира (безвозвратно)? # Подсказки @@ -100,6 +101,7 @@ settings.Controls Search Mode=Поиск по привязанной кнопк settings.Limit Background FPS=Ограничить фоновую частоту кадров settings.Advanced render=Продвинутый рендер settings.Shadows quality=Качество теней +settings.Conflict=Найдены возможные конфликты # Управление chunks.reload=Перезагрузить Чанки diff --git a/res/textures/gui/half_block.png b/res/textures/gui/half_block.png new file mode 100644 index 00000000..4d6b699e Binary files /dev/null and b/res/textures/gui/half_block.png differ diff --git a/src/assets/assetload_funcs.cpp b/src/assets/assetload_funcs.cpp index 78d89a0b..88dd66bd 100644 --- a/src/assets/assetload_funcs.cpp +++ b/src/assets/assetload_funcs.cpp @@ -71,8 +71,8 @@ static auto process_program(const ResPaths& paths, const std::string& filename) auto& preprocessor = *Shader::preprocessor; - auto vertex = preprocessor.process(vertexFile, vertexSource); - auto fragment = preprocessor.process(fragmentFile, fragmentSource); + auto vertex = preprocessor.process(vertexFile, vertexSource, false, {}); + auto fragment = preprocessor.process(fragmentFile, fragmentSource, false, {}); return std::make_pair(vertex, fragment); } @@ -121,7 +121,7 @@ assetload::postfunc assetload::posteffect( auto& preprocessor = *Shader::preprocessor; preprocessor.addHeader( - "__effect__", preprocessor.process(effectFile, effectSource, true) + "__effect__", preprocessor.process(effectFile, effectSource, true, {}) ); auto [vertex, fragment] = process_program(paths, SHADERS_FOLDER + "/effect"); diff --git a/src/coders/GLSLExtension.cpp b/src/coders/GLSLExtension.cpp index 8105b09e..5d53cb35 100644 --- a/src/coders/GLSLExtension.cpp +++ b/src/coders/GLSLExtension.cpp @@ -22,6 +22,10 @@ void GLSLExtension::setPaths(const ResPaths* paths) { this->paths = paths; } +void GLSLExtension::setTraceOutput(bool enabled) { + this->traceOutput = enabled; +} + void GLSLExtension::loadHeader(const std::string& name) { if (paths == nullptr) { return; @@ -29,7 +33,7 @@ void GLSLExtension::loadHeader(const std::string& name) { io::path file = paths->find("shaders/lib/" + name + ".glsl"); std::string source = io::read_string(file); addHeader(name, {}); - addHeader(name, process(file, source, true)); + addHeader(name, process(file, source, true, {})); } void GLSLExtension::addHeader(const std::string& name, ProcessingResult header) { @@ -123,13 +127,22 @@ static Value default_value_for(Type type) { class GLSLParser : public BasicParser { public: - GLSLParser(GLSLExtension& glsl, std::string_view file, std::string_view source, bool header) + GLSLParser( + GLSLExtension& glsl, + std::string_view file, + std::string_view source, + bool header, + const std::vector& defines + ) : BasicParser(file, source), glsl(glsl) { if (!header) { ss << "#version " << GLSLExtension::VERSION << '\n'; - } - for (auto& entry : glsl.getDefines()) { - ss << "#define " << entry.first << " " << entry.second << '\n'; + for (auto& entry : defines) { + ss << "#define " << entry << '\n'; + } + for (auto& entry : defines) { + ss << "#define " << entry << '\n'; + } } uint linenum = 1; source_line(ss, linenum); @@ -289,10 +302,34 @@ private: std::stringstream ss; }; +static void trace_output( + const io::path& file, + const std::string& source, + const GLSLExtension::ProcessingResult& result +) { + std::stringstream ss; + ss << "export:trace/" << file.name(); + io::path outfile = ss.str(); + try { + io::create_directories(outfile.parent()); + io::write_string(outfile, result.code); + } catch (const std::runtime_error& err) { + logger.error() << "error on saving GLSLExtension::preprocess output (" + << outfile.string() << "): " << err.what(); + } +} + GLSLExtension::ProcessingResult GLSLExtension::process( - const io::path& file, const std::string& source, bool header + const io::path& file, + const std::string& source, + bool header, + const std::vector& defines ) { std::string filename = file.string(); - GLSLParser parser(*this, filename, source, header); - return parser.process(); + GLSLParser parser(*this, filename, source, header, defines); + auto result = parser.process(); + if (traceOutput) { + trace_output(file, source, result); + } + return result; } diff --git a/src/coders/GLSLExtension.hpp b/src/coders/GLSLExtension.hpp index b61ca512..53f52930 100644 --- a/src/coders/GLSLExtension.hpp +++ b/src/coders/GLSLExtension.hpp @@ -5,6 +5,7 @@ #include #include "io/io.hpp" +#include "data/setting.hpp" #include "graphics/core/PostEffect.hpp" class ResPaths; @@ -19,6 +20,7 @@ public: }; void setPaths(const ResPaths* paths); + void setTraceOutput(bool enabled); void define(const std::string& name, std::string value); void undefine(const std::string& name); @@ -37,7 +39,8 @@ public: ProcessingResult process( const io::path& file, const std::string& source, - bool header = false + bool header, + const std::vector& defines ); static inline std::string VERSION = "330 core"; @@ -46,4 +49,5 @@ private: std::unordered_map defines; const ResPaths* paths = nullptr; + bool traceOutput = false; }; diff --git a/src/coders/vcm.cpp b/src/coders/vcm.cpp index a12524c0..9b564787 100644 --- a/src/coders/vcm.cpp +++ b/src/coders/vcm.cpp @@ -174,7 +174,6 @@ std::unique_ptr vcm::parse( "'model' tag expected as root, got '" + root.getTag() + "'" ); } - std::cout << xml::stringify(*doc) << std::endl; return load_model(root); } catch (const parsing_error& err) { throw std::runtime_error(err.errorLog()); diff --git a/src/constants.hpp b/src/constants.hpp index de954717..0a6df9e5 100644 --- a/src/constants.hpp +++ b/src/constants.hpp @@ -6,7 +6,7 @@ #include inline constexpr int ENGINE_VERSION_MAJOR = 0; -inline constexpr int ENGINE_VERSION_MINOR = 28; +inline constexpr int ENGINE_VERSION_MINOR = 29; #ifdef NDEBUG inline constexpr bool ENGINE_DEBUG_BUILD = false; @@ -14,7 +14,7 @@ inline constexpr bool ENGINE_DEBUG_BUILD = false; inline constexpr bool ENGINE_DEBUG_BUILD = true; #endif // NDEBUG -inline const std::string ENGINE_VERSION_STRING = "0.28"; +inline const std::string ENGINE_VERSION_STRING = "0.29"; /// @brief world regions format version inline constexpr uint REGION_FORMAT_VERSION = 3; diff --git a/src/content/Content.cpp b/src/content/Content.cpp index ce92be18..c0093491 100644 --- a/src/content/Content.cpp +++ b/src/content/Content.cpp @@ -35,13 +35,15 @@ Content::Content( UptrsMap blockMaterials, UptrsMap skeletons, ResourceIndicesSet resourceIndices, - dv::value defaults + dv::value defaults, + std::unordered_map tags ) : indices(std::move(indices)), packs(std::move(packs)), blockMaterials(std::move(blockMaterials)), skeletons(std::move(skeletons)), defaults(std::move(defaults)), + tags(std::move(tags)), blocks(std::move(blocks)), items(std::move(items)), entities(std::move(entities)), @@ -63,6 +65,14 @@ const rigging::SkeletonConfig* Content::getSkeleton(const std::string& id return found->second.get(); } +const rigging::SkeletonConfig& Content::requireSkeleton(const std::string& id) const { + auto skeleton = getSkeleton(id); + if (skeleton == nullptr) { + throw std::runtime_error("skeleton '" + id + "' not loaded"); + } + return *skeleton; +} + const BlockMaterial* Content::findBlockMaterial(const std::string& id) const { auto found = blockMaterials.find(id); if (found == blockMaterials.end()) { diff --git a/src/content/Content.hpp b/src/content/Content.hpp index 539325a8..599d50fe 100644 --- a/src/content/Content.hpp +++ b/src/content/Content.hpp @@ -176,6 +176,7 @@ class Content { UptrsMap blockMaterials; UptrsMap skeletons; dv::value defaults = nullptr; + std::unordered_map tags; public: ContentUnitDefs blocks; ContentUnitDefs items; @@ -195,7 +196,8 @@ public: UptrsMap blockMaterials, UptrsMap skeletons, ResourceIndicesSet resourceIndices, - dv::value defaults + dv::value defaults, + std::unordered_map tags ); ~Content(); @@ -211,7 +213,16 @@ public: return defaults; } + int getTagIndex(const std::string& tag) const { + const auto& found = tags.find(tag); + if (found == tags.end()) { + return -1; + } + return found->second; + } + const rigging::SkeletonConfig* getSkeleton(const std::string& id) const; + const rigging::SkeletonConfig& requireSkeleton(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); diff --git a/src/content/ContentBuilder.cpp b/src/content/ContentBuilder.cpp index ff0bb18a..66ad1acf 100644 --- a/src/content/ContentBuilder.cpp +++ b/src/content/ContentBuilder.cpp @@ -28,6 +28,9 @@ std::unique_ptr ContentBuilder::build() { // Generating runtime info def.rt.id = blockDefsIndices.size(); def.rt.emissive = *reinterpret_cast(def.emission); + for (const auto& tag : def.tags) { + def.rt.tags.insert(tags.add(tag)); + } if (def.variants) { for (auto& variant : def.variants->variants) { @@ -58,7 +61,7 @@ std::unique_ptr ContentBuilder::build() { } blockDefsIndices.push_back(&def); - groups->insert(def.defaults.drawGroup); // FIXME + groups->insert(def.defaults.drawGroup); // FIXME: variants } std::vector itemDefsIndices; @@ -93,7 +96,8 @@ std::unique_ptr ContentBuilder::build() { std::move(blockMaterials), std::move(skeletons), std::move(resourceIndices), - std::move(defaults) + std::move(defaults), + std::move(tags.map) ); // Now, it's time to resolve foreign keys diff --git a/src/content/ContentBuilder.hpp b/src/content/ContentBuilder.hpp index 7d3df1f6..8a938819 100644 --- a/src/content/ContentBuilder.hpp +++ b/src/content/ContentBuilder.hpp @@ -62,6 +62,27 @@ public: } }; +struct TagsIndices { + int nextIndex = 1; + std::unordered_map map; + + int add(const std::string& tag) { + const auto& found = map.find(tag); + if (found != map.end()) { + return found->second; + } + return map[tag] = nextIndex++; + } + + int indexOf(const std::string& tag) { + const auto& found = map.find(tag); + if (found == map.end()) { + return -1; + } + return found->second; + } +}; + class ContentBuilder { UptrsMap blockMaterials; UptrsMap skeletons; @@ -74,6 +95,7 @@ public: ContentUnitBuilder generators {allNames, ContentType::GENERATOR}; ResourceIndicesSet resourceIndices {}; dv::value defaults = nullptr; + TagsIndices tags {}; ~ContentBuilder(); diff --git a/src/content/ContentLoader.cpp b/src/content/ContentLoader.cpp index e260ebd7..ea29e27c 100644 --- a/src/content/ContentLoader.cpp +++ b/src/content/ContentLoader.cpp @@ -288,6 +288,7 @@ void ContentLoader::loadContent(const dv::value& root) { item.iconType = ItemIconType::BLOCK; item.icon = def.name; item.placingBlock = def.name; + item.tags = def.tags; for (uint j = 0; j < 4; j++) { item.emission[j] = def.emission[j]; @@ -412,6 +413,25 @@ void ContentLoader::load() { if (io::exists(contentFile)) { loadContent(io::read_json(contentFile)); } + + // Load attached tags + io::path tagsFile = folder / "tags.toml"; + if (io::exists(tagsFile)) { + auto tagsMap = io::read_object(tagsFile); + for (const auto& [key, list] : tagsMap.asObject()) { + for (const auto& id : list) { + const auto& stringId = id.asString(); + if (auto block = builder.blocks.get(stringId)) { + block->tags.push_back(key); + if (auto item = builder.items.get(stringId + BLOCK_ITEM_SUFFIX)) { + item->tags.push_back(key); + } + } else if (auto item = builder.items.get(stringId)) { + item->tags.push_back(key); + } + } + } + } } template diff --git a/src/content/ContentPack.cpp b/src/content/ContentPack.cpp index ac8c8d59..522b818f 100644 --- a/src/content/ContentPack.cpp +++ b/src/content/ContentPack.cpp @@ -118,21 +118,58 @@ ContentPack ContentPack::read(const io::path& folder) { const auto& dependencies = *found; for (const auto& elem : dependencies) { std::string depName = elem.asString(); - auto level = DependencyLevel::required; + auto level = DependencyLevel::REQUIRED; switch (depName.at(0)) { case '!': depName = depName.substr(1); break; case '?': depName = depName.substr(1); - level = DependencyLevel::optional; + level = DependencyLevel::OPTIONAL; break; case '~': depName = depName.substr(1); - level = DependencyLevel::weak; + level = DependencyLevel::WEAK; break; } - pack.dependencies.push_back({level, depName}); + + std::string depVer = "*"; + std::string depVerOperator = "="; + + size_t versionPos = depName.rfind("@"); + if (versionPos != std::string::npos) { + depVer = depName.substr(versionPos + 1); + depName = depName.substr(0, versionPos); + + if (depVer.size() >= 2) { + std::string op = depVer.substr(0, 2); + std::uint8_t op_size = 0; + + // Two symbol operators + if (op == ">=" || op == "=>" || op == "<=" || op == "=<") { + op_size = 2; + depVerOperator = op; + } + + // One symbol operators + else { + op = depVer.substr(0, 1); + + if (op == ">" || op == "<") { + op_size = 1; + depVerOperator = op; + } + } + + depVer = depVer.substr(op_size); + } else { + if (depVer == ">" || depVer == "<"){ + depVer = "*"; + } + } + } + + pack.dependencies.push_back({level, depName, depVer, depVerOperator}); } } diff --git a/src/content/ContentPack.hpp b/src/content/ContentPack.hpp index f4e44801..1aa3a9ea 100644 --- a/src/content/ContentPack.hpp +++ b/src/content/ContentPack.hpp @@ -20,21 +20,28 @@ public: io::path folder, const std::string& message ); - + std::string getPackId() const; io::path getFolder() const; }; +enum class DependencyVersionOperator { + EQUAL, GREATHER, LESS, + GREATHER_OR_EQUAL, LESS_OR_EQUAL +}; + enum class DependencyLevel { - required, // dependency must be installed - optional, // dependency will be installed if found - weak, // only affects packs order + REQUIRED, // dependency must be installed + OPTIONAL, // dependency will be installed if found + WEAK, // only affects packs order }; /// @brief Content-pack that should be installed earlier the dependent struct DependencyPack { DependencyLevel level; std::string id; + std::string version; + std::string op; }; struct ContentPackStats { diff --git a/src/content/ContentPackVersion.cpp b/src/content/ContentPackVersion.cpp new file mode 100644 index 00000000..7a3d8801 --- /dev/null +++ b/src/content/ContentPackVersion.cpp @@ -0,0 +1,67 @@ +#include "ContentPackVersion.hpp" + +#include +#include +#include + +#include "coders/commons.hpp" + +Version::Version(const std::string& version) { + major = 0; + minor = 0; + patch = 0; + + std::vector parts; + + std::stringstream ss(version); + std::string part; + while (std::getline(ss, part, '.')) { + if (!part.empty()) { + parts.push_back(std::stoi(part)); + } + } + + if (parts.size() > 0) major = parts[0]; + if (parts.size() > 1) minor = parts[1]; + if (parts.size() > 2) patch = parts[2]; +} + +DependencyVersionOperator Version::string_to_operator(const std::string& op) { + if (op == "=") + return DependencyVersionOperator::EQUAL; + else if (op == ">") + return DependencyVersionOperator::GREATHER; + else if (op == "<") + return DependencyVersionOperator::LESS; + else if (op == ">=" || op == "=>") + return DependencyVersionOperator::GREATHER_OR_EQUAL; + else if (op == "<=" || op == "=<") + return DependencyVersionOperator::LESS_OR_EQUAL; + else + return DependencyVersionOperator::EQUAL; +} + +bool isNumber(const std::string& s) { + return !s.empty() && std::all_of(s.begin(), s.end(), ::is_digit); +} + +bool Version::matches_pattern(const std::string& version) { + for (char c : version) { + if (!isdigit(c) && c != '.') { + return false; + } + } + + std::stringstream ss(version); + + std::vector parts; + std::string part; + while (std::getline(ss, part, '.')) { + if (part.empty()) return false; + if (!isNumber(part)) return false; + + parts.push_back(part); + } + + return parts.size() == 2 || parts.size() == 3; +} diff --git a/src/content/ContentPackVersion.hpp b/src/content/ContentPackVersion.hpp new file mode 100644 index 00000000..e922ddd3 --- /dev/null +++ b/src/content/ContentPackVersion.hpp @@ -0,0 +1,57 @@ +#include + +#include "content/ContentPack.hpp" + +class Version { +public: + int major; + int minor; + int patch; + + Version(const std::string& version); + + bool operator==(const Version& other) const { + return major == other.major && minor == other.minor && + patch == other.patch; + } + + bool operator<(const Version& other) const { + if (major != other.major) return major < other.major; + if (minor != other.minor) return minor < other.minor; + return patch < other.patch; + } + + bool operator>(const Version& other) const { + return other < *this; + } + + bool operator>=(const Version& other) const { + return !(*this < other); + } + + bool operator<=(const Version& other) const { + return !(*this > other); + } + + bool process_operator(const std::string& op, const Version& other) const { + auto dep_op = Version::string_to_operator(op); + + switch (dep_op) { + case DependencyVersionOperator::EQUAL: + return *this == other; + case DependencyVersionOperator::GREATHER: + return *this > other; + case DependencyVersionOperator::LESS: + return *this < other; + case DependencyVersionOperator::LESS_OR_EQUAL: + return *this <= other; + case DependencyVersionOperator::GREATHER_OR_EQUAL: + return *this >= other; + default: + return false; + } + } + + static DependencyVersionOperator string_to_operator(const std::string& op); + static bool matches_pattern(const std::string& version); +}; diff --git a/src/content/PacksManager.cpp b/src/content/PacksManager.cpp index 0e9925c8..2d9759cb 100644 --- a/src/content/PacksManager.cpp +++ b/src/content/PacksManager.cpp @@ -3,6 +3,7 @@ #include #include +#include "ContentPackVersion.hpp" #include "util/listutil.hpp" PacksManager::PacksManager() = default; @@ -90,7 +91,7 @@ static bool resolve_dependencies( } auto found = packs.find(dep.id); bool exists = found != packs.end(); - if (!exists && dep.level == DependencyLevel::required) { + if (!exists && dep.level == DependencyLevel::REQUIRED) { throw contentpack_error( dep.id, io::path(), "dependency of '" + pack->id + "'" ); @@ -99,15 +100,32 @@ static bool resolve_dependencies( // ignored for optional or weak dependencies continue; } - if (resolveWeaks && dep.level == DependencyLevel::weak) { + if (resolveWeaks && dep.level == DependencyLevel::WEAK) { // dependency pack is found but not added yet // resolveWeaks is used on second iteration, so it's will not be // added continue; } + auto dep_pack = found -> second; + + if (Version::matches_pattern(dep.version) && Version::matches_pattern(dep_pack.version) + && Version(dep_pack.version) + .process_operator(dep.op, Version(dep.version)) + ) { + // dependency pack version meets the required one + continue; + } else if (dep.version == "*" || dep.version == dep_pack.version){ + // fallback: dependency pack version also meets required one + continue; + } else { + throw contentpack_error( + dep.id, io::path(), "does not meet required version '" + dep.op + dep.version +"' of '" + pack->id + "'" + ); + } + if (!util::contains(allNames, dep.id) && - dep.level != DependencyLevel::weak) { + dep.level != DependencyLevel::WEAK) { allNames.push_back(dep.id); queue.push(&found->second); } diff --git a/src/content/loading/BlockLoader.cpp b/src/content/loading/BlockLoader.cpp index 65e35a69..a8615e3f 100644 --- a/src/content/loading/BlockLoader.cpp +++ b/src/content/loading/BlockLoader.cpp @@ -1,5 +1,6 @@ #define VC_ENABLE_REFLECTION #include "ContentUnitLoader.hpp" +#include "ContentLoadingCommons.hpp" #include "../ContentBuilder.hpp" #include "coders/json.hpp" @@ -87,20 +88,8 @@ template<> void ContentUnitLoader::loadUnit( Block& def, const std::string& name, const io::path& file ) { auto root = io::read_json(file); - if (def.properties == nullptr) { - def.properties = dv::object(); - def.properties["name"] = name; - } - for (auto& [key, value] : root.asObject()) { - auto pos = key.rfind('@'); - if (pos == std::string::npos) { - def.properties[key] = value; - continue; - } - auto field = key.substr(0, pos); - auto suffix = key.substr(pos + 1); - process_method(def.properties, suffix, field, value); - } + process_properties(def, name, root); + process_tags(def, root); if (root.has("parent")) { const auto& parentName = root["parent"].asString(); diff --git a/src/content/loading/ContentLoadingCommons.hpp b/src/content/loading/ContentLoadingCommons.hpp new file mode 100644 index 00000000..a4a81c98 --- /dev/null +++ b/src/content/loading/ContentLoadingCommons.hpp @@ -0,0 +1,37 @@ +#pragma once + +#include "data/dv.hpp" + +#include + +template +inline void process_properties(T& def, const std::string& name, const dv::value& root) { + if (def.properties == nullptr) { + def.properties = dv::object(); + def.properties["name"] = name; + } + for (auto& [key, value] : root.asObject()) { + auto pos = key.rfind('@'); + if (pos == std::string::npos) { + def.properties[key] = value; + continue; + } + auto field = key.substr(0, pos); + auto suffix = key.substr(pos + 1); + process_method(def.properties, suffix, field, value); + } +} + +template +inline void process_tags(T& def, const dv::value& root) { + if (!root.has("tags")) { + return; + } + const auto& tags = root["tags"]; + for (const auto& tagValue : tags) { + if (!tagValue.isString()) { + continue; + } + def.tags.push_back(tagValue.asString()); + } +} diff --git a/src/content/loading/EntityLoader.cpp b/src/content/loading/EntityLoader.cpp index 7778282c..29a72cb7 100644 --- a/src/content/loading/EntityLoader.cpp +++ b/src/content/loading/EntityLoader.cpp @@ -30,7 +30,18 @@ template<> void ContentUnitLoader::loadUnit( if (auto found = root.at("components")) { for (const auto& elem : *found) { - def.components.emplace_back(elem.asString()); + std::string name; + dv::value params; + if (elem.isObject()) { + name = elem["name"].asString(); + if (elem.has("args")) { + params = elem["args"]; + } + } else { + name = elem.asString(); + } + def.components.push_back(ComponentInstance { + std::move(name), std::move(params)}); } } if (auto found = root.at("hitbox")) { diff --git a/src/content/loading/ItemLoader.cpp b/src/content/loading/ItemLoader.cpp index 6367ccd5..cba07771 100644 --- a/src/content/loading/ItemLoader.cpp +++ b/src/content/loading/ItemLoader.cpp @@ -1,5 +1,6 @@ #define VC_ENABLE_REFLECTION #include "ContentUnitLoader.hpp" +#include "ContentLoadingCommons.hpp" #include "../ContentBuilder.hpp" #include "coders/json.hpp" @@ -12,11 +13,13 @@ static debug::Logger logger("item-content-loader"); + template<> void ContentUnitLoader::loadUnit( ItemDef& def, const std::string& name, const io::path& file ) { auto root = io::read_json(file); - def.properties = root; + process_properties(def, name, root); + process_tags(def, root); if (root.has("parent")) { const auto& parentName = root["parent"].asString(); @@ -29,6 +32,7 @@ template<> void ContentUnitLoader::loadUnit( parentDef->cloneTo(def); } root.at("caption").get(def.caption); + root.at("description").get(def.description); std::string iconTypeStr = ""; root.at("icon-type").get(iconTypeStr); diff --git a/src/data/dv_util.hpp b/src/data/dv_util.hpp index 3f09ffde..ffec61dd 100644 --- a/src/data/dv_util.hpp +++ b/src/data/dv_util.hpp @@ -46,12 +46,12 @@ namespace dv { if (!map.has(key)) { return; } - auto& list = map[key]; + const auto& srcList = map[key]; for (size_t i = 0; i < n; i++) { if constexpr (std::is_floating_point()) { - vec[i] = list[i].asNumber(); + vec[i] = srcList[i].asNumber(); } else { - vec[i] = list[i].asInteger(); + vec[i] = srcList[i].asInteger(); } } } diff --git a/src/devtools/Project.cpp b/src/devtools/Project.cpp index c1d382ee..881f6cc7 100644 --- a/src/devtools/Project.cpp +++ b/src/devtools/Project.cpp @@ -1,6 +1,9 @@ #include "Project.hpp" #include "data/dv_util.hpp" +#include "logic/scripting/scripting.hpp" + +Project::~Project() = default; dv::value Project::serialize() const { return dv::object({ diff --git a/src/devtools/Project.hpp b/src/devtools/Project.hpp index 857b58f1..be9d88a3 100644 --- a/src/devtools/Project.hpp +++ b/src/devtools/Project.hpp @@ -2,13 +2,21 @@ #include #include +#include #include "interfaces/Serializable.hpp" +namespace scripting { + class IClientProjectScript; +} + struct Project : Serializable { std::string name; std::string title; std::vector basePacks; + std::unique_ptr clientScript; + + ~Project(); dv::value serialize() const override; void deserialize(const dv::value& src) override; diff --git a/src/engine/Engine.cpp b/src/engine/Engine.cpp index 6596733f..212765f3 100644 --- a/src/engine/Engine.cpp +++ b/src/engine/Engine.cpp @@ -60,6 +60,17 @@ static std::unique_ptr load_icon() { return nullptr; } +static std::unique_ptr load_client_project_script() { + io::path scriptFile = "project:project_client.lua"; + if (io::exists(scriptFile)) { + logger.info() << "starting project script"; + return scripting::load_client_project_script(scriptFile); + } else { + logger.warning() << "project script does not exists"; + } + return nullptr; +} + Engine::Engine() = default; Engine::~Engine() = default; @@ -72,6 +83,74 @@ Engine& Engine::getInstance() { return *instance; } +void Engine::onContentLoad() { + editor->loadTools(); + langs::setup(langs::get_current(), paths.resPaths.collectRoots()); + + if (isHeadless()) { + return; + } + for (auto& pack : content->getAllContentPacks()) { + auto configFolder = pack.folder / "config"; + auto bindsFile = configFolder / "bindings.toml"; + if (io::is_regular_file(bindsFile)) { + input->getBindings().read( + toml::parse( + bindsFile.string(), io::read_string(bindsFile) + ), + BindType::BIND + ); + } + } + loadAssets(); +} + +void Engine::initializeClient() { + std::string title = project->title; + if (title.empty()) { + title = "VoxelCore v" + + std::to_string(ENGINE_VERSION_MAJOR) + "." + + std::to_string(ENGINE_VERSION_MINOR); + } + if (ENGINE_DEBUG_BUILD) { + title += " [debug]"; + } + auto [window, input] = Window::initialize(&settings.display, title); + if (!window || !input){ + throw initialize_error("could not initialize window"); + } + window->setFramerate(settings.display.framerate.get()); + + time.set(window->time()); + if (auto icon = load_icon()) { + icon->flipY(); + window->setIcon(icon.get()); + } + this->window = std::move(window); + this->input = std::move(input); + + loadControls(); + + gui = std::make_unique(*this); + if (ENGINE_DEBUG_BUILD) { + menus::create_version_label(*gui); + } + keepAlive(settings.display.fullscreen.observe( + [this](bool value) { + if (value != this->window->isFullscreen()) { + this->window->toggleFullscreen(); + } + }, + true + )); + keepAlive(settings.debug.doTraceShaders.observe( + [](bool value) { + Shader::preprocessor->setTraceOutput(value); + }, + true + )); +} + void Engine::initialize(CoreParameters coreParameters) { params = std::move(coreParameters); settingsHandler = std::make_unique(settings); @@ -100,78 +179,28 @@ void Engine::initialize(CoreParameters coreParameters) { controller = std::make_unique(*this); if (!params.headless) { - std::string title = project->title; - if (title.empty()) { - title = "VoxelCore v" + - std::to_string(ENGINE_VERSION_MAJOR) + "." + - std::to_string(ENGINE_VERSION_MINOR); - } - if (ENGINE_DEBUG_BUILD) { - title += " [debug]"; - } - auto [window, input] = Window::initialize(&settings.display, title); - if (!window || !input){ - throw initialize_error("could not initialize window"); - } - window->setFramerate(settings.display.framerate.get()); - - time.set(window->time()); - if (auto icon = load_icon()) { - icon->flipY(); - window->setIcon(icon.get()); - } - this->window = std::move(window); - this->input = std::move(input); - - loadControls(); - - gui = std::make_unique(*this); - if (ENGINE_DEBUG_BUILD) { - menus::create_version_label(*gui); - } - keepAlive(settings.display.fullscreen.observe( - [this](bool value) { - if (value != this->window->isFullscreen()) { - this->window->toggleFullscreen(); - } - }, - true - )); + initializeClient(); } audio::initialize(!params.headless, settings.audio); - bool langNotSet = settings.ui.language.get() == "auto"; - if (langNotSet) { + if (settings.ui.language.get() == "auto") { settings.ui.language.set( langs::locale_by_envlocale(platform::detect_locale()) ); } content = std::make_unique(*project, paths, *input, [this]() { - editor->loadTools(); - langs::setup(langs::get_current(), paths.resPaths.collectRoots()); - if (!isHeadless()) { - for (auto& pack : content->getAllContentPacks()) { - auto configFolder = pack.folder / "config"; - auto bindsFile = configFolder / "bindings.toml"; - if (io::is_regular_file(bindsFile)) { - input->getBindings().read( - toml::parse( - bindsFile.string(), io::read_string(bindsFile) - ), - BindType::BIND - ); - } - } - loadAssets(); - } + onContentLoad(); }); scripting::initialize(this); + if (!isHeadless()) { gui->setPageLoader(scripting::create_page_loader()); } keepAlive(settings.ui.language.observe([this](auto lang) { langs::setup(lang, paths.resPaths.collectRoots()); }, true)); + + project->clientScript = load_client_project_script(); } void Engine::loadSettings() { @@ -286,6 +315,7 @@ void Engine::close() { audio::close(); network.reset(); clearKeepedObjects(); + project.reset(); scripting::close(); logger.info() << "scripting finished"; if (!params.headless) { @@ -345,10 +375,19 @@ void Engine::loadProject() { } void Engine::setScreen(std::shared_ptr screen) { + if (project->clientScript && this->screen) { + project->clientScript->onScreenChange(this->screen->getName(), false); + } // reset audio channels (stop all sources) audio::reset_channel(audio::get_channel_index("regular")); audio::reset_channel(audio::get_channel_index("ambient")); this->screen = std::move(screen); + if (this->screen) { + this->screen->onOpen(); + } + if (project->clientScript && this->screen) { + project->clientScript->onScreenChange(this->screen->getName(), true); + } } void Engine::onWorldOpen(std::unique_ptr level, int64_t localPlayer) { diff --git a/src/engine/Engine.hpp b/src/engine/Engine.hpp index 8f8683da..98bcd497 100644 --- a/src/engine/Engine.hpp +++ b/src/engine/Engine.hpp @@ -82,6 +82,9 @@ class Engine : public util::ObjectsKeeper { void updateHotkeys(); void loadAssets(); void loadProject(); + + void initializeClient(); + void onContentLoad(); public: Engine(); ~Engine(); @@ -174,4 +177,8 @@ public: devtools::Editor& getEditor() { return *editor; } + + const Project& getProject() { + return *project; + } }; diff --git a/src/engine/Mainloop.cpp b/src/engine/Mainloop.cpp index ecfd17bc..a14c5464 100644 --- a/src/engine/Mainloop.cpp +++ b/src/engine/Mainloop.cpp @@ -2,10 +2,13 @@ #include "Engine.hpp" #include "debug/Logger.hpp" +#include "devtools/Project.hpp" #include "frontend/screens/MenuScreen.hpp" #include "frontend/screens/LevelScreen.hpp" #include "window/Window.hpp" #include "world/Level.hpp" +#include "graphics/ui/GUI.hpp" +#include "graphics/ui/elements/Container.hpp" static debug::Logger logger("mainloop"); @@ -36,6 +39,7 @@ void Mainloop::run() { while (!window.isShouldClose()){ time.update(window.time()); engine.updateFrontend(); + if (!window.isIconified()) { engine.renderFrame(); } diff --git a/src/frontend/UiDocument.cpp b/src/frontend/UiDocument.cpp index 7c6f8d13..3e9a3a27 100644 --- a/src/frontend/UiDocument.cpp +++ b/src/frontend/UiDocument.cpp @@ -14,11 +14,13 @@ UiDocument::UiDocument( const std::shared_ptr& root, scriptenv env ) : id(std::move(id)), script(script), root(root), env(std::move(env)) { - gui::UINode::getIndices(root, map); + rebuildIndices(); } void UiDocument::rebuildIndices() { + map.clear(); gui::UINode::getIndices(root, map); + map["root"] = root; } const UINodesMap& UiDocument::getMap() const { diff --git a/src/frontend/debug_panel.cpp b/src/frontend/debug_panel.cpp index 06ab5fc8..10c6b85c 100644 --- a/src/frontend/debug_panel.cpp +++ b/src/frontend/debug_panel.cpp @@ -12,12 +12,14 @@ #include "graphics/render/WorldRenderer.hpp" #include "graphics/render/ParticlesRenderer.hpp" #include "graphics/render/ChunksRenderer.hpp" +#include "graphics/render/DebugLinesRenderer.hpp" #include "logic/scripting/scripting.hpp" #include "network/Network.hpp" #include "objects/Player.hpp" #include "objects/Players.hpp" #include "objects/Entities.hpp" #include "objects/EntityDef.hpp" +#include "objects/Entity.hpp" #include "physics/Hitbox.hpp" #include "util/stringutil.hpp" #include "voxels/Block.hpp" @@ -44,6 +46,7 @@ static std::shared_ptr