Merge pull request #618 from MihailRis/release-0.29

Release 0.29
This commit is contained in:
MihailRis 2025-09-20 02:40:59 +03:00 committed by GitHub
commit e08c51aa43
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
241 changed files with 7951 additions and 1861 deletions

View File

@ -2,9 +2,9 @@ name: x86-64 AppImage
on:
push:
branches: [ "main", "release-**"]
branches: [ "main", "dev", "release-**"]
pull_request:
branches: [ "main" ]
branches: [ "main", "dev" ]
jobs:
build-appimage:

View File

@ -2,9 +2,9 @@ name: Macos Build
on:
push:
branches: [ "main", "release-**"]
branches: [ "main", "dev", "release-**"]
pull_request:
branches: [ "main" ]
branches: [ "main", "dev" ]
jobs:
build-dmg:

View File

@ -2,9 +2,9 @@ name: Windows Build (CLang)
on:
push:
branches: [ "main", "release-**"]
branches: [ "main", "dev", "release-**"]
pull_request:
branches: [ "main" ]
branches: [ "main", "dev" ]
jobs:
build-windows:

View File

@ -2,9 +2,9 @@ name: MSVC Build
on:
push:
branches: [ "main", "release-**"]
branches: [ "main", "dev", "release-**"]
pull_request:
branches: [ "main" ]
branches: [ "main", "dev" ]
jobs:
build-windows:

1
.gitignore vendored
View File

@ -9,6 +9,7 @@ Debug/voxel_engine
/export
/config
/out
/projects
/misc
/world

View File

@ -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)

View File

@ -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)

45
dev/tests/network_tcp.lua Normal file
View File

@ -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

View File

@ -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" = [
# ...
]
``

View File

@ -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
{

View File

@ -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

View File

@ -18,6 +18,14 @@ Name of the item model. The model will be loaded automatically.
Default value is `packid:itemname.model`.
If the model is not specified, an automatic one will be generated.
### Caption and Description
`caption` - name of item in inventory
`description` - item description in inventory
this props allow to use `md`
*see [Text Styles](/doc/en/text-styles.md)*
## Behaviour
### *placing-block*
@ -58,3 +66,29 @@ Property status is displayed in the inventory interface. Display method is defin
- `number` - number
- `relation` - current value to initial value (x/y)
- `vbar` - vertical scale (used by default)
## Tags
Tags allow you to designate general properties of items. Names should be formatted as `prefix:tag_name`.
The prefix is optional, but helps avoid unwanted logical collisions. Example:
```json
{
"tags": [
"core:fuel",
"base_survival:poison",
]
}
```
Tags can also be added to items from other packs using the `your_pack:tags.toml` file. Example
```toml
"prefix:tag_name" = [
"random_pack:item",
"another_pack:some_block",
]
"other_prefix:other_tag_name" = [
# ...
]
```

View File

@ -1,6 +1,6 @@
# Documentation
Documentation for release 0.28.
Documentation for 0.29.
## Sections

View File

@ -10,6 +10,7 @@ Subsections:
- [Entities and components](scripting/ecs.md)
- [Libraries](#)
- [app](scripting/builtins/libapp.md)
- [assets](scripting/builtins/libassets.md)
- [base64](scripting/builtins/libbase64.md)
- [bjson, json, toml, yaml](scripting/filesystem.md)
- [block](scripting/builtins/libblock.md)
@ -20,6 +21,7 @@ Subsections:
- [gfx.blockwraps](scripting/builtins/libgfx-blockwraps.md)
- [gfx.particles](particles.md#gfxparticles-library)
- [gfx.posteffects](scripting/builtins/libgfx-posteffects.md)
- [gfx.skeletons](scripting/builtins/libgfx-skeletons.md)
- [gfx.text3d](3d-text.md#gfxtext3d-library)
- [gfx.weather](scripting/builtins/libgfx-weather.md)
- [gui](scripting/builtins/libgui.md)
@ -30,8 +32,10 @@ Subsections:
- [mat4](scripting/builtins/libmat4.md)
- [network](scripting/builtins/libnetwork.md)
- [pack](scripting/builtins/libpack.md)
- [pathfinding](scripting/builtins/libpathfinding.md)
- [player](scripting/builtins/libplayer.md)
- [quat](scripting/builtins/libquat.md)
- [random](scripting/builtins/librandom.md)
- [rules](scripting/builtins/librules.md)
- [time](scripting/builtins/libtime.md)
- [utf8](scripting/builtins/libutf8.md)

View File

@ -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
)
```

View File

@ -0,0 +1,28 @@
# *assets* library
A library for working with audio/visual assets.
## Functions
```lua
-- Loads a texture
assets.load_texture(
-- Array of bytes of an image file
data: table | Bytearray,
-- Texture name after loading
name: str,
-- Image file format (only png is supported)
[optional]
format: str = "png"
)
-- Parses and loads a 3D model
assets.parse_model(
-- Model file format (xml / vcm)
format: str,
-- Contents of the model file
content: str,
-- Model name after loading
name: str
)
```

View File

@ -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
```

View File

@ -68,6 +68,9 @@ block.get_variant(x: int, y: int, z: int) -> int
-- Sets the block variant by index
block.set_variant(x: int, y: int, z: int, index: int) -> int
-- Checks if an block has specified tag
block.has_tag(id: int, tag: str) -> bool
```
## Rotation
@ -119,13 +122,13 @@ block.seek_origin(x: int, y: int, z: int) -> int, int, int
Part of a voxel data used for scripting. Size: 8 bit.
```python
```lua
block.get_user_bits(x: int, y: int, z: int, offset: int, bits: int) -> int
```
Get specified bits as an unsigned integer.
```python
```lua
block.set_user_bits(x: int, y: int, z: int, offset: int, bits: int, value: int) -> int
```
Set specified bits.
@ -151,6 +154,21 @@ The function returns a table with the results or nil if the ray does not hit any
The result will use the destination table instead of creating a new one if the optional argument specified.
## Model
Block model information.
```lua
-- returns block model type (block/aabb/custom/...)
block.get_model(id: int) -> str
-- returns block model name
block.model_name(id: int) -> str
-- returns array of 6 textures assigned to sides of block
block.get_textures(id: int) -> string table
```
## Data fields
```lua

View File

@ -0,0 +1,48 @@
# gfx.skeletons library
A library for working with named skeletons, such as 'hand',
used to control the hand and the carried item displayed in first-person view.
The set of functions is similar to the skeleton component of entities.
The first argument to the function is the name of the skeleton.
```lua
-- Returns an object wrapper over the skeleton
local skeleton = gfx.skeletons.get(name: str)
-- Returns the index of the bone by name or nil
skeleton:index(name: str) -> int
-- Returns the name of the model assigned to the bone with the specified index
skeleton:get_model(index: int) -> str
-- Reassigns the model of the bone with the specified index
-- Resets to the original if you do not specify a name
skeleton:set_model(index: int, name: str)
-- Returns the transformation matrix of the bone with the specified index
skeleton:get_matrix(index: int) -> mat4
-- Sets the transformation matrix of the bone with the specified index
skeleton:set_matrix(index: int, matrix: mat4)
-- Returns the texture by key (dynamically assigned textures - '$name')
skeleton:get_texture(key: str) -> str
-- Assigns a texture by key
skeleton:set_texture(key: str, value: str)
-- Checks the visibility status of a bone by index
-- or the entire skeleton if index is not specified
skeleton:is_visible([optional] index: int) -> bool
-- Sets the visibility status of a bone by index
-- or the entire skeleton if index is not specified
skeleton:set_visible([optional] index: int, status: bool)
-- Returns the color of the entity
skeleton:get_color() -> vec3
-- Sets the color of the entity
skeleton:set_color(color: vec3)
```

View File

@ -103,3 +103,9 @@ gui.load_document(
```
Loads a UI document with its script, returns the name of the document if successfully loaded.
```lua
gui.root: Document
```
Root UI document

View File

@ -65,4 +65,7 @@ hud.is_inventory_open() -> bool
-- Sets whether to allow pausing. If false, the pause menu will not pause the game.
hud.set_allow_pause(flag: bool)
-- Function that controls the named skeleton 'hand' (see gfx.skeletons)
hud.hand_controller: function()
```

View File

@ -97,6 +97,40 @@ inventory.set(...)
inventory.set_all_data(...)
```
for moving is inefficient, use inventory.move or inventory.move_range.
```lua
-- Get item caption
inventory.get_caption(
-- id of inventory
invid: int,
-- slot id
slot: int
)
-- Set item caption
inventory.set_caption(
-- id of inventory
invid: int,
-- slot id
slot: int,
-- Item Caption
caption: string
)
-- Get item description
inventory.get_description(
-- id of inventory
invid: int,
-- slot id
slot: int
)
-- Set item description
inventory.set_description(
-- id of inventory
invid: int,
-- slot id
slot: int,
-- Item Description
description: string
)
```
```lua
-- Returns a copy of value of a local property of an item by name or nil.

View File

@ -10,6 +10,9 @@ item.index(name: str) -> int
-- Returns the item display name.
block.caption(blockid: int) -> str
-- Returns the item display description.
item.description(itemid: int) -> str
-- Returns max stack size for the item
item.stack_size(itemid: int) -> int
@ -30,4 +33,7 @@ item.emission(itemid: int) -> str
-- Returns the value of the `uses` property
item.uses(itemid: int) -> int
-- Checks if an item has specified tag
item.has_tag(itemid: int, tag: str) -> bool
```

View File

@ -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<str>
)
-- 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<str>
)
-- 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<str>
)
```
## TCP Connections

View File

@ -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<vec3> 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<vec3>
--- 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<vec3> 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
)
```

View File

@ -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)
```

View File

@ -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}

View File

@ -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)
```

View File

@ -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)
```

View File

@ -306,3 +306,29 @@
### `имя_свойства@append`
Добавляет элементы в конец списка, вместо его полной перезаписи.
## Теги - *tags*
Теги позволяют обозначать обобщённые свойства блоков. Названия следует формировать как `префикс:имя_тега`.
Префикс не является обязательным, но позволяет избегать нежелательных логических коллизий. Пример:
```json
{
"tags": [
"core:ore",
"base_survival:food",
]
}
```
Теги блокам можно добавлять и из других паков, с помощью файла аш_пак:tags.toml`. Пример
```toml
"префикс:имя_тега" = [
"рандомный_пак:какой_то_блок",
"ещё_один_пак:предмет",
]
ругой_префиксругое_имя_тега" = [
# ...
]
```

View File

@ -32,6 +32,11 @@
Пример: '~randutil' - слабая зависимость 'randutil'.
Версии зависимостей указываются после '@' и имеют операторы для ограничения допустимых версий.
Отсутствие версии зависимости интерпретируется как '\*', т.е. любая версия.
Пример: 'randutil@>=1.0' - зависимость 'randutil' версии 1.0 и старше.
Пример:
```json
{

View File

@ -20,6 +20,22 @@
]
```
Из конфигурации сущности можно передавать значения в ARGS.
Они будут передаваться как при создании новой сущности, так и при загрузке сохранённой.
Для этого используется список `args`:
```json
"components": [
{
"name": "base:drop",
"args": {
"item": "base:stone.item",
"count": 1
}
}
]
```
Код компонентов должен находиться в `scripts/components`.
## Физика

View File

@ -17,6 +17,14 @@
Значение по-умолчанию - `packid:itemname.model`.
Если модель не указана, будет сгенерирована автоматическию
### Имя и Описание
`caption` - имя предмета в инвентаре
`description` - описание предмета в инвентаре
Можно использовать `md`
*см. [Text Styles](/doc/en/text-styles.md)*
## Поведение
### Устанавливаемый блок - `placing-block`
@ -57,3 +65,30 @@
- `number` - число
- `relation` - отношение текущего значения к изначальному (x/y)
- `vbar` - вертикальная шкала (используется по-умолчанию)
## Теги - *tags*
Теги позволяют обозначать обобщённые свойства предметов. Названия следует формировать как `префикс:имя_тега`.
Префикс не является обязательным, но позволяет избегать нежелательных логических коллизий. Пример:
```json
{
"tags": [
"core:fuel",
"base_survival:poison",
]
}
```
Теги предметам можно добавлять и из других паков, с помощью файла аш_пак:tags.toml`. Пример
```toml
"префикс:имя_тега" = [
"рандомный_пак:предмет",
"ещё_один_пак:какой_то_блок",
]
ругой_префиксругое_имя_тега" = [
# ...
]
```

View File

@ -1,6 +1,6 @@
# Документация
Документация версии 0.28.
Документация версии 0.29.
## Разделы

View File

@ -10,6 +10,7 @@
- [Сущности и компоненты](scripting/ecs.md)
- [Библиотеки](#)
- [app](scripting/builtins/libapp.md)
- [assets](scripting/builtins/libassets.md)
- [base64](scripting/builtins/libbase64.md)
- [bjson, json, toml, yaml](scripting/filesystem.md)
- [block](scripting/builtins/libblock.md)
@ -20,6 +21,7 @@
- [gfx.blockwraps](scripting/builtins/libgfx-blockwraps.md)
- [gfx.particles](particles.md#библиотека-gfxparticles)
- [gfx.posteffects](scripting/builtins/libgfx-posteffects.md)
- [gfx.skeletons](scripting/builtins/libgfx-skeletons.md)
- [gfx.text3d](3d-text.md#библиотека-gfxtext3d)
- [gfx.weather](scripting/builtins/libgfx-weather.md)
- [gui](scripting/builtins/libgui.md)
@ -30,8 +32,10 @@
- [mat4](scripting/builtins/libmat4.md)
- [network](scripting/builtins/libnetwork.md)
- [pack](scripting/builtins/libpack.md)
- [pathfinding](scripting/builtins/libpathfinding.md)
- [player](scripting/builtins/libplayer.md)
- [quat](scripting/builtins/libquat.md)
- [random](scripting/builtins/librandom.md)
- [rules](scripting/builtins/librules.md)
- [time](scripting/builtins/libtime.md)
- [utf8](scripting/builtins/libutf8.md)

View File

@ -27,7 +27,10 @@ app.sleep_until(
predicate: function() -> bool,
-- максимальное количество тактов цикла движка, после истечения которых
-- будет брошено исключение "max ticks exceed"
[опционально] max_ticks = 1e9
[опционально] max_ticks = 1e9,
-- максимальное длительность ожидания в секундах.
-- (работает с системным временем, включая test-режим)
[опционально] timeout = 1e9
)
```

View File

@ -0,0 +1,28 @@
# Библиотека *assets*
Библиотека для работы с аудио/визуальными загружаемыми ресурсами.
## Функции
```lua
-- Загружает текстуру
assets.load_texture(
-- Массив байт файла изображения
data: table | Bytearray,
-- Имя текстуры после загрузки
name: str,
-- Формат файла изображения (поддерживается только png)
[опционально]
format: str = "png"
)
-- Парсит и загружает 3D модель
assets.parse_model(
-- Формат файла модели (xml / vcm)
format: str,
-- Содержимое файла модели
content: str,
-- Имя модели после загрузки
name: str
)
```

View File

@ -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
```

View File

@ -67,6 +67,9 @@ block.get_variant(x: int, y: int, z: int) -> int
-- Устанавливает вариант блока по индексу
block.set_variant(x: int, y: int, z: int, index: int) -> int
-- Проверяет наличие тега у блока
block.has_tag(id: int, tag: str) -> bool
```
### Raycast
@ -171,6 +174,9 @@ block.get_hitbox(id: int, rotation_index: int) -> {vec3, vec3}
-- возвращает тип модели блока (block/aabb/custom/...)
block.get_model(id: int) -> str
-- возвращает имя модели блока
block.model_name(id: int) -> str
-- возвращает массив из 6 текстур, назначенных на стороны блока
block.get_textures(id: int) -> таблица строк
```

View File

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

View File

@ -0,0 +1,49 @@
# Библиотека gfx.skeletons
Библиотека для работы с именованными скелетами, такими как 'hand',
использующийся для управления, отображаемыми при виде от первого лица,
рукой и переносимым предметом. Набор функций аналогичен компоненту skeleton
у сущностей.
Первым аргументом в функции передаётся имя скелета.
```lua
-- Возвращает объектную обёртку над скелетом
local skeleton = gfx.skeletons.get(name: str)
-- Возвращает индекс кости по имени или nil
skeleton:index(name: str) -> int
-- Возвращает имя модели, назначенной на кость с указанным индексом
skeleton:get_model(index: int) -> str
-- Переназначает модель кости с указанным индексом
-- Сбрасывает до изначальной, если не указывать имя
skeleton:set_model(index: int, name: str)
-- Возвращает матрицу трансформации кости с указанным индексом
skeleton:get_matrix(index: int) -> mat4
-- Устанавливает матрицу трансформации кости с указанным индексом
skeleton:set_matrix(index: int, matrix: mat4)
-- Возвращает текстуру по ключу (динамически назначаемые текстуры - '$имя')
skeleton:get_texture(key: str) -> str
-- Назначает текстуру по ключу
skeleton:set_texture(key: str, value: str)
-- Проверяет статус видимости кости по индесу
-- или всего скелета, если индекс не указан
skeleton:is_visible([опционально] index: int) -> bool
-- Устанавливает статус видимости кости по индексу
-- или всего скелета, если индекс не указан
skeleton:set_visible([опционально] index: int, status: bool)
-- Возвращает цвет сущности
skeleton:get_color() -> vec3
-- Устанавливает цвет сущности
skeleton:set_color(color: vec3)
```

View File

@ -100,3 +100,9 @@ gui.load_document(
```
Загружает UI документ с его скриптом, возвращает имя документа, если успешно загружен.
```lua
gui.root: Document
```
Корневой UI документ

View File

@ -68,4 +68,7 @@ hud.is_inventory_open() -> bool
-- Устанавливает разрешение на паузу. При значении false меню паузы не приостанавливает игру.
hud.set_allow_pause(flag: bool)
-- Функция, управляющая именованным скелетом 'hand' (см. gfx.skeletons)
hud.hand_controller: function()
```

View File

@ -94,6 +94,40 @@ inventory.set(...)
inventory.set_all_data(...)
```
для перемещения вляется неэффективным, используйте inventory.move или inventory.move_range.
```lua
-- Получает имя предмета в слоте
inventory.get_caption(
-- id инвентаря
invid: int,
-- индекс слота
slot: int
)
-- Задает имя предмету в слоте
inventory.set_caption(
-- id инвентаря
invid: int,
-- индекс слота
slot: int,
-- Имя предмета
caption: string
)
-- Получает описание предмета в слоте
inventory.get_description(
-- id инвентаря
invid: int,
-- индекс слота
slot: int
)
-- Задает описание предмету в слоте
inventory.set_description(
-- id инвентаря
invid: int,
-- индекс слота
slot: int,
-- Описание предмета
description: string
)
```
```lua
-- Проверяет наличие локального свойства по имени без копирования его значения.

View File

@ -10,6 +10,9 @@ item.index(name: str) -> int
-- Возвращает название предмета, отображаемое в интерфейсе.
item.caption(itemid: int) -> str
-- Возвращает описание предмета, отображаемое в интерфейсе.
item.description(itemid: int) -> str
-- Возвращает максимальный размер стопки для предмета.
item.stack_size(itemid: int) -> int
@ -30,6 +33,9 @@ item.emission(itemid: int) -> str
-- Возвращает значение свойства `uses`
item.uses(itemid: int) -> int
-- Проверяет наличие тега у предмета
item.has_tag(itemid: int, tag: str) -> bool
```

View File

@ -2,13 +2,19 @@
Библиотека для работы с сетью.
## HTTP-запросы
## HTTP-Запросы
```lua
-- Выполняет GET запрос к указанному URL.
-- После получения ответа, передаёт текст в функцию callback.
-- В случае ошибки в onfailure будет передан HTTP-код ответа.
network.get(url: str, callback: function(str), [опционально] onfailure: function(int))
network.get(
url: str,
-- Функция, вызываемая при получении ответа
callback: function(str),
-- Обработчик ошибок
[опционально] onfailure: function(int, str),
-- Список дополнительных заголовков запроса
[опционально] headers: table<str>
)
-- Пример:
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<str>
)
-- Выполняет 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<str>
)
```
## TCP-Соединения
@ -98,6 +119,65 @@ server:is_open() --> bool
server:get_port() --> int
```
## UDP-Датаграммы
```lua
network.udp_connect(
address: str,
port: int,
-- Функция, вызываемая при получении датаграммы с указанного при открытии сокета адреса и порта
datagramHandler: function(Bytearray),
-- Функция, вызываемая после открытия сокета
-- Опциональна, так как в UDP нет handshake
[опционально] openCallback: function(WriteableSocket),
) --> WriteableSocket
```
Открывает UDP-сокет с привязкой к удалённому адресу и порту
Класс WriteableSocket имеет следующие методы:
```lua
-- Отправляет датаграмму на адрес и порт, заданные при открытии сокета
socket:send(table|Bytearray|str)
-- Закрывает сокет
socket:close()
-- Проверяет открыт ли сокет
socket:is_open() --> bool
-- Возвращает адрес и порт, на которые привязан сокет
socket:get_address() --> str, int
```
```lua
network.udp_open(
port: int,
-- Функция, вызываемая при получении датаграмы
-- В параметры передаётся адрес и порт отправителя, а также сами данные
datagramHandler: function(address: str, port: int, data: Bytearray, server: DatagramServerSocket)
) --> DatagramServerSocket
```
Открывает UDP-сервер на указанном порту
Класс DatagramServerSocket имеет следующие методы:
```lua
-- Отправляет датаграмму на переданный адрес и порт
server:send(address: str, port: int, data: table|Bytearray|str)
-- Завершает принятие датаграмм
server:stop()
-- Проверяет возможность принятия датаграмм
server:is_open() --> bool
-- Возвращает порт, который слушает сервер
server:get_port() --> int
```
## Аналитика
```lua

View File

@ -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<vec3> или 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<vec3>
--- Асинхронное создание маршрута на основе заданных точек.
--- Функция позволяет выполнять поиск пути в фоновом режиме, не блокируя основной поток выполнения
pathfinding.make_route_async(agent: int, start: vec3, target: vec3)
--- Получение маршрута, который агент уже нашел. Используется для получения маршрута после асинхронного поиска.
--- Если поиск ещё не завершён, возвращает nil. Если маршрут не найден, возвращает пустую таблицу.
pathfinding.pull_route(agent: int) --> table<vec3> или nil
--- Установка максимального количества посещенных блоков для агента. Используется для ограничения объема работы алгоритма поиска пути.
pathfinding.set_max_visited(agent: int, max_visited: int)
--- Добавление тега избегаемых блоков
pathfinding.avoid_tag(
agent: int,
-- тег избегаемых блоков
tag: string, [опционально],
-- стоимость пересечения блока
cost: int = 10
)
```

View File

@ -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)
```

View File

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

View File

@ -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}
```

View File

@ -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)
```

View File

@ -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)
```

View File

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

View File

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

View File

@ -1,4 +1,5 @@
{
"texture": "coal_ore",
"tags": ["base:ore"],
"base:durability": 16.0
}

View File

@ -7,5 +7,6 @@
"obstacle": false,
"selectable": false,
"replaceable": true,
"translucent": true
"translucent": true,
"tags": ["core:liquid"]
}

View File

@ -1,2 +1,3 @@
generator = "base:demo"
player-entity = "base:player"
hand-skeleton = "base:hand"

View File

@ -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": [

View File

@ -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]

View File

@ -1,6 +1,6 @@
{
"id": "base",
"title": "Base",
"version": "0.28",
"version": "0.29",
"description": "basic content package"
}

View File

@ -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

View File

@ -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
@ -19,9 +18,10 @@ function on_render()
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)

View File

@ -21,6 +21,9 @@ function on_hud_open()
local ppos = vec3.add({player.get_pos(pid)}, {0, 0.7, 0})
local throw_force = vec3.mul(player.get_dir(pid), DROP_FORCE)
local drop = base_util.drop(ppos, itemid, 1, data, 1.5)
if not drop then
return
end
local velocity = vec3.add(throw_force, vec3.add(pvel, DROP_INIT_VEL))
drop.rigidbody:set_vel(velocity)
end)

View File

@ -1,6 +1,11 @@
function on_block_broken(id, x, y, z, playerid)
if gfx then
gfx.particles.emit({x+0.5, y+0.5, z+0.5}, 64, {
local size = {block.get_size(id)}
gfx.particles.emit({
x + size[1] * 0.5,
y + size[1] * 0.5,
z + size[1] * 0.5
}, 64, {
lifetime=1.0,
spawn_interval=0.0001,
explosion={4, 4, 4},
@ -8,7 +13,7 @@ function on_block_broken(id, x, y, z, playerid)
random_sub_uv=0.1,
size={0.1, 0.1, 0.1},
spawn_shape="box",
spawn_spread={0.4, 0.4, 0.4}
spawn_spread=vec3.mul(size, 0.4)
})
end

View File

@ -0,0 +1,9 @@
{
"root": {
"nodes": [
{
"name": "item"
}
]
}
}

View File

@ -81,6 +81,11 @@ local function refresh_file_title()
document.saveIcon.enabled = edited
document.title.text = gui.str('File')..' - '..current_file.filename
..(edited and ' *' or '')
local info = registry.get_info(current_file.filename)
if info and info.type == "model" then
pcall(run_current_file)
end
end
function on_control_combination(keycode)
@ -118,7 +123,6 @@ function run_current_file()
local unit = info and info.unit
if script_type == "model" then
print(current_file.filename)
clear_output()
local _, err = pcall(reload_model, current_file.filename, unit)
if err then
@ -256,7 +260,7 @@ function open_file_in_editor(filename, line, mutable)
end
function on_open(mode)
registry = require "core:internal/scripts_registry"
registry = __vc_scripts_registry
document.codePanel:setInterval(200, refresh_file_title)

View File

@ -4,7 +4,7 @@ history = session.get_entry("commands_history")
history_pointer = #history
events.on("core:open_traceback", function()
if modes then
if modes and modes.current ~= 'debug' then
modes:set('debug')
end
end)

View File

@ -43,11 +43,8 @@ function build_files_list(filenames, highlighted_part)
end
end
function on_open(mode)
registry = require "core:internal/scripts_registry"
local files_list = document.filesList
function on_open()
registry = __vc_scripts_registry
filenames = registry.filenames
table.sort(filenames)
build_files_list(filenames)

View File

@ -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)
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

View File

@ -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

View File

@ -1,3 +1,12 @@
local WARNING_COLORS = {
{208, 104, 107, 255},
{250, 75, 139, 255},
{250, 151, 75, 255},
{246, 233, 44, 255},
{252, 200, 149, 255}
}
local GENERAL_WARNING_COLOR = {208, 138, 0, 255}
function refresh_search()
local search_text = document.search_textbox.text
@ -40,6 +49,49 @@ function change_sensitivity(val)
refresh_sensitivity()
end
function refresh_binding_marks()
local prev_bindings = {}
local conflicts_colors = {}
local available_colors = table.copy(WARNING_COLORS)
local bindings = input.get_bindings()
table.sort(bindings, function(a, b) return a > b end)
for _, bind_name in ipairs(bindings) do
local key = input.get_binding_text(bind_name)
local prev = prev_bindings[key]
if prev then
local color = GENERAL_WARNING_COLOR
local conflict_color = conflicts_colors[key]
local available_colors_len = #available_colors
if conflict_color then
color = conflict_color
elseif available_colors_len > 0 then
color = available_colors[available_colors_len]
conflicts_colors[key] = color
table.remove(available_colors, available_colors_len)
end
local tooltip = gui.str("settings.Conflict", "settings")
local prev_bindmark = "bindmark_" .. prev
local cur_bindmark = "bindmark_" .. bind_name
document[prev_bindmark].visible = true
document[cur_bindmark].visible = true
document[prev_bindmark].color = color
document[cur_bindmark].color = color
document["bind_" .. prev].tooltip = tooltip
document["bind_" .. bind_name].tooltip = tooltip
else
document["bindmark_" .. bind_name].visible = false
document["bind_" .. bind_name].tooltip = ''
prev_bindings[key] = bind_name
end
end
end
function on_open()
document.sensitivity_track.value = core.get_setting("camera.sensitivity")
refresh_sensitivity()
@ -52,4 +104,8 @@ function on_open()
id=name, name=gui.str(name)
}))
end
document.bindings_panel:setInterval(100, function ()
refresh_binding_marks()
end)
end

View File

@ -1,4 +1,5 @@
<panel size='400,40' padding='4' color='0' orientation='horizontal'>
<panel id='bind_%{id}' size='400,40' padding='4' color='0' orientation='horizontal'>
<bindbox binding='%{id}'/>
<label margin='6'>%{name}</label>
<image id="bindmark_%{id}"src='gui/left_half_block' size='4,28' visible="false"/>
<label id='bindlabel_%{id}' color="#ffffffff" margin='6'>%{name}</label>
</panel>

View File

@ -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

View File

@ -0,0 +1,48 @@
local events = {
handlers = {}
}
function events.on(event, func)
if events.handlers[event] == nil then
events.handlers[event] = {}
end
table.insert(events.handlers[event], func)
end
function events.reset(event, func)
if func == nil then
events.handlers[event] = nil
else
events.handlers[event] = {func}
end
end
function events.remove_by_prefix(prefix)
for name, handlers in pairs(events.handlers) do
local actualname = name
if type(name) == 'table' then
actualname = name[1]
end
if actualname:sub(1, #prefix+1) == prefix..':' then
events.handlers[actualname] = nil
end
end
end
function events.emit(event, ...)
local result = nil
local handlers = events.handlers[event]
if handlers == nil then
return nil
end
for _, func in ipairs(handlers) do
local status, newres = xpcall(func, __vc__error, ...)
if not status then
debug.error("error in event ("..event..") handler: "..newres)
else
result = result or newres
end
end
return result
end
return events

View File

@ -0,0 +1,240 @@
-- =================================================== --
-- ====================== vec3 ======================= --
-- =================================================== --
function vec3.add(a, b, dst)
local btype = type(b)
if dst then
if btype == "table" then
dst[1] = a[1] + b[1]
dst[2] = a[2] + b[2]
dst[3] = a[3] + b[3]
else
dst[1] = a[1] + b
dst[2] = a[2] + b
dst[3] = a[3] + b
end
return dst
else
if btype == "table" then
return {a[1] + b[1], a[2] + b[2], a[3] + b[3]}
else
return {a[1] + b, a[2] + b, a[3] + b}
end
end
end
function vec3.sub(a, b, dst)
local btype = type(b)
if dst then
if btype == "table" then
dst[1] = a[1] - b[1]
dst[2] = a[2] - b[2]
dst[3] = a[3] - b[3]
else
dst[1] = a[1] - b
dst[2] = a[2] - b
dst[3] = a[3] - b
end
return dst
else
if btype == "table" then
return {a[1] - b[1], a[2] - b[2], a[3] - b[3]}
else
return {a[1] - b, a[2] - b, a[3] - b}
end
end
end
function vec3.mul(a, b, dst)
local btype = type(b)
if dst then
if btype == "table" then
dst[1] = a[1] * b[1]
dst[2] = a[2] * b[2]
dst[3] = a[3] * b[3]
else
dst[1] = a[1] * b
dst[2] = a[2] * b
dst[3] = a[3] * b
end
return dst
else
if btype == "table" then
return {a[1] * b[1], a[2] * b[2], a[3] * b[3]}
else
return {a[1] * b, a[2] * b, a[3] * b}
end
end
end
function vec3.div(a, b, dst)
local btype = type(b)
if dst then
if btype == "table" then
dst[1] = a[1] / b[1]
dst[2] = a[2] / b[2]
dst[3] = a[3] / b[3]
else
dst[1] = a[1] / b
dst[2] = a[2] / b
dst[3] = a[3] / b
end
return dst
else
if btype == "table" then
return {a[1] / b[1], a[2] / b[2], a[3] / b[3]}
else
return {a[1] / b, a[2] / b, a[3] / b}
end
end
end
function vec3.abs(a, dst)
local x = a[1]
local y = a[2]
local z = a[3]
if dst then
dst[1] = x < 0.0 and -x or x
dst[2] = y < 0.0 and -y or y
dst[3] = z < 0.0 and -z or z
else
return {
x < 0.0 and -x or x,
y < 0.0 and -y or y,
z < 0.0 and -z or z,
}
end
end
function vec3.dot(a, b)
return a[1] * b[1] + a[2] * b[2] + a[3] * b[3]
end
function vec3.mix(a, b, t, dest)
if dest then
dest[1] = a[1] * (1.0 - t) + b[1] * t
dest[2] = a[2] * (1.0 - t) + b[2] * t
dest[3] = a[3] * (1.0 - t) + b[3] * t
return dest
else
return {
a[1] * (1.0 - t) + b[1] * t,
a[2] * (1.0 - t) + b[2] * t,
a[3] * (1.0 - t) + b[3] * t,
}
end
end
-- =================================================== --
-- ====================== vec2 ======================= --
-- =================================================== --
function vec2.add(a, b, dst)
local btype = type(b)
if dst then
if btype == "table" then
dst[1] = a[1] + b[1]
dst[2] = a[2] + b[2]
else
dst[1] = a[1] + b
dst[2] = a[2] + b
end
return dst
else
if btype == "table" then
return {a[1] + b[1], a[2] + b[2]}
else
return {a[1] + b, a[2] + b}
end
end
end
function vec2.sub(a, b, dst)
local btype = type(b)
if dst then
if btype == "table" then
dst[1] = a[1] - b[1]
dst[2] = a[2] - b[2]
else
dst[1] = a[1] - b
dst[2] = a[2] - b
end
return dst
else
if btype == "table" then
return {a[1] - b[1], a[2] - b[2]}
else
return {a[1] - b, a[2] - b}
end
end
end
function vec2.mul(a, b, dst)
local btype = type(b)
if dst then
if btype == "table" then
dst[1] = a[1] * b[1]
dst[2] = a[2] * b[2]
else
dst[1] = a[1] * b
dst[2] = a[2] * b
end
return dst
else
if btype == "table" then
return {a[1] * b[1], a[2] * b[2]}
else
return {a[1] * b, a[2] * b}
end
end
end
function vec2.div(a, b, dst)
local btype = type(b)
if dst then
if btype == "table" then
dst[1] = a[1] / b[1]
dst[2] = a[2] / b[2]
else
dst[1] = a[1] / b
dst[2] = a[2] / b
end
return dst
else
if btype == "table" then
return {a[1] / b[1], a[2] / b[2]}
else
return {a[1] / b, a[2] / b}
end
end
end
function vec2.abs(a, dst)
local x = a[1]
local y = a[2]
if dst then
dst[1] = x < 0.0 and -x or x
dst[2] = y < 0.0 and -y or y
else
return {
x < 0.0 and -x or x,
y < 0.0 and -y or y,
}
end
end
function vec2.dot(a, b)
return a[1] * b[1] + a[2] * b[2]
end
function vec2.mix(a, b, t, dest)
if dest then
dest[1] = a[1] * (1.0 - t) + b[1] * t
dest[2] = a[2] * (1.0 - t) + b[2] * t
return dest
else
return {
a[1] * (1.0 - t) + b[1] * t,
a[2] * (1.0 - t) + b[2] * t,
}
end
end

View File

@ -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

View File

@ -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

View File

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

View File

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

View File

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

View File

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

View File

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

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

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

50
res/modules/schedule.lua Normal file
View File

@ -0,0 +1,50 @@
local Schedule = {
__index = {
set_interval = function(self, ms, callback, repetions)
local id = self._next_interval
self._intervals[id] = {
last_called = 0.0,
delay = ms / 1000.0,
callback = callback,
repetions = repetions,
}
self._next_interval = id + 1
return id
end,
set_timeout = function(self, ms, callback)
self:set_interval(ms, callback, 1)
end,
tick = function(self, dt)
local timer = self._timer + dt
for id, interval in pairs(self._intervals) do
if timer - interval.last_called >= interval.delay then
local stack_size = debug.count_frames()
xpcall(interval.callback, function(msg)
__vc__error(msg, 1, 1, stack_size)
end)
interval.last_called = timer
local repetions = interval.repetions
if repetions then
if repetions <= 1 then
self:remove_interval(id)
else
interval.repetions = repetions - 1
end
end
end
end
self._timer = timer
end,
remove_interval = function (self, id)
self._intervals[id] = nil
end
}
}
return function ()
return setmetatable({
_next_interval = 1,
_timer = 0.0,
_intervals = {},
}, Schedule)
end

View File

@ -47,7 +47,8 @@
"gui/info",
"gui/world",
"gui/hud",
"gui/entity"
"gui/entity",
"gui/half_block"
],
"fonts": [
{

27
res/project_client.lua Normal file
View File

@ -0,0 +1,27 @@
local menubg
function on_menu_clear()
if menubg then
menubg:destruct()
menubg = nil
end
end
function on_menu_setup()
local controller = {}
function controller.resize_menu_bg()
local w, h = unpack(gui.get_viewport())
if menubg then
menubg.region = {0, math.floor(h / 48), math.floor(w / 48), 0}
menubg.pos = {0, 0}
end
return w, h
end
gui.root.root:add(
"<image id='menubg' src='gui/menubg' size-func='DATA.resize_menu_bg' "..
"z-index='-1' interactive='true'/>", controller)
menubg = gui.root.menubg
controller.resize_menu_bg()
menu.page = "main"
menu.visible = true
end

View File

@ -46,18 +46,67 @@ local Socket = {__index={
get_address=function(self) return network.__get_address(self.id) end,
}}
local WriteableSocket = {__index={
send=function(self, ...) return network.__send(self.id, ...) end,
close=function(self) return network.__close(self.id) end,
is_open=function(self) return network.__is_alive(self.id) end,
get_address=function(self) return network.__get_address(self.id) end,
}}
local ServerSocket = {__index={
close=function(self) return network.__closeserver(self.id) end,
is_open=function(self) return network.__is_serveropen(self.id) end,
get_port=function(self) return network.__get_serverport(self.id) end,
}}
local DatagramServerSocket = {__index={
close=function(self) return network.__closeserver(self.id) end,
is_open=function(self) return network.__is_serveropen(self.id) end,
get_port=function(self) return network.__get_serverport(self.id) end,
send=function(self, ...) return network.__udp_server_send_to(self.id, ...) end
}}
local _tcp_server_callbacks = {}
local _tcp_client_callbacks = {}
local _udp_server_callbacks = {}
local _udp_client_datagram_callbacks = {}
local _udp_client_open_callbacks = {}
local _http_response_callbacks = {}
local _http_error_callbacks = {}
network.get = function(url, callback, errorCallback, headers)
local id = network.__get(url, headers)
if callback then
_http_response_callbacks[id] = callback
end
if errorCallback then
_http_error_callbacks[id] = errorCallback
end
end
network.get_binary = function(url, callback, errorCallback, headers)
local id = network.__get_binary(url, headers)
if callback then
_http_response_callbacks[id] = callback
end
if errorCallback then
_http_error_callbacks[id] = errorCallback
end
end
network.post = function(url, data, callback, errorCallback, headers)
local id = network.__post(url, data, headers)
if callback then
_http_response_callbacks[id] = callback
end
if errorCallback then
_http_error_callbacks[id] = errorCallback
end
end
network.tcp_open = function (port, handler)
local socket = setmetatable({id=network.__open(port)}, ServerSocket)
local socket = setmetatable({id=network.__open_tcp(port)}, ServerSocket)
_tcp_server_callbacks[socket.id] = function(id)
handler(setmetatable({id=id}, Socket))
@ -67,19 +116,133 @@ end
network.tcp_connect = function(address, port, callback)
local socket = setmetatable({id=0}, Socket)
socket.id = network.__connect(address, port)
socket.id = network.__connect_tcp(address, port)
_tcp_client_callbacks[socket.id] = function() callback(socket) end
return socket
end
network.udp_open = function (port, datagramHandler)
if type(datagramHandler) ~= 'function' then
error "udp server cannot be opened without datagram handler"
end
local socket = setmetatable({id=network.__open_udp(port)}, DatagramServerSocket)
_udp_server_callbacks[socket.id] = function(address, port, data)
datagramHandler(address, port, data, socket)
end
return socket
end
network.udp_connect = function (address, port, datagramHandler, openCallback)
if type(datagramHandler) ~= 'function' then
error "udp client socket cannot be opened without datagram handler"
end
local socket = setmetatable({id=0}, WriteableSocket)
socket.id = network.__connect_udp(address, port)
_udp_client_datagram_callbacks[socket.id] = datagramHandler
_udp_client_open_callbacks[socket.id] = openCallback
return socket
end
local function clean(iterable, checkFun, ...)
local tables = { ... }
for id, _ in pairs(iterable) do
if not checkFun(id) then
for i = 1, #tables do
tables[i][id] = nil
end
end
end
end
local updating_blocks = {}
local TYPE_REGISTER = 0
local TYPE_UNREGISTER = 1
block.__perform_ticks = function(delta)
for id, entry in pairs(updating_blocks) do
entry.timer = entry.timer + delta
local steps = math.floor(entry.timer / entry.delta * #entry / 3)
if steps == 0 then
goto continue
end
entry.timer = 0.0
local event = entry.event
local tps = entry.tps
for i=1, steps do
local x = entry[entry.pointer + 1]
local y = entry[entry.pointer + 2]
local z = entry[entry.pointer + 3]
entry.pointer = (entry.pointer + 3) % #entry
events.emit(event, x, y, z, tps)
end
::continue::
end
end
block.__process_register_events = function()
local register_events = block.__pull_register_events()
if not register_events then
return
end
for i=1, #register_events, 4 do
local header = register_events[i]
local type = bit.band(header, 0xFFFF)
local id = bit.rshift(header, 16)
local x = register_events[i + 1]
local y = register_events[i + 2]
local z = register_events[i + 3]
local list = updating_blocks[id]
if type == TYPE_REGISTER then
if not list then
list = {}
list.event = block.name(id) .. ".blocktick"
list.tps = 20 / (block.properties[id]["tick-interval"] or 1)
list.delta = 1.0 / list.tps
list.timer = 0.0
list.pointer = 0
updating_blocks[id] = list
end
table.insert(list, x)
table.insert(list, y)
table.insert(list, z)
elseif type == TYPE_UNREGISTER then
if list then
for j=1, #list, 3 do
if list[j] == x and list[j + 1] == y and list[j + 2] == z then
for k=1,3 do
table.remove(list, j)
end
j = j - 3
end
end
end
end
print(type, id, x, y, z)
end
end
network.__process_events = function()
local CLIENT_CONNECTED = 1
local CONNECTED_TO_SERVER = 2
local DATAGRAM = 3
local RESPONSE = 4
local ON_SERVER = 1
local ON_CLIENT = 2
local cleaned = false
local events = network.__pull_events()
for i, event in ipairs(events) do
local etype, sid, cid = unpack(event)
local etype, sid, cid, addr, port, side, data = unpack(event)
if etype == CLIENT_CONNECTED then
local callback = _tcp_server_callbacks[sid]
@ -87,24 +250,42 @@ network.__process_events = function()
callback(cid)
end
elseif etype == CONNECTED_TO_SERVER then
local callback = _tcp_client_callbacks[cid]
local callback = _tcp_client_callbacks[cid] or _udp_client_open_callbacks[cid]
if callback then
callback()
end
elseif etype == DATAGRAM then
if side == ON_CLIENT then
_udp_client_datagram_callbacks[cid](data)
elseif side == ON_SERVER then
_udp_server_callbacks[sid](addr, port, data)
end
elseif etype == RESPONSE then
if event[2] / 100 == 2 then
local callback = _http_response_callbacks[event[3]]
_http_response_callbacks[event[3]] = nil
_http_error_callbacks[event[3]] = nil
if callback then
callback(event[4])
end
else
local callback = _http_error_callbacks[event[3]]
_http_response_callbacks[event[3]] = nil
_http_error_callbacks[event[3]] = nil
if callback then
callback(event[2], event[4])
end
end
end
-- remove dead servers
if not cleaned then
for sid, _ in pairs(_tcp_server_callbacks) do
if not network.__is_serveropen(sid) then
_tcp_server_callbacks[sid] = nil
end
end
for cid, _ in pairs(_tcp_client_callbacks) do
if not network.__is_alive(cid) then
_tcp_client_callbacks[cid] = nil
end
end
clean(_tcp_server_callbacks, network.__is_serveropen, _tcp_server_callbacks)
clean(_tcp_client_callbacks, network.__is_alive, _tcp_client_callbacks)
clean(_udp_server_callbacks, network.__is_serveropen, _udp_server_callbacks)
clean(_udp_client_datagram_callbacks, network.__is_alive, _udp_client_open_callbacks, _udp_client_datagram_callbacks)
cleaned = true
end
end

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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
@ -81,4 +114,14 @@ function on_hud_open()
end)
configure_SSAO()
hud.default_hand_controller = update_hand
end
function on_hud_render()
if hud.hand_controller then
hud.hand_controller()
else
update_hand()
end
end

View File

@ -12,7 +12,28 @@ local Text3D = {__index={
update_settings=function(self, t) return gfx.text3d.update_settings(self.id, t) end,
}}
local Skeleton = {__index={
index=function(self, s) return gfx.skeletons.index(self.name, s) end,
get_model=function(self, i) return gfx.skeletons.get_model(self.name, i) end,
set_model=function(self, i, s) return gfx.skeletons.set_model(self.name, i, s) end,
get_matrix=function(self, i) return gfx.skeletons.get_matrix(self.name, i) end,
set_matrix=function(self, i, m) return gfx.skeletons.set_matrix(self.name, i, m) end,
get_texture=function(self, i) return gfx.skeletons.get_texture(self.name, i) end,
set_texture=function(self, i, s) return gfx.skeletons.set_texture(self.name, i, s) end,
is_visible=function(self, i) return gfx.skeletons.is_visible(self.name, i) end,
set_visible=function(self, i, b) return gfx.skeletons.set_visible(self.name, i, b) end,
get_color=function(self, i) return gfx.skeletons.get_color(self.name, i) end,
set_color=function(self, i, c) return gfx.skeletons.set_color(self.name, i, c) end,
}}
gfx.text3d.new = function(pos, text, preset, extension)
local id = gfx.text3d.show(pos, text, preset, extension)
return setmetatable({id=id}, Text3D)
end
gfx.skeletons = __skeleton
gfx.skeletons.get = function(name)
if gfx.skeletons.exists(name) then
return setmetatable({name=name}, Skeleton)
end
end

View File

@ -12,22 +12,23 @@ local names = {
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
-- 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
blockprops[propname] = nil
props[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
end
props.tags_set = lib.__get_tags(id)
end
end
process_properties(block)
process_properties(item)
local function make_read_only(t)
setmetatable(t, {
__newindex = function()
@ -57,10 +58,22 @@ local function cache_names(library)
function library.index(name)
return indices[name]
end
function library.has_tag(id, tag)
if id == nil then
error("id is nil")
end
local props = library.properties[id]
local tags_set = props.tags_set
if tags_set then
return tags_set[tag]
else
return false
end
end
end
cache_names(block)
cache_names(item)
local scripts_registry = require "core:internal/scripts_registry"
scripts_registry.build_registry()
__vc_scripts_registry.build_registry()

View File

@ -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",

View File

@ -1,3 +1,5 @@
local enable_experimental = core.get_setting("debug.enable-experimental")
------------------------------------------------
------ Extended kit of standard functions ------
------------------------------------------------
@ -77,13 +79,20 @@ local function complete_app_lib(app)
coroutine.yield()
end
function app.sleep_until(predicate, max_ticks)
function app.sleep_until(predicate, max_ticks, max_time)
max_ticks = max_ticks or 1e9
max_time = max_time or 1e9
local ticks = 0
while ticks < max_ticks and not predicate() do
local start_time = os.clock()
while ticks < max_ticks and
os.clock() - start_time < max_time
and not predicate() do
app.tick()
ticks = ticks + 1
end
if os.clock() - start_time >= max_time then
error("timeout")
end
if ticks == max_ticks then
error("max ticks exceed")
end
@ -130,61 +139,55 @@ function inventory.decrement(invid, slot, count)
end
end
------------------------------------------------
------------------- Events ---------------------
------------------------------------------------
events = {
handlers = {}
}
function inventory.get_caption(invid, slot)
local item_id, count = inventory.get(invid, slot)
local caption = inventory.get_data(invid, slot, "caption")
if not caption then return item.caption(item_id) end
function events.on(event, func)
if events.handlers[event] == nil then
events.handlers[event] = {}
end
table.insert(events.handlers[event], func)
return caption
end
function events.reset(event, func)
if func == nil then
events.handlers[event] = nil
else
events.handlers[event] = {func}
function inventory.set_caption(invid, slot, caption)
local itemid, itemcount = inventory.get(invid, slot)
if itemid == 0 then
return
end
if caption == nil or type(caption) ~= "string" then
caption = ""
end
inventory.set_data(invid, slot, "caption", caption)
end
function events.remove_by_prefix(prefix)
for name, handlers in pairs(events.handlers) do
local actualname = name
if type(name) == 'table' then
actualname = name[1]
function inventory.get_description(invid, slot)
local item_id, count = inventory.get(invid, slot)
local description = inventory.get_data(invid, slot, "description")
if not description then return item.description(item_id) end
return description
end
if actualname:sub(1, #prefix+1) == prefix..':' then
events.handlers[actualname] = nil
function inventory.set_description(invid, slot, description)
local itemid, itemcount = inventory.get(invid, slot)
if itemid == 0 then
return
end
if description == nil or type(description) ~= "string" then
description = ""
end
inventory.set_data(invid, slot, "description", description)
end
if enable_experimental then
require "core:internal/maths_inline"
end
asserts = require "core:internal/asserts"
events = require "core:internal/events"
function pack.unload(prefix)
events.remove_by_prefix(prefix)
end
function events.emit(event, ...)
local result = nil
local handlers = events.handlers[event]
if handlers == nil then
return nil
end
for _, func in ipairs(handlers) do
local status, newres = xpcall(func, __vc__error, ...)
if not status then
debug.error("error in event ("..event..") handler: "..newres)
else
result = result or newres
end
end
return result
end
gui_util = require "core:internal/gui_util"
Document = gui_util.Document
@ -199,6 +202,7 @@ end
_GUI_ROOT = Document.new("core:root")
_MENU = _GUI_ROOT.menu
menu = _MENU
gui.root = _GUI_ROOT
--- Console library extension ---
console.cheats = {}
@ -278,11 +282,33 @@ entities.get_all = function(uids)
return stdcomp.get_all(uids)
end
end
local bytearray = require "core:internal/bytearray"
Bytearray = bytearray.FFIBytearray
Bytearray_as_string = bytearray.FFIBytearray_as_string
Bytearray_construct = function(...) return Bytearray(...) end
__vc_scripts_registry = require "core:internal/scripts_registry"
file.open = require "core:internal/stream_providers/file"
file.open_named_pipe = require "core:internal/stream_providers/named_pipe"
if ffi.os == "Windows" then
ffi.cdef[[
unsigned long GetCurrentProcessId();
]]
os.pid = ffi.C.GetCurrentProcessId()
else
ffi.cdef[[
int getpid(void);
]]
os.pid = ffi.C.getpid()
end
ffi = nil
__vc_lock_internal_modules()
math.randomseed(time.uptime() * 1536227939)
@ -411,8 +437,41 @@ function __vc_on_hud_open()
hud.open_permanent("core:ingame_chat")
end
local Schedule = require "core:schedule"
local ScheduleGroup_mt = {
__index = {
publish = function(self, schedule)
local id = self._next_schedule
self._schedules[id] = schedule
self._next_schedule = id + 1
end,
tick = function(self, dt)
for id, schedule in pairs(self._schedules) do
schedule:tick(dt)
end
self.common:tick(dt)
end,
remove = function(self, id)
self._schedules[id] = nil
end,
}
}
local function ScheduleGroup()
return setmetatable({
_next_schedule = 1,
_schedules = {},
common = Schedule()
}, ScheduleGroup_mt)
end
time.schedules = {}
local RULES_FILE = "world:rules.toml"
function __vc_on_world_open()
time.schedules.world = ScheduleGroup()
if not file.exists(RULES_FILE) then
return
end
@ -422,6 +481,10 @@ function __vc_on_world_open()
end
end
function __vc_on_world_tick(tps)
time.schedules.world:tick(1.0 / tps)
end
function __vc_on_world_save()
local rule_values = {}
for name, rule in pairs(rules.rules) do
@ -434,6 +497,7 @@ function __vc_on_world_quit()
_rules.clear()
gui_util:__reset_local()
stdcomp.__reset()
file.__close_all_descriptors()
end
local __vc_coroutines = {}
@ -475,6 +539,8 @@ 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()
if hud then
gui.alert(fullmsg, function()
if world.is_open() then
__vc_app.close_world()
@ -484,6 +550,7 @@ function start_coroutine(chunk, name)
menu.page = "main"
end
end)
end
return fullmsg
end)
if not status then
@ -521,6 +588,8 @@ function __process_post_runnables()
end
network.__process_events()
block.__process_register_events()
block.__perform_ticks(time.delta())
end
function time.post_runnable(runnable)

View File

@ -20,6 +20,18 @@ if not ipairs_mt_supported then
end
end
function await(co)
local res, err
while coroutine.status(co) ~= 'dead' do
coroutine.yield()
res, err = coroutine.resume(co)
if err then
return res, err
end
end
return res, err
end
local _ffi = ffi
function __vc_Canvas_set_data(self, data)
if type(data) == "cdata" then
@ -538,6 +550,8 @@ function reload_module(name)
end
end
local internal_locked = false
-- Load script with caching
--
-- path - script path `contentpack:filename`.
@ -547,6 +561,11 @@ end
function __load_script(path, nocache)
local packname, filename = parse_path(path)
if internal_locked and (packname == "res" or packname == "core")
and filename:starts_with("modules/internal") then
error("access to core:internal modules outside of [core]")
end
-- __cached_scripts used in condition because cached result may be nil
if not nocache and __cached_scripts[path] ~= nil then
return package.loaded[path]
@ -567,6 +586,10 @@ function __load_script(path, nocache)
return result
end
function __vc_lock_internal_modules()
internal_locked = true
end
function require(path)
if not string.find(path, ':') then
local prefix, _ = parse_path(_debug_getinfo(2).source)
@ -645,3 +668,5 @@ end
bit.compile = require "core:bitwise/compiler"
bit.execute = require "core:bitwise/executor"
random.Random = require "core:internal/random_generator"

View File

@ -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

View File

@ -4,7 +4,7 @@
#include <constants>
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);

View File

@ -7,6 +7,7 @@ world.convert-block-layouts=Blocks fields have changes! Convert world files?
pack.remove-confirm=Do you want to erase all pack(s) content from the world forever?
error.pack-not-found=Could not to find pack
error.dependency-not-found=Dependency pack is not found
error.dependency-version-not-met=Dependency pack version is not met.
world.delete-confirm=Do you want to delete world forever?
world.generators.default=Default
world.generators.flat=Flat
@ -22,6 +23,7 @@ graphics.dense-render.tooltip=Enables transparency in blocks like leaves
# settings
settings.Controls Search Mode=Search by attached button name
settings.Conflict=Possible conflicts found
# Bindings
chunks.reload=Reload Chunks

View File

@ -32,6 +32,7 @@ devtools.output=Вывод
error.pack-not-found=Не удалось найти пакет
error.dependency-not-found=Используемая зависимость не найдена
error.dependency-version-not-met=Версия зависимости не соответствует необходимой
pack.remove-confirm=Удалить весь поставляемый паком/паками контент из мира (безвозвратно)?
# Подсказки
@ -100,6 +101,7 @@ settings.Controls Search Mode=Поиск по привязанной кнопк
settings.Limit Background FPS=Ограничить фоновую частоту кадров
settings.Advanced render=Продвинутый рендер
settings.Shadows quality=Качество теней
settings.Conflict=Найдены возможные конфликты
# Управление
chunks.reload=Перезагрузить Чанки

Some files were not shown because too many files have changed in this diff Show More