Skip to content
5 changes: 4 additions & 1 deletion assets/cubyz/items/apple.zig.zon
Original file line number Diff line number Diff line change
@@ -1,4 +1,7 @@
.{
.texture = "apple.png",
.food = 4,
.onUse = .{
.type = .eat,
.energy = 0.5,
},
}
5 changes: 4 additions & 1 deletion assets/cubyz/items/raw_meat.zig.zon
Original file line number Diff line number Diff line change
@@ -1,4 +1,7 @@
.{
.texture = "raw_meat.png",
.food = 3,
.onUse = .{
.type = .eat,
.energy = 3,
},
}
45 changes: 41 additions & 4 deletions src/Inventory.zig
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ const ToolTypeIndex = main.items.ToolTypeIndex;

const Gamemode = main.game.Gamemode;

const Side = enum {client, server};
pub const Side = enum {client, server};

pub const InventoryId = enum(u32) {_};

Expand Down Expand Up @@ -507,6 +507,13 @@ pub const Sync = struct { // MARK: Sync
Sync.ServerSide.executeCommand(.{.addHealth = .{.target = userId, .health = health, .cause = cause}}, null);
}
}
pub fn useItem(source: Command.InventoryAndSlot, side: Side) void {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Since this function is only used on the client, it should only exist in ClientSide.

if(side == .client) {
Sync.ClientSide.executeCommand(.{.useItem = .{.source = source}});
} else {
Sync.ServerSide.executeCommand(.{.useItem = .{.source = source}}, null);
}
}

pub fn getInventory(id: InventoryId, side: Side, user: ?*main.server.User) ?Inventory {
return switch(side) {
Expand Down Expand Up @@ -538,6 +545,7 @@ pub const Command = struct { // MARK: Command
clear = 8,
updateBlock = 9,
addHealth = 10,
useItem = 13,
};
pub const Payload = union(PayloadType) {
open: Open,
Expand All @@ -552,6 +560,7 @@ pub const Command = struct { // MARK: Command
clear: Clear,
updateBlock: UpdateBlock,
addHealth: AddHealth,
useItem: UseItem,
};

const BaseOperationType = enum(u8) {
Expand All @@ -564,11 +573,11 @@ pub const Command = struct { // MARK: Command
addEnergy = 6,
};

const InventoryAndSlot = struct {
pub const InventoryAndSlot = struct {
inv: Inventory,
slot: u32,

fn ref(self: InventoryAndSlot) *ItemStack {
pub fn ref(self: InventoryAndSlot) *ItemStack {
return &self.inv._items[self.slot];
}

Expand Down Expand Up @@ -979,7 +988,7 @@ pub const Command = struct { // MARK: Command
}
}

fn executeBaseOperation(self: *Command, allocator: NeverFailingAllocator, _op: BaseOperation, side: Side) void { // MARK: executeBaseOperation()
pub fn executeBaseOperation(self: *Command, allocator: NeverFailingAllocator, _op: BaseOperation, side: Side) void { // MARK: executeBaseOperation()
var op = _op;
switch(op) {
.move => |info| {
Expand Down Expand Up @@ -1947,6 +1956,34 @@ pub const Command = struct { // MARK: Command
return result;
}
};

const UseItem = struct { // MARK: UseItem
source: InventoryAndSlot,

pub fn run(self: UseItem, allocator: NeverFailingAllocator, cmd: *Command, side: Side, user: ?*main.server.User, gamemode: Gamemode) error{serverFailure}!void {
if(self.source.inv.type != .normal) return;
const stack = self.source.ref();

switch(stack.item) {
.baseItem => |baseItem| {
_ = baseItem.onUse().run(.{.source = self.source, .allocator = allocator, .cmd = cmd, .side = side, .user = user, .gamemode = gamemode});
},
.tool => {},
.null => {},
}
}

fn serialize(self: UseItem, writer: *utils.BinaryWriter) void {
self.source.write(writer);
}

fn deserialize(reader: *utils.BinaryReader, side: Side, user: ?*main.server.User) !UseItem {
const result: UseItem = .{
.source = try InventoryAndSlot.read(reader, side, user),
};
return result;
}
};
};

const SourceType = enum(u8) {
Expand Down
5 changes: 5 additions & 0 deletions src/assets.zig
Original file line number Diff line number Diff line change
Expand Up @@ -593,6 +593,11 @@ pub fn loadWorldAssets(assetFolder: []const u8, blockPalette: *Palette, itemPale
// block drops:
blocks_zig.finishBlocks(worldAssets.blocks);

// Finalize all item (setting their callbacks)
for(0..main.items.itemListSize) |i| {
main.items.itemList[i].finalize();
}

iterator = worldAssets.recipes.iterator();
while(iterator.next()) |entry| {
registerRecipesFromZon(entry.value_ptr.*);
Expand Down
4 changes: 4 additions & 0 deletions src/callbacks/callbacks.zig
Original file line number Diff line number Diff line change
Expand Up @@ -4,18 +4,22 @@ const main = @import("main");
const Block = main.blocks.Block;
const vec = main.vec;
const Vec3i = vec.Vec3i;
const Inventory = main.items.Inventory;

pub const ClientBlockCallback = Callback(struct {block: Block, blockPos: Vec3i}, @import("block/client/_list.zig"));
pub const ServerBlockCallback = Callback(struct {block: Block, chunk: *main.chunk.ServerChunk, blockPos: main.chunk.BlockPos}, @import("block/server/_list.zig"));

pub const BlockTouchCallback = Callback(struct {entity: *main.server.Entity, source: Block, blockPos: Vec3i, deltaTime: f64}, @import("block/touch/_list.zig"));

pub const UseItemCallback = Callback(struct {cmd: *Inventory.Command, source: Inventory.Command.InventoryAndSlot, allocator: main.heap.NeverFailingAllocator, side: Inventory.Side, user: ?*main.server.User, gamemode: main.game.Gamemode}, @import("item/_list.zig"));

pub const Result = enum {handled, ignored};

pub fn init() void {
ClientBlockCallback.globalInit();
ServerBlockCallback.globalInit();
BlockTouchCallback.globalInit();
UseItemCallback.globalInit();
}

fn Callback(_Params: type, list: type) type {
Expand Down
1 change: 1 addition & 0 deletions src/callbacks/item/_list.zig
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
pub const eat = @import("eat.zig");
45 changes: 45 additions & 0 deletions src/callbacks/item/eat.zig
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
const std = @import("std");

const main = @import("main");

energy: f32,

pub fn init(zon: main.ZonElement) ?*@This() {
const result = main.worldArena.create(@This());
result.* = .{
.energy = zon.get(f32, "energy", 1.0),
};
return result;
}

pub fn run(self: *@This(), params: main.callbacks.UseItemCallback.Params) main.callbacks.Result {
const cmd = params.cmd;
const allocator = params.allocator;
const user = params.user;
const side = params.side;
const stack = params.source.ref();

// enough items there?
if(stack.amount < 1)
return .ignored;

const previous = if(side == .server) user.?.player.energy else main.game.Player.super.energy;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please check that user is not null, because in fact in this PR you added code that passes null for this (see comment above)

const maxEnergy = if(side == .server) user.?.player.maxEnergy else main.game.Player.super.maxEnergy;
if(self.energy > 0 and previous >= maxEnergy)
return .ignored;

cmd.executeBaseOperation(allocator, .{.addEnergy = .{
.target = user,
.energy = self.energy,
.previous = previous,
}}, side);

// Apply inventory changes:
if(params.gamemode == .creative) return .handled;

cmd.executeBaseOperation(allocator, .{.delete = .{
.source = params.source,
.amount = 1,
}}, side);
return .handled;
}
18 changes: 16 additions & 2 deletions src/game.zig
Original file line number Diff line number Diff line change
Expand Up @@ -528,6 +528,18 @@ pub const Player = struct { // MARK: Player
}
}

pub fn useItem(_: main.Window.Key.Modifiers) bool {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please avoid boolean return values or parameters (unless it's immediately obvious from the name what they mean which is not the case here). I'd suggest to just use the callback result.

const item = inventory.getItem(selectedSlot);
main.items.Inventory.Sync.useItem(.{.inv = inventory, .slot = selectedSlot}, .client);
switch(item) {
.baseItem => {
return !item.baseItem.onUse().isNoop();
},
.null => {},
.tool => {},
}
return false;
}
pub fn placeBlock(mods: main.Window.Key.Modifiers) void {
if(main.renderer.MeshSelection.selectedBlockPos) |blockPos| {
if(!mods.shift) {
Expand All @@ -548,7 +560,7 @@ pub const Player = struct { // MARK: Player
Player.super.vel = .{0, 0, 0};

Player.super.health = Player.super.maxHealth;
Player.super.energy = Player.super.maxEnergy;
Player.super.energy = 0;

Player.eye = .{};
Player.jumpCoyote = 0;
Expand Down Expand Up @@ -771,7 +783,9 @@ var nextBlockBreakTime: ?std.Io.Timestamp = null;
pub fn pressPlace(mods: main.Window.Key.Modifiers) void {
const time = main.timestamp();
nextBlockPlaceTime = time.addDuration(main.settings.updateRepeatDelay);
Player.placeBlock(mods);
if(!Player.useItem(mods)) {
Player.placeBlock(mods);
}
}

pub fn releasePlace(_: main.Window.Key.Modifiers) void {
Expand Down
18 changes: 17 additions & 1 deletion src/items.zig
Original file line number Diff line number Diff line change
Expand Up @@ -242,22 +242,28 @@ pub const BaseItemIndex = enum(u16) { // MARK: BaseItemIndex
pub fn getTooltip(self: BaseItemIndex) []const u8 {
return itemList[@intFromEnum(self)].getTooltip();
}
pub fn onUse(self: BaseItemIndex) main.callbacks.UseItemCallback {
return itemList[@intFromEnum(self)].onUse;
}
};

pub const BaseItem = struct { // MARK: BaseItem
image: graphics.Image,
texture: ?graphics.Texture, // TODO: Properly deinit
id: []const u8,
zon: ZonElement,
name: []const u8,
tags: []const Tag,
tooltip: []const u8,
onUse: main.callbacks.UseItemCallback,

stackSize: u16,
material: ?Material,
block: ?u16,
foodValue: f32, // TODO: Effects.

fn init(self: *BaseItem, allocator: NeverFailingAllocator, texturePath: []const u8, replacementTexturePath: []const u8, id: []const u8, zon: ZonElement) void {
self.zon = zon.clone(allocator);
self.id = allocator.dupe(u8, id);
if(texturePath.len == 0) {
self.image = graphics.Image.defaultImage;
Expand All @@ -281,7 +287,7 @@ pub const BaseItem = struct { // MARK: BaseItem
break :blk blocks.getTypeById(zon.get(?[]const u8, "block", null) orelse break :blk null);
};
self.texture = null;
self.foodValue = zon.get(f32, "food", 0);
self.foodValue = zon.get(f32, "energy", 0);

var tooltip: main.List(u8) = .init(allocator);
tooltip.appendSlice(self.name);
Expand All @@ -301,6 +307,16 @@ pub const BaseItem = struct { // MARK: BaseItem
_ = tooltip.swapRemove(tooltip.items.len - 1);
}
self.tooltip = tooltip.toOwnedSlice();
self.onUse = .noop;
}

pub fn finalize(self: *BaseItem) void {
self.onUse = blk: {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Callbacks should be registered after everything else, to allow it to reference other items or blocks.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

turns out to be more complex then for blocks, since blocks have an array of zon elements, but Item don't.
I only see ugly ways to implement this, like storing the zon in the BaseItem until everything is finished and then parsing the callbacks

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

did it the ugly way with a finalize() function and storing .zon object

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't like this hack. We shouldn't mix runtime and loadtime data in one struct.
Can you please use the same method as for blocks by aggregating all the item zons in a hashmap?

break :blk main.callbacks.UseItemCallback.init(self.zon.getChildOrNull("onUse") orelse break :blk .noop) orelse {
std.log.err("Failed to load onUse event for item {s}", .{self.id});
break :blk .noop;
};
};
}

fn hashCode(self: BaseItem) u32 {
Expand Down
2 changes: 1 addition & 1 deletion src/server/Entity.zig
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ pub fn loadFrom(self: *@This(), zon: ZonElement) void {
self.vel = zon.get(Vec3d, "velocity", .{0, 0, 0});
self.rot = zon.get(Vec3f, "rotation", .{0, 0, 0});
self.health = zon.get(f32, "health", self.maxHealth);
self.energy = zon.get(f32, "energy", self.maxEnergy);
self.energy = zon.get(f32, "energy", 0);
}

pub fn save(self: *@This(), allocator: NeverFailingAllocator) ZonElement {
Expand Down