Cubyz/src/items.zig
Krzysztof Wiśniewski 254546c789
Use enum instead of packed struct for *Index objects (#1640)
Resolves: #1600

---------

Co-authored-by: IntegratedQuantum <43880493+IntegratedQuantum@users.noreply.github.com>
2025-06-28 15:45:34 +02:00

1146 lines
35 KiB
Zig
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

const std = @import("std");
const blocks = @import("blocks.zig");
const Block = blocks.Block;
const graphics = @import("graphics.zig");
const Color = graphics.Color;
const Tag = main.Tag;
const ZonElement = @import("zon.zig").ZonElement;
const main = @import("main");
const chunk = main.chunk;
const random = @import("random.zig");
const vec = @import("vec.zig");
const Mat4f = vec.Mat4f;
const Vec2f = vec.Vec2f;
const Vec2i = vec.Vec2i;
const Vec3i = vec.Vec3i;
const Vec3f = vec.Vec3f;
const NeverFailingAllocator = main.heap.NeverFailingAllocator;
const ListUnmanaged = main.ListUnmanaged;
const modifierList = @import("tool/modifiers/_list.zig");
const modifierRestrictionList = @import("tool/modifiers/restrictions/_list.zig");
pub const Inventory = @import("Inventory.zig");
const Material = struct { // MARK: Material
density: f32 = undefined,
elasticity: f32 = undefined,
hardness: f32 = undefined,
textureRoughness: f32 = undefined,
colorPalette: []Color = undefined,
modifiers: []Modifier = undefined,
pub fn init(self: *Material, allocator: NeverFailingAllocator, zon: ZonElement) void {
self.density = zon.get(f32, "density", 1.0);
self.elasticity = zon.get(f32, "elasticity", 1.0);
self.hardness = zon.get(f32, "hardness", 1.0);
self.textureRoughness = @max(0, zon.get(f32, "textureRoughness", 1.0));
const colors = zon.getChild("colors");
self.colorPalette = allocator.alloc(Color, colors.toSlice().len);
for(colors.toSlice(), self.colorPalette) |item, *color| {
const colorInt: u32 = @intCast(item.as(i64, 0xff000000) & 0xffffffff);
color.* = Color{
.r = @intCast(colorInt >> 16 & 0xff),
.g = @intCast(colorInt >> 8 & 0xff),
.b = @intCast(colorInt >> 0 & 0xff),
.a = @intCast(colorInt >> 24 & 0xff),
};
}
const modifiersZon = zon.getChild("modifiers");
self.modifiers = allocator.alloc(Modifier, modifiersZon.toSlice().len);
for(modifiersZon.toSlice(), self.modifiers) |item, *modifier| {
const id = item.get([]const u8, "id", "not specified");
const vTable = modifiers.get(id) orelse blk: {
std.log.err("Couldn't find modifier with id '{s}'. Replacing it with 'durable'", .{id});
break :blk modifiers.get("durable") orelse unreachable;
};
modifier.* = .{
.vTable = vTable,
.data = vTable.loadData(item),
.restriction = ModifierRestriction.loadFromZon(allocator, item.getChild("restriction")),
};
}
}
pub fn hashCode(self: Material) u32 {
var hash: u32 = @bitCast(self.density);
hash = 101*%hash +% @as(u32, @bitCast(self.density));
hash = 101*%hash +% @as(u32, @bitCast(self.elasticity));
hash = 101*%hash +% @as(u32, @bitCast(self.hardness));
hash = 101*%hash +% @as(u32, @bitCast(self.textureRoughness));
hash ^= hash >> 24;
return hash;
}
fn getProperty(self: Material, prop: MaterialProperty) f32 {
switch(prop) {
inline else => |field| return @field(self, @tagName(field)),
}
}
};
pub const ModifierRestriction = struct {
vTable: *const VTable,
data: *anyopaque,
pub const VTable = struct {
satisfied: *const fn(data: *anyopaque, tool: *const Tool, x: i32, y: i32) bool,
loadFromZon: *const fn(allocator: NeverFailingAllocator, zon: ZonElement) *anyopaque,
};
pub fn satisfied(self: ModifierRestriction, tool: *const Tool, x: i32, y: i32) bool {
return self.vTable.satisfied(self.data, tool, x, y);
}
pub fn loadFromZon(allocator: NeverFailingAllocator, zon: ZonElement) ModifierRestriction {
const id = zon.get([]const u8, "id", "always");
const vTable = modifierRestrictions.get(id) orelse blk: {
std.log.err("Couldn't find modifier restriction with id '{s}'. Replacing it with 'always'", .{id});
break :blk modifierRestrictions.get("always") orelse unreachable;
};
return .{
.vTable = vTable,
.data = vTable.loadFromZon(allocator, zon),
};
}
};
const Modifier = struct {
data: VTable.Data,
restriction: ModifierRestriction,
vTable: *const VTable,
pub const VTable = struct {
const Data = packed struct(u128) {pad: u128};
combineModifiers: *const fn(data1: Data, data2: Data) ?Data,
changeToolParameters: *const fn(tool: *Tool, data: Data) void,
changeBlockDamage: *const fn(damage: f32, block: main.blocks.Block, data: Data) f32,
printTooltip: *const fn(outString: *main.List(u8), data: Data) void,
loadData: *const fn(zon: ZonElement) Data,
priority: f32,
};
pub fn combineModifiers(a: Modifier, b: Modifier) ?Modifier {
std.debug.assert(a.vTable == b.vTable);
return .{
.data = a.vTable.combineModifiers(a.data, b.data) orelse return null,
.vTable = a.vTable,
.restriction = undefined,
};
}
pub fn changeToolParameters(self: Modifier, tool: *Tool) void {
self.vTable.changeToolParameters(tool, self.data);
}
pub fn changeBlockDamage(self: Modifier, damage: f32, block: main.blocks.Block) f32 {
return self.vTable.changeBlockDamage(damage, block, self.data);
}
pub fn printTooltip(self: Modifier, outString: *main.List(u8)) void {
self.vTable.printTooltip(outString, self.data);
}
};
const MaterialProperty = enum {
density,
elasticity,
hardness,
fn fromString(string: []const u8) ?MaterialProperty {
return std.meta.stringToEnum(MaterialProperty, string) orelse {
std.log.err("Couldn't find material property {s}.", .{string});
return null;
};
}
};
pub const BaseItemIndex = enum(u16) {
_,
pub fn fromId(_id: []const u8) ?BaseItemIndex {
return reverseIndices.get(_id);
}
pub fn image(self: BaseItemIndex) graphics.Image {
return itemList[@intFromEnum(self)].image;
}
pub fn texture(self: BaseItemIndex) ?graphics.Texture {
return itemList[@intFromEnum(self)].texture;
}
pub fn id(self: BaseItemIndex) []const u8 {
return itemList[@intFromEnum(self)].id;
}
pub fn name(self: BaseItemIndex) []const u8 {
return itemList[@intFromEnum(self)].name;
}
pub fn tags(self: BaseItemIndex) []const Tag {
return itemList[@intFromEnum(self)].tags;
}
pub fn stackSize(self: BaseItemIndex) u16 {
return itemList[@intFromEnum(self)].stackSize;
}
pub fn material(self: BaseItemIndex) ?Material {
return itemList[@intFromEnum(self)].material;
}
pub fn block(self: BaseItemIndex) ?u16 {
return itemList[@intFromEnum(self)].block;
}
pub fn hasTag(self: BaseItemIndex, tag: Tag) bool {
return itemList[@intFromEnum(self)].hasTag(tag);
}
pub fn hashCode(self: BaseItemIndex) u32 {
return itemList[@intFromEnum(self)].hashCode();
}
pub fn getTexture(self: BaseItemIndex) graphics.Texture {
return itemList[@intFromEnum(self)].getTexture();
}
pub fn getTooltip(self: BaseItemIndex) []const u8 {
return itemList[@intFromEnum(self)].getTooltip();
}
};
pub const BaseItem = struct { // MARK: BaseItem
image: graphics.Image,
texture: ?graphics.Texture, // TODO: Properly deinit
id: []const u8,
name: []const u8,
tags: []const Tag,
stackSize: u16,
material: ?Material,
block: ?u16,
foodValue: f32, // TODO: Effects.
var unobtainable = BaseItem{
.image = graphics.Image.defaultImage,
.texture = null,
.id = "unobtainable",
.name = "unobtainable",
.stackSize = 0,
.material = null,
.block = null,
.foodValue = 0,
};
fn init(self: *BaseItem, allocator: NeverFailingAllocator, texturePath: []const u8, replacementTexturePath: []const u8, id: []const u8, zon: ZonElement) void {
self.id = allocator.dupe(u8, id);
if(texturePath.len == 0) {
self.image = graphics.Image.defaultImage;
} else {
self.image = graphics.Image.readFromFile(allocator, texturePath) catch graphics.Image.readFromFile(allocator, replacementTexturePath) catch blk: {
std.log.err("Item texture not found in {s} and {s}.", .{texturePath, replacementTexturePath});
break :blk graphics.Image.defaultImage;
};
}
self.name = allocator.dupe(u8, zon.get([]const u8, "name", id));
self.tags = Tag.loadTagsFromZon(allocator, zon.getChild("tags"));
self.stackSize = zon.get(u16, "stackSize", 120);
const material = zon.getChild("material");
if(material == .object) {
self.material = Material{};
self.material.?.init(allocator, material);
} else {
self.material = null;
}
self.block = blk: {
break :blk blocks.getTypeById(zon.get(?[]const u8, "block", null) orelse break :blk null);
};
self.texture = null;
self.foodValue = zon.get(f32, "food", 0);
}
fn hashCode(self: BaseItem) u32 {
var hash: u32 = 0;
for(self.id) |char| {
hash = hash*%33 +% char;
}
return hash;
}
pub fn getTexture(self: *BaseItem) graphics.Texture {
if(self.texture == null) {
if(self.image.imageData.ptr == graphics.Image.defaultImage.imageData.ptr) {
if(self.block) |blockType| {
self.texture = graphics.generateBlockTexture(blockType);
} else {
self.texture = graphics.Texture.init();
self.texture.?.generate(self.image);
}
} else {
self.texture = graphics.Texture.init();
self.texture.?.generate(self.image);
}
}
return self.texture.?;
}
fn getTooltip(self: BaseItem) []const u8 {
return self.name;
}
pub fn hasTag(self: *const BaseItem, tag: Tag) bool {
for(self.tags) |other| {
if(other == tag) return true;
}
return false;
}
};
///Generates the texture of a Tool using the material information.
const TextureGenerator = struct { // MARK: TextureGenerator
fn generateHeightMap(itemGrid: *[16][16]?BaseItemIndex, seed: *u64) [17][17]f32 {
var heightMap: [17][17]f32 = undefined;
var x: u8 = 0;
while(x < 17) : (x += 1) {
var y: u8 = 0;
while(y < 17) : (y += 1) {
heightMap[x][y] = 0;
// The heighmap basically consists of the amount of neighbors this pixel has.
// Also check if there are different neighbors.
const oneItem = itemGrid[if(x == 0) x else x - 1][if(y == 0) y else y - 1];
var hasDifferentItems: bool = false;
var dx: i32 = -1;
while(dx <= 0) : (dx += 1) {
if(x + dx < 0 or x + dx >= 16) continue;
var dy: i32 = -1;
while(dy <= 0) : (dy += 1) {
if(y + dy < 0 or y + dy >= 16) continue;
const otherItem = itemGrid[@intCast(x + dx)][@intCast(y + dy)];
heightMap[x][y] = if(otherItem) |item| (if(item.material()) |material| 1 + (4*random.nextFloat(seed) - 2)*material.textureRoughness else 0) else 0;
if(otherItem != oneItem) {
hasDifferentItems = true;
}
}
}
// If there is multiple items at this junction, make it go inward to make embedded parts stick out more:
if(hasDifferentItems) {
heightMap[x][y] -= 1;
}
// Take into account further neighbors with lower priority:
dx = -2;
while(dx <= 1) : (dx += 1) {
if(x + dx < 0 or x + dx >= 16) continue;
var dy: i32 = -2;
while(dy <= 1) : (dy += 1) {
if(y + dy < 0 or y + dy >= 16) continue;
const otherItem = itemGrid[@intCast(x + dx)][@intCast(y + dy)];
const dVec = Vec2f{@as(f32, @floatFromInt(dx)) + 0.5, @as(f32, @floatFromInt(dy)) + 0.5};
heightMap[x][y] += if(otherItem != null) 1.0/vec.dot(dVec, dVec) else 0;
}
}
}
}
return heightMap;
}
pub fn generate(tool: *Tool) void {
const img = tool.image;
for(0..16) |x| {
for(0..16) |y| {
const source = tool.type.pixelSources()[x][y];
const sourceOverlay = tool.type.pixelSourcesOverlay()[x][y];
if(sourceOverlay < 25 and tool.craftingGrid[sourceOverlay] != null) {
tool.materialGrid[x][y] = tool.craftingGrid[sourceOverlay];
} else if(source < 25) {
tool.materialGrid[x][y] = tool.craftingGrid[source];
} else {
tool.materialGrid[x][y] = null;
}
}
}
var seed: u64 = tool.seed;
random.scrambleSeed(&seed);
// Generate a height map, which will be used for lighting calulations.
const heightMap = generateHeightMap(&tool.materialGrid, &seed);
var x: u8 = 0;
while(x < 16) : (x += 1) {
var y: u8 = 0;
while(y < 16) : (y += 1) {
if(tool.materialGrid[x][y]) |item| {
if(item.material()) |material| {
// Calculate the lighting based on the nearest free space:
const lightTL = heightMap[x][y] - heightMap[x + 1][y + 1];
const lightTR = heightMap[x + 1][y] - heightMap[x][y + 1];
var light = 2 - @as(i32, @intFromFloat(@round((lightTL*2 + lightTR)/6)));
light = @max(@min(light, 4), 0);
img.setRGB(x, 15 - y, material.colorPalette[@intCast(light)]);
} else {
img.setRGB(x, 15 - y, if((x ^ y) & 1 == 0) Color{.r = 255, .g = 0, .b = 255, .a = 255} else Color{.r = 0, .g = 0, .b = 0, .a = 255});
}
} else {
img.setRGB(x, 15 - y, Color{.r = 0, .g = 0, .b = 0, .a = 0});
}
}
}
}
};
/// Determines the physical properties of a tool to caclulate in-game parameters such as durability and speed.
const ToolPhysics = struct { // MARK: ToolPhysics
/// Determines all the basic properties of the tool.
pub fn evaluateTool(tool: *Tool) void {
inline for(comptime std.meta.fieldNames(ToolProperty)) |name| {
@field(tool, name) = 0;
}
var tempModifiers: main.List(Modifier) = .init(main.stackAllocator);
defer tempModifiers.deinit();
for(tool.type.properties()) |property| {
var sum: f32 = 0;
var weight: f32 = 0;
for(0..25) |i| {
const material = (tool.craftingGrid[i] orelse continue).material() orelse continue;
sum += property.weigths[i]*material.getProperty(property.source orelse break);
weight += property.weigths[i];
}
if(weight == 0) continue;
switch(property.method) {
.sum => {},
.average => {
sum /= weight;
},
}
sum *= property.resultScale;
tool.getProperty(property.destination orelse continue).* += sum;
}
if(tool.damage < 1) tool.damage = 1/(2 - tool.damage);
if(tool.swingTime < 1) tool.swingTime = 1/(2 - tool.swingTime);
for(0..25) |i| {
const material = (tool.craftingGrid[i] orelse continue).material() orelse continue;
outer: for(material.modifiers) |newMod| {
if(!newMod.restriction.satisfied(tool, @intCast(i%5), @intCast(i/5))) continue;
for(tempModifiers.items) |*oldMod| {
if(oldMod.vTable == newMod.vTable) {
oldMod.* = oldMod.combineModifiers(newMod) orelse continue;
continue :outer;
}
}
tempModifiers.append(newMod);
}
}
std.sort.insertion(Modifier, tempModifiers.items, {}, struct {
fn lessThan(_: void, lhs: Modifier, rhs: Modifier) bool {
return lhs.vTable.priority < rhs.vTable.priority;
}
}.lessThan);
tool.modifiers = main.globalAllocator.dupe(Modifier, tempModifiers.items);
for(tempModifiers.items) |mod| {
mod.changeToolParameters(tool);
}
tool.maxDurability = @round(tool.maxDurability);
if(tool.maxDurability < 1) tool.maxDurability = 1;
tool.durability = std.math.lossyCast(u32, tool.maxDurability);
if(!checkConnectivity(tool)) {
tool.maxDurability = 0;
tool.durability = 1;
}
}
fn checkConnectivity(tool: *Tool) bool {
var gridCellsReached: [16][16]bool = @splat(@splat(false));
var floodfillQueue = main.utils.CircularBufferQueue(Vec2i).init(main.stackAllocator, 16);
defer floodfillQueue.deinit();
outer: for(tool.materialGrid, 0..) |row, x| {
for(row, 0..) |entry, y| {
if(entry != null) {
floodfillQueue.enqueue(.{@intCast(x), @intCast(y)});
gridCellsReached[x][y] = true;
break :outer;
}
}
}
while(floodfillQueue.dequeue()) |pos| {
for([4]Vec2i{.{-1, 0}, .{1, 0}, .{0, -1}, .{0, 1}}) |delta| {
const newPos = pos + delta;
if(newPos[0] < 0 or newPos[0] >= gridCellsReached.len) continue;
if(newPos[1] < 0 or newPos[1] >= gridCellsReached.len) continue;
const x: usize = @intCast(newPos[0]);
const y: usize = @intCast(newPos[1]);
if(gridCellsReached[x][y]) continue;
if(tool.materialGrid[x][y] == null) continue;
gridCellsReached[x][y] = true;
floodfillQueue.enqueue(newPos);
}
}
for(tool.materialGrid, 0..) |row, x| {
for(row, 0..) |entry, y| {
if(entry != null and !gridCellsReached[x][y]) {
return false;
}
}
}
return true;
}
};
const SlotInfo = packed struct { // MARK: SlotInfo
disabled: bool = false,
optional: bool = false,
};
const PropertyMatrix = struct { // MARK: PropertyMatrix
source: ?MaterialProperty,
destination: ?ToolProperty,
weigths: [25]f32,
resultScale: f32,
method: Method,
const Method = enum {
average,
sum,
fn fromString(string: []const u8) ?Method {
return std.meta.stringToEnum(Method, string) orelse {
std.log.err("Couldn't find property matrix method {s}.", .{string});
return null;
};
}
};
};
pub const ToolTypeIndex = enum(u16) {
_,
const ToolTypeIterator = struct {
i: u16 = 0,
pub fn next(self: *ToolTypeIterator) ?ToolTypeIndex {
if(self.i >= toolTypeList.items.len) return null;
defer self.i += 1;
return @enumFromInt(self.i);
}
};
pub fn iterator() ToolTypeIterator {
return .{};
}
pub fn fromId(_id: []const u8) ?ToolTypeIndex {
return toolTypeIdToIndex.get(_id);
}
pub fn id(self: ToolTypeIndex) []const u8 {
return toolTypeList.items[@intFromEnum(self)].id;
}
pub fn blockTags(self: ToolTypeIndex) []const Tag {
return toolTypeList.items[@intFromEnum(self)].blockTags;
}
pub fn properties(self: ToolTypeIndex) []const PropertyMatrix {
return toolTypeList.items[@intFromEnum(self)].properties;
}
pub fn slotInfos(self: ToolTypeIndex) *const [25]SlotInfo {
return &toolTypeList.items[@intFromEnum(self)].slotInfos;
}
pub fn pixelSources(self: ToolTypeIndex) *const [16][16]u8 {
return &toolTypeList.items[@intFromEnum(self)].pixelSources;
}
pub fn pixelSourcesOverlay(self: ToolTypeIndex) *const [16][16]u8 {
return &toolTypeList.items[@intFromEnum(self)].pixelSourcesOverlay;
}
};
pub const ToolType = struct { // MARK: ToolType
id: []const u8,
blockTags: []main.Tag,
properties: []PropertyMatrix,
slotInfos: [25]SlotInfo,
pixelSources: [16][16]u8,
pixelSourcesOverlay: [16][16]u8,
};
const ToolProperty = enum {
damage,
maxDurability,
swingTime,
fn fromString(string: []const u8) ?ToolProperty {
return std.meta.stringToEnum(ToolProperty, string) orelse {
std.log.err("Couldn't find tool property {s}.", .{string});
return null;
};
}
};
pub const Tool = struct { // MARK: Tool
craftingGrid: [25]?BaseItemIndex,
materialGrid: [16][16]?BaseItemIndex,
modifiers: []Modifier,
tooltip: main.List(u8),
image: graphics.Image,
texture: ?graphics.Texture,
seed: u32,
type: ToolTypeIndex,
damage: f32,
durability: u32,
maxDurability: f32,
/// How long it takes to swing the tool in seconds.
swingTime: f32,
mass: f32,
/// Where the player holds the tool.
handlePosition: Vec2f,
/// Moment of inertia relative to the handle.
inertiaHandle: f32,
/// Where the tool rotates around when being thrown.
centerOfMass: Vec2f,
/// Moment of inertia relative to the center of mass.
inertiaCenterOfMass: f32,
pub fn init() *Tool {
const self = main.globalAllocator.create(Tool);
self.image = graphics.Image.init(main.globalAllocator, 16, 16);
self.texture = null;
self.tooltip = .init(main.globalAllocator);
return self;
}
pub fn deinit(self: *const Tool) void {
// TODO: This is leaking textures!
//if(self.texture) |texture| {
//texture.deinit();
//}
self.image.deinit(main.globalAllocator);
self.tooltip.deinit();
main.globalAllocator.free(self.modifiers);
main.globalAllocator.destroy(self);
}
pub fn clone(self: *const Tool) *Tool {
const result = main.globalAllocator.create(Tool);
result.* = .{
.craftingGrid = self.craftingGrid,
.materialGrid = self.materialGrid,
.modifiers = main.globalAllocator.dupe(Modifier, self.modifiers),
.tooltip = .init(main.globalAllocator),
.image = graphics.Image.init(main.globalAllocator, self.image.width, self.image.height),
.texture = null,
.seed = self.seed,
.type = self.type,
.damage = self.damage,
.durability = self.durability,
.maxDurability = self.maxDurability,
.swingTime = self.swingTime,
.mass = self.mass,
.handlePosition = self.handlePosition,
.inertiaHandle = self.inertiaHandle,
.centerOfMass = self.centerOfMass,
.inertiaCenterOfMass = self.inertiaCenterOfMass,
};
@memcpy(result.image.imageData, self.image.imageData);
return result;
}
pub fn initFromCraftingGrid(craftingGrid: [25]?BaseItemIndex, seed: u32, typ: ToolTypeIndex) *Tool {
const self = init();
self.seed = seed;
self.craftingGrid = craftingGrid;
self.type = typ;
// Produce the tool and its textures:
// The material grid, which comes from texture generation, is needed on both server and client, to generate the tool properties.
TextureGenerator.generate(self);
ToolPhysics.evaluateTool(self);
return self;
}
pub fn initFromZon(zon: ZonElement) *Tool {
const self = initFromCraftingGrid(extractItemsFromZon(zon.getChild("grid")), zon.get(u32, "seed", 0), ToolTypeIndex.fromId(zon.get([]const u8, "type", "cubyz:pickaxe")) orelse blk: {
std.log.err("Couldn't find tool with type {s}. Replacing it with cubyz:pickaxe", .{zon.get([]const u8, "type", "cubyz:pickaxe")});
break :blk ToolTypeIndex.fromId("cubyz:pickaxe") orelse @panic("cubyz:pickaxe tool not found. Did you load the game with the correct assets?");
});
self.durability = zon.get(u32, "durability", std.math.lossyCast(u32, self.maxDurability));
return self;
}
fn extractItemsFromZon(zonArray: ZonElement) [25]?BaseItemIndex {
var items: [25]?BaseItemIndex = undefined;
for(&items, 0..) |*item, i| {
item.* = .fromId(zonArray.getAtIndex([]const u8, i, "null"));
if(item.* != null and item.*.?.material() == null) item.* = null;
}
return items;
}
pub fn save(self: *const Tool, allocator: NeverFailingAllocator) ZonElement {
const zonObject = ZonElement.initObject(allocator);
const zonArray = ZonElement.initArray(allocator);
for(self.craftingGrid) |nullableItem| {
if(nullableItem) |item| {
zonArray.array.append(.{.string = item.id()});
} else {
zonArray.array.append(.null);
}
}
zonObject.put("grid", zonArray);
zonObject.put("durability", self.durability);
zonObject.put("seed", self.seed);
zonObject.put("type", self.type.id());
return zonObject;
}
pub fn hashCode(self: Tool) u32 {
var hash: u32 = 0;
for(self.craftingGrid) |nullItem| {
if(nullItem) |item| {
hash = 33*%hash +% item.material().?.hashCode();
}
}
return hash;
}
pub fn getItemAt(self: *const Tool, x: i32, y: i32) ?BaseItemIndex {
if(x < 0 or x >= 5) return null;
if(y < 0 or y >= 5) return null;
return self.craftingGrid[@intCast(x + y*5)];
}
fn getProperty(self: *Tool, prop: ToolProperty) *f32 {
switch(prop) {
inline else => |field| return &@field(self, @tagName(field)),
}
}
fn getTexture(self: *Tool) graphics.Texture {
if(self.texture == null) {
self.texture = graphics.Texture.init();
self.texture.?.generate(self.image);
}
return self.texture.?;
}
fn getTooltip(self: *Tool) []const u8 {
self.tooltip.clearRetainingCapacity();
self.tooltip.writer().print(
\\{s}
\\Time to swing: {d:.2} s
\\Damage: {d:.2}
\\Durability: {}/{}
, .{
self.type.id(),
self.swingTime,
self.damage,
self.durability,
std.math.lossyCast(u32, self.maxDurability),
}) catch unreachable;
if(self.modifiers.len != 0) {
self.tooltip.appendSlice("\nModifiers:\n");
for(self.modifiers) |modifier| {
modifier.printTooltip(&self.tooltip);
self.tooltip.appendSlice("§\n");
}
_ = self.tooltip.pop();
}
return self.tooltip.items;
}
pub fn getBlockDamage(self: *Tool, block: main.blocks.Block) f32 {
var damage = self.damage;
for(self.modifiers) |modifier| {
damage = modifier.changeBlockDamage(damage, block);
}
for(block.blockTags()) |blockTag| {
for(self.type.blockTags()) |toolTag| {
if(toolTag == blockTag) return damage;
}
}
return 0;
}
pub fn onUseReturnBroken(self: *Tool) bool {
self.durability -|= 1;
return self.durability == 0;
}
};
pub const Item = union(enum) { // MARK: Item
baseItem: BaseItemIndex,
tool: *Tool,
pub fn init(zon: ZonElement) !Item {
if(BaseItemIndex.fromId(zon.get([]const u8, "item", "null"))) |baseItem| {
return Item{.baseItem = baseItem};
} else {
const toolZon = zon.getChild("tool");
if(toolZon != .object) return error.ItemNotFound;
return Item{.tool = Tool.initFromZon(toolZon)};
}
}
pub fn deinit(self: Item) void {
switch(self) {
.baseItem => {},
.tool => |_tool| {
_tool.deinit();
},
}
}
pub fn clone(self: Item) Item {
switch(self) {
.baseItem => return self,
.tool => |tool| {
return .{.tool = tool.clone()};
},
}
}
pub fn stackSize(self: Item) u16 {
switch(self) {
.baseItem => |_baseItem| {
return _baseItem.stackSize();
},
.tool => {
return 1;
},
}
}
pub fn insertIntoZon(self: Item, allocator: NeverFailingAllocator, zonObject: ZonElement) void {
switch(self) {
.baseItem => |_baseItem| {
zonObject.put("item", _baseItem.id());
},
.tool => |_tool| {
zonObject.put("tool", _tool.save(allocator));
},
}
}
pub fn getTexture(self: Item) graphics.Texture {
switch(self) {
.baseItem => |_baseItem| {
return _baseItem.getTexture();
},
.tool => |_tool| {
return _tool.getTexture();
},
}
}
pub fn getTooltip(self: Item) []const u8 {
switch(self) {
.baseItem => |_baseItem| {
return _baseItem.getTooltip();
},
.tool => |_tool| {
return _tool.getTooltip();
},
}
}
pub fn getImage(self: Item) graphics.Image {
switch(self) {
.baseItem => |_baseItem| {
return _baseItem.image();
},
.tool => |_tool| {
return _tool.image;
},
}
}
pub fn hashCode(self: Item) u32 {
switch(self) {
inline else => |item| {
return item.hashCode();
},
}
}
};
pub const ItemStack = struct { // MARK: ItemStack
item: ?Item = null,
amount: u16 = 0,
pub fn load(zon: ZonElement) !ItemStack {
return .{
.item = try Item.init(zon),
.amount = zon.get(?u16, "amount", null) orelse return error.InvalidAmount,
};
}
pub fn deinit(self: *ItemStack) void {
if(self.item) |item| {
item.deinit();
}
}
pub fn clone(self: *const ItemStack) ItemStack {
const item = self.item orelse return .{};
return .{
.item = item.clone(),
.amount = self.amount,
};
}
pub fn empty(self: *const ItemStack) bool {
return self.amount == 0;
}
pub fn clear(self: *ItemStack) void {
self.item = null;
self.amount = 0;
}
pub fn storeToZon(self: *const ItemStack, allocator: NeverFailingAllocator, zonObject: ZonElement) void {
if(self.item) |item| {
item.insertIntoZon(allocator, zonObject);
zonObject.put("amount", self.amount);
}
}
pub fn store(self: *const ItemStack, allocator: NeverFailingAllocator) ZonElement {
const result = ZonElement.initObject(allocator);
self.storeToZon(allocator, result);
return result;
}
};
pub const Recipe = struct { // MARK: Recipe
sourceItems: []BaseItemIndex,
sourceAmounts: []u16,
resultItem: BaseItemIndex,
resultAmount: u16,
cachedInventory: ?Inventory = null,
};
var arena: main.heap.NeverFailingArenaAllocator = undefined;
var toolTypeList: ListUnmanaged(ToolType) = undefined;
var toolTypeIdToIndex: std.StringHashMapUnmanaged(ToolTypeIndex) = undefined;
var reverseIndices: std.StringHashMap(BaseItemIndex) = undefined;
var modifiers: std.StringHashMap(*const Modifier.VTable) = undefined;
var modifierRestrictions: std.StringHashMap(*const ModifierRestriction.VTable) = undefined;
pub var itemList: [65536]BaseItem = undefined;
pub var itemListSize: u16 = 0;
var recipeList: main.List(Recipe) = undefined;
pub fn hasRegistered(id: []const u8) bool {
return reverseIndices.contains(id);
}
pub fn hasRegisteredTool(id: []const u8) bool {
return toolTypeIdToIndex.contains(id);
}
pub fn iterator() std.StringHashMap(BaseItemIndex).ValueIterator {
return reverseIndices.valueIterator();
}
pub fn recipes() []Recipe {
return recipeList.items;
}
pub fn globalInit() void {
arena = .init(main.globalAllocator);
toolTypeList = .{};
toolTypeIdToIndex = .{};
reverseIndices = .init(arena.allocator().allocator);
recipeList = .init(arena.allocator());
itemListSize = 0;
modifiers = .init(main.globalAllocator.allocator);
inline for(@typeInfo(modifierList).@"struct".decls) |decl| {
const ModifierStruct = @field(modifierList, decl.name);
modifiers.put(decl.name, &.{
.changeToolParameters = @ptrCast(&ModifierStruct.changeToolParameters),
.changeBlockDamage = @ptrCast(&ModifierStruct.changeBlockDamage),
.combineModifiers = @ptrCast(&ModifierStruct.combineModifiers),
.printTooltip = @ptrCast(&ModifierStruct.printTooltip),
.loadData = @ptrCast(&ModifierStruct.loadData),
.priority = ModifierStruct.priority,
}) catch unreachable;
}
modifierRestrictions = .init(main.globalAllocator.allocator);
inline for(@typeInfo(modifierRestrictionList).@"struct".decls) |decl| {
const ModifierRestrictionStruct = @field(modifierRestrictionList, decl.name);
modifierRestrictions.put(decl.name, &.{
.satisfied = comptime main.utils.castFunctionSelfToAnyopaque(ModifierRestrictionStruct.satisfied),
.loadFromZon = comptime main.utils.castFunctionReturnToAnyopaque(ModifierRestrictionStruct.loadFromZon),
}) catch unreachable;
}
Inventory.Sync.ClientSide.init();
}
pub fn register(_: []const u8, texturePath: []const u8, replacementTexturePath: []const u8, id: []const u8, zon: ZonElement) *BaseItem {
if(reverseIndices.contains(id)) {
std.log.err("Registered item with id '{s}' twice!", .{id});
}
const newItem = &itemList[itemListSize];
defer itemListSize += 1;
newItem.init(arena.allocator(), texturePath, replacementTexturePath, id, zon);
reverseIndices.put(newItem.id, @enumFromInt(itemListSize)) catch unreachable;
std.log.debug("Registered item: {d: >5} '{s}'", .{itemListSize, id});
return newItem;
}
fn loadPixelSources(assetFolder: []const u8, id: []const u8, layerPostfix: []const u8, pixelSources: *[16][16]u8) void {
var split = std.mem.splitScalar(u8, id, ':');
const mod = split.first();
const tool = split.rest();
const path = std.fmt.allocPrint(main.stackAllocator.allocator, "{s}/{s}/tools/{s}{s}.png", .{assetFolder, mod, tool, layerPostfix}) catch unreachable;
defer main.stackAllocator.free(path);
const image = main.graphics.Image.readFromFile(main.stackAllocator, path) catch |err| blk: {
if(err != error.FileNotFound) {
std.log.err("Error while reading tool image '{s}': {s}", .{path, @errorName(err)});
}
const replacementPath = std.fmt.allocPrint(main.stackAllocator.allocator, "assets/{s}/tools/{s}{s}.png", .{mod, tool, layerPostfix}) catch unreachable;
defer main.stackAllocator.free(replacementPath);
break :blk main.graphics.Image.readFromFile(main.stackAllocator, replacementPath) catch |err2| {
if(layerPostfix.len == 0 or err2 != error.FileNotFound)
std.log.err("Error while reading tool image. Tried '{s}' and '{s}': {s}", .{path, replacementPath, @errorName(err2)});
break :blk main.graphics.Image.emptyImage;
};
};
defer image.deinit(main.stackAllocator);
if((image.width != 16 or image.height != 16) and image.imageData.ptr != main.graphics.Image.emptyImage.imageData.ptr) {
std.log.err("Truncating image for {s} with incorrect dimensions. Should be 16×16.", .{id});
}
for(0..16) |x| {
for(0..16) |y| {
const color = if(image.width != 0 and image.height != 0) image.getRGB(@min(image.width - 1, x), image.height - 1 - @min(image.height - 1, y)) else main.graphics.Color{.r = 0, .g = 0, .b = 0, .a = 0};
pixelSources[x][y] = blk: {
if(color.a == 0) break :blk 255;
const xPos = color.r/52;
const yPos = color.b/52;
break :blk xPos + 5*yPos;
};
}
}
}
pub fn registerTool(assetFolder: []const u8, id: []const u8, zon: ZonElement) void {
if(toolTypeIdToIndex.contains(id)) {
std.log.err("Registered tool type with id {s} twice!", .{id});
}
var slotInfos: [25]SlotInfo = @splat(.{});
for(zon.getChild("disabled").toSlice(), 0..) |zonDisabled, i| {
if(i >= 25) {
std.log.err("disabled array of {s} has too many entries", .{id});
break;
}
slotInfos[i].disabled = zonDisabled.as(usize, 0) != 0;
}
for(zon.getChild("optional").toSlice(), 0..) |zonDisabled, i| {
if(i >= 25) {
std.log.err("disabled array of {s} has too many entries", .{id});
break;
}
slotInfos[i].optional = zonDisabled.as(usize, 0) != 0;
}
var parameterMatrices: main.List(PropertyMatrix) = .init(arena.allocator());
for(zon.getChild("parameters").toSlice()) |paramZon| {
const val = parameterMatrices.addOne();
val.source = MaterialProperty.fromString(paramZon.get([]const u8, "source", "not specified"));
val.destination = ToolProperty.fromString(paramZon.get([]const u8, "destination", "not specified"));
val.resultScale = paramZon.get(f32, "factor", 1.0);
val.method = PropertyMatrix.Method.fromString(paramZon.get([]const u8, "method", "not specified")) orelse .sum;
const matrixZon = paramZon.getChild("matrix");
for(0..25) |i| {
val.weigths[i] = matrixZon.getAtIndex(f32, i, 0.0);
}
}
var pixelSources: [16][16]u8 = undefined;
loadPixelSources(assetFolder, id, "", &pixelSources);
var pixelSourcesOverlay: [16][16]u8 = undefined;
loadPixelSources(assetFolder, id, "_overlay", &pixelSourcesOverlay);
const idDupe = arena.allocator().dupe(u8, id);
toolTypeList.append(arena.allocator(), .{
.id = idDupe,
.blockTags = Tag.loadTagsFromZon(arena.allocator(), zon.getChild("blockTags")),
.slotInfos = slotInfos,
.properties = parameterMatrices.toOwnedSlice(),
.pixelSources = pixelSources,
.pixelSourcesOverlay = pixelSourcesOverlay,
});
toolTypeIdToIndex.put(arena.allocator().allocator, idDupe, @enumFromInt(toolTypeList.items.len - 1)) catch unreachable;
std.log.debug("Registered tool: '{s}'", .{id});
}
fn parseRecipeItem(zon: ZonElement) !ItemStack {
var id = zon.as([]const u8, "");
id = std.mem.trim(u8, id, &std.ascii.whitespace);
var result: ItemStack = .{.amount = 1};
if(std.mem.indexOfScalar(u8, id, ' ')) |index| blk: {
result.amount = std.fmt.parseInt(u16, id[0..index], 0) catch break :blk;
id = id[index + 1 ..];
id = std.mem.trim(u8, id, &std.ascii.whitespace);
}
result.item = .{.baseItem = BaseItemIndex.fromId(id) orelse return error.ItemNotFound};
return result;
}
fn parseRecipe(zon: ZonElement) !Recipe {
const inputs = zon.getChild("inputs").toSlice();
const output = try parseRecipeItem(zon.getChild("output"));
const recipe = Recipe{
.sourceItems = arena.allocator().alloc(BaseItemIndex, inputs.len),
.sourceAmounts = arena.allocator().alloc(u16, inputs.len),
.resultItem = output.item.?.baseItem,
.resultAmount = output.amount,
};
errdefer {
arena.allocator().free(recipe.sourceAmounts);
arena.allocator().free(recipe.sourceItems);
}
for(inputs, 0..) |inputZon, i| {
const input = try parseRecipeItem(inputZon);
recipe.sourceItems[i] = input.item.?.baseItem;
recipe.sourceAmounts[i] = input.amount;
}
return recipe;
}
pub fn registerRecipes(zon: ZonElement) void {
for(zon.toSlice()) |recipeZon| {
const recipe = parseRecipe(recipeZon) catch continue;
recipeList.append(recipe);
}
}
pub fn reset() void {
toolTypeList.clearAndFree(arena.allocator());
toolTypeIdToIndex.clearAndFree(arena.allocator().allocator);
reverseIndices.clearAndFree();
for(recipeList.items) |recipe| {
if(recipe.cachedInventory) |inv| {
inv.deinit(main.globalAllocator);
}
}
recipeList.clearAndFree();
itemListSize = 0;
_ = arena.reset(.free_all);
}
pub fn deinit() void {
toolTypeList.deinit(arena.allocator());
toolTypeIdToIndex.deinit(arena.allocator().allocator);
reverseIndices.clearAndFree();
for(recipeList.items) |recipe| {
if(recipe.cachedInventory) |inv| {
inv.deinit(main.globalAllocator);
}
}
recipeList.clearAndFree();
modifiers.deinit();
modifierRestrictions.deinit();
arena.deinit();
Inventory.Sync.ClientSide.deinit();
}