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 8abcb93c..7bef0766 100644 --- a/doc/en/item-properties.md +++ b/doc/en/item-properties.md @@ -66,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 b9a1044f..5eb767f9 100644 --- a/doc/en/main-page.md +++ b/doc/en/main-page.md @@ -1,8 +1,8 @@ # Documentation -Documentation for 0.29 (in-development). +Documentation for 0.30 (in development). -[Documentation for release 0.28](https://github.com/MihailRis/voxelcore/blob/release-0.28/doc/en/main-page.md) +[Documentation for 0.29.](https://github.com/MihailRis/voxelcore/blob/release-0.29/doc/ru/main-page.md) ## Sections diff --git a/doc/en/scripting.md b/doc/en/scripting.md index 131b9cfa..9b2a0297 100644 --- a/doc/en/scripting.md +++ b/doc/en/scripting.md @@ -32,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/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 8d3b07f8..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 diff --git a/doc/en/scripting/builtins/libitem.md b/doc/en/scripting/builtins/libitem.md index 25a7e48f..d8b42f5c 100644 --- a/doc/en/scripting/builtins/libitem.md +++ b/doc/en/scripting/builtins/libitem.md @@ -33,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/libplayer.md b/doc/en/scripting/builtins/libplayer.md index fa799198..b406f694 100644 --- a/doc/en/scripting/builtins/libplayer.md +++ b/doc/en/scripting/builtins/libplayer.md @@ -89,6 +89,13 @@ player.set_loading_chunks(playerid: int, bool) Getter and setter of the property that determines whether the player is loading chunks. +```lua +player.get_interaction_distance(playerid: int) -> float +player.set_interaction_distance(playerid: int, distance: float) +``` + +Getter and setter of the property for max interaction distance. + ``` lua player.set_spawnpoint(playerid: int, x: number, y: number, z: number) player.get_spawnpoint(playerid: int) -> number, number, number 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 8a75ca72..cb45827a 100644 --- a/doc/ru/item-properties.md +++ b/doc/ru/item-properties.md @@ -65,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 b823042c..60d0794d 100644 --- a/doc/ru/main-page.md +++ b/doc/ru/main-page.md @@ -1,8 +1,8 @@ # Документация -Документация версии 0.29 (в разработке). +Документация версии 0.30 (в разработке). -[Документация версии 0.28](https://github.com/MihailRis/voxelcore/blob/release-0.28/doc/ru/main-page.md) +[Документация 0.29.](https://github.com/MihailRis/voxelcore/blob/release-0.29/doc/ru/main-page.md) ## Разделы diff --git a/doc/ru/scripting.md b/doc/ru/scripting.md index aa25e072..d8f1b3be 100644 --- a/doc/ru/scripting.md +++ b/doc/ru/scripting.md @@ -32,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/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 67547909..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 diff --git a/doc/ru/scripting/builtins/libitem.md b/doc/ru/scripting/builtins/libitem.md index 265bf099..2c312dc4 100644 --- a/doc/ru/scripting/builtins/libitem.md +++ b/doc/ru/scripting/builtins/libitem.md @@ -33,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 16f9dd5c..69fe42d5 100644 --- a/doc/ru/scripting/builtins/libnetwork.md +++ b/doc/ru/scripting/builtins/libnetwork.md @@ -6,9 +6,15 @@ ```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-Соединения 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/libplayer.md b/doc/ru/scripting/builtins/libplayer.md index 2228f42c..c40cd86d 100644 --- a/doc/ru/scripting/builtins/libplayer.md +++ b/doc/ru/scripting/builtins/libplayer.md @@ -89,6 +89,13 @@ player.set_loading_chunks(playerid: int, bool) Геттер и сеттер свойства, определяющего, прогружает ли игрок чанки вокруг. +```lua +player.get_interaction_distance(playerid: int) -> float +player.set_interaction_distance(playerid: int, distance: float) +``` + +Геттер и сеттер свойства, определяющего максимальную дистанцию взаимодействия. + ```lua player.set_spawnpoint(playerid: int, x: number, y: number, z: number) player.get_spawnpoint(playerid: int) -> number, number, number 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/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/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/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 4aeca4d9..f270a6d5 100644 --- a/res/content/base/package.json +++ b/res/content/base/package.json @@ -1,6 +1,6 @@ { "id": "base", "title": "Base", - "version": "0.29", + "version": "0.30", "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/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/worlds.xml b/res/layouts/pages/worlds.xml index 13765930..a8a536a2 100644 --- a/res/layouts/pages/worlds.xml +++ b/res/layouts/pages/worlds.xml @@ -1,5 +1,4 @@ - 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/maths_inline.lua b/res/modules/internal/maths_inline.lua index e3e37fcd..36c0aecf 100644 --- a/res/modules/internal/maths_inline.lua +++ b/res/modules/internal/maths_inline.lua @@ -110,6 +110,21 @@ 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 ======================= -- -- =================================================== -- @@ -210,3 +225,16 @@ 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/schedule.lua b/res/modules/schedule.lua index 5d24a086..457cbf90 100644 --- a/res/modules/schedule.lua +++ b/res/modules/schedule.lua @@ -11,6 +11,9 @@ local Schedule = { 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 diff --git a/res/project_client.lua b/res/project_client.lua index a1952c08..03c43a4b 100644 --- a/res/project_client.lua +++ b/res/project_client.lua @@ -23,4 +23,5 @@ function on_menu_setup() 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 0d422b04..d7c93f31 100644 --- a/res/scripts/classes.lua +++ b/res/scripts/classes.lua @@ -74,6 +74,38 @@ 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_tcp(port)}, ServerSocket) @@ -131,10 +163,80 @@ local function clean(iterable, checkFun, ...) 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 @@ -160,6 +262,22 @@ network.__process_events = function() 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 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 abd1eaf4..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,39 +114,8 @@ function on_hud_open() end) configure_SSAO() -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() - 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)) + hud.default_hand_controller = update_hand end function on_hud_render() diff --git a/res/scripts/post_content.lua b/res/scripts/post_content.lua index 2ad3633b..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,6 +58,19 @@ 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) 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 fa93d2c9..b08e8c02 100644 --- a/res/scripts/stdlib.lua +++ b/res/scripts/stdlib.lua @@ -79,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 @@ -174,6 +181,7 @@ if enable_experimental then require "core:internal/maths_inline" end +asserts = require "core:internal/asserts" events = require "core:internal/events" function pack.unload(prefix) @@ -429,6 +437,8 @@ 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) @@ -440,10 +450,11 @@ local ScheduleGroup_mt = { 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 + end, } } @@ -451,6 +462,7 @@ local function ScheduleGroup() return setmetatable({ _next_schedule = 1, _schedules = {}, + common = Schedule() }, ScheduleGroup_mt) end @@ -527,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 @@ -573,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 669d5874..742c9506 100644 --- a/res/scripts/stdmin.lua +++ b/res/scripts/stdmin.lua @@ -668,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/be_BY.txt b/res/texts/be_BY.txt index 879e5a35..1fb2a993 100644 --- a/res/texts/be_BY.txt +++ b/res/texts/be_BY.txt @@ -25,6 +25,7 @@ Grant %{0} pack modification permission?=Выдаць дазвол на мады Error at line %{0}=Памылка ў радку %{0} Run=Запусціць Filter=Фільтр +Are you sure you want to open the link: =Ці вы ўпэўненыя, што хочаце адкрыць спасылку: editor.info.tooltip=CTRL+S - Захаваць\nCTRL+R - Запусціць\nCTRL+Z - Скасаваць\nCTRL+Y - Паўтарыць devtools.traceback=Стэк выклікаў (ад апошняга) diff --git a/res/texts/de_DE.txt b/res/texts/de_DE.txt index 8e125159..81c39571 100644 --- a/res/texts/de_DE.txt +++ b/res/texts/de_DE.txt @@ -7,6 +7,7 @@ Back=Zurück Continue=Weitermachen Add=Hinzufügen Converting world...=Weltkonvertierung im Gange... +Are you sure you want to open the link: =Sind Sie sicher, dass Sie den Link öffnen möchten: error.pack-not-found=Paket konnte nicht gefunden werden error.dependency-not-found=Die verwendete Abhängigkeit wurde nicht gefunden diff --git a/res/texts/en_US.txt b/res/texts/en_US.txt index b3be4c92..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 diff --git a/res/texts/fi_FI.txt b/res/texts/fi_FI.txt index 52f4b3e0..6a21f73f 100644 --- a/res/texts/fi_FI.txt +++ b/res/texts/fi_FI.txt @@ -19,6 +19,7 @@ Problems=Ongelmia Monitor=Valvonta Debug=Virheenkorjaus File=Tiedosto +Are you sure you want to open the link: =Haluatko varmasti avata linkin: devtools.traceback=Puhelupino (viimeisestä) error.pack-not-found=Pakettia ei löytynyt! diff --git a/res/texts/pl_PL.txt b/res/texts/pl_PL.txt index caae775b..3e039824 100644 --- a/res/texts/pl_PL.txt +++ b/res/texts/pl_PL.txt @@ -7,6 +7,7 @@ Back=Powrót Continue=Kontynuacja Add=Dodać Converting world...=Konwersja świata w toku... +Are you sure you want to open the link: =Czy na pewno chcesz otworzyć link: error.pack-not-found=Nie udało się znaleźć pakietu diff --git a/res/texts/ru_RU.txt b/res/texts/ru_RU.txt index 5d8d22ca..1c650560 100644 --- a/res/texts/ru_RU.txt +++ b/res/texts/ru_RU.txt @@ -25,6 +25,7 @@ Grant %{0} pack modification permission?=Выдать разрешение на Error at line %{0}=Ошибка на строке %{0} Run=Запустить Filter=Фильтр +Are you sure you want to open the link: =Вы уверены, что хотите открыть ссылку: editor.info.tooltip=CTRL+S - Сохранить\nCTRL+R - Запустить\nCTRL+Z - Отменить\nCTRL+Y - Повторить devtools.traceback=Стек вызовов (от последнего) @@ -32,6 +33,7 @@ devtools.output=Вывод error.pack-not-found=Не удалось найти пакет error.dependency-not-found=Используемая зависимость не найдена +error.dependency-version-not-met=Версия зависимости не соответствует необходимой pack.remove-confirm=Удалить весь поставляемый паком/паками контент из мира (безвозвратно)? # Подсказки diff --git a/res/texts/uk_UA.txt b/res/texts/uk_UA.txt index 7ce046ea..a288f516 100644 --- a/res/texts/uk_UA.txt +++ b/res/texts/uk_UA.txt @@ -23,6 +23,7 @@ devtools.traceback=Стек викликів (від останнього) error.pack-not-found=Не вдалося знайти пакет error.dependency-not-found=Використовувана залежність не знайдена pack.remove-confirm=Видалити весь контент, що постачається паком/паками зі світу (безповоротно)? +Are you sure you want to open the link: =Ви впевнені, що хочете відкрити посилання: # Меню menu.Apply=Застосувати diff --git a/res/texts/uz_UZ.txt b/res/texts/uz_UZ.txt index 845c0cf5..edae1c02 100644 --- a/res/texts/uz_UZ.txt +++ b/res/texts/uz_UZ.txt @@ -24,6 +24,7 @@ Save=Saqlash Grant %{0} pack modification permission?=%{0} to‘plamini o‘zgartirish ruxsatini berilsinmi? Error at line %{0}=%{0}-qatorida xatolik Run=Ishga tushirish +Are you sure you want to open the link: =Haqiqatan ham havolani ochmoqchimisiz: editor.info.tooltip=CTRL+S - Saqlash\nCTRL+R - Ishga tushirish\nCTRL+Z - Bekor qilish\nCTRL+Y - Qayta bajarish devtools.traceback=Chaqiruvlar steki (so`nggisidan boshlab) 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/constants.hpp b/src/constants.hpp index 0a6df9e5..6544316e 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 = 29; +inline constexpr int ENGINE_VERSION_MINOR = 30; #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.29"; +inline const std::string ENGINE_VERSION_STRING = "0.30"; /// @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 5ee68859..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)), diff --git a/src/content/Content.hpp b/src/content/Content.hpp index 81e816b6..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,6 +213,14 @@ 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; 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 1703b778..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(); 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/engine/Engine.cpp b/src/engine/Engine.cpp index 0288d609..212765f3 100644 --- a/src/engine/Engine.cpp +++ b/src/engine/Engine.cpp @@ -143,6 +143,12 @@ void Engine::initializeClient() { }, true )); + keepAlive(settings.debug.doTraceShaders.observe( + [](bool value) { + Shader::preprocessor->setTraceOutput(value); + }, + true + )); } void Engine::initialize(CoreParameters coreParameters) { 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