Merge branch 'master' into 826-use-AABB-for-collision

This commit is contained in:
codemob-dev 2025-07-27 16:53:37 -04:00
commit 288ab2ebfb
30 changed files with 460 additions and 334 deletions

View File

@ -5,7 +5,7 @@ body:
- type: markdown
attributes:
value: |
Please check if the feature aligns with the [Game Design Principles](https://github.com/PixelGuys/Cubyz/blob/master/GAME_DESIGN_PRINCIPLES.md) before making an issue.
Please check if the feature aligns with the [Game Design Principles](https://github.com/PixelGuys/Cubyz/blob/master/docs/GAME_DESIGN_PRINCIPLES.md) before making an issue.
- type: textarea
id: description
attributes:

View File

@ -1,110 +0,0 @@
# Game Design Principles
This document is intended for contributors and may contain spoilers.
## Cubyz is a Sandbox Game, not a Survival Game
The goal of this project is to create a game about exploration, adventure, and building. There is no looming threat of hunger or monsters; the player should be able to build in peace and choose how they would like to play.
### Avoid Unavoidable Threats
A player should have to create or approach a threat on their own terms, rather than the threat approach them. This should allow the player to interact with whatever they want without feeling forced to.
**Examples:**
- A player will not be attacked by monsters at night. They have to actively approach a monster-infested place such as a structure or cave. This is so they have free reign to do activites during the night without being interrupted or frustrated.
- A player has lost much health, and must risk what little life they have left to gather food. Note that the player cannot heal unless they have an accessory that consumes their energy. This is a situation the player has to face because of their own skill level.
- A player summons an event that spawns waves of enemies to their base.
- A player can claim a naturally generated structure by killing all monsters monsters in the area. Note that monsters do not respawn naturally.
### Progression = Risk
In order to progress in the game, the player must take risks. The player is otherwise free to stay where they are in terms of progression.
**Examples:**
- The deeper the player goes into the world's caves, the more valuables they can find, but consequentially, the more monsters they'll come across.
- The player must defeat an enemy, boss, or event to obtain an item that helps them progress.
- In a multiplayer world, a player could attack an enemy player to rise up in power.
## Break the Cycle
### No Dimensions
Instead of creating seperate dimensions, we can fit these places physically into Cubyz' massive world to allow the player to come across them on their own.
### No Teleportation
To immerse the player and let them feel the sense of scale the world of Cubyz has, teleportation is not allowed, as the player will see less of the world that way.
### No Automation
Instead of staying in one place and farming everything, the player should have to take advantage of the infinite nature of the world to gain resources, meaning that a majority of resources are non-renewable.
### Mobs Don't Respawn
To prevent farming, mobs will not respawn naturally once killed. This will also make the player more aware that killing has long-term consequences in the area it occurs in.
### No Passive Animals
Instead of animals standing by to let you kill them, they will run from the player, attack back, and use defense mechanisms. This should make hunting and taming much more engaging.
### No Unbreakable Tools
In Minecraft, a tool is an investment you can make. You can enchant the tool, rename it, make it your own; there's an incentive to make it last forever with the use of mending. In Cubyz though, you are not creating an investment, but rather a tool to use until it breaks. If a player gets too attached to their tool, then they won't want to make other types of tools, and lower-tier materials will see less use as they will never be used in tool-making.
### Avoid Clutter
Inventories will often fill up with random items that the player does not want, making their inventory hard to manage. To mitigate this, try finding uses for existing items before adding new ones, and find ways to prevent items from finding their way into a player's inventory when they don't want it.
## Player Engagement
### "How would the player feel?"
This is a very important part of game design in general. When a player encounters and re-encounters a mechanic or feature, put yourself in the player's shoes and ask "how would the player feel?" The goal is to make the player feel what you want them to feel, whether that be satisfaction, frustration, excitement, fear, or all of the above! It also helps to ask for feedback from players and asking how they feel about your addition.
### Depth in Simplicity
Cubyz' special sauce is its simplicity; keeping everything simple on the surface makes the game approachable for beginners, while the hidden depth keeps it interesting for skilled players. Avoid features that would add unnecessary tedium to the player's experience. Always think about how it would effect the player's first impressions of the game.
### Fuel the Player's Curiosity
The world is filled with secrets, and we want the player to find these secrets on their own without any outside help or guide.
### Problems have Multiple Solutions
Problems faced in a particular age of progression should have multiple solutions, instead of just one solution for everything.
For example, in the "Pre-Caves Age," the player will not have access to coal, as coal spawns low in the world and is shrouded in darkness. There's many solutions to this, however:
- Find a dim, above ground light source.
- Search for a cave with exposed coal.
- Wander into the darkness to find a cave that's bright enough to mine in.
- Find an above-ground structure that can grant the player coal.
- Dig straight down until you find coal.
Of course, this problem is completely solved as soon as the player gets coal, as they can now explore caves to find more coal with their newfound torches, but it's important that the player has these options in the first place to prevent them from getting stuck on a seemingly insurmountable barrier. This also gives them more freedom in how they can approach the game. Not one option is better or easier than the other, and if one turns out to be, then it should be allowed to be adjusted to encourage other methods.
### Explain when Needed
If something progress-related has no obvious explanation, the player will have to rely on a wiki to find out how to progress. One example is the bellows; players won't know to place it next to a furnace and jump on it repeatedly. The issue can be fixed by adding a tooltip that tells them how it works, while leaving out details they can intuitively find out themselves.
Having a mechanic that isn't explained or illuded to makes it an "Invisible Mechanic." These can only be found out through looking at the game's code, wiki reading, or asking a developer, thus giving a disadvantage to casual players. It's best to avoid these at all costs.
### Micro Moments
These are tiny things the player does in between larger events; examples include:
- Travelling
- Parkour
- Mining
- Building
- Crafting
- Managing inventory
- Fighting
These moments are extremely important as they let the player mentally rest, so making sure they're as satisfying and consistent as possible is a must.
### Make the World Feel Alive
To add immersion to the game, creatures should perform behaviors outside of player input, such as hunting, playing, migrating, eating, or sleeping.
## Balancing
When balancing the game, keep in mind how players might interact with the world, the wildlife, and each other.
### 2OP4ME
At no point should the player be extremely hard to kill. Armor, tools, accessories, and buffs should be designed around aiding the player in skill-based encounters, not letting them win regardless of skill.
### Trade-offs
If the player is given something to aid them, then it should have an appropriate take-away to balance it.
**Examples:**
- Using rare resources to create a strong tool.
- An accessory that heals the player, but takes away energy or some other resource.
- Enemies have strengths and weaknesses towards particular damage types
## Little Details
### Big Trees vs Small Trees
There are two categories of trees, big and small. Big trees are designed to be built upon or left as decoration, whereas small trees are designed to be chopped down.
### Vegetation
Vegetation should always fit the biome's climate. For example, Toadstools prefer humid areas, while Boletes prefer nutritious areas. Think about how a plant would fit into a biome or structure.
### Caves are Creepy and Mysterious
As the player goes deeper into Cubyz, they'll find that the music gets scarier, the monsters get harder and more disturbing, and the cave generation becomes an utter spectacle. We want the player to feel uneasy and stressed as they go down because it makes finding underground resources feel more rewarding.

View File

@ -50,10 +50,10 @@ sudo apt install libgl-dev libasound2-dev libx11-dev libxcursor-dev libxrandr-de
# Contributing
### Code
Check out the [Contributing Guidelines](https://github.com/PixelGuys/Cubyz/blob/master/CONTRIBUTING.md)
Check out the [Contributing Guidelines](https://github.com/PixelGuys/Cubyz/blob/master/docs/CONTRIBUTING.md)
### Gameplay Additions
Check out the [Game Design Principles](https://github.com/PixelGuys/Cubyz/blob/master/GAME_DESIGN_PRINCIPLES.md)
Check out the [Game Design Principles](https://github.com/PixelGuys/Cubyz/blob/master/docs/GAME_DESIGN_PRINCIPLES.md)
### Textures
If you want to add new textures, make sure they fit the style of the game. It's recommended that you have baseline skills in pixel art before attempting to make textures. A great collection of tutorials can be found [here](https://lospec.com/pixel-art-tutorials)
@ -65,9 +65,9 @@ If any of the following points are ignored, your texture will be rejected:
4. Reference other block textures to see how colours & contrast is used. Test your textures ingame alongside other blocks.
5. Blocks should tile smoothly. Avoid creating seams or repetitive patterns.
6. Use hue shifting conservatively. Take the material into account when choosing colours.
7. Items have full, coloured, 1-pixel outlines. It should be shaded so that the side in light (top left) is brighter, while the side in shadow (bottom right) is darker.
7. Items have full, coloured, 1-pixel outlines. It should be shaded so that the side in light (top left) is brighter, while the side in shadow (bottom right) is darker.
8. Items should have higher contrast than their block counterparts.
Your texture may be edited or replaced to ensure a consistent art style throughout the game.
Your texture may be edited or replaced to ensure a consistent art style throughout the game.
For further information, ask <img src="https://avatars.githubusercontent.com/u/122191047" width="20" height="20">[careeoki](https://github.com/careeoki) on [Discord](https://discord.gg/XtqCRRG). She has made a majority of the art for Cubyz.

View File

@ -12,6 +12,8 @@
.music = "cubyz:DarkTimes",
.soilCreep = 1.0,
.ground_structure = .{
"3 to 6 cubyz:snow",
"1 to 3 cubyz:permafrost",

View File

@ -8,6 +8,8 @@
.mountains = 30,
.chance = 0,
.soilCreep = 1.5,
.music = "cubyz:out_of_breath",
.parentBiomes = .{

View File

@ -3,6 +3,6 @@
It is easy to suggest something, but often it's much harder to implement and check if it fits the game.
So, before making a suggestion here on github (you can of course freely discuss ideas on the community discord server), please do the following steps:
- check if it follows the [Game Design Principles](https://github.com/PixelGuys/Cubyz/blob/master/GAME_DESIGN_PRINCIPLES.md)
- check if it follows the [Game Design Principles](https://github.com/PixelGuys/Cubyz/blob/master/docs/GAME_DESIGN_PRINCIPLES.md)
- make a reference implementation in the form of an addon, mod or fork of Cubyz (no content from other games)
- create a pull request with the suggested changes or make an issue using the blank issue template, don't forget to add some screenshots

View File

@ -0,0 +1,106 @@
# Game Design Principles
This document is intended for contributors and may contain spoilers.
## What is Cubyz?
Cubyz is a voxel sandbox game with infinite world and focus on great adventures, immersive exploration with numerous challenges along the way. We cherish freedom and we are determined to give it to the players in abundance, so they can chose their own path through the world.
### Progression
We want to encourage interacting with different mechanics, such as exploring, building, crafting, and etc. Progression therefore encourages the player to interact with all of these systems.
Examples:
- Explore to Build: The player needs to interact with exploration to get the resources they want for building.
- Survive to Explore: Caves and structures may have enemies and traps that the player has to face if they want to explore in peace.
- Strategize to Survive: Prepare for a trip with tools, weapons, and gear in order to make survival easier.
- Build to Strategize: The player has to build defenses for events, invasions, and bosses. They also need to upgrade their workstations to allow for better gear.
## Break the Cycle
### No Dimensions
Instead of creating seperate dimensions, we can fit these places physically into Cubyz' massive world for the player to come across.
### No Teleportation
Teleportation makes the game less immersive, as it diminishes the exploring aspect and doesn't let the player get a good sense of the scale of the world.
### No Automation
Having quick, infinite resources at the palm of players' hands discourages exploration, as the player will never need to forage or search for blocks they desire.
### Mobs Don't Respawn Naturally
Clearing a dangerous area of its monsters will make it safe to build and explore.
This rule is also here to prevent mob farming.
### No Passive Animals
Animals do not want to die, so they will either run from an attacking player or attempt to defend themselves. The player will have to strategize to be able to hunt.
### No Unbreakable Tools
If a player gets too attached to their amazing tool, they won't want to make other types of tools, and low-tier materials will see less use as they will never be used in tool-making.
### Avoid Clutter
Inventories will often fill up with random items that the player does not want, making their inventory hard to manage. To mitigate this, find uses for existing items before adding new ones and prevent items from finding their way into a player's inventory when they don't want it.
### Use Realism Tastefully
Cubyz is not a realistic game. The world is very whimsical and has rules of its own, sometimes inspired by the real world, but never copied without a good reason.
### Avoid Excessive Additions
Unless there is a genuine functional reason for a variant or addition to be added, they should be avoided. Try to use or modify existing content first before creating new stuff.
## Player Engagement
### "How would the player feel?"
Put yourself in the shoes of a player encountering a mechanic for the first time and ask yourself this. The goal is to make the player feel how you want them to, whether that be satisfaction, fear, or intrigue.
### Depth in Simplicity
It's important to remember to keep the game approachable to new players, so appearing simple is key. However, what will keep a player around is the game's depth. Design mechanics around being simple on the surface, but have them able to be expanded upon, upgraded, or customized to meet the player's needs.
### Fuel the Player's Curiosity
Fill the world with secrets that will keep the player interested and engaged with exploration.
### Problems have Multiple Solutions
Problems faced in a particular age of progression should have multiple solutions, instead of just one solution for everything.
For example, in the "Pre-Caves Age," the player will not have access to coal, as coal spawns low in the world and is shrouded in darkness. There's many solutions to this, however:
- Find a dim, above ground light source.
- Search for a cave with exposed coal.
- Wander into the darkness to find a cave that's bright enough to mine in.
- Find an above-ground structure that can grant the player coal.
- Dig straight down until you find coal.
This problem is completely solved as soon as the player gets coal, as they can now explore caves to find more coal with their newfound torches. It's important that the player has these options to prevent them from getting stuck on a seemingly insurmountable barrier. This gives them more freedom in how they can approach the game, as no one option is particularly the best and can change depending on situation and location.
### Explain when Needed
Progress-related mechanics need explanations or subtle hints so players don't get stuck. Keep it simple on the surface so it's easily approachable, but leave out details that the player can find themselves through intuition.
### Embrace Micro Moments
These are tiny things the player does in between larger events; examples include:
- Travelling
- Parkour
- Mining
- Building
- Crafting
- Managing inventory
- Fighting
These moments are extremely important as they largely influence the player's mood, so making sure they're as satisfying and consistent as possible is a must.
### Make the World Feel Alive
To add immersion to the game, creatures should perform behaviors outside of player input, such as hunting, playing, migrating, eating, or sleeping.
## Balancing
When balancing the game, keep in mind how players might interact with the world, the wildlife, and each other.
### 2OP4ME
Players need to be vulnerable at all times to avoid power imbalances. Armor, tools, accessories, and buffs should aid the player, not let them win outright.
### Trade-offs
If the player is given something to aid them, then it should have an appropriate take-away to balance it.
**Examples:**
- Using rare resources to create a strong tool.
- An accessory that heals the player, but takes away energy or some other resource.
- Enemies have strengths and weaknesses towards particular damage types.
## Little Details
### Big Trees vs Small Trees
Big trees are designed to be built upon or left as decoration, whereas small trees are designed to be chopped down.
### Vegetation
Vegetation should fit the biome's climate. For example, Toadstools prefer humid areas, while Boletes prefer nutritious areas. Give some extra thought to the plants you place.
### Caves are Creepy and Mysterious
As the player descends into Cubyz, the music gets scarier, the monsters become more vicious and disturbing, and the cave generation becomes increasingly precarious. Outside of difficulty, this adds stress that the player needs to overcome to gain large rewards.

View File

@ -7,6 +7,8 @@ const Item = main.items.Item;
const ItemStack = main.items.ItemStack;
const Tool = main.items.Tool;
const utils = main.utils;
const BinaryWriter = utils.BinaryWriter;
const BinaryReader = utils.BinaryReader;
const NeverFailingAllocator = main.heap.NeverFailingAllocator;
const vec = main.vec;
const Vec3d = vec.Vec3d;
@ -314,12 +316,12 @@ pub const Sync = struct { // MARK: Sync
executeCommand(payload, source);
}
pub fn createExternallyManagedInventory(len: usize, typ: Inventory.Type, source: Source, zon: ZonElement) u32 {
pub fn createExternallyManagedInventory(len: usize, typ: Inventory.Type, source: Source, data: *BinaryReader) u32 {
mutex.lock();
defer mutex.unlock();
const inventory = ServerInventory.init(len, typ, source, .externallyManaged);
inventories.items[inventory.inv.id] = inventory;
inventory.inv.loadFromZon(zon);
inventory.inv.fromBytes(data);
return inventory.inv.id;
}
@ -1821,7 +1823,7 @@ const SourceType = enum(u8) {
blockInventory = 5,
other = 0xff, // TODO: List every type separately here.
};
const Source = union(SourceType) {
pub const Source = union(SourceType) {
alreadyFreed: void,
playerInventory: u32,
sharedTestingInventory: void,
@ -1990,18 +1992,7 @@ pub fn getAmount(self: Inventory, slot: usize) u16 {
return self._items[slot].amount;
}
pub fn save(self: Inventory, allocator: NeverFailingAllocator) ZonElement {
const zonObject = ZonElement.initObject(allocator);
zonObject.put("capacity", self._items.len);
for(self._items, 0..) |stack, i| {
if(!stack.empty()) {
var buf: [1024]u8 = undefined;
zonObject.put(buf[0..std.fmt.printInt(&buf, i, 10, .lower, .{})], stack.store(allocator));
}
}
return zonObject;
}
// TODO: Remove after #480
pub fn loadFromZon(self: Inventory, zon: ZonElement) void {
for(self._items, 0..) |*stack, i| {
stack.clear();
@ -2019,3 +2010,33 @@ pub fn loadFromZon(self: Inventory, zon: ZonElement) void {
}
}
}
pub fn toBytes(self: Inventory, writer: *BinaryWriter) void {
writer.writeVarInt(u32, @intCast(self._items.len));
for(self._items) |stack| {
stack.toBytes(writer);
}
}
pub fn fromBytes(self: Inventory, reader: *BinaryReader) void {
var remainingCount = reader.readVarInt(u32) catch 0;
for(self._items) |*stack| {
if(remainingCount == 0) {
stack.clear();
continue;
}
remainingCount -= 1;
stack.* = ItemStack.fromBytes(reader) catch |err| {
std.log.err("Failed to read item stack from bytes: {s}", .{@errorName(err)});
stack.clear();
continue;
};
}
for(0..remainingCount) |_| {
var stack = ItemStack.fromBytes(reader) catch continue;
if(stack.item) |item| {
std.log.err("Lost {} of {s}", .{stack.amount, item.id()});
}
stack.deinit();
}
}

View File

@ -602,8 +602,7 @@ pub const ServerChunk = struct { // MARK: ServerChunk
for(0..2) |dy| {
const mapX = mapStartX +% main.server.terrain.SurfaceMap.MapFragment.mapSize*@as(i32, @intCast(dx));
const mapY = mapStartY +% main.server.terrain.SurfaceMap.MapFragment.mapSize*@as(i32, @intCast(dy));
const map = main.server.terrain.SurfaceMap.getOrGenerateFragmentAndIncreaseRefCount(mapX, mapY, self.super.pos.voxelSize);
defer map.decreaseRefCount();
const map = main.server.terrain.SurfaceMap.getOrGenerateFragment(mapX, mapY, self.super.pos.voxelSize);
if(!map.wasStored.swap(true, .monotonic)) {
map.save(null, .{});
}

View File

@ -214,6 +214,7 @@ pub const collision = struct {
const VolumeProperties = struct {
terminalVelocity: f64,
density: f64,
maxDensity: f64,
mobility: f64,
};
@ -238,6 +239,7 @@ pub const collision = struct {
var invTerminalVelocitySum: f64 = 0;
var densitySum: f64 = 0;
var maxDensity: f64 = defaults.maxDensity;
var mobilitySum: f64 = 0;
var volumeSum: f64 = 0;
@ -267,6 +269,7 @@ pub const collision = struct {
mobilitySum += emptyVolume*defaults.mobility;
invTerminalVelocitySum += filledVolume/block.terminalVelocity();
densitySum += filledVolume*block.density();
maxDensity = @max(maxDensity, block.density());
mobilitySum += filledVolume*block.mobility();
} else {
invTerminalVelocitySum += gridVolume/defaults.terminalVelocity;
@ -280,6 +283,7 @@ pub const collision = struct {
return .{
.terminalVelocity = volumeSum/invTerminalVelocitySum,
.density = densitySum/volumeSum,
.maxDensity = maxDensity,
.mobility = mobilitySum/volumeSum,
};
}
@ -779,7 +783,7 @@ pub fn update(deltaTime: f64) void { // MARK: update()
const playerDensity = 1.2;
var move: Vec3d = .{0, 0, 0};
if(main.renderer.mesh_storage.getBlockFromRenderThread(@intFromFloat(@floor(Player.super.pos[0])), @intFromFloat(@floor(Player.super.pos[1])), @intFromFloat(@floor(Player.super.pos[2]))) != null) {
const volumeProperties = collision.calculateVolumeProperties(.client, Player.super.pos, Player.outerBoundingBox, .{.density = 0.001, .terminalVelocity = airTerminalVelocity, .mobility = 1.0});
const volumeProperties = collision.calculateVolumeProperties(.client, Player.super.pos, Player.outerBoundingBox, .{.density = 0.001, .terminalVelocity = airTerminalVelocity, .maxDensity = 0.001, .mobility = 1.0});
const effectiveGravity = gravity*(playerDensity - volumeProperties.density)/playerDensity;
const volumeFrictionCoeffecient: f32 = @floatCast(gravity/volumeProperties.terminalVelocity);
var acc = Vec3d{0, 0, 0};
@ -790,6 +794,8 @@ pub fn update(deltaTime: f64) void { // MARK: update()
const groundFriction = if(!Player.onGround and !Player.isFlying.load(.monotonic)) 0 else collision.calculateFriction(.client, Player.super.pos, Player.outerBoundingBox, 20);
Player.currentFriction = if(Player.isFlying.load(.monotonic)) 20 else groundFriction + volumeFrictionCoeffecient;
const mobility = if(Player.isFlying.load(.monotonic)) 1.0 else volumeProperties.mobility;
const density = if(Player.isFlying.load(.monotonic)) 0.0 else volumeProperties.density;
const maxDensity = if(Player.isFlying.load(.monotonic)) 0.0 else volumeProperties.maxDensity;
const baseFrictionCoefficient: f32 = Player.currentFriction;
var directionalFrictionCoefficients: Vec3f = @splat(0);
const speedMultiplier: f32 = if(Player.hyperSpeed.load(.monotonic)) 4.0 else 1.0;
@ -799,8 +805,9 @@ pub fn update(deltaTime: f64) void { // MARK: update()
// At equillibrium we want to have dv/dt = a - λv = 0 a = λ*v
const fricMul = speedMultiplier*baseFrictionCoefficient*if(Player.isFlying.load(.monotonic)) 1.0 else mobility;
const forward = vec.rotateZ(Vec3d{0, 1, 0}, -camera.rotation[2]);
const right = Vec3d{-forward[1], forward[0], 0};
const horizontalForward = vec.rotateZ(Vec3d{0, 1, 0}, -camera.rotation[2]);
const forward = vec.normalize(std.math.lerp(horizontalForward, camera.direction, @as(Vec3d, @splat(density/@max(1.0, maxDensity)))));
const right = Vec3d{-horizontalForward[1], horizontalForward[0], 0};
var movementDir: Vec3d = .{0, 0, 0};
var movementSpeed: f64 = 0;
@ -882,8 +889,13 @@ pub fn update(deltaTime: f64) void { // MARK: update()
movementDir[2] -= walkingSpeed;
}
}
if(movementSpeed != 0 and vec.lengthSquare(movementDir) != 0) {
movementDir = vec.normalize(movementDir);
if(vec.lengthSquare(movementDir) > movementSpeed*movementSpeed) {
movementDir = vec.normalize(movementDir);
} else {
movementDir /= @splat(movementSpeed);
}
acc += movementDir*@as(Vec3d, @splat(movementSpeed*fricMul));
}

View File

@ -183,7 +183,7 @@ pub fn deinit() void {
}
pub fn save() void { // MARK: save()
const guiZon = ZonElement.initObject(main.stackAllocator);
var guiZon = ZonElement.initObject(main.stackAllocator);
defer guiZon.deinit(main.stackAllocator);
for(windowList.items) |window| {
const windowZon = ZonElement.initObject(main.stackAllocator);
@ -218,7 +218,7 @@ pub fn save() void { // MARK: save()
}
// Merge with the old settings file to preserve unknown settings.
const oldZon: ZonElement = main.files.cubyzDir().readToZon(main.stackAllocator, "gui_layout.zig.zon") catch |err| blk: {
var oldZon: ZonElement = main.files.cubyzDir().readToZon(main.stackAllocator, "gui_layout.zig.zon") catch |err| blk: {
if(err != error.FileNotFound) {
std.log.err("Could not read gui_layout.zig.zon: {s}", .{@errorName(err)});
}
@ -226,7 +226,13 @@ pub fn save() void { // MARK: save()
};
defer oldZon.deinit(main.stackAllocator);
oldZon.join(guiZon);
if(oldZon == .object) {
oldZon.join(guiZon);
} else {
oldZon.deinit(main.stackAllocator);
oldZon = guiZon;
guiZon = .null;
}
main.files.cubyzDir().writeZon("gui_layout.zig.zon", oldZon) catch |err| {
std.log.err("Could not write gui_layout.zig.zon: {s}", .{@errorName(err)});

View File

@ -182,7 +182,7 @@ const MaterialProperty = enum {
}
};
pub const BaseItemIndex = enum(u16) {
pub const BaseItemIndex = enum(u16) { // MARK: BaseItemIndex
_,
pub fn fromId(_id: []const u8) ?BaseItemIndex {
@ -804,6 +804,10 @@ pub const Tool = struct { // MARK: Tool
return self.texture.?;
}
fn id(self: *Tool) []const u8 {
return self.type.id();
}
fn getTooltip(self: *Tool) []const u8 {
self.tooltip.clearRetainingCapacity();
self.tooltip.writer().print(
@ -938,6 +942,14 @@ pub const Item = union(ItemType) { // MARK: Item
}
}
pub fn id(self: Item) []const u8 {
switch(self) {
inline else => |item| {
return item.id();
},
}
}
pub fn getTooltip(self: Item) []const u8 {
switch(self) {
.baseItem => |_baseItem| {

View File

@ -659,8 +659,8 @@ pub fn main() void { // MARK: main()
gui.openWindow("main");
}
server.terrain.initGenerators();
defer server.terrain.deinitGenerators();
server.terrain.globalInit();
defer server.terrain.globalDeinit();
const c = Window.c;

View File

@ -676,7 +676,6 @@ pub const ChunkMesh = struct { // MARK: ChunkMesh
sortingOutputBuffer: []FaceData = &.{},
culledSortingCount: u31 = 0,
lastTransparentUpdatePos: Vec3i = Vec3i{0, 0, 0},
refCount: std.atomic.Value(u32) = .init(1),
needsLightRefresh: std.atomic.Value(bool) = .init(false),
needsMeshUpdate: bool = false,
finishedMeshing: bool = false, // Must be synced with node.finishedMeshing in mesh_storage.zig
@ -712,7 +711,7 @@ pub const ChunkMesh = struct { // MARK: ChunkMesh
return self;
}
pub fn deinit(self: *ChunkMesh, _: usize) void {
fn privateDeinit(self: *ChunkMesh, _: usize) void {
chunkBuffer.free(self.chunkAllocation);
self.opaqueMesh.deinit();
self.transparentMesh.deinit();
@ -730,6 +729,10 @@ pub const ChunkMesh = struct { // MARK: ChunkMesh
mesh_storage.meshMemoryPool.destroy(self);
}
pub fn deferredDeinit(self: *ChunkMesh) void {
main.heap.GarbageCollection.deferredFree(.{.ptr = self, .freeFunction = main.utils.castFunctionSelfToAnyopaque(privateDeinit)});
}
pub fn scheduleLightRefresh(pos: chunk.ChunkPosition) void {
LightRefreshTask.schedule(pos);
}

View File

@ -371,10 +371,7 @@ fn freeOldMeshes(olderPx: i32, olderPy: i32, olderPz: i32, olderRD: u16) void {
if(oldMesh) |mesh| {
node.finishedMeshing = false;
updateHigherLodNodeFinishedMeshing(mesh.pos, false);
main.heap.GarbageCollection.deferredFree(.{
.ptr = mesh,
.freeFunction = main.utils.castFunctionSelfToAnyopaque(ChunkMesh.deinit),
});
mesh.deferredDeinit();
}
node.isNeighborLod = @splat(false);
}
@ -835,7 +832,6 @@ fn batchUpdateBlocks() void {
// MARK: adders
pub fn addToUpdateList(mesh: *chunk_meshing.ChunkMesh) void {
std.debug.assert(mesh.refCount.load(.monotonic) != 0);
mutex.lock();
defer mutex.unlock();
if(mesh.finishedMeshing) {
@ -898,7 +894,7 @@ pub const MeshGenerationTask = struct { // MARK: MeshGenerationTask
defer main.globalAllocator.destroy(self);
const pos = self.mesh.pos;
const mesh = ChunkMesh.init(pos, self.mesh);
mesh.generateLightingData() catch mesh.deinit(undefined);
mesh.generateLightingData() catch mesh.deferredDeinit();
}
pub fn clean(self: *MeshGenerationTask) void {

View File

@ -27,8 +27,7 @@ pub fn execute(args: []const u8, source: *User) void {
var dir: main.chunk.Neighbor = .dirNegX;
var stepsRemaining: usize = 1;
for(0..spiralLen) |_| {
const map = main.server.terrain.ClimateMap.getOrGenerateFragmentAndIncreaseRefCount(wx, wy);
defer map.decreaseRefCount();
const map = main.server.terrain.ClimateMap.getOrGenerateFragment(wx, wy);
for(0..map.map.len) |_| {
const x = main.random.nextIntBounded(u31, &main.seed, map.map.len);
const y = main.random.nextIntBounded(u31, &main.seed, map.map.len);

View File

@ -27,7 +27,6 @@ pub const CaveBiomeMapFragment = struct { // MARK: caveBiomeMapFragment
pos: main.chunk.ChunkPosition,
biomeMap: [1 << 3*(caveBiomeMapShift - caveBiomeShift)][2]*const Biome = undefined,
refCount: std.atomic.Value(u16) = .init(0),
pub fn init(self: *CaveBiomeMapFragment, wx: i32, wy: i32, wz: i32) void {
self.* = .{
@ -40,6 +39,14 @@ pub const CaveBiomeMapFragment = struct { // MARK: caveBiomeMapFragment
};
}
fn privateDeinit(self: *CaveBiomeMapFragment, _: usize) void {
memoryPool.destroy(self);
}
pub fn deferredDeinit(self: *CaveBiomeMapFragment) void {
main.heap.GarbageCollection.deferredFree(.{.ptr = self, .freeFunction = main.utils.castFunctionSelfToAnyopaque(privateDeinit)});
}
const rotationMatrixShift = 30;
const fac: comptime_int = @intFromFloat(@as(comptime_float, 1 << rotationMatrixShift)/25.0);
const rotationMatrix = .{
@ -82,19 +89,6 @@ pub const CaveBiomeMapFragment = struct { // MARK: caveBiomeMapFragment
relZ >>= caveBiomeShift;
return relX << 2*(caveBiomeMapShift - caveBiomeShift) | relY << caveBiomeMapShift - caveBiomeShift | relZ;
}
pub fn increaseRefCount(self: *CaveBiomeMapFragment) void {
const prevVal = self.refCount.fetchAdd(1, .monotonic);
std.debug.assert(prevVal != 0);
}
pub fn decreaseRefCount(self: *CaveBiomeMapFragment) void {
const prevVal = self.refCount.fetchSub(1, .monotonic);
std.debug.assert(prevVal != 0);
if(prevVal == 1) {
memoryPool.destroy(self);
}
}
};
/// A generator for the cave biome map.
@ -160,10 +154,10 @@ pub const InterpolatableCaveBiomeMapView = struct { // MARK: InterpolatableCaveB
var result = InterpolatableCaveBiomeMapView{
.fragments = Array3D(*CaveBiomeMapFragment).init(allocator, caveBiomeFragmentWidth, caveBiomeFragmentWidth, caveBiomeFragmentWidth),
.surfaceFragments = [_]*MapFragment{
SurfaceMap.getOrGenerateFragmentAndIncreaseRefCount(center[0] -% SurfaceMap.MapFragment.mapSize/2*pos.voxelSize, center[1] -% SurfaceMap.MapFragment.mapSize/2*pos.voxelSize, pos.voxelSize),
SurfaceMap.getOrGenerateFragmentAndIncreaseRefCount(center[0] -% SurfaceMap.MapFragment.mapSize/2*pos.voxelSize, center[1] +% SurfaceMap.MapFragment.mapSize/2*pos.voxelSize, pos.voxelSize),
SurfaceMap.getOrGenerateFragmentAndIncreaseRefCount(center[0] +% SurfaceMap.MapFragment.mapSize/2*pos.voxelSize, center[1] -% SurfaceMap.MapFragment.mapSize/2*pos.voxelSize, pos.voxelSize),
SurfaceMap.getOrGenerateFragmentAndIncreaseRefCount(center[0] +% SurfaceMap.MapFragment.mapSize/2*pos.voxelSize, center[1] +% SurfaceMap.MapFragment.mapSize/2*pos.voxelSize, pos.voxelSize),
SurfaceMap.getOrGenerateFragment(center[0] -% SurfaceMap.MapFragment.mapSize/2*pos.voxelSize, center[1] -% SurfaceMap.MapFragment.mapSize/2*pos.voxelSize, pos.voxelSize),
SurfaceMap.getOrGenerateFragment(center[0] -% SurfaceMap.MapFragment.mapSize/2*pos.voxelSize, center[1] +% SurfaceMap.MapFragment.mapSize/2*pos.voxelSize, pos.voxelSize),
SurfaceMap.getOrGenerateFragment(center[0] +% SurfaceMap.MapFragment.mapSize/2*pos.voxelSize, center[1] -% SurfaceMap.MapFragment.mapSize/2*pos.voxelSize, pos.voxelSize),
SurfaceMap.getOrGenerateFragment(center[0] +% SurfaceMap.MapFragment.mapSize/2*pos.voxelSize, center[1] +% SurfaceMap.MapFragment.mapSize/2*pos.voxelSize, pos.voxelSize),
},
.pos = pos,
.width = width,
@ -175,7 +169,7 @@ pub const InterpolatableCaveBiomeMapView = struct { // MARK: InterpolatableCaveB
for(0..caveBiomeFragmentWidth) |x| {
for(0..caveBiomeFragmentWidth) |y| {
for(0..caveBiomeFragmentWidth) |z| {
result.fragments.set(x, y, z, getOrGenerateFragmentAndIncreaseRefCount(
result.fragments.set(x, y, z, getOrGenerateFragment(
startX +% CaveBiomeMapFragment.caveBiomeMapSize*@as(i32, @intCast(x)),
startY +% CaveBiomeMapFragment.caveBiomeMapSize*@as(i32, @intCast(y)),
startZ +% CaveBiomeMapFragment.caveBiomeMapSize*@as(i32, @intCast(z)),
@ -187,13 +181,7 @@ pub const InterpolatableCaveBiomeMapView = struct { // MARK: InterpolatableCaveB
}
pub fn deinit(self: InterpolatableCaveBiomeMapView) void {
for(self.fragments.mem) |mapFragment| {
mapFragment.decreaseRefCount();
}
self.fragments.deinit(self.allocator);
for(self.surfaceFragments) |mapFragment| {
mapFragment.decreaseRefCount();
}
}
fn rotate231(in: Vec3i) Vec3i {
@ -523,31 +511,31 @@ pub const CaveBiomeMapView = struct { // MARK: CaveBiomeMapView
const cacheSize = 1 << 8; // Must be a power of 2!
const cacheMask = cacheSize - 1;
const associativity = 8; // 128 MiB
var cache: Cache(CaveBiomeMapFragment, cacheSize, associativity, CaveBiomeMapFragment.decreaseRefCount) = .{};
var cache: Cache(CaveBiomeMapFragment, cacheSize, associativity, CaveBiomeMapFragment.deferredDeinit) = .{};
var profile: TerrainGenerationProfile = undefined;
var memoryPool: main.heap.MemoryPool(CaveBiomeMapFragment) = undefined;
pub fn initGenerators() void {
pub fn globalInit() void {
const list = @import("cavebiomegen/_list.zig");
inline for(@typeInfo(list).@"struct".decls) |decl| {
CaveBiomeGenerator.registerGenerator(@field(list, decl.name));
}
memoryPool = .init(main.globalAllocator);
}
pub fn deinitGenerators() void {
pub fn globalDeinit() void {
CaveBiomeGenerator.generatorRegistry.clearAndFree(main.globalAllocator.allocator);
memoryPool.deinit();
}
pub fn init(_profile: TerrainGenerationProfile) void {
profile = _profile;
memoryPool = .init(main.globalAllocator);
}
pub fn deinit() void {
cache.clear();
memoryPool.deinit();
}
fn cacheInit(pos: ChunkPosition) *CaveBiomeMapFragment {
@ -556,11 +544,10 @@ fn cacheInit(pos: ChunkPosition) *CaveBiomeMapFragment {
for(profile.caveBiomeGenerators) |generator| {
generator.generate(mapFragment, profile.seed ^ generator.generatorSeed);
}
_ = @atomicRmw(u16, &mapFragment.refCount.raw, .Add, 1, .monotonic);
return mapFragment;
}
fn getOrGenerateFragmentAndIncreaseRefCount(_wx: i32, _wy: i32, _wz: i32) *CaveBiomeMapFragment {
fn getOrGenerateFragment(_wx: i32, _wy: i32, _wz: i32) *CaveBiomeMapFragment {
const wx = _wx & ~@as(i32, CaveBiomeMapFragment.caveBiomeMapMask);
const wy = _wy & ~@as(i32, CaveBiomeMapFragment.caveBiomeMapMask);
const wz = _wz & ~@as(i32, CaveBiomeMapFragment.caveBiomeMapMask);
@ -570,6 +557,6 @@ fn getOrGenerateFragmentAndIncreaseRefCount(_wx: i32, _wy: i32, _wz: i32) *CaveB
.wz = wz,
.voxelSize = CaveBiomeMapFragment.caveBiomeSize,
};
const result = cache.findOrCreate(compare, cacheInit, CaveBiomeMapFragment.increaseRefCount);
const result = cache.findOrCreate(compare, cacheInit, null);
return result;
}

View File

@ -21,7 +21,6 @@ pub const CaveMapFragment = struct { // MARK: CaveMapFragment
data: [width*width]u64 = undefined,
pos: ChunkPosition,
voxelShift: u5,
refCount: Atomic(u16) = .init(0),
pub fn init(self: *CaveMapFragment, wx: i32, wy: i32, wz: i32, voxelSize: u31) void {
self.* = .{
@ -36,6 +35,14 @@ pub const CaveMapFragment = struct { // MARK: CaveMapFragment
@memset(&self.data, std.math.maxInt(u64));
}
fn privateDeinit(self: *CaveMapFragment, _: usize) void {
memoryPool.destroy(self);
}
pub fn deferredDeinit(self: *CaveMapFragment) void {
main.heap.GarbageCollection.deferredFree(.{.ptr = self, .freeFunction = main.utils.castFunctionSelfToAnyopaque(privateDeinit)});
}
fn getIndex(x: i32, y: i32) usize {
std.debug.assert(x >= 0 and x < width and y >= 0 and y < width); // Coordinates out of range.
return @intCast(x*width + y);
@ -50,19 +57,6 @@ pub const CaveMapFragment = struct { // MARK: CaveMapFragment
return maskLower | maskUpper;
}
pub fn increaseRefCount(self: *CaveMapFragment) void {
const prevVal = self.refCount.fetchAdd(1, .monotonic);
std.debug.assert(prevVal != 0);
}
pub fn decreaseRefCount(self: *CaveMapFragment) void {
const prevVal = self.refCount.fetchSub(1, .monotonic);
std.debug.assert(prevVal != 0);
if(prevVal == 1) {
memoryPool.destroy(self);
}
}
pub fn addRange(self: *CaveMapFragment, _relX: i32, _relY: i32, _start: i32, _end: i32) void {
const relX = _relX >> self.voxelShift;
const relY = _relY >> self.voxelShift;
@ -132,30 +126,24 @@ pub const CaveMapView = struct { // MARK: CaveMapView
reference: *ServerChunk,
fragments: [8]*CaveMapFragment,
pub fn init(chunk: *ServerChunk) CaveMapView {
pub fn findMapsAround(chunk: *ServerChunk) CaveMapView {
const pos = chunk.super.pos;
const width = chunk.super.width;
return CaveMapView{
.reference = chunk,
.fragments = [_]*CaveMapFragment{
getOrGenerateFragmentAndIncreaseRefCount(pos.wx -% width, pos.wy -% width, pos.wz -% width, pos.voxelSize),
getOrGenerateFragmentAndIncreaseRefCount(pos.wx -% width, pos.wy -% width, pos.wz +% width, pos.voxelSize),
getOrGenerateFragmentAndIncreaseRefCount(pos.wx -% width, pos.wy +% width, pos.wz -% width, pos.voxelSize),
getOrGenerateFragmentAndIncreaseRefCount(pos.wx -% width, pos.wy +% width, pos.wz +% width, pos.voxelSize),
getOrGenerateFragmentAndIncreaseRefCount(pos.wx +% width, pos.wy -% width, pos.wz -% width, pos.voxelSize),
getOrGenerateFragmentAndIncreaseRefCount(pos.wx +% width, pos.wy -% width, pos.wz +% width, pos.voxelSize),
getOrGenerateFragmentAndIncreaseRefCount(pos.wx +% width, pos.wy +% width, pos.wz -% width, pos.voxelSize),
getOrGenerateFragmentAndIncreaseRefCount(pos.wx +% width, pos.wy +% width, pos.wz +% width, pos.voxelSize),
getOrGenerateFragment(pos.wx -% width, pos.wy -% width, pos.wz -% width, pos.voxelSize),
getOrGenerateFragment(pos.wx -% width, pos.wy -% width, pos.wz +% width, pos.voxelSize),
getOrGenerateFragment(pos.wx -% width, pos.wy +% width, pos.wz -% width, pos.voxelSize),
getOrGenerateFragment(pos.wx -% width, pos.wy +% width, pos.wz +% width, pos.voxelSize),
getOrGenerateFragment(pos.wx +% width, pos.wy -% width, pos.wz -% width, pos.voxelSize),
getOrGenerateFragment(pos.wx +% width, pos.wy -% width, pos.wz +% width, pos.voxelSize),
getOrGenerateFragment(pos.wx +% width, pos.wy +% width, pos.wz -% width, pos.voxelSize),
getOrGenerateFragment(pos.wx +% width, pos.wy +% width, pos.wz +% width, pos.voxelSize),
},
};
}
pub fn deinit(self: CaveMapView) void {
for(self.fragments) |mapFragment| {
mapFragment.decreaseRefCount();
}
}
pub fn isSolid(self: CaveMapView, relX: i32, relY: i32, relZ: i32) bool {
const wx = relX +% self.reference.super.pos.wx;
const wy = relY +% self.reference.super.pos.wy;
@ -289,7 +277,7 @@ pub const CaveMapView = struct { // MARK: CaveMapView
const cacheSize = 1 << 11; // Must be a power of 2!
const cacheMask = cacheSize - 1;
const associativity = 8; // 512 MiB Cache size
var cache: Cache(CaveMapFragment, cacheSize, associativity, CaveMapFragment.decreaseRefCount) = .{};
var cache: Cache(CaveMapFragment, cacheSize, associativity, CaveMapFragment.deferredDeinit) = .{};
var profile: TerrainGenerationProfile = undefined;
var memoryPool: main.heap.MemoryPool(CaveMapFragment) = undefined;
@ -300,38 +288,37 @@ fn cacheInit(pos: ChunkPosition) *CaveMapFragment {
for(profile.caveGenerators) |generator| {
generator.generate(mapFragment, profile.seed ^ generator.generatorSeed);
}
_ = @atomicRmw(u16, &mapFragment.refCount.raw, .Add, 1, .monotonic);
return mapFragment;
}
pub fn initGenerators() void {
pub fn globalInit() void {
const list = @import("cavegen/_list.zig");
inline for(@typeInfo(list).@"struct".decls) |decl| {
CaveGenerator.registerGenerator(@field(list, decl.name));
}
memoryPool = .init(main.globalAllocator);
}
pub fn deinitGenerators() void {
pub fn globalDeinit() void {
CaveGenerator.generatorRegistry.clearAndFree(main.globalAllocator.allocator);
memoryPool.deinit();
}
pub fn init(_profile: TerrainGenerationProfile) void {
profile = _profile;
memoryPool = .init(main.globalAllocator);
}
pub fn deinit() void {
cache.clear();
memoryPool.deinit();
}
fn getOrGenerateFragmentAndIncreaseRefCount(wx: i32, wy: i32, wz: i32, voxelSize: u31) *CaveMapFragment {
fn getOrGenerateFragment(wx: i32, wy: i32, wz: i32, voxelSize: u31) *CaveMapFragment {
const compare = ChunkPosition{
.wx = wx & ~@as(i32, CaveMapFragment.widthMask*voxelSize | voxelSize - 1),
.wy = wy & ~@as(i32, CaveMapFragment.widthMask*voxelSize | voxelSize - 1),
.wz = wz & ~@as(i32, CaveMapFragment.heightMask*voxelSize | voxelSize - 1),
.voxelSize = voxelSize,
};
const result = cache.findOrCreate(compare, cacheInit, CaveMapFragment.increaseRefCount);
const result = cache.findOrCreate(compare, cacheInit, null);
return result;
}

View File

@ -48,30 +48,23 @@ pub const ClimateMapFragment = struct {
pos: ClimateMapFragmentPosition,
map: [mapEntrysSize][mapEntrysSize]BiomeSample = undefined,
refCount: Atomic(u16) = .init(0),
pub fn init(self: *ClimateMapFragment, wx: i32, wy: i32) void {
self.* = .{
.pos = .{.wx = wx, .wy = wy},
};
}
fn privateDeinit(self: *ClimateMapFragment, _: usize) void {
memoryPool.destroy(self);
}
pub fn deferredDeinit(self: *ClimateMapFragment) void {
main.heap.GarbageCollection.deferredFree(.{.ptr = self, .freeFunction = main.utils.castFunctionSelfToAnyopaque(privateDeinit)});
}
pub fn hashCode(wx: i32, wy: i32) u32 {
return @bitCast((wx >> mapShift)*%33 + (wy >> mapShift));
}
pub fn increaseRefCount(self: *ClimateMapFragment) void {
const prevVal = self.refCount.fetchAdd(1, .monotonic);
std.debug.assert(prevVal != 0);
}
pub fn decreaseRefCount(self: *ClimateMapFragment) void {
const prevVal = self.refCount.fetchSub(1, .monotonic);
std.debug.assert(prevVal != 0);
if(prevVal == 1) {
memoryPool.destroy(self);
}
}
};
/// Generates the climate(aka Biome) map, which is a rough representation of the world.
@ -102,43 +95,42 @@ pub const ClimateMapGenerator = struct {
const cacheSize = 1 << 5; // Must be a power of 2!
const cacheMask = cacheSize - 1;
const associativity = 8; // ~400 MiB
var cache: Cache(ClimateMapFragment, cacheSize, associativity, ClimateMapFragment.decreaseRefCount) = .{};
var cache: Cache(ClimateMapFragment, cacheSize, associativity, ClimateMapFragment.deferredDeinit) = .{};
var profile: TerrainGenerationProfile = undefined;
var memoryPool: main.heap.MemoryPool(ClimateMapFragment) = undefined;
pub fn initGenerators() void {
pub fn globalInit() void {
const list = @import("climategen/_list.zig");
inline for(@typeInfo(list).@"struct".decls) |decl| {
ClimateMapGenerator.registerGenerator(@field(list, decl.name));
}
memoryPool = .init(main.globalAllocator);
}
pub fn deinitGenerators() void {
pub fn globalDeinit() void {
ClimateMapGenerator.generatorRegistry.clearAndFree(main.globalAllocator.allocator);
memoryPool.deinit();
}
fn cacheInit(pos: ClimateMapFragmentPosition) *ClimateMapFragment {
const mapFragment = memoryPool.create();
mapFragment.init(pos.wx, pos.wy);
profile.climateGenerator.generateMapFragment(mapFragment, profile.seed);
_ = @atomicRmw(u16, &mapFragment.refCount.raw, .Add, 1, .monotonic);
return mapFragment;
}
pub fn init(_profile: TerrainGenerationProfile) void {
profile = _profile;
memoryPool = .init(main.globalAllocator);
}
pub fn deinit() void {
cache.clear();
memoryPool.deinit();
}
pub fn getOrGenerateFragmentAndIncreaseRefCount(wx: i32, wy: i32) *ClimateMapFragment {
pub fn getOrGenerateFragment(wx: i32, wy: i32) *ClimateMapFragment {
const compare = ClimateMapFragmentPosition{.wx = wx, .wy = wy};
const result = cache.findOrCreate(compare, cacheInit, ClimateMapFragment.increaseRefCount);
const result = cache.findOrCreate(compare, cacheInit, null);
return result;
}
@ -152,8 +144,7 @@ pub fn getBiomeMap(allocator: NeverFailingAllocator, wx: i32, wy: i32, width: u3
while(wxEnd -% x >= 0) : (x +%= ClimateMapFragment.mapSize) {
var y = wzStart;
while(wzEnd -% y >= 0) : (y +%= ClimateMapFragment.mapSize) {
const mapPiece = getOrGenerateFragmentAndIncreaseRefCount(x, y);
defer mapPiece.decreaseRefCount();
const mapPiece = getOrGenerateFragment(x, y);
// Offset of the indices in the result map:
const xOffset = (x -% wx) >> MapFragment.biomeShift;
const yOffset = (y -% wy) >> MapFragment.biomeShift;

View File

@ -26,12 +26,12 @@ pub const LightMapFragment = struct {
};
}
fn deinit(self: *const LightMapFragment, _: usize) void {
fn privateDeinit(self: *const LightMapFragment, _: usize) void {
main.globalAllocator.destroy(self);
}
pub fn deferredDeinit(self: *LightMapFragment) void {
main.heap.GarbageCollection.deferredFree(.{.ptr = self, .freeFunction = main.utils.castFunctionSelfToAnyopaque(LightMapFragment.deinit)});
main.heap.GarbageCollection.deferredFree(.{.ptr = self, .freeFunction = main.utils.castFunctionSelfToAnyopaque(privateDeinit)});
}
pub fn getHeight(self: *LightMapFragment, wx: i32, wy: i32) i32 {
@ -49,8 +49,7 @@ var cache: Cache(LightMapFragment, cacheSize, associativity, LightMapFragment.de
fn cacheInit(pos: MapFragmentPosition) *LightMapFragment {
const mapFragment = main.globalAllocator.create(LightMapFragment);
mapFragment.init(pos.wx, pos.wy, pos.voxelSize);
const surfaceMap = terrain.SurfaceMap.getOrGenerateFragmentAndIncreaseRefCount(pos.wx, pos.wy, pos.voxelSize);
defer surfaceMap.decreaseRefCount();
const surfaceMap = terrain.SurfaceMap.getOrGenerateFragment(pos.wx, pos.wy, pos.voxelSize);
comptime std.debug.assert(LightMapFragment.mapSize == terrain.SurfaceMap.MapFragment.mapSize);
for(0..LightMapFragment.mapSize) |x| {
for(0..LightMapFragment.mapSize) |y| {

View File

@ -40,7 +40,6 @@ pub const StructureMapFragment = struct {
pos: ChunkPosition,
voxelShift: u5,
refCount: Atomic(u16) = .init(0),
arena: main.heap.NeverFailingArenaAllocator,
allocator: main.heap.NeverFailingAllocator,
@ -68,11 +67,15 @@ pub const StructureMapFragment = struct {
@memset(self.tempData.lists, .{});
}
pub fn deinit(self: *StructureMapFragment) void {
fn privateDeinit(self: *StructureMapFragment, _: usize) void {
self.arena.deinit();
memoryPool.destroy(self);
}
pub fn deferredDeinit(self: *StructureMapFragment) void {
main.heap.GarbageCollection.deferredFree(.{.ptr = self, .freeFunction = main.utils.castFunctionSelfToAnyopaque(privateDeinit)});
}
fn finishGeneration(self: *StructureMapFragment) void {
for(0..self.data.len) |i| {
std.sort.insertion(Structure, self.tempData.lists[i].items, {}, Structure.lessThan);
@ -93,19 +96,6 @@ pub const StructureMapFragment = struct {
return @intCast(((x >> main.chunk.chunkShift + self.voxelShift)*chunkedSize + (y >> main.chunk.chunkShift + self.voxelShift))*chunkedSize + (z >> main.chunk.chunkShift + self.voxelShift));
}
pub fn increaseRefCount(self: *StructureMapFragment) void {
const prevVal = self.refCount.fetchAdd(1, .monotonic);
std.debug.assert(prevVal != 0);
}
pub fn decreaseRefCount(self: *StructureMapFragment) void {
const prevVal = self.refCount.fetchSub(1, .monotonic);
std.debug.assert(prevVal != 0);
if(prevVal == 1) {
self.deinit();
}
}
pub fn generateStructuresInChunk(self: *const StructureMapFragment, chunk: *ServerChunk, caveMap: terrain.CaveMap.CaveMapView, biomeMap: terrain.CaveBiomeMap.CaveBiomeMapView) void {
const index = self.getIndex(chunk.super.pos.wx - self.pos.wx, chunk.super.pos.wy - self.pos.wy, chunk.super.pos.wz - self.pos.wz);
for(self.data[index]) |structure| {
@ -175,7 +165,7 @@ pub const StructureMapGenerator = struct {
const cacheSize = 1 << 10; // Must be a power of 2!
const cacheMask = cacheSize - 1;
const associativity = 8;
var cache: Cache(StructureMapFragment, cacheSize, associativity, StructureMapFragment.decreaseRefCount) = .{};
var cache: Cache(StructureMapFragment, cacheSize, associativity, StructureMapFragment.deferredDeinit) = .{};
var profile: TerrainGenerationProfile = undefined;
var memoryPool: main.heap.MemoryPool(StructureMapFragment) = undefined;
@ -187,38 +177,37 @@ fn cacheInit(pos: ChunkPosition) *StructureMapFragment {
generator.generate(mapFragment, profile.seed ^ generator.generatorSeed);
}
mapFragment.finishGeneration();
_ = @atomicRmw(u16, &mapFragment.refCount.raw, .Add, 1, .monotonic);
return mapFragment;
}
pub fn initGenerators() void {
pub fn globalInit() void {
const list = @import("structuremapgen/_list.zig");
inline for(@typeInfo(list).@"struct".decls) |decl| {
StructureMapGenerator.registerGenerator(@field(list, decl.name));
}
memoryPool = .init(main.globalAllocator);
}
pub fn deinitGenerators() void {
pub fn globalDeinit() void {
StructureMapGenerator.generatorRegistry.clearAndFree(main.globalAllocator.allocator);
memoryPool.deinit();
}
pub fn init(_profile: TerrainGenerationProfile) void {
profile = _profile;
memoryPool = .init(main.globalAllocator);
}
pub fn deinit() void {
cache.clear();
memoryPool.deinit();
}
pub fn getOrGenerateFragmentAndIncreaseRefCount(wx: i32, wy: i32, wz: i32, voxelSize: u31) *StructureMapFragment {
pub fn getOrGenerateFragment(wx: i32, wy: i32, wz: i32, voxelSize: u31) *StructureMapFragment {
const compare = ChunkPosition{
.wx = wx & ~@as(i32, StructureMapFragment.sizeMask*voxelSize | voxelSize - 1),
.wy = wy & ~@as(i32, StructureMapFragment.sizeMask*voxelSize | voxelSize - 1),
.wz = wz & ~@as(i32, StructureMapFragment.sizeMask*voxelSize | voxelSize - 1),
.voxelSize = voxelSize,
};
const result = cache.findOrCreate(compare, cacheInit, StructureMapFragment.increaseRefCount);
const result = cache.findOrCreate(compare, cacheInit, null);
return result;
}

View File

@ -74,25 +74,18 @@ pub const MapFragment = struct { // MARK: MapFragment
wasStored: Atomic(bool) = .init(false),
refCount: Atomic(u16) = .init(0),
pub fn init(self: *MapFragment, wx: i32, wy: i32, voxelSize: u31) void {
self.* = .{
.pos = MapFragmentPosition.init(wx, wy, voxelSize),
};
}
pub fn increaseRefCount(self: *MapFragment) void {
const prevVal = self.refCount.fetchAdd(1, .monotonic);
std.debug.assert(prevVal != 0);
fn privateDeinit(self: *MapFragment, _: usize) void {
memoryPool.destroy(self);
}
pub fn decreaseRefCount(self: *MapFragment) void {
const prevVal = self.refCount.fetchSub(1, .monotonic);
std.debug.assert(prevVal != 0);
if(prevVal == 1) {
memoryPool.destroy(self);
}
pub fn deferredDeinit(self: *MapFragment) void {
main.heap.GarbageCollection.deferredFree(.{.ptr = self, .freeFunction = main.utils.castFunctionSelfToAnyopaque(privateDeinit)});
}
pub fn getBiome(self: *MapFragment, wx: i32, wy: i32) *const Biome {
@ -254,20 +247,22 @@ pub const MapGenerator = struct {
const cacheSize = 1 << 6; // Must be a power of 2!
const cacheMask = cacheSize - 1;
const associativity = 8; // ~400MiB MiB Cache size
var cache: Cache(MapFragment, cacheSize, associativity, MapFragment.decreaseRefCount) = .{};
var cache: Cache(MapFragment, cacheSize, associativity, MapFragment.deferredDeinit) = .{};
var profile: TerrainGenerationProfile = undefined;
var memoryPool: main.heap.MemoryPool(MapFragment) = undefined;
pub fn initGenerators() void {
pub fn globalInit() void {
const list = @import("mapgen/_list.zig");
inline for(@typeInfo(list).@"struct".decls) |decl| {
MapGenerator.registerGenerator(@field(list, decl.name));
}
memoryPool = .init(main.globalAllocator);
}
pub fn deinitGenerators() void {
pub fn globalDeinit() void {
MapGenerator.generatorRegistry.clearAndFree(main.globalAllocator.allocator);
memoryPool.deinit();
}
fn cacheInit(pos: MapFragmentPosition) *MapFragment {
@ -276,7 +271,6 @@ fn cacheInit(pos: MapFragmentPosition) *MapFragment {
_ = mapFragment.load(main.server.world.?.biomePalette, null) catch {
profile.mapFragmentGenerator.generateMapFragment(mapFragment, profile.seed);
};
_ = @atomicRmw(u16, &mapFragment.refCount.raw, .Add, 1, .monotonic);
return mapFragment;
}
@ -575,7 +569,6 @@ pub fn regenerateLOD(worldName: []const u8) !void { // MARK: regenerateLOD()
mapFragment.save(&originalHeightMap, neighborInfo); // Store the interpolated map
// Generate LODs
var cur = mapFragment;
defer if(cur.pos.voxelSize != 1) cur.decreaseRefCount();
while(cur.pos.voxelSizeShift < main.settings.highestSupportedLod) {
var nextPos = cur.pos;
nextPos.voxelSize *= 2;
@ -583,7 +576,7 @@ pub fn regenerateLOD(worldName: []const u8) !void { // MARK: regenerateLOD()
const nextMask = ~@as(i32, nextPos.voxelSize*MapFragment.mapSize - 1);
nextPos.wx &= nextMask;
nextPos.wy &= nextMask;
const next = getOrGenerateFragmentAndIncreaseRefCount(nextPos.wx, nextPos.wy, nextPos.voxelSize);
const next = getOrGenerateFragment(nextPos.wx, nextPos.wy, nextPos.voxelSize);
const offSetX: usize = @intCast((cur.pos.wx -% nextPos.wx) >> nextPos.voxelSizeShift);
const offSetY: usize = @intCast((cur.pos.wy -% nextPos.wy) >> nextPos.voxelSizeShift);
for(0..MapFragment.mapSize/2) |x| {
@ -625,7 +618,6 @@ pub fn regenerateLOD(worldName: []const u8) !void { // MARK: regenerateLOD()
}
next.save(null, .{});
next.wasStored.store(true, .monotonic);
if(cur.pos.voxelSize != 1) cur.decreaseRefCount();
cur = next;
}
}
@ -634,21 +626,19 @@ pub fn regenerateLOD(worldName: []const u8) !void { // MARK: regenerateLOD()
pub fn init(_profile: TerrainGenerationProfile) void {
profile = _profile;
memoryPool = .init(main.globalAllocator);
}
pub fn deinit() void {
cache.clear();
memoryPool.deinit();
}
/// Call deinit on the result.
pub fn getOrGenerateFragmentAndIncreaseRefCount(wx: i32, wy: i32, voxelSize: u31) *MapFragment {
pub fn getOrGenerateFragment(wx: i32, wy: i32, voxelSize: u31) *MapFragment {
const compare = MapFragmentPosition.init(
wx & ~@as(i32, MapFragment.mapMask*voxelSize | voxelSize - 1),
wy & ~@as(i32, MapFragment.mapMask*voxelSize | voxelSize - 1),
voxelSize,
);
const result = cache.findOrCreate(compare, cacheInit, MapFragment.increaseRefCount);
const result = cache.findOrCreate(compare, cacheInit, null);
return result;
}

View File

@ -289,6 +289,8 @@ pub const Biome = struct { // MARK: Biome
caves: f32,
caveRadiusFactor: f32,
crystals: u32,
/// How much of the surface structure should be eroded depending on the slope.
soilCreep: f32,
stoneBlock: main.blocks.Block,
fogLower: f32,
fogHigher: f32,
@ -334,6 +336,7 @@ pub const Biome = struct { // MARK: Biome
.caves = zon.get(f32, "caves", -0.375),
.caveRadiusFactor = @max(-2, @min(2, zon.get(f32, "caveRadiusFactor", 1))),
.crystals = zon.get(u32, "crystals", 0),
.soilCreep = zon.get(f32, "soilCreep", 0.5),
.minHeight = zon.get(i32, "minHeight", std.math.minInt(i32)),
.maxHeight = zon.get(i32, "maxHeight", std.math.maxInt(i32)),
.supportsRivers = zon.get(bool, "rivers", false),
@ -462,11 +465,16 @@ pub const BlockStructure = struct { // MARK: BlockStructure
allocator.free(self.structure);
}
pub fn addSubTerranian(self: BlockStructure, chunk: *ServerChunk, startingDepth: i32, minDepth: i32, x: i32, y: i32, seed: *u64) i32 {
pub fn addSubTerranian(self: BlockStructure, chunk: *ServerChunk, startingDepth: i32, minDepth: i32, slope: i32, soilCreep: f32, x: i32, y: i32, seed: *u64) i32 {
var depth = startingDepth;
var remainingSkippedBlocks = @as(i32, @intFromFloat(@as(f32, @floatFromInt(slope))*soilCreep)) - 1;
for(self.structure) |blockStack| {
const total = blockStack.min + main.random.nextIntBounded(u32, seed, @as(u32, 1) + blockStack.max - blockStack.min);
for(0..total) |_| {
if(remainingSkippedBlocks > 0) {
remainingSkippedBlocks -= 1;
continue;
}
if(chunk.liesInChunk(x, y, depth)) {
chunk.updateBlockInGeneration(x, y, depth, blockStack.block);
}

View File

@ -26,7 +26,6 @@ pub fn init(parameters: ZonElement) void {
pub fn deinit() void {}
pub fn generate(_: u64, chunk: *main.chunk.ServerChunk, caveMap: CaveMap.CaveMapView, biomeMap: CaveBiomeMap.CaveBiomeMapView) void {
const structureMap = terrain.StructureMap.getOrGenerateFragmentAndIncreaseRefCount(chunk.super.pos.wx, chunk.super.pos.wy, chunk.super.pos.wz, chunk.super.pos.voxelSize);
defer structureMap.decreaseRefCount();
const structureMap = terrain.StructureMap.getOrGenerateFragment(chunk.super.pos.wx, chunk.super.pos.wy, chunk.super.pos.wz, chunk.super.pos.voxelSize);
structureMap.generateStructuresInChunk(chunk, caveMap, biomeMap);
}

View File

@ -73,11 +73,32 @@ pub fn generate(worldSeed: u64, chunk: *main.chunk.ServerChunk, caveMap: CaveMap
while(z >= zBiome) : (z -= chunk.super.pos.voxelSize) {
const mask = @as(u64, 1) << @intCast(z >> voxelSizeShift);
if(heightData & mask != 0) {
const cardinalDirections = [_]Vec3i{
Vec3i{1, 0, 0},
Vec3i{-1, 0, 0},
Vec3i{0, 1, 0},
Vec3i{0, -1, 0},
};
const surfaceBlock = caveMap.findTerrainChangeAbove(x, y, z) - chunk.super.pos.voxelSize;
var maxUp: i32 = 0;
var maxDown: i32 = 0;
for(cardinalDirections) |direction| {
if(caveMap.isSolid(x + direction[0], y + direction[1], z + direction[2])) {
const diff = caveMap.findTerrainChangeAbove(x + direction[0], y + direction[1], z + direction[2]) - chunk.super.pos.voxelSize - surfaceBlock;
maxUp = @max(maxUp, diff);
} else {
const diff = caveMap.findTerrainChangeBelow(x + direction[0], y + direction[1], z + direction[2]) - surfaceBlock;
maxDown = @max(maxDown, -diff);
}
}
const slope = @min(maxUp, maxDown);
const soilCreep: f32 = biome.soilCreep;
var bseed: u64 = random.initSeed3D(worldSeed, .{chunk.super.pos.wx + x, chunk.super.pos.wy + y, chunk.super.pos.wz + z});
const airBlockBelow = caveMap.findTerrainChangeBelow(x, y, z);
// Add the biomes surface structure:
z = @min(z + chunk.super.pos.voxelSize, biome.structure.addSubTerranian(chunk, surfaceBlock, @max(airBlockBelow, zBiome - 1), x, y, &bseed));
z = @min(z + chunk.super.pos.voxelSize, biome.structure.addSubTerranian(chunk, surfaceBlock, @max(airBlockBelow, zBiome - 1), slope, soilCreep, x, y, &bseed));
z -= chunk.super.pos.voxelSize;
if(z < zBiome) break;
if(z > airBlockBelow) {

View File

@ -133,12 +133,12 @@ pub const TerrainGenerationProfile = struct {
}
};
pub fn initGenerators() void {
SurfaceMap.initGenerators();
ClimateMap.initGenerators();
CaveBiomeMap.initGenerators();
CaveMap.initGenerators();
StructureMap.initGenerators();
pub fn globalInit() void {
SurfaceMap.globalInit();
ClimateMap.globalInit();
CaveBiomeMap.globalInit();
CaveMap.globalInit();
StructureMap.globalInit();
const list = @import("chunkgen/_list.zig");
inline for(@typeInfo(list).@"struct".decls) |decl| {
BlockGenerator.registerGenerator(@field(list, decl.name));
@ -148,12 +148,12 @@ pub fn initGenerators() void {
std.log.info("Blue noise took {} ms to load", .{std.time.milliTimestamp() -% t1});
}
pub fn deinitGenerators() void {
SurfaceMap.deinitGenerators();
ClimateMap.deinitGenerators();
CaveBiomeMap.deinitGenerators();
CaveMap.deinitGenerators();
StructureMap.deinitGenerators();
pub fn globalDeinit() void {
CaveBiomeMap.globalDeinit();
CaveMap.globalDeinit();
StructureMap.globalDeinit();
ClimateMap.globalDeinit();
SurfaceMap.globalDeinit();
BlockGenerator.generatorRegistry.clearAndFree(main.globalAllocator.allocator);
}

View File

@ -321,8 +321,7 @@ const ChunkManager = struct { // MARK: ChunkManager
return ch;
}
ch.generated = true;
const caveMap = terrain.CaveMap.CaveMapView.init(ch);
defer caveMap.deinit();
const caveMap = terrain.CaveMap.CaveMapView.findMapsAround(ch);
const biomeMap = terrain.CaveBiomeMap.CaveBiomeMapView.init(main.stackAllocator, ch.super.pos, ch.super.width, 32);
defer biomeMap.deinit();
for(server.world.?.chunkManager.terrainGenerationProfile.generators) |generator| {
@ -539,6 +538,85 @@ pub const ServerWorld = struct { // MARK: ServerWorld
try files.writeZon(try std.fmt.allocPrint(arenaAllocator.allocator, "saves/{s}/tool_palette.zig.zon", .{path}), self.toolPalette.storeToZon(arenaAllocator));
try files.writeZon(try std.fmt.allocPrint(arenaAllocator.allocator, "saves/{s}/biome_palette.zig.zon", .{path}), self.biomePalette.storeToZon(arenaAllocator));
convert_player_data_to_binary: { // TODO: Remove after #480
std.log.debug("Migrating old player inventory format to binary.", .{});
const playerDataPath = std.fmt.allocPrint(main.stackAllocator.allocator, "saves/{s}/players", .{path}) catch unreachable;
defer main.stackAllocator.free(playerDataPath);
var playerDataDirectory = std.fs.cwd().openDir(playerDataPath, .{.iterate = true}) catch break :convert_player_data_to_binary;
defer playerDataDirectory.close();
{
var walker = playerDataDirectory.walk(main.stackAllocator.allocator) catch unreachable;
defer walker.deinit();
while(walker.next() catch |err| {
std.log.err("Couldn't fetch next directory entry due to an error: {s}", .{@errorName(err)});
break :convert_player_data_to_binary;
}) |entry| {
if(entry.kind != .file) continue;
if(!std.ascii.endsWithIgnoreCase(entry.basename, ".zon")) continue;
const absolutePath = std.fmt.allocPrint(main.stackAllocator.allocator, "{s}/{s}", .{playerDataPath, entry.path}) catch unreachable;
defer main.stackAllocator.free(absolutePath);
const playerData = files.readToZon(main.stackAllocator, absolutePath) catch |err| {
std.log.err("Could not read player data file '{s}'': {s}.", .{absolutePath, @errorName(err)});
continue;
};
defer playerData.deinit(main.stackAllocator);
std.log.debug("Migrating player data file: '{s}'", .{absolutePath});
const entryKeys: [2][]const u8 = .{
"playerInventory",
"hand",
};
for(entryKeys) |key| {
const zon = playerData.getChild(key);
switch(zon) {
.object => {
std.log.debug("Migrating inventory '{s}' '{s}'", .{key, absolutePath});
var temp: main.items.Inventory = undefined;
temp._items = main.stackAllocator.alloc(ItemStack, zon.get(u32, "capacity", 0));
defer main.stackAllocator.free(temp._items);
for(temp._items) |*stack| stack.* = ItemStack{};
defer for(temp._items) |*stack| stack.deinit();
temp.loadFromZon(zon);
for(temp._items, 0..) |*stack, i| {
std.log.debug("Item #{}: {} x {s}", .{i, stack.amount, if(stack.item) |item| item.id() else "null"});
}
const base64Data = savePlayerInventory(main.stackAllocator, temp);
const old = playerData.object.fetchPut(key, .{.stringOwned = base64Data}) catch unreachable orelse unreachable;
old.value.deinit(main.stackAllocator);
},
.string, .stringOwned => |field| {
std.log.debug("Skipping key '{s}', type is 'string', value is '{s}'", .{key, field});
},
.null => {
std.log.debug("Skipping key '{s}', type is 'null'", .{key});
},
else => |other| {
const representation = zon.toString(main.stackAllocator);
defer main.stackAllocator.free(representation);
std.log.err("Encountered unexpected type ({s}) while migrating '{s}': {s}", .{@tagName(other), absolutePath, representation});
},
}
}
files.writeZon(absolutePath, playerData) catch |err| {
std.log.err("Could not write player data file {s}: {s}.", .{absolutePath, @errorName(err)});
continue;
};
}
}
}
var gamerules = files.readToZon(arenaAllocator, try std.fmt.allocPrint(arenaAllocator.allocator, "saves/{s}/gamerules.zig.zon", .{path})) catch ZonElement.initObject(arenaAllocator);
self.defaultGamemode = std.meta.stringToEnum(main.game.Gamemode, gamerules.get([]const u8, "default_gamemode", "creative")) orelse .creative;
@ -642,8 +720,7 @@ pub const ServerWorld = struct { // MARK: ServerWorld
for(0..2) |dy| {
const mapX = mapStartX +% main.server.terrain.SurfaceMap.MapFragment.mapSize*@as(i32, @intCast(dx));
const mapY = mapStartY +% main.server.terrain.SurfaceMap.MapFragment.mapSize*@as(i32, @intCast(dy));
const map = main.server.terrain.SurfaceMap.getOrGenerateFragmentAndIncreaseRefCount(mapX, mapY, ch.super.pos.voxelSize);
defer map.decreaseRefCount();
const map = main.server.terrain.SurfaceMap.getOrGenerateFragment(mapX, mapY, ch.super.pos.voxelSize);
if(!map.wasStored.swap(true, .monotonic)) {
map.save(null, .{});
}
@ -776,8 +853,7 @@ pub const ServerWorld = struct { // MARK: ServerWorld
var dir: main.chunk.Neighbor = .dirNegX;
var stepsRemaining: usize = 1;
for(0..spiralLen) |_| {
const map = main.server.terrain.ClimateMap.getOrGenerateFragmentAndIncreaseRefCount(wx, wy);
defer map.decreaseRefCount();
const map = main.server.terrain.ClimateMap.getOrGenerateFragment(wx, wy);
for(0..map.map.len) |_| {
const x = main.random.nextIntBounded(u31, &main.seed, map.map.len);
const y = main.random.nextIntBounded(u31, &main.seed, map.map.len);
@ -816,8 +892,7 @@ pub const ServerWorld = struct { // MARK: ServerWorld
}
std.log.err("Found no valid spawn location", .{});
}
const map = terrain.SurfaceMap.getOrGenerateFragmentAndIncreaseRefCount(self.spawn[0], self.spawn[1], 1);
defer map.decreaseRefCount();
const map = terrain.SurfaceMap.getOrGenerateFragment(self.spawn[0], self.spawn[1], 1);
self.spawn[2] = map.getHeight(self.spawn[0], self.spawn[1]) + 1;
}
const newBiomeCheckSum: i64 = @bitCast(terrain.biomes.getBiomeCheckSum(self.seed));
@ -862,9 +937,37 @@ pub const ServerWorld = struct { // MARK: ServerWorld
main.items.Inventory.Sync.setGamemode(user, std.meta.stringToEnum(main.game.Gamemode, playerData.get([]const u8, "gamemode", @tagName(self.defaultGamemode))) orelse self.defaultGamemode);
}
user.inventory = loadPlayerInventory(main.game.Player.inventorySize, playerData.get([]const u8, "playerInventory", ""), .{.playerInventory = user.id}, path);
user.handInventory = loadPlayerInventory(1, playerData.get([]const u8, "hand", ""), .{.hand = user.id}, path);
}
user.inventory = main.items.Inventory.Sync.ServerSide.createExternallyManagedInventory(main.game.Player.inventorySize, .normal, .{.playerInventory = user.id}, playerData.getChild("playerInventory"));
user.handInventory = main.items.Inventory.Sync.ServerSide.createExternallyManagedInventory(1, .normal, .{.hand = user.id}, playerData.getChild("hand"));
fn loadPlayerInventory(size: usize, base64EncodedData: []const u8, source: main.items.Inventory.Source, playerDataFilePath: []const u8) u32 {
const decodedSize = std.base64.url_safe.Decoder.calcSizeForSlice(base64EncodedData) catch |err| blk: {
std.log.err("Encountered incorrectly encoded inventory data ({s}) while loading data from file '{s}': '{s}'", .{@errorName(err), playerDataFilePath, base64EncodedData});
break :blk 0;
};
const bytes: []u8 = main.stackAllocator.alloc(u8, decodedSize);
defer main.stackAllocator.free(bytes);
var readerInput: []const u8 = bytes;
std.base64.url_safe.Decoder.decode(bytes, base64EncodedData) catch |err| {
std.log.err("Encountered incorrectly encoded inventory data ({s}) while loading data from file '{s}': '{s}'", .{@errorName(err), playerDataFilePath, base64EncodedData});
readerInput = "";
};
var reader: main.utils.BinaryReader = .init(readerInput);
return main.items.Inventory.Sync.ServerSide.createExternallyManagedInventory(size, .normal, source, &reader);
}
fn savePlayerInventory(allocator: NeverFailingAllocator, inv: main.items.Inventory) []const u8 {
var writer = main.utils.BinaryWriter.init(main.stackAllocator);
defer writer.deinit();
inv.toBytes(&writer);
const destination: []u8 = allocator.alloc(u8, std.base64.url_safe.Encoder.calcSize(writer.data.items.len));
return std.base64.url_safe.Encoder.encode(destination, writer.data.items);
}
pub fn savePlayer(self: *ServerWorld, user: *User) !void {
@ -892,11 +995,11 @@ pub const ServerWorld = struct { // MARK: ServerWorld
main.items.Inventory.Sync.ServerSide.mutex.lock();
defer main.items.Inventory.Sync.ServerSide.mutex.unlock();
if(main.items.Inventory.Sync.ServerSide.getInventoryFromSource(.{.playerInventory = user.id})) |inv| {
playerZon.put("playerInventory", inv.save(main.stackAllocator));
playerZon.put("playerInventory", ZonElement{.stringOwned = savePlayerInventory(main.stackAllocator, inv)});
} else @panic("The player inventory wasn't found. Cannot save player data.");
if(main.items.Inventory.Sync.ServerSide.getInventoryFromSource(.{.hand = user.id})) |inv| {
playerZon.put("hand", inv.save(main.stackAllocator));
playerZon.put("hand", ZonElement{.stringOwned = savePlayerInventory(main.stackAllocator, inv)});
} else @panic("The player hand inventory wasn't found. Cannot save player data.");
}
@ -931,8 +1034,7 @@ pub const ServerWorld = struct { // MARK: ServerWorld
}
fn isValidSpawnLocation(_: *ServerWorld, wx: i32, wy: i32) bool {
const map = terrain.SurfaceMap.getOrGenerateFragmentAndIncreaseRefCount(wx, wy, 1);
defer map.decreaseRefCount();
const map = terrain.SurfaceMap.getOrGenerateFragment(wx, wy, 1);
return map.getBiome(wx, wy).isValidPlayerSpawn;
}

View File

@ -124,7 +124,7 @@ pub fn deinit() void {
}
pub fn save() void {
const zonObject = ZonElement.initObject(main.stackAllocator);
var zonObject = ZonElement.initObject(main.stackAllocator);
defer zonObject.deinit(main.stackAllocator);
inline for(@typeInfo(@This()).@"struct".decls) |decl| {
@ -154,7 +154,7 @@ pub fn save() void {
zonObject.put("keyboard", keyboard);
// Merge with the old settings file to preserve unknown settings.
const oldZonObject: ZonElement = main.files.cubyzDir().readToZon(main.stackAllocator, settingsFile) catch |err| blk: {
var oldZonObject: ZonElement = main.files.cubyzDir().readToZon(main.stackAllocator, settingsFile) catch |err| blk: {
if(err != error.FileNotFound) {
std.log.err("Could not read settings file: {s}", .{@errorName(err)});
}
@ -162,7 +162,13 @@ pub fn save() void {
};
defer oldZonObject.deinit(main.stackAllocator);
oldZonObject.join(zonObject);
if(oldZonObject == .object) {
oldZonObject.join(zonObject);
} else {
oldZonObject.deinit(main.stackAllocator);
oldZonObject = zonObject;
zonObject = .null;
}
main.files.cubyzDir().writeZon(settingsFile, oldZonObject) catch |err| {
std.log.err("Couldn't write settings to file: {s}", .{@errorName(err)});

View File

@ -122,9 +122,8 @@ pub const StackAllocator = struct { // MARK: StackAllocator
previousTrailer = self.getTrailerBefore(previousTrailer.previousAllocationTrailer);
}
}
} else {
trailer.wasFreed = true;
}
trailer.wasFreed = true;
} else {
self.backingAllocator.rawFree(memory, alignment, ret_addr);
}
@ -618,7 +617,7 @@ pub const GarbageCollection = struct { // MARK: GarbageCollection
_ = old.totalThreads - 1; // Assert no overflow
if(old.cycle != threadCycle) removeThreadFromWaiting();
const newTime = std.time.milliTimestamp();
if(newTime -% lastSyncPointTime > 10_000) {
if(newTime -% lastSyncPointTime > 20_000) {
std.log.err("No sync point executed in {} ms for thread. Did you forget to add a sync point in the thread's main loop?", .{newTime -% lastSyncPointTime});
std.debug.dumpCurrentStackTrace(null);
}
@ -653,7 +652,7 @@ pub const GarbageCollection = struct { // MARK: GarbageCollection
/// Must be called when no objects originating from other threads are held on the current function stack
pub fn syncPoint() void {
const newTime = std.time.milliTimestamp();
if(newTime -% lastSyncPointTime > 10_000) {
if(newTime -% lastSyncPointTime > 20_000) {
std.log.err("No sync point executed in {} ms. Did you forget to add a sync point in the thread's main loop", .{newTime -% lastSyncPointTime});
std.debug.dumpCurrentStackTrace(null);
}