diff --git a/src/network.zig b/src/network.zig index 0f6e4515..1834beaa 100644 --- a/src/network.zig +++ b/src/network.zig @@ -6,6 +6,7 @@ const assets = @import("assets.zig"); const Block = @import("blocks.zig").Block; const chunk = @import("chunk.zig"); const entity = @import("entity.zig"); +const particles = @import("particles.zig"); const items = @import("items.zig"); const Inventory = items.Inventory; const ItemStack = items.ItemStack; @@ -1018,6 +1019,7 @@ pub const Protocols = struct { worldEditPos = 2, time = 3, biome = 4, + particle = 5, }; const WorldEditPosition = enum(u2) { @@ -1090,6 +1092,20 @@ pub const Protocols = struct { } } }, + .particle => { + if(conn.manager.world) |_| { + const particleId = particles.ParticleManager.getIdByTypeIndex(try reader.readInt(u16)); + const pos = try reader.readVec(Vec3d); + const collides = try reader.readInt(u8) == 1; + const count = try reader.readInt(u32); + + const emitter: particles.Emitter = .init(particleId, collides); + emitter.spawnParticles(count, particles.Emitter.SpawnPoint, .{ + .mode = .spread, + .position = pos, + }); + } + }, } } @@ -1130,6 +1146,19 @@ pub const Protocols = struct { conn.send(.fast, id, writer.data.items); } + pub fn sendParticle(conn: *Connection, particleId: u16, pos: Vec3d, collides: bool, count: u32) void { + var writer = utils.BinaryWriter.initCapacity(main.stackAllocator, 32); + defer writer.deinit(); + + writer.writeEnum(UpdateType, .particle); + writer.writeInt(u16, particleId); + writer.writeVec(Vec3d, pos); + writer.writeInt(u8, if(collides) 1 else 0); + writer.writeInt(u32, count); + + conn.send(.fast, id, writer.data.items); + } + pub fn sendTime(conn: *Connection, world: *const main.server.ServerWorld) void { var writer = utils.BinaryWriter.initCapacity(main.stackAllocator, 13); defer writer.deinit(); diff --git a/src/particles.zig b/src/particles.zig index 61414ac1..15fbff67 100644 --- a/src/particles.zig +++ b/src/particles.zig @@ -35,6 +35,7 @@ pub const ParticleManager = struct { const ParticleIndex = u16; var particleTypeHashmap: std.StringHashMapUnmanaged(ParticleIndex) = .{}; + var typeIds: main.List([]const u8) = undefined; pub fn init() void { types = .init(arenaAllocator); @@ -42,6 +43,7 @@ pub const ParticleManager = struct { emissionTextures = .init(arenaAllocator); textureArray = .init(); emissionTextureArray = .init(); + typeIds = .init(arenaAllocator); particleTypesSSBO = SSBO.init(); ParticleSystem.init(); } @@ -52,6 +54,7 @@ pub const ParticleManager = struct { emissionTextures.deinit(); textureArray.deinit(); emissionTextureArray.deinit(); + typeIds.deinit(); particleTypeHashmap.deinit(arenaAllocator.allocator); ParticleSystem.deinit(); particleTypesSSBO.deinit(); @@ -67,6 +70,7 @@ pub const ParticleManager = struct { const particleType = readTextureDataAndParticleType(assetsFolder, textureId); particleTypeHashmap.put(arenaAllocator.allocator, id, @intCast(types.items.len)) catch unreachable; + typeIds.append(id); types.append(particleType); std.log.debug("Registered particle type: {s}", .{id}); @@ -147,6 +151,16 @@ pub const ParticleManager = struct { particleTypesSSBO.bufferData(ParticleType, ParticleManager.types.items); particleTypesSSBO.bind(14); } + + pub fn getTypeIndexById(id: []const u8) ?ParticleIndex { + return particleTypeHashmap.get(id); + } + + pub fn getIdByTypeIndex(index: ParticleIndex) []const u8 { + std.debug.assert(index < typeIds.items.len); + + return typeIds.items[index]; + } }; pub const ParticleSystem = struct { diff --git a/src/server/command/_list.zig b/src/server/command/_list.zig index 3d187ecb..fccd9b10 100644 --- a/src/server/command/_list.zig +++ b/src/server/command/_list.zig @@ -3,6 +3,7 @@ pub const gamemode = @import("gamemode.zig"); pub const help = @import("help.zig"); pub const invite = @import("invite.zig"); pub const kill = @import("kill.zig"); +pub const particle = @import("particle.zig"); pub const time = @import("time.zig"); pub const tp = @import("tp.zig"); diff --git a/src/server/command/particle.zig b/src/server/command/particle.zig new file mode 100644 index 00000000..5bc6249b --- /dev/null +++ b/src/server/command/particle.zig @@ -0,0 +1,90 @@ +const std = @import("std"); + +const main = @import("main"); +const particles = main.particles; +const User = main.server.User; + +pub const description = "Spawns particles."; +pub const usage = + \\/particle + \\/particle + \\/particle +; + +pub fn execute(args: []const u8, source: *User) void { + const particleId, const x, const y, const z, const collides, const particleCount = parseArguments(source, args) catch |err| { + switch(err) { + error.TooFewArguments => source.sendMessage("#ff0000Too few arguments for command /particle", .{}), + error.TooManyArguments => source.sendMessage("#ff0000Too many arguments for command /particle", .{}), + error.InvalidParticleId => source.sendMessage("#ff0000Invalid particle id", .{}), + error.InvalidBoolean => source.sendMessage("#ff0000Invalid argument. Expected \"true\" or \"false\"", .{}), + error.InvalidNumber => return, + } + return; + }; + + const users = main.server.getUserListAndIncreaseRefCount(main.stackAllocator); + defer main.server.freeUserListAndDecreaseRefCount(main.stackAllocator, users); + for(users) |user| { + main.network.Protocols.genericUpdate.sendParticle(user.conn, particleId, .{x, y, z}, collides, particleCount); + } +} + +pub const CommandError = error{ + TooManyArguments, + TooFewArguments, + InvalidBoolean, + InvalidNumber, + InvalidParticleId, +}; + +fn parseArguments(source: *User, args: []const u8) CommandError!struct {u16, f64, f64, f64, bool, u32} { + var split = std.mem.splitScalar(u8, args, ' '); + const particleId = split.next() orelse return error.TooFewArguments; + const indexId = particles.ParticleManager.getTypeIndexById(particleId) orelse return error.InvalidParticleId; + + const x = try parsePosition(split.next() orelse return error.TooFewArguments, source.player.pos[0], source); + const y = try parsePosition(split.next() orelse return error.TooFewArguments, source.player.pos[1], source); + const z = try parsePosition(split.next() orelse return error.TooFewArguments, source.player.pos[2], source); + const collides = try parseBool(split.next() orelse "false"); + const particleCount = try parseNumber(split.next() orelse "1", source); + if(split.next() != null) return error.TooManyArguments; + + return .{indexId, x, y, z, collides, particleCount}; +} + +fn parsePosition(arg: []const u8, playerPos: f64, source: *User) CommandError!f64 { + const hasTilde = if(arg.len == 0) false else arg[0] == '~'; + const numberSlice = if(hasTilde) arg[1..] else arg; + const num: f64 = std.fmt.parseFloat(f64, numberSlice) catch ret: { + if(arg.len > 1 or arg.len == 0) { + source.sendMessage("#ff0000Expected number, found \"{s}\"", .{arg}); + return error.InvalidNumber; + } + break :ret 0; + }; + + return if(hasTilde) playerPos + num else num; +} + +fn parseBool(arg: []const u8) CommandError!bool { + if(std.mem.eql(u8, arg, "true")) { + return true; + } else if(std.mem.eql(u8, arg, "false")) { + return false; + } + + return error.InvalidBoolean; +} + +fn parseNumber(arg: []const u8, source: *User) CommandError!u32 { + return std.fmt.parseUnsigned(u32, arg, 0) catch |err| { + switch(err) { + error.Overflow => return std.math.maxInt(u32), + error.InvalidCharacter => { + source.sendMessage("#ff0000Expected number, found \"{s}\"", .{arg}); + return error.InvalidNumber; + }, + } + }; +}