diff --git a/client/assets/audio/footsteps/footstep-01.wav b/client/assets/audio/footsteps/footstep-01.wav new file mode 100644 index 0000000..75ecbf7 Binary files /dev/null and b/client/assets/audio/footsteps/footstep-01.wav differ diff --git a/client/assets/audio/footsteps/footstep-02.wav b/client/assets/audio/footsteps/footstep-02.wav new file mode 100644 index 0000000..3e31f1c Binary files /dev/null and b/client/assets/audio/footsteps/footstep-02.wav differ diff --git a/client/assets/audio/footsteps/footstep-03.wav b/client/assets/audio/footsteps/footstep-03.wav new file mode 100644 index 0000000..7defcf6 Binary files /dev/null and b/client/assets/audio/footsteps/footstep-03.wav differ diff --git a/client/assets/audio/footsteps/footstep-04.wav b/client/assets/audio/footsteps/footstep-04.wav new file mode 100644 index 0000000..bb54eca Binary files /dev/null and b/client/assets/audio/footsteps/footstep-04.wav differ diff --git a/client/assets/audio/footsteps/footstep-05.wav b/client/assets/audio/footsteps/footstep-05.wav new file mode 100644 index 0000000..f40b51a Binary files /dev/null and b/client/assets/audio/footsteps/footstep-05.wav differ diff --git a/client/assets/audio/footsteps/footstep-06.wav b/client/assets/audio/footsteps/footstep-06.wav new file mode 100644 index 0000000..df5dc15 Binary files /dev/null and b/client/assets/audio/footsteps/footstep-06.wav differ diff --git a/client/assets/audio/footsteps/footstep-07.wav b/client/assets/audio/footsteps/footstep-07.wav new file mode 100644 index 0000000..9995702 Binary files /dev/null and b/client/assets/audio/footsteps/footstep-07.wav differ diff --git a/client/assets/audio/footsteps/footstep-08.wav b/client/assets/audio/footsteps/footstep-08.wav new file mode 100644 index 0000000..08ef7ee Binary files /dev/null and b/client/assets/audio/footsteps/footstep-08.wav differ diff --git a/client/assets/audio/footsteps/footstep-09.wav b/client/assets/audio/footsteps/footstep-09.wav new file mode 100644 index 0000000..3a10301 Binary files /dev/null and b/client/assets/audio/footsteps/footstep-09.wav differ diff --git a/client/assets/audio/place.wav b/client/assets/audio/place.wav new file mode 100644 index 0000000..66e1236 Binary files /dev/null and b/client/assets/audio/place.wav differ diff --git a/client/assets/audio/snowball_throw.wav b/client/assets/audio/snowball_throw.wav new file mode 100644 index 0000000..dd0d8c3 Binary files /dev/null and b/client/assets/audio/snowball_throw.wav differ diff --git a/client/assets/player.obj b/client/assets/player.obj deleted file mode 100644 index e246a0f..0000000 --- a/client/assets/player.obj +++ /dev/null @@ -1,15 +0,0 @@ -# Blender 3.2.2 -# www.blender.org -o Plane -v 0.000000 1.000000 4.000000 -v -0.000000 1.000000 0.000000 -v 0.000000 -1.000000 4.000000 -v -0.000000 -1.000000 0.000000 -vn -1.0000 -0.0000 -0.0000 -vt 1.001373 0.000100 -vt 1.001373 0.999900 -vt -0.001373 0.000100 -vt -0.001373 0.999900 -s 0 -f 2/2/1 3/3/1 1/1/1 -f 2/2/1 4/4/1 3/3/1 diff --git a/client/assets/quad.obj b/client/assets/quad.obj deleted file mode 100644 index 8863897..0000000 --- a/client/assets/quad.obj +++ /dev/null @@ -1,15 +0,0 @@ -# Blender 3.2.2 -# www.blender.org -o Plane.001 -v 0.000000 1.000000 -1.000000 -v 0.000000 -1.000000 -1.000000 -v -0.000000 1.000000 1.000000 -v 0.000000 -1.000000 1.000000 -vn -1.0000 -0.0000 -0.0000 -vt 0.000000 1.000000 -vt 1.000000 1.000000 -vt 0.000000 0.000000 -vt 1.000000 0.000000 -s 0 -f 2/2/1 3/3/1 1/1/1 -f 2/2/1 4/4/1 3/3/1 diff --git a/client/assets/snowball.png b/client/assets/snowball.png new file mode 100644 index 0000000..c195962 Binary files /dev/null and b/client/assets/snowball.png differ diff --git a/client/main.lua b/client/main.lua index 96f34a9..a30d03b 100644 --- a/client/main.lua +++ b/client/main.lua @@ -1,32 +1,19 @@ if arg[#arg] == "vsc_debug" then require("lldebugger").start() end package.path = package.path .. ";?/init.lua" -lg = love.graphics ---@diagnostic disable-next-line: missing-parameter -lg.setDefaultFilter("nearest") +love.graphics.setDefaultFilter("nearest") io.stdout:setvbuf("no") -CHANNEL_ONE = 0 -CHANNEL_EVENTS = 1 -CHANNEL_UPDATES = 2 - g3d = require("lib/g3d") -scene = require("lib/scene") -enet = require("enet") - require("common") -require("scenes/gameworld") -require("physics") - -packets = require("packets") -local nethandler = require("nethandler") - -local focused = false +CHANNEL_ONE = 0 +CHANNEL_EVENTS = 1 +CHANNEL_UPDATES = 2 -local socket -_G.master = nil +local game function love.load(args) if #args < 3 then @@ -34,79 +21,44 @@ function love.load(args) love.event.quit(-1) return end - local address = args[1] .. ":" .. args[2] - username = args[3] + local username = args[3] - socket = loex.socket.connect(address) - socket.onconnect:catch(onconnect) - socket.ondisconnect:catch(ondisconnect) - socket.onreceive:catch(onreceive) + local socket = loex.socket.connect(address) + assert(socket) - font = love.graphics.newFont(50) + local font = love.graphics.newFont(23) love.graphics.setFont(font) - scene(require("scenes/joinscreen")) -end - -function onconnect(peer) - print("Connected!") + game = {} + game.gravity = 42 -- TODO - master = peer - master:send(packets.join(username), CHANNEL_ONE) -end - -function ondisconnect(_) scene(require("scenes/errorscreen"), "disconnected :(") end + game.socket = socket + game.username = username -function onreceive(_, packet) - print("Received " .. packet.type) + game.ondraw = loex.signal.new() + game.onupdate = loex.signal.new() + game.onmousemoved = loex.signal.new() + game.onmousepressed = loex.signal.new() + game.onkeypressed = loex.signal.new() + game.onresize = loex.signal.new() + game.onquit = loex.signal.new() - local handle = nethandler[packet.type] - if not handle then - error("Unknown packet type " .. packet.type) - else - handle(scene(), packet) - end + require("screens.joinscreen").init(game) end -function love.update(dt) - socket:service() +function love.update(dt) game.onupdate:emit(game, dt) end - local scene = scene() - if scene and scene.update then scene:update(dt) end -end +function love.draw() game.ondraw:emit(game) end -function love.draw() - local scene = scene() - if scene and scene.draw then scene:draw() end +function love.mousepressed(x, y, button, istouch, presses) + game.onmousepressed:emit(game, x, y, button, istouch, presses) end -function love.mousepressed() - if not focused then focused = true end -end +function love.mousemoved(x, y, dx, dy, istouch) game.onmousemoved:emit(game, x, y, dx, dy, istouch) end -function love.focus(hasFocus) focused = hasFocus end +function love.keypressed(k, scancode, isrepeat) game.onkeypressed:emit(game, k, scancode, isrepeat) end -function love.mousemoved(x, y, dx, dy) - local scene = scene() - if scene and focused and scene.mousemoved then scene:mousemoved(x, y, dx, dy) end -end +function love.resize(w, h) game.onresize:emit(game, w, h) end -function love.keypressed(k) - if k == "escape" then - love.mouse.setRelativeMode(false) - focused = false - end -end - -function love.resize(w, h) - g3d.camera.aspectRatio = w / h - g3d.camera.updateProjectionMatrix() -end - -function love.quit() - if socket then - print("Disconnecting ....") - socket:disconnect() - end -end +function love.quit() game.onquit:emit(game) end diff --git a/client/packets.lua b/client/packets.lua index d6a452b..52c6c6d 100644 --- a/client/packets.lua +++ b/client/packets.lua @@ -1,11 +1,12 @@ local packets = {} +local encode = loex.socket.encode -function packets.place(x, y, z, t) return ("[type=place;x=%d;y=%d;z=%d;t=%d;]"):format(x, y, z, t) end +function packets.place(x, y, z, t) return encode { type = "place", x = x, y = y, z = z, t = t } end -function packets.breaktile(x, y, z) return ("[type=breaktile;x=%d;y=%d;z=%d;]"):format(x, y, z) end +function packets.breaktile(x, y, z) return encode { type = "breaktile", x = x, y = y, z = z } end -function packets.join(username) return ("[type=join;username=%s;]"):format(username) end +function packets.join(username) return encode { type = "join", username = username } end -function packets.move(x, y, z) return ("[type=move;x=%f;y=%f;z=%f;]"):format(x, y, z) end +function packets.move(x, y, z) return encode { type = "move", x = x, y = y, z = z } end return packets diff --git a/client/quad.lua b/client/quad.lua new file mode 100644 index 0000000..0579383 --- /dev/null +++ b/client/quad.lua @@ -0,0 +1,13 @@ +local a = -1.0 +local b = 1.0 +return function(texture) + return g3d.newModel({ + { 0, a, a, 1, 1 }, + { 0, b, b, 0, 0 }, + { 0, b, a, 0, 1 }, + + { 0, a, a, 1, 1 }, + { 0, a, b, 1, 0 }, + { 0, b, b, 0, 0 }, + }, texture) +end diff --git a/client/scenes/errorscreen.lua b/client/scenes/errorscreen.lua deleted file mode 100644 index 7140a26..0000000 --- a/client/scenes/errorscreen.lua +++ /dev/null @@ -1,16 +0,0 @@ -local errorscreen = {} -local lg = love.graphics - -function errorscreen:init(cause) self.msg = cause end - -function errorscreen:draw() - lg.clear(1, 0, 0) - local w, h = lg.getWidth(), lg.getHeight() - - w = w - lg.getFont():getWidth(self.msg) - h = h - lg.getFont():getHeight() - - lg.print(self.msg, w / 2, h / 2) -end - -return errorscreen diff --git a/client/scenes/gameworld.lua b/client/scenes/gameworld.lua deleted file mode 100644 index cbc8e2b..0000000 --- a/client/scenes/gameworld.lua +++ /dev/null @@ -1,374 +0,0 @@ -local gameworld = {} -local size = loex.chunk.size -local floor = math.floor -local min = math.min -local threadpool = {} -local threadusage = 0 --- load up some threads so that chunk meshing won't block the main thread -for i = 1, 8 do - threadpool[i] = love.thread.newThread("scenes/chunkremesh.lua") -end -local texturepack = lg.newImage("assets/texturepack.png") - -local mouse = {} -- mouse state -local playerbox = { - x = 0, - y = 0, - z = 0, - w = 0.3, - d = 0.3, - h = 0.9, -} - -local playerobj = g3d.newModel("assets/player.obj", "assets/saul.png") - --- create the mesh for the block cursor -local cursor, cursormodel - -do - local a = -0.005 - local b = 1.005 - cursormodel = g3d.newModel { - { a, a, a }, - { b, a, a }, - { b, a, a }, - { a, a, a }, - { a, a, b }, - { a, a, b }, - { b, a, b }, - { a, a, b }, - { a, a, b }, - { b, a, b }, - { b, a, a }, - { b, a, a }, - - { a, b, a }, - { b, b, a }, - { b, b, a }, - { a, b, a }, - { a, b, b }, - { a, b, b }, - { b, b, b }, - { a, b, b }, - { a, b, b }, - { b, b, b }, - { b, b, a }, - { b, b, a }, - - { a, a, a }, - { a, b, a }, - { a, b, a }, - { b, a, a }, - { b, b, a }, - { b, b, a }, - { a, a, b }, - { a, b, b }, - { a, b, b }, - { b, a, b }, - { b, b, b }, - { b, b, b }, - } -end - -function gameworld:init(player) - self.master = master - local world = loex.world.new() - world.ontilemodified:catch(self.ontilemodified, self) - world.onentityinserted:catch(self.onentityinserted, self) - world.onentityremoved:catch(self.onentityremoved, self) - world.onchunkinserted:catch(self.onchunkinserted, self) - world.onchunkremoved:catch(self.onchunkremoved, self) - - self.world = world - - self.placequeue = {} - self.breakqueue = {} - self.remeshqueue = {} - self.remeshchannel = love.thread.newChannel() - self.frameremeshes = 0 - self.synctimer = 0 - - self.player = player - self.player:tag("player") - - self.world:insert(self.player) - - lg.setMeshCullMode("back") -end - -function gameworld:onchunkinserted(chunk) - local x, y, z = chunk.x, chunk.y, chunk.z - self:requestremesh(chunk) -end - -function gameworld:onchunkremoved(chunk) end -function gameworld:onentityinserted(entity) print(entity.id .. " added") end - -function gameworld:onentityremoved(entity) print(entity.id .. " removed") end - -function gameworld:update(dt) - local world = self.world - -- collect mouse inputs - mouse.wasleft, mouse.wasright = mouse.left, mouse.right - mouse.left, mouse.right = love.mouse.isDown(1), love.mouse.isDown(2) - mouse.leftclick, mouse.rightclick = mouse.left and not mouse.wasleft, mouse.right and not mouse.wasright - - local lagdelay = 0.5 - -- handle place and break timeouts - for key, places in pairs(self.placequeue) do - if love.timer.getTime() - places.timestamp > lagdelay then - self.world:tile(places.x, places.y, places.z, loex.tiles.air.id) - self.placequeue[key] = nil - end - end - - for key, breaks in pairs(self.breakqueue) do - if love.timer.getTime() - breaks.timestamp > lagdelay then - self.world:tile(breaks.x, breaks.y, breaks.z, breaks.prev) - self.breakqueue[key] = nil - end - end - - -- count how many threads are being used right now - for _, thread in ipairs(threadpool) do - local err = thread:getError() - assert(not err, err) - end - - -- listen for finished meshes on the thread channels - while self.remeshchannel:peek() do - local data = self.remeshchannel:pop() - if not data then break end - local c = self.world:chunk(loex.hash.spatial(data.cx, data.cy, data.cz)) - if c.model then c.model.mesh:release() end - c.model = nil - c.inremesh = false - if data.count > 0 then - c.model = g3d.newModel(data.count, texturepack) - c.model.mesh:setVertices(data.data) - c.model:setTranslation(data.cx * size, data.cy * size, data.cz * size) - end - threadusage = threadusage - 1 -- free up thread - end - - -- remesh the chunks in the queue - local remeshesquota = #self.remeshqueue - local remeshes = 0 - local offi = 0 - - while threadusage < #threadpool and #self.remeshqueue > 0 and remeshes < remeshesquota do - local c = self.remeshqueue[1 + offi] - remeshes = remeshes + 1 - - for _, thread in ipairs(threadpool) do - if not thread:isRunning() then - -- send over the neighboring chunks to the thread - -- so that voxels on the edges can face themselves properly - local n1, n2, n3, n4, n5, n6 = world:neighbourhood(c.x, c.y, c.z) - if not (n1 and n2 and n3 and n4 and n5 and n6) then - offi = offi + 1 - break - end - - n1, n2, n3, n4, n5, n6 = n1.data, n2.data, n3.data, n4.data, n5.data, n6.data - thread:start(self.remeshchannel, c.x, c.y, c.z, c.data, size, loex.tiles.id, n1, n2, n3, n4, n5, n6) - table.remove(self.remeshqueue, 1 + offi) - threadusage = threadusage + 1 -- use up thread - break - end - end - end - - local keyboard = love.keyboard - local speed, jumpforce, gravity = 5, 12, 42 - local dirx, diry, dirz = g3d.camera.getLookVector() - local move = { x = 0, y = 0, z = 0 } - local p = self.player - - if keyboard.isDown("w") then - move.x = dirx - move.y = diry - elseif keyboard.isDown("s") then - move.x = -dirx - move.y = -diry - end - - if keyboard.isDown("a") then - move.x = -diry - move.y = dirx - elseif keyboard.isDown("d") then - move.x = diry - move.y = -dirx - end - - p.vx, p.vy, _ = g3d.vectors.scalarMultiply(speed, g3d.vectors.normalize(move.x, move.y, move.z)) - p.vz = p.vz - gravity * dt - - local onground = moveandcollide(self.world, p, playerbox, dt) - - g3d.camera.position[1] = p.x - g3d.camera.position[2] = p.y - g3d.camera.position[3] = p.z + 0.7 - g3d.camera.lookInDirection() - - if onground and keyboard.isDown("space") then p.vz = p.vz + jumpforce end - - local syncinterval = 1 / 20 - self.synctimer = self.synctimer + dt - if self.synctimer >= syncinterval then - self.master:send(packets.move(p.x, p.y, p.z), CHANNEL_UPDATES, "unreliable") - self.synctimer = 0 - end - - -- casts a ray from the camera five blocks in the look vector - -- finds the first intersecting block - cursor = nil - do - local dx, dy, dz = g3d.camera.getLookVector() - local x, y, z = g3d.camera.position[1], g3d.camera.position[2], g3d.camera.position[3] - local ox, oy, oz = x, y, z - - local inf = 99999999 - local clipdistance = 10 - local epsilon = 0.00001 - - while true do - local maxx, maxy, maxz = inf, inf, inf - if dx > 0 then - maxx = (floor(x) + 1 - x) / dx - elseif dx < 0 then - maxx = (floor(x) - x) / dx - end - if dy > 0 then - maxy = (floor(y) + 1 - y) / dy - elseif dy < 0 then - maxy = (floor(y) - y) / dy - end - if dz > 0 then - maxz = (floor(z) + 1 - z) / dz - elseif dz < 0 then - maxz = (floor(z) - z) / dz - end - - local step = min(maxx, min(maxy, maxz)) - x = x + dx * step * (1 + epsilon) - y = y + dy * step * (1 + epsilon) - z = z + dz * step * (1 + epsilon) - - if loex.utils.distance3d(ox, oy, oz, x, y, z, true) > clipdistance * clipdistance or step == 0 then break end - - local tx, ty, tz = floor(x), floor(y), floor(z) - local tile = world:tile(tx, ty, tz) - if tile == -1 then break end - if tile > 0 then - cursor = {} - cursor.placex, cursor.placey, cursor.placez = floor(x - dx * step), floor(y - dy * step), floor(z - dz * step) - cursor.x, cursor.y, cursor.z = tx, ty, tz - break - end - end - end - - local placetile = loex.tiles.bricks.id - - if mouse.leftclick and cursor then - local x, y, z = cursor.x, cursor.y, cursor.z - self.breakqueue[("%d/%d/%d"):format(x, y, z)] = { - x = x, - y = y, - z = z, - timestamp = love.timer.getTime(), - prev = self.world:tile(x, y, z), - } - self.master:send(packets.breaktile(x, y, z), CHANNEL_EVENTS, "reliable") - self.world:tile(x, y, z, loex.tiles.air.id) - end - - -- right click to place blocks - if mouse.rightclick and cursor then - local x, y, z = cursor.placex, cursor.placey, cursor.placez - local cube = { x = x + 0.5, y = y + 0.5, z = z + 0.5, w = 0.5, h = 0.5, d = 0.5 } - local translatedplayerbox = lume.clone(playerbox) - translatedplayerbox.x = translatedplayerbox.x + p.x - translatedplayerbox.y = translatedplayerbox.y + p.y - translatedplayerbox.z = translatedplayerbox.z + p.z - if not loex.utils.intersectbb(cube, translatedplayerbox) then - self.placequeue[("%d/%d/%d"):format(x, y, z)] = { - x = x, - y = y, - z = z, - timestamp = love.timer.getTime(), - t = placetile, - } - self.master:send(packets.place(x, y, z, placetile), CHANNEL_EVENTS, "reliable") - self.world:tile(x, y, z, placetile) - end - end -end - -function gameworld:draw() - lg.clear(lume.color("#4488ff")) - - lg.setColor(1, 1, 1) - for _, chunk in pairs(self.world.chunks) do - if chunk.model then chunk.model:draw() end - end - - lg.setMeshCullMode("none") - if cursor then - lg.setColor(0, 0, 0) - lg.setWireframe(true) - cursormodel:setTranslation(cursor.x, cursor.y, cursor.z) - cursormodel:draw() - lg.setWireframe(false) - end - - local camera = g3d.camera.position - lg.setColor(1, 1, 1) - for _, entity in pairs(self.world.entities) do - if entity ~= self.player then - playerobj:setTranslation(entity.x, entity.y, entity.z - 0.9) - playerobj:setRotation(0, 0, math.atan2(entity.y - camera[2], entity.x - camera[1])) - playerobj:setScale(0.1, 1, 0.6) - playerobj:draw() - end - end - - lg.setMeshCullMode("back") -end - -function gameworld:mousemoved(x, y, dx, dy) g3d.camera.firstPersonLook(dx, dy) end - -function gameworld:ontilemodified(x, y, z, _) - local spatial = loex.hash.spatial - local chunk = self.world:chunk(spatial(floor(x / size), floor(y / size), floor(z / size))) - assert(chunk) - - local tx, ty, tz = x % size, y % size, z % size - local cx, cy, cz = chunk.x, chunk.y, chunk.z - local world = self.world - - if tx >= size - 1 then self:requestremesh(world:chunk(spatial(cx + 1, cy, cz)), true) end - if tx <= 0 then self:requestremesh(world:chunk(spatial(cx - 1, cy, cz)), true) end - if ty >= size - 1 then self:requestremesh(world:chunk(spatial(cx, cy + 1, cz)), true) end - if ty <= 0 then self:requestremesh(world:chunk(spatial(cx, cy - 1, cz)), true) end - if tz >= size - 1 then self:requestremesh(world:chunk(spatial(cx, cy, cz + 1)), true) end - if tz <= 0 then self:requestremesh(world:chunk(spatial(cx, cy, cz - 1)), true) end - - self:requestremesh(chunk, true) -end - -function gameworld:requestremesh(c, priority) - -- don't add a nil chunk or a chunk that's already in the queue - local world = self.world - if not c or c.inremesh or not c.data then return end - - c.inremesh = true - if priority then - table.insert(self.remeshqueue, 1, c) - else - table.insert(self.remeshqueue, c) - end -end - -return gameworld diff --git a/client/scenes/joinscreen.lua b/client/scenes/joinscreen.lua deleted file mode 100644 index b249f48..0000000 --- a/client/scenes/joinscreen.lua +++ /dev/null @@ -1,19 +0,0 @@ -local joinscreen = {} -local lg = love.graphics - -function joinscreen:draw() - local w, h = lg.getWidth(), lg.getHeight() - - local msg = "Joining" - - for _ = 1, math.floor(love.timer.getTime()) % 4 do - msg = msg .. "." - end - - w = w - lg.getFont():getWidth(msg) - h = h - lg.getFont():getHeight() - - lg.print(msg, w / 2, h / 2) -end - -return joinscreen diff --git a/client/scenes/chunkremesh.lua b/client/screens/chunkremesh.lua similarity index 82% rename from client/scenes/chunkremesh.lua rename to client/screens/chunkremesh.lua index 71ab2f1..eb32159 100644 --- a/client/scenes/chunkremesh.lua +++ b/client/screens/chunkremesh.lua @@ -2,7 +2,8 @@ require("love.math") require("love.data") local ffi = require("ffi") -local channel, cx, cy, cz, blockdata, size, tids, n1, n2, n3, n4, n5, n6 = ... +local channel, cx, cy, cz, blockdata, size, tids, n1, n2, n3, n4, n5, n6, c0, c1, c2, c3, c4, c5, c6 = ... + local blockdatapointer = ffi.cast("uint8_t *", blockdata:getFFIPointer()) local n1p = n1 and ffi.cast("uint8_t *", n1:getFFIPointer()) local n2p = n2 and ffi.cast("uint8_t *", n2:getFFIPointer()) @@ -11,9 +12,28 @@ local n4p = n4 and ffi.cast("uint8_t *", n4:getFFIPointer()) local n5p = n5 and ffi.cast("uint8_t *", n5:getFFIPointer()) local n6p = n6 and ffi.cast("uint8_t *", n6:getFFIPointer()) -local c1 = 1 -local c2 = 0.75 -local c3 = 0.5 +local c0p = c0 and ffi.cast("uint8_t *", c0:getFFIPointer()) +local c1p = c1 and ffi.cast("uint8_t *", c1:getFFIPointer()) +local c2p = c2 and ffi.cast("uint8_t *", c2:getFFIPointer()) +local c3p = c3 and ffi.cast("uint8_t *", c3:getFFIPointer()) +local c4p = c4 and ffi.cast("uint8_t *", c4:getFFIPointer()) +local c5p = c5 and ffi.cast("uint8_t *", c5:getFFIPointer()) +local c6p = c6 and ffi.cast("uint8_t *", c6:getFFIPointer()) + +local function getc(pointer, x, y, z) + if true then return 255 end + local i = x + size * y + size * size * z + + -- if this block is outside of the chunk, check the neighboring chunks if they exist + if x >= size then return c1p and getc(c1p, x % size, y % size, z % size) or -1 end + if x < 0 then return c2p and getc(c2p, x % size, y % size, z % size) or -1 end + if y >= size then return c3p and getc(c3p, x % size, y % size, z % size) or -1 end + if y < 0 then return c4p and getc(c4p, x % size, y % size, z % size) or -1 end + if z >= size then return c5p and getc(c5p, x % size, y % size, z % size) or -1 end + if z < 0 then return c6p and getc(c6p, x % size, y % size, z % size) or -1 end + + return pointer[i] +end local function gettile(pointer, x, y, z) local i = x + size * y + size * size * z @@ -70,6 +90,7 @@ if count > 0 then for i = start, stop, step do local primary = i % 2 == 1 local secondary = i > 2 and i < 6 + local c = getc(c0p, x, y, z) / 255 datapointer[dataindex].x = x + (mx == 1 and primary and 1 or 0) + (mx == 2 and secondary and 1 or 0) datapointer[dataindex].y = y + (my == 1 and primary and 1 or 0) + (my == 2 and secondary and 1 or 0) datapointer[dataindex].z = z + (mz == 1 and primary and 1 or 0) + (mz == 2 and secondary and 1 or 0) diff --git a/client/screens/cursormodel.lua b/client/screens/cursormodel.lua new file mode 100644 index 0000000..4d0bc28 --- /dev/null +++ b/client/screens/cursormodel.lua @@ -0,0 +1,42 @@ +local a = -0.005 +local b = 1.005 +return g3d.newModel { + { a, a, a }, + { b, a, a }, + { b, a, a }, + { a, a, a }, + { a, a, b }, + { a, a, b }, + { b, a, b }, + { a, a, b }, + { a, a, b }, + { b, a, b }, + { b, a, a }, + { b, a, a }, + + { a, b, a }, + { b, b, a }, + { b, b, a }, + { a, b, a }, + { a, b, b }, + { a, b, b }, + { b, b, b }, + { a, b, b }, + { a, b, b }, + { b, b, b }, + { b, b, a }, + { b, b, a }, + + { a, a, a }, + { a, b, a }, + { a, b, a }, + { b, a, a }, + { b, b, a }, + { b, b, a }, + { a, a, b }, + { a, b, b }, + { a, b, b }, + { b, a, b }, + { b, b, b }, + { b, b, b }, +} diff --git a/client/screens/errorscreen.lua b/client/screens/errorscreen.lua new file mode 100644 index 0000000..d5e9c1f --- /dev/null +++ b/client/screens/errorscreen.lua @@ -0,0 +1,21 @@ +local errorscreen = {} +local lg = love.graphics + +function errorscreen.init(g, cause) + g.errorscreen = {} + g.errorscreen.cause = cause + g.ondraw:catch(errorscreen.draw) +end + +function errorscreen.draw(g) + lg.clear(1, 0, 0) + local w, h = lg.getWidth(), lg.getHeight() + + local cause = g.errorscreen.cause + w = w - lg.getFont():getWidth(cause) + h = h - lg.getFont():getHeight() + + lg.print(cause, w / 2, h / 2) +end + +return errorscreen diff --git a/client/screens/gamescreen.lua b/client/screens/gamescreen.lua new file mode 100644 index 0000000..79aef75 --- /dev/null +++ b/client/screens/gamescreen.lua @@ -0,0 +1,439 @@ +local packets = require("packets") +local quad = require("quad") + +local physics = loex.physics +local socket = loex.socket +local size = loex.chunk.size +local lg = love.graphics + +local floor = math.floor +local min = math.min + +local CEILING = 10 + +local gamescreen = {} + +function gamescreen.init(g, player) + g.gamescreen = { + cursor = {}, + cursormodel = require("screens.cursormodel"), + texturepack = lg.newImage("assets/texturepack.png"), + threadpool = {}, + threadusage = 0, + mouse = {}, + placequeue = {}, + breakqueue = {}, + remeshqueue = {}, + remeshchannel = love.thread.newChannel(), + frameremeshes = 0, + synctimer = 0, + player = player, + } + + player.box = { + x = 0, + y = 0, + z = 0, + w = 0.3, + d = 0.3, + h = 0.9, + } + + -- load up some threads so that chunk meshing won't block the main thread + for i = 1, 8 do + g.gamescreen.threadpool[i] = love.thread.newThread("screens/chunkremesh.lua") + end + + g.gamescreen.player_model = quad(lg.newImage("assets/saul.png")) + g.gamescreen.snowball_model = quad(lg.newImage("assets/snowball.png")) + g.gamescreen.place_sound = love.sound.newSoundData("assets/audio/place.wav") + g.gamescreen.footstep_sounds = { + love.sound.newSoundData("assets/audio/footsteps/footstep-01.wav"), + love.sound.newSoundData("assets/audio/footsteps/footstep-02.wav"), + love.sound.newSoundData("assets/audio/footsteps/footstep-03.wav"), + love.sound.newSoundData("assets/audio/footsteps/footstep-04.wav"), + love.sound.newSoundData("assets/audio/footsteps/footstep-05.wav"), + love.sound.newSoundData("assets/audio/footsteps/footstep-06.wav"), + love.sound.newSoundData("assets/audio/footsteps/footstep-07.wav"), + love.sound.newSoundData("assets/audio/footsteps/footstep-08.wav"), + love.sound.newSoundData("assets/audio/footsteps/footstep-09.wav"), + } + + g.gamescreen.gravity = 42 + + lg.setMeshCullMode("back") + love.audio.setDistanceModel("inverseclamped") + + g.ondraw:catch(gamescreen.draw) + g.onupdate:catch(gamescreen.update) + g.onmousemoved:catch(gamescreen.onmousemoved) + --g.onmousepressed:catch(gamescreen.onmousepressed) + g.onkeypressed:catch(gamescreen.onkeypressed) + g.world.ontilemodified:catch(gamescreen.ontilemodified, g) + g.world.onentityinserted:catch(gamescreen.onentityinserted, g) + g.world.onentityremoved:catch(gamescreen.onentityremoved, g) + g.world.onchunkinserted:catch(gamescreen.onchunkinserted, g) + g.world.onchunkremoved:catch(gamescreen.onchunkremoved, g) + + require("services.nethandler").init(g) + require("services.player").init(g) + require("common.services.snowball").init(g) +end + +function gamescreen.onchunkinserted(g, chunk) + local x, y, z = chunk.x, chunk.y, chunk.z + gamescreen.requestremesh(g, chunk) +end + +function gamescreen.onchunkremoved(g, chunk) end + +function gamescreen.onentityinserted(g, entity) print(entity.id .. " added") end + +function gamescreen.onentityremoved(g, entity) print(entity.id .. " removed") end + +function gamescreen.onresize(g, w, h) + g3d.camera.aspectRatio = w / h + g3d.camera.updateProjectionMatrix() +end + +function gamescreen.update(g, dt) + local self = g.gamescreen + local mouse = self.mouse + local threadpool = self.threadpool + + -- collect mouse inputs + mouse.wasleft, mouse.wasright = mouse.left, mouse.right + mouse.left, mouse.right = love.mouse.isDown(1), love.mouse.isDown(2) + mouse.leftclick, mouse.rightclick = mouse.left and not mouse.wasleft, mouse.right and not mouse.wasright + + local lagdelay = 0.5 + -- handle place and break timeouts + for key, places in pairs(self.placequeue) do + if love.timer.getTime() - places.timestamp > lagdelay then + g.world:tile(places.x, places.y, places.z, loex.tiles.air.id) + self.placequeue[key] = nil + end + end + + for key, breaks in pairs(self.breakqueue) do + if love.timer.getTime() - breaks.timestamp > lagdelay then + g.world:tile(breaks.x, breaks.y, breaks.z, breaks.prev) + self.breakqueue[key] = nil + end + end + + -- count how many threads are being used right now + for _, thread in ipairs(threadpool) do + local err = thread:getError() + assert(not err, err) + end + + -- listen for finished meshes on the thread channels + while self.remeshchannel:peek() do + local data = self.remeshchannel:pop() + if not data then break end + local c = g.world:chunk(loex.hash.spatial(data.cx, data.cy, data.cz)) + if c.model then c.model.mesh:release() end + c.model = nil + c.inremesh = false + if data.count > 0 then + c.model = g3d.newModel(data.count, self.texturepack) + c.model.mesh:setVertices(data.data) + c.model:setTranslation(data.cx * size, data.cy * size, data.cz * size) + end + self.threadusage = self.threadusage - 1 -- free up thread + end + + -- remesh the chunks in the queue + local remeshesquota = #self.remeshqueue + local remeshes = 0 + local offi = 0 + + while self.threadusage < #threadpool and #self.remeshqueue > 0 and remeshes < remeshesquota do + local c = self.remeshqueue[1 + offi] + remeshes = remeshes + 1 + + for _, thread in ipairs(threadpool) do + if not thread:isRunning() then + -- send over the neighboring chunks to the thread + -- so that voxels on the edges can face themselves properly + local n1, n2, n3, n4, n5, n6 = g.world:neighbourhood(c.x, c.y, c.z) + if + not (n1 and n2 and n3 and n4 and n5 and n6) or not g.world:chunk(loex.hash.spatial(c.x, c.y, CEILING - 1)) + then + offi = offi + 1 + break + end + + c1, c2, c3, c4, c5, c6 = n1.cdata, n2.cdata, n3.cdata, n4.cdata, n5.cdata, n6.cdata + n1, n2, n3, n4, n5, n6 = n1.data, n2.data, n3.data, n4.data, n5.data, n6.data + thread:start( + self.remeshchannel, + c.x, + c.y, + c.z, + c.data, + size, + loex.tiles.id, + n1, + n2, + n3, + n4, + n5, + n6, + c.cdata, + c1, + c2, + c3, + c4, + c5, + c6 + ) + table.remove(self.remeshqueue, 1 + offi) + self.threadusage = self.threadusage + 1 -- use up thread + break + end + end + end + + -- player movement + local keyboard = love.keyboard + local speed, jumpforce = 5, 12 + local dirx, diry, dirz = g3d.camera.getLookVector() + local move = { x = 0, y = 0, z = 0 } + local p = self.player + + if keyboard.isDown("w") then + move.x = move.x + dirx + move.y = move.y + diry + end + if keyboard.isDown("s") then + move.x = move.x - dirx + move.y = move.y - diry + end + + if keyboard.isDown("a") then + move.x = move.x - diry + move.y = move.y + dirx + end + if keyboard.isDown("d") then + move.x = move.x + diry + move.y = move.y - dirx + end + + p.vx, p.vy, _ = g3d.vectors.scalarMultiply(speed, g3d.vectors.normalize(move.x, move.y, move.z)) + p.vz = p.vz - self.gravity * dt + + local onground = physics.moveandcollide(g.world, p, p.box, dt) + if onground and (move.x ~= 0 or move.y ~= 0) then + if not p.ssfootsteps:isPlaying() then + p.ssfootsteps:queue(self.footstep_sounds[math.random(1, #self.footstep_sounds)]) + assert(p.ssfootsteps:play()) + end + end + + g3d.camera.position[1] = p.x + g3d.camera.position[2] = p.y + g3d.camera.position[3] = p.z + 0.7 + g3d.camera.lookInDirection() + + -- player jump + if onground and keyboard.isDown("space") then p.vz = jumpforce end + + do + local dx, dy, dz = g3d.camera.getLookVector() + local x, y, z = g3d.camera.position[1], g3d.camera.position[2], g3d.camera.position[3] + love.audio.setPosition(x, y, z) + love.audio.setOrientation(dx, dy, dz, 0, 0, 1) + end + + local syncinterval = 1 / 20 + self.synctimer = self.synctimer + dt + if self.synctimer >= syncinterval then + g.master:send(packets.move(p.x, p.y, p.z), CHANNEL_UPDATES, "unreliable") + self.synctimer = 0 + end + + -- casts a ray from the camera five blocks in the look vector + -- finds the first intersecting block + self.cursor = nil + do + local dx, dy, dz = g3d.camera.getLookVector() + local x, y, z = g3d.camera.position[1], g3d.camera.position[2], g3d.camera.position[3] + local ox, oy, oz = x, y, z + + local inf = 99999999 + local clipdistance = 10 + local epsilon = 0.00001 + + while true do + local maxx, maxy, maxz = inf, inf, inf + if dx > 0 then + maxx = (floor(x) + 1 - x) / dx + elseif dx < 0 then + maxx = (floor(x) - x) / dx + end + if dy > 0 then + maxy = (floor(y) + 1 - y) / dy + elseif dy < 0 then + maxy = (floor(y) - y) / dy + end + if dz > 0 then + maxz = (floor(z) + 1 - z) / dz + elseif dz < 0 then + maxz = (floor(z) - z) / dz + end + + local step = min(maxx, min(maxy, maxz)) + x = x + dx * step * (1 + epsilon) + y = y + dy * step * (1 + epsilon) + z = z + dz * step * (1 + epsilon) + + if loex.utils.distance3d(ox, oy, oz, x, y, z, true) > clipdistance * clipdistance or step == 0 then break end + + local tx, ty, tz = floor(x), floor(y), floor(z) + local tile = g.world:tile(tx, ty, tz) + if tile == -1 then break end + if tile > 0 then + self.cursor = {} + self.cursor.placex, self.cursor.placey, self.cursor.placez = + floor(x - dx * step), floor(y - dy * step), floor(z - dz * step) + self.cursor.x, self.cursor.y, self.cursor.z = tx, ty, tz + break + end + end + end + + local placetile = loex.tiles.slime.id + + -- left click to break block + if mouse.leftclick and self.cursor then + local x, y, z = self.cursor.x, self.cursor.y, self.cursor.z + self.breakqueue[("%d/%d/%d"):format(x, y, z)] = { + x = x, + y = y, + z = z, + timestamp = love.timer.getTime(), + prev = g.world:tile(x, y, z), + } + g.master:send(packets.breaktile(x, y, z), CHANNEL_EVENTS, "reliable") + g.world:tile(x, y, z, loex.tiles.air.id) + end + + -- right click to place block + if mouse.rightclick and self.cursor then + local x, y, z = self.cursor.placex, self.cursor.placey, self.cursor.placez + local cube = { x = x + 0.5, y = y + 0.5, z = z + 0.5, w = 0.5, h = 0.5, d = 0.5 } + local translatedplayerbox = lume.clone(p.box) + translatedplayerbox.x = translatedplayerbox.x + p.x + translatedplayerbox.y = translatedplayerbox.y + p.y + translatedplayerbox.z = translatedplayerbox.z + p.z + if not loex.utils.intersectbb(cube, translatedplayerbox) then + self.placequeue[("%d/%d/%d"):format(x, y, z)] = { + x = x, + y = y, + z = z, + timestamp = love.timer.getTime(), + t = placetile, + } + g.master:send(packets.place(x, y, z, placetile), CHANNEL_EVENTS, "reliable") + g.world:tile(x, y, z, placetile) + gamescreen.play_place_sound(g, x, y, z) + end + end +end + +function gamescreen.play_place_sound(g, x, y, z) + local self = g.gamescreen + local source = love.audio.newSource(self.place_sound) + source:setPosition(x, y, z) + source:setAttenuationDistances(0.5, 2000000) + source:setRolloff(0.3) + source:play() +end + +function gamescreen.draw(g) + local self = g.gamescreen + lg.clear(lume.color("#4488ff")) + + lg.setColor(1, 1, 1) + for _, chunk in pairs(g.world.chunks) do + if chunk.model then chunk.model:draw() end + end + + lg.setMeshCullMode("none") + if self.cursor then + lg.setColor(0, 0, 0) + lg.setWireframe(true) + self.cursormodel:setTranslation(self.cursor.x, self.cursor.y, self.cursor.z) + self.cursormodel:draw() + lg.setWireframe(false) + end + lg.setMeshCullMode("back") + + -- draw crosshair + local cross = 4 + lg.setColor(1, 1, 1) + lg.rectangle("fill", (lg.getWidth() - cross) / 2, (lg.getHeight() - cross) / 2, cross, cross) +end + +function gamescreen.throw_snowball(g) + local x, y, z = g3d.camera.position[1], g3d.camera.position[2], g3d.camera.position[3] + local dx, dy, dz = g3d.camera.getLookVector() + local force = 30 + g.master:send(socket.encode { + type = "snowball_throw", + x = x, + y = y, + z = z, + vx = dx * force, + vy = dy * force, + vz = dz * force, + }) + g.gamescreen.player.sssnowball_throw:play() +end + +function gamescreen.onmousemoved(g, x, y, dx, dy, istouch) g3d.camera.firstPersonLook(dx, dy) end + +function gamescreen.onkeypressed(g, k) + if k == "q" then + print("thrown snowball") + gamescreen.throw_snowball(g) + end +end + +function gamescreen.ontilemodified(g, x, y, z, _) + local spatial = loex.hash.spatial + local chunk = g.world:chunk(spatial(floor(x / size), floor(y / size), floor(z / size))) + assert(chunk) + + local tx, ty, tz = x % size, y % size, z % size + local cx, cy, cz = chunk.x, chunk.y, chunk.z + local world = g.world + + if tx >= size - 1 then gamescreen.requestremesh(g, world:chunk(spatial(cx + 1, cy, cz)), true) end + if tx <= 0 then gamescreen.requestremesh(g, world:chunk(spatial(cx - 1, cy, cz)), true) end + if ty >= size - 1 then gamescreen.requestremesh(g, world:chunk(spatial(cx, cy + 1, cz)), true) end + if ty <= 0 then gamescreen.requestremesh(g, world:chunk(spatial(cx, cy - 1, cz)), true) end + if tz >= size - 1 then gamescreen.requestremesh(g, world:chunk(spatial(cx, cy, cz + 1)), true) end + if tz <= 0 then gamescreen.requestremesh(g, world:chunk(spatial(cx, cy, cz - 1)), true) end + + gamescreen.requestremesh(g, chunk, true) +end + +function gamescreen.requestremesh(g, c, priority) + local self = g.gamescreen + -- don't add a nil chunk or a chunk that's already in the queue + local world = g.world + if not c or c.inremesh or not c.data then return end + if not c.cdata then c.cdata = love.data.newByteData(size ^ 3) end + + c.inremesh = true + if priority then + table.insert(self.remeshqueue, 1, c) + else + table.insert(self.remeshqueue, c) + end +end + +return gamescreen diff --git a/client/screens/joinscreen.lua b/client/screens/joinscreen.lua new file mode 100644 index 0000000..f8d28a8 --- /dev/null +++ b/client/screens/joinscreen.lua @@ -0,0 +1,106 @@ +local errorscreen = require("screens.errorscreen") +local gamescreen = require("screens.gamescreen") +local packets = require("packets") + +local lg = love.graphics + +local joinscreen = {} + +function joinscreen.init(g) + local signals = { + g.ondraw:catch(joinscreen.draw), + g.socket.onconnect:catch(joinscreen.socket_onconnect, g), + g.socket.onreceive:catch(joinscreen.socket_onreceive, g), + g.socket.ondisconnect:catch(joinscreen.socket_ondisconnect, g), + } + g.joinscreen = { signals = signals } + + -- base functions + g.onupdate:catch(function(g) g.socket:service() end) + g.onquit:catch(function(g) g.socket:disconnect() end) +end + +function joinscreen.cleanup(g) + for _, signal in ipairs(g.joinscreen.signals) do + signal:destroy() + end + g.joinscreen = nil +end + +function joinscreen.error(g, cause) + joinscreen.cleanup(g) + errorscreen.init(g, cause) +end + +function joinscreen.socket_onreceive(g, peer, d) + print("received " .. d.type) + if d.type == "joinsuccess" then + local spawnx, spawny, spawnz = d.x, d.y, d.z + assert(d.id) + + g.world = loex.world.new() + + local player = { id = d.id } + player.x, player.y, player.z = spawnx, spawny, spawnz + player.vx, player.vy, player.vz = 0, 0, 0 + player.username = g.username + + player.ssfootsteps = love.audio.newQueueableSource(48000, 16, 1) + player.ssfootsteps:setAttenuationDistances(1, 1) + player.sssnowball_throw = love.audio.newSource("assets/audio/snowball_throw.wav", "static") + + g.world:insert(player) + g.world:tag(player, "player") + + print( + ("Joined under username " .. player.username .. " (ID: " .. player.id .. ") at spawn point %d, %d, %d"):format( + spawnx, + spawny, + spawnz + ) + ) + joinscreen.cleanup(g) + gamescreen.init(g, player) + elseif d.type == "joinfailure" then + joinscreen.error(g, d.cause) + else + joinscreen.error(g, "unexpected packet type " .. d.typed) + end +end + +function joinscreen.socket_onconnect(g, peer) + print("connected!") + + -- set master peer + g.master = peer + + -- send join + local join_packet = packets.join(g.username) + g.master:send(join_packet) +end + +function joinscreen.socket_ondisconnect(g) joinscreen.error(g, "connection closed") end + +function joinscreen.update(g) g.socket:service() end + +function joinscreen.draw() + local w, h = lg.getWidth(), lg.getHeight() + + local l = 3 + local s = "" + local k = math.floor(love.timer.getTime()) % l + for i = 0, l - 1 do + if i == k then + s = s .. "O" + else + s = s .. "o" + end + end + + w = w - lg.getFont():getWidth(s) + h = h - lg.getFont():getHeight() + + lg.print(s, w / 2, h / 2) +end + +return joinscreen diff --git a/client/nethandler.lua b/client/services/nethandler.lua similarity index 52% rename from client/nethandler.lua rename to client/services/nethandler.lua index 375785a..b87c3df 100644 --- a/client/nethandler.lua +++ b/client/services/nethandler.lua @@ -1,36 +1,30 @@ local nethandler = {} -function nethandler.joinsuccess(_, d) - local spawnx, spawny, spawnz = tonumber(d.x), tonumber(d.y), tonumber(d.z) - assert(d.id) - local player = loex.entity.new(spawnx, spawny, spawnz, d.id) - player.username = username +function nethandler.init(g) g.socket.onreceive:catch(nethandler.onreceive, g) end - print( - ("Joined under username " .. player.username .. " (ID: " .. player.id .. ") at spawn point %d, %d, %d"):format( - spawnx, - spawny, - spawnz - ) - ) +function nethandler.onreceive(g, peer, d) + if d.type == "joinsuccess" then return end -- FIXME - scene(require("scenes/gameworld"), player) + print("received " .. d.type) + nethandler[d.type](g, d) end -function nethandler.joinfailure(_, d) scene(require("scenes/errorscreen"), d.cause) end - function nethandler.broken(g, d) local x, y, z = tonumber(d.x), tonumber(d.y), tonumber(d.z) local hash = ("%d/%d/%d"):format(x, y, z) - if not g.breakqueue[hash] then g.world:tile(x, y, z, loex.tiles.air.id, true) end - g.breakqueue[hash] = nil + if not g.gamescreen.breakqueue[hash] then g.world:tile(x, y, z, loex.tiles.air.id, true) end + g.gamescreen.breakqueue[hash] = nil end function nethandler.placed(g, d) + local gamescreen = require("screens.gamescreen") local x, y, z, t = tonumber(d.x), tonumber(d.y), tonumber(d.z), tonumber(d.t) local hash = ("%d/%d/%d"):format(x, y, z) - if not g.placequeue[hash] or g.placequeue[hash].placed ~= t then g.world:tile(x, y, z, t) end - g.placequeue[hash] = nil + if not g.gamescreen.placequeue[hash] or g.gamescreen.placequeue[hash].t ~= t then + g.world:tile(x, y, z, t) + gamescreen.play_place_sound(g, x, y, z) + end + g.gamescreen.placequeue[hash] = nil end function nethandler.entitymove(g, d) @@ -43,20 +37,17 @@ function nethandler.entitymove(g, d) entity.z = z end -function nethandler.entityadd(g, d) - if d.id == g.player.id then return end +function nethandler.entityadd(g, d) end - local x, y, z = tonumber(d.x), tonumber(d.y), tonumber(d.z) - local entity = loex.entity.new(x, y, z, d.id) - g.world:insert(entity) -end - -function nethandler.entityremove(g, d) g.world:remove(d.id) end +function nethandler.entityremove(g, d) g.world:remove(g.world:entity(d.id)) end function nethandler.entityremoteset(g, d) local entity = g.world:entity(d.id) - if entity.id == g.player.id and d.property:match("[xyz]") then return end -- TODO: position correction - entity[d.property] = d.value + for k, v in pairs(d.properties) do + if not (entity.id == g.gamescreen.player.id and k:match("[xyz]")) then -- TODO: position correction + entity[k] = v + end + end end function nethandler.chunkadd(g, d) diff --git a/client/services/player.lua b/client/services/player.lua new file mode 100644 index 0000000..159f388 --- /dev/null +++ b/client/services/player.lua @@ -0,0 +1,38 @@ +local player = {} +local lg = love.graphics + +function player.init(g) + g.socket.onreceive:catch(player.onreceive, g) + + g.onupdate:catch(player.onupdate) + g.ondraw:catch(player.ondraw) +end + +function player.onreceive(g, _, packet) + if packet.type == "entityadd" and packet.entity_type == "player" then + local e = { id = packet.id, x = packet.x, y = packet.y, z = packet.z, username = packet.username } + g.world:insert(e) + g.world:tag(e, "player") + end +end + +function player.onupdate(g) end + +function player.ondraw(g) + local player_model = g.gamescreen.player_model + local camera = g3d.camera.position + + lg.setMeshCullMode("none") + lg.setColor(1, 1, 1) + + for _, e in pairs(g.world:query("player")) do + if e ~= g.gamescreen.player then + player_model:setTranslation(e.x, e.y, e.z) + player_model:setRotation(0, 0, math.atan2(e.y - camera[2], e.x - camera[1])) + player_model:setScale(1, 0.6, 0.9) + player_model:draw() + end + end +end + +return player diff --git a/common/entity.lua b/common/entity.lua index 77c7528..e69de29 100644 --- a/common/entity.lua +++ b/common/entity.lua @@ -1,25 +0,0 @@ -local entity = {} -entity.__index = entity - -function entity.new(x, y, z, id) - local new = {} - new.id = id or lume.uuid() - new.x, new.y, new.z = x, y, z - new.vx, new.vy, new.vz = 0, 0, 0 - new.tags = {} - - setmetatable(new, entity) - return new -end - -function entity:tag(tag) self.tags[tag] = true end - -function entity:untag(tag) self.tags[tag] = nil end - -function entity:has(tag) return self.tags[tag] == true end - -function entity:destroy() end - -function entity:__tostring() return entity.type end - -return entity diff --git a/common/init.lua b/common/init.lua index 14a8996..47891c2 100644 --- a/common/init.lua +++ b/common/init.lua @@ -31,6 +31,8 @@ loex = { assert(love, "this package needs löve!") lume = require(loex.lpath .. "lib.lume") +json = require(loex.lpath .. "lib.json") +inspect = require(loex.lpath .. "lib.inspect") loex.hash = require(loex.lpath .. "hash") loex.signal = require(loex.lpath .. "signal") @@ -41,5 +43,6 @@ loex.entity = require(loex.lpath .. "entity") loex.world = require(loex.lpath .. "world") loex.brush = require(loex.lpath .. "brush") loex.socket = require(loex.lpath .. "socket") +loex.physics = require(loex.lpath .. "physics") return loex diff --git a/common/lib/inspect.lua b/common/lib/inspect.lua new file mode 100644 index 0000000..9900a0b --- /dev/null +++ b/common/lib/inspect.lua @@ -0,0 +1,371 @@ +local _tl_compat; if (tonumber((_VERSION or ''):match('[%d.]*$')) or 0) < 5.3 then local p, m = pcall(require, 'compat53.module'); if p then _tl_compat = m end end; local math = _tl_compat and _tl_compat.math or math; local string = _tl_compat and _tl_compat.string or string; local table = _tl_compat and _tl_compat.table or table +local inspect = {Options = {}, } + + + + + + + + + + + + + + + + + +inspect._VERSION = 'inspect.lua 3.1.0' +inspect._URL = 'http://github.com/kikito/inspect.lua' +inspect._DESCRIPTION = 'human-readable representations of tables' +inspect._LICENSE = [[ + MIT LICENSE + + Copyright (c) 2022 Enrique García Cota + + Permission is hereby granted, free of charge, to any person obtaining a + copy of this software and associated documentation files (the + "Software"), to deal in the Software without restriction, including + without limitation the rights to use, copy, modify, merge, publish, + distribute, sublicense, and/or sell copies of the Software, and to + permit persons to whom the Software is furnished to do so, subject to + the following conditions: + + The above copyright notice and this permission notice shall be included + in all copies or substantial portions of the Software. + + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS + OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF + MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. + IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY + CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, + TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE + SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +]] +inspect.KEY = setmetatable({}, { __tostring = function() return 'inspect.KEY' end }) +inspect.METATABLE = setmetatable({}, { __tostring = function() return 'inspect.METATABLE' end }) + +local tostring = tostring +local rep = string.rep +local match = string.match +local char = string.char +local gsub = string.gsub +local fmt = string.format + +local _rawget +if rawget then + _rawget = rawget +else + _rawget = function(t, k) return t[k] end +end + +local function rawpairs(t) + return next, t, nil +end + + + +local function smartQuote(str) + if match(str, '"') and not match(str, "'") then + return "'" .. str .. "'" + end + return '"' .. gsub(str, '"', '\\"') .. '"' +end + + +local shortControlCharEscapes = { + ["\a"] = "\\a", ["\b"] = "\\b", ["\f"] = "\\f", ["\n"] = "\\n", + ["\r"] = "\\r", ["\t"] = "\\t", ["\v"] = "\\v", ["\127"] = "\\127", +} +local longControlCharEscapes = { ["\127"] = "\127" } +for i = 0, 31 do + local ch = char(i) + if not shortControlCharEscapes[ch] then + shortControlCharEscapes[ch] = "\\" .. i + longControlCharEscapes[ch] = fmt("\\%03d", i) + end +end + +local function escape(str) + return (gsub(gsub(gsub(str, "\\", "\\\\"), + "(%c)%f[0-9]", longControlCharEscapes), + "%c", shortControlCharEscapes)) +end + +local luaKeywords = { + ['and'] = true, + ['break'] = true, + ['do'] = true, + ['else'] = true, + ['elseif'] = true, + ['end'] = true, + ['false'] = true, + ['for'] = true, + ['function'] = true, + ['goto'] = true, + ['if'] = true, + ['in'] = true, + ['local'] = true, + ['nil'] = true, + ['not'] = true, + ['or'] = true, + ['repeat'] = true, + ['return'] = true, + ['then'] = true, + ['true'] = true, + ['until'] = true, + ['while'] = true, +} + +local function isIdentifier(str) + return type(str) == "string" and + not not str:match("^[_%a][_%a%d]*$") and + not luaKeywords[str] +end + +local flr = math.floor +local function isSequenceKey(k, sequenceLength) + return type(k) == "number" and + flr(k) == k and + 1 <= (k) and + k <= sequenceLength +end + +local defaultTypeOrders = { + ['number'] = 1, ['boolean'] = 2, ['string'] = 3, ['table'] = 4, + ['function'] = 5, ['userdata'] = 6, ['thread'] = 7, +} + +local function sortKeys(a, b) + local ta, tb = type(a), type(b) + + + if ta == tb and (ta == 'string' or ta == 'number') then + return (a) < (b) + end + + local dta = defaultTypeOrders[ta] or 100 + local dtb = defaultTypeOrders[tb] or 100 + + + return dta == dtb and ta < tb or dta < dtb +end + +local function getKeys(t) + + local seqLen = 1 + while _rawget(t, seqLen) ~= nil do + seqLen = seqLen + 1 + end + seqLen = seqLen - 1 + + local keys, keysLen = {}, 0 + for k in rawpairs(t) do + if not isSequenceKey(k, seqLen) then + keysLen = keysLen + 1 + keys[keysLen] = k + end + end + table.sort(keys, sortKeys) + return keys, keysLen, seqLen +end + +local function countCycles(x, cycles) + if type(x) == "table" then + if cycles[x] then + cycles[x] = cycles[x] + 1 + else + cycles[x] = 1 + for k, v in rawpairs(x) do + countCycles(k, cycles) + countCycles(v, cycles) + end + countCycles(getmetatable(x), cycles) + end + end +end + +local function makePath(path, a, b) + local newPath = {} + local len = #path + for i = 1, len do newPath[i] = path[i] end + + newPath[len + 1] = a + newPath[len + 2] = b + + return newPath +end + + +local function processRecursive(process, + item, + path, + visited) + if item == nil then return nil end + if visited[item] then return visited[item] end + + local processed = process(item, path) + if type(processed) == "table" then + local processedCopy = {} + visited[item] = processedCopy + local processedKey + + for k, v in rawpairs(processed) do + processedKey = processRecursive(process, k, makePath(path, k, inspect.KEY), visited) + if processedKey ~= nil then + processedCopy[processedKey] = processRecursive(process, v, makePath(path, processedKey), visited) + end + end + + local mt = processRecursive(process, getmetatable(processed), makePath(path, inspect.METATABLE), visited) + if type(mt) ~= 'table' then mt = nil end + setmetatable(processedCopy, mt) + processed = processedCopy + end + return processed +end + +local function puts(buf, str) + buf.n = buf.n + 1 + buf[buf.n] = str +end + + + +local Inspector = {} + + + + + + + + + + +local Inspector_mt = { __index = Inspector } + +local function tabify(inspector) + puts(inspector.buf, inspector.newline .. rep(inspector.indent, inspector.level)) +end + +function Inspector:getId(v) + local id = self.ids[v] + local ids = self.ids + if not id then + local tv = type(v) + id = (ids[tv] or 0) + 1 + ids[v], ids[tv] = id, id + end + return tostring(id) +end + +function Inspector:putValue(v) + local buf = self.buf + local tv = type(v) + if tv == 'string' then + puts(buf, smartQuote(escape(v))) + elseif tv == 'number' or tv == 'boolean' or tv == 'nil' or + tv == 'cdata' or tv == 'ctype' then + puts(buf, tostring(v)) + elseif tv == 'table' and not self.ids[v] then + local t = v + + if t == inspect.KEY or t == inspect.METATABLE then + puts(buf, tostring(t)) + elseif self.level >= self.depth then + puts(buf, '{...}') + else + if self.cycles[t] > 1 then puts(buf, fmt('<%d>', self:getId(t))) end + + local keys, keysLen, seqLen = getKeys(t) + + puts(buf, '{') + self.level = self.level + 1 + + for i = 1, seqLen + keysLen do + if i > 1 then puts(buf, ',') end + if i <= seqLen then + puts(buf, ' ') + self:putValue(t[i]) + else + local k = keys[i - seqLen] + tabify(self) + if isIdentifier(k) then + puts(buf, k) + else + puts(buf, "[") + self:putValue(k) + puts(buf, "]") + end + puts(buf, ' = ') + self:putValue(t[k]) + end + end + + local mt = getmetatable(t) + if type(mt) == 'table' then + if seqLen + keysLen > 0 then puts(buf, ',') end + tabify(self) + puts(buf, ' = ') + self:putValue(mt) + end + + self.level = self.level - 1 + + if keysLen > 0 or type(mt) == 'table' then + tabify(self) + elseif seqLen > 0 then + puts(buf, ' ') + end + + puts(buf, '}') + end + + else + puts(buf, fmt('<%s %d>', tv, self:getId(v))) + end +end + + + + +function inspect.inspect(root, options) + options = options or {} + + local depth = options.depth or (math.huge) + local newline = options.newline or '\n' + local indent = options.indent or ' ' + local process = options.process + + if process then + root = processRecursive(process, root, {}, {}) + end + + local cycles = {} + countCycles(root, cycles) + + local inspector = setmetatable({ + buf = { n = 0 }, + ids = {}, + cycles = cycles, + depth = depth, + level = 0, + newline = newline, + indent = indent, + }, Inspector_mt) + + inspector:putValue(root) + + return table.concat(inspector.buf) +end + +setmetatable(inspect, { + __call = function(_, root, options) + return inspect.inspect(root, options) + end, +}) + +return inspect diff --git a/common/lib/json.lua b/common/lib/json.lua new file mode 100644 index 0000000..52b9530 --- /dev/null +++ b/common/lib/json.lua @@ -0,0 +1,388 @@ +-- +-- json.lua +-- +-- Copyright (c) 2020 rxi +-- +-- Permission is hereby granted, free of charge, to any person obtaining a copy of +-- this software and associated documentation files (the "Software"), to deal in +-- the Software without restriction, including without limitation the rights to +-- use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies +-- of the Software, and to permit persons to whom the Software is furnished to do +-- so, subject to the following conditions: +-- +-- The above copyright notice and this permission notice shall be included in all +-- copies or substantial portions of the Software. +-- +-- THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +-- IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +-- FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +-- AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +-- LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +-- OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +-- SOFTWARE. +-- + +local json = { _version = "0.1.2" } + +------------------------------------------------------------------------------- +-- Encode +------------------------------------------------------------------------------- + +local encode + +local escape_char_map = { + [ "\\" ] = "\\", + [ "\"" ] = "\"", + [ "\b" ] = "b", + [ "\f" ] = "f", + [ "\n" ] = "n", + [ "\r" ] = "r", + [ "\t" ] = "t", +} + +local escape_char_map_inv = { [ "/" ] = "/" } +for k, v in pairs(escape_char_map) do + escape_char_map_inv[v] = k +end + + +local function escape_char(c) + return "\\" .. (escape_char_map[c] or string.format("u%04x", c:byte())) +end + + +local function encode_nil(val) + return "null" +end + + +local function encode_table(val, stack) + local res = {} + stack = stack or {} + + -- Circular reference? + if stack[val] then error("circular reference") end + + stack[val] = true + + if rawget(val, 1) ~= nil or next(val) == nil then + -- Treat as array -- check keys are valid and it is not sparse + local n = 0 + for k in pairs(val) do + if type(k) ~= "number" then + error("invalid table: mixed or invalid key types") + end + n = n + 1 + end + if n ~= #val then + error("invalid table: sparse array") + end + -- Encode + for i, v in ipairs(val) do + table.insert(res, encode(v, stack)) + end + stack[val] = nil + return "[" .. table.concat(res, ",") .. "]" + + else + -- Treat as an object + for k, v in pairs(val) do + if type(k) ~= "string" then + error("invalid table: mixed or invalid key types") + end + table.insert(res, encode(k, stack) .. ":" .. encode(v, stack)) + end + stack[val] = nil + return "{" .. table.concat(res, ",") .. "}" + end +end + + +local function encode_string(val) + return '"' .. val:gsub('[%z\1-\31\\"]', escape_char) .. '"' +end + + +local function encode_number(val) + -- Check for NaN, -inf and inf + if val ~= val or val <= -math.huge or val >= math.huge then + error("unexpected number value '" .. tostring(val) .. "'") + end + return string.format("%.14g", val) +end + + +local type_func_map = { + [ "nil" ] = encode_nil, + [ "table" ] = encode_table, + [ "string" ] = encode_string, + [ "number" ] = encode_number, + [ "boolean" ] = tostring, +} + + +encode = function(val, stack) + local t = type(val) + local f = type_func_map[t] + if f then + return f(val, stack) + end + error("unexpected type '" .. t .. "'") +end + + +function json.encode(val) + return ( encode(val) ) +end + + +------------------------------------------------------------------------------- +-- Decode +------------------------------------------------------------------------------- + +local parse + +local function create_set(...) + local res = {} + for i = 1, select("#", ...) do + res[ select(i, ...) ] = true + end + return res +end + +local space_chars = create_set(" ", "\t", "\r", "\n") +local delim_chars = create_set(" ", "\t", "\r", "\n", "]", "}", ",") +local escape_chars = create_set("\\", "/", '"', "b", "f", "n", "r", "t", "u") +local literals = create_set("true", "false", "null") + +local literal_map = { + [ "true" ] = true, + [ "false" ] = false, + [ "null" ] = nil, +} + + +local function next_char(str, idx, set, negate) + for i = idx, #str do + if set[str:sub(i, i)] ~= negate then + return i + end + end + return #str + 1 +end + + +local function decode_error(str, idx, msg) + local line_count = 1 + local col_count = 1 + for i = 1, idx - 1 do + col_count = col_count + 1 + if str:sub(i, i) == "\n" then + line_count = line_count + 1 + col_count = 1 + end + end + error( string.format("%s at line %d col %d", msg, line_count, col_count) ) +end + + +local function codepoint_to_utf8(n) + -- http://scripts.sil.org/cms/scripts/page.php?site_id=nrsi&id=iws-appendixa + local f = math.floor + if n <= 0x7f then + return string.char(n) + elseif n <= 0x7ff then + return string.char(f(n / 64) + 192, n % 64 + 128) + elseif n <= 0xffff then + return string.char(f(n / 4096) + 224, f(n % 4096 / 64) + 128, n % 64 + 128) + elseif n <= 0x10ffff then + return string.char(f(n / 262144) + 240, f(n % 262144 / 4096) + 128, + f(n % 4096 / 64) + 128, n % 64 + 128) + end + error( string.format("invalid unicode codepoint '%x'", n) ) +end + + +local function parse_unicode_escape(s) + local n1 = tonumber( s:sub(1, 4), 16 ) + local n2 = tonumber( s:sub(7, 10), 16 ) + -- Surrogate pair? + if n2 then + return codepoint_to_utf8((n1 - 0xd800) * 0x400 + (n2 - 0xdc00) + 0x10000) + else + return codepoint_to_utf8(n1) + end +end + + +local function parse_string(str, i) + local res = "" + local j = i + 1 + local k = j + + while j <= #str do + local x = str:byte(j) + + if x < 32 then + decode_error(str, j, "control character in string") + + elseif x == 92 then -- `\`: Escape + res = res .. str:sub(k, j - 1) + j = j + 1 + local c = str:sub(j, j) + if c == "u" then + local hex = str:match("^[dD][89aAbB]%x%x\\u%x%x%x%x", j + 1) + or str:match("^%x%x%x%x", j + 1) + or decode_error(str, j - 1, "invalid unicode escape in string") + res = res .. parse_unicode_escape(hex) + j = j + #hex + else + if not escape_chars[c] then + decode_error(str, j - 1, "invalid escape char '" .. c .. "' in string") + end + res = res .. escape_char_map_inv[c] + end + k = j + 1 + + elseif x == 34 then -- `"`: End of string + res = res .. str:sub(k, j - 1) + return res, j + 1 + end + + j = j + 1 + end + + decode_error(str, i, "expected closing quote for string") +end + + +local function parse_number(str, i) + local x = next_char(str, i, delim_chars) + local s = str:sub(i, x - 1) + local n = tonumber(s) + if not n then + decode_error(str, i, "invalid number '" .. s .. "'") + end + return n, x +end + + +local function parse_literal(str, i) + local x = next_char(str, i, delim_chars) + local word = str:sub(i, x - 1) + if not literals[word] then + decode_error(str, i, "invalid literal '" .. word .. "'") + end + return literal_map[word], x +end + + +local function parse_array(str, i) + local res = {} + local n = 1 + i = i + 1 + while 1 do + local x + i = next_char(str, i, space_chars, true) + -- Empty / end of array? + if str:sub(i, i) == "]" then + i = i + 1 + break + end + -- Read token + x, i = parse(str, i) + res[n] = x + n = n + 1 + -- Next token + i = next_char(str, i, space_chars, true) + local chr = str:sub(i, i) + i = i + 1 + if chr == "]" then break end + if chr ~= "," then decode_error(str, i, "expected ']' or ','") end + end + return res, i +end + + +local function parse_object(str, i) + local res = {} + i = i + 1 + while 1 do + local key, val + i = next_char(str, i, space_chars, true) + -- Empty / end of object? + if str:sub(i, i) == "}" then + i = i + 1 + break + end + -- Read key + if str:sub(i, i) ~= '"' then + decode_error(str, i, "expected string for key") + end + key, i = parse(str, i) + -- Read ':' delimiter + i = next_char(str, i, space_chars, true) + if str:sub(i, i) ~= ":" then + decode_error(str, i, "expected ':' after key") + end + i = next_char(str, i + 1, space_chars, true) + -- Read value + val, i = parse(str, i) + -- Set + res[key] = val + -- Next token + i = next_char(str, i, space_chars, true) + local chr = str:sub(i, i) + i = i + 1 + if chr == "}" then break end + if chr ~= "," then decode_error(str, i, "expected '}' or ','") end + end + return res, i +end + + +local char_func_map = { + [ '"' ] = parse_string, + [ "0" ] = parse_number, + [ "1" ] = parse_number, + [ "2" ] = parse_number, + [ "3" ] = parse_number, + [ "4" ] = parse_number, + [ "5" ] = parse_number, + [ "6" ] = parse_number, + [ "7" ] = parse_number, + [ "8" ] = parse_number, + [ "9" ] = parse_number, + [ "-" ] = parse_number, + [ "t" ] = parse_literal, + [ "f" ] = parse_literal, + [ "n" ] = parse_literal, + [ "[" ] = parse_array, + [ "{" ] = parse_object, +} + + +parse = function(str, idx) + local chr = str:sub(idx, idx) + local f = char_func_map[chr] + if f then + return f(str, idx) + end + decode_error(str, idx, "unexpected character '" .. chr .. "'") +end + + +function json.decode(str) + if type(str) ~= "string" then + error("expected argument of type string, got " .. type(str)) + end + local res, idx = parse(str, next_char(str, 1, space_chars, true)) + idx = next_char(str, idx, space_chars, true) + --if idx <= #str then + -- decode_error(str, idx, "trailing garbage") + --end + return res, idx +end + + +return json diff --git a/client/physics.lua b/common/physics.lua similarity index 79% rename from client/physics.lua rename to common/physics.lua index 82e1fe0..b60424d 100644 --- a/client/physics.lua +++ b/common/physics.lua @@ -1,16 +1,28 @@ +local physics = {} + +local tiles = loex.tiles local cube = { w = 0.5, h = 0.5, d = 0.5, x = 0, y = 0, z = 0 } local epsilonx, epsilony, epsilonz = 0.0045, 0.002, 0.005 local utils = loex.utils -local min, max = math.min, math.max +local floor, min, max = math.floor, math.min, math.max local expandb, iterb, intersectbb = utils.expandb, utils.iterb, utils.intersectbb -function moveandcollide(world, e, box, dt) +function physics.intersect_point_world(world, x, y, z) + local i, j, k = floor(x), floor(y), floor(z) + return world:tile(i, j, k) > 0 +end + +function physics.moveandcollide(world, e, box, dt) + local bounce = 0 + local dx, dy, dz = e.vx * dt, e.vy * dt, e.vz * dt local x, y, z = e.x, e.y, e.z local w, h, d = box.w, box.h, box.d local onground = false + -- Z MOVEMENT + local collidedz = false local boxz = expandb(lume.clone(box), 0, 0, dz) boxz.x = boxz.x + x boxz.y = boxz.y + y @@ -25,14 +37,17 @@ function moveandcollide(world, e, box, dt) if dz < 0 then dz = min(max(dz, (k + 1 + epsilonz) - (z - h)), 0) onground = true + bounce = (bounce + (tiles.id[t].bounce or 0)) * 0.5 else dz = max(min(dz, (k - epsilonz) - (z + h)), 0) end - e.vz = 0 + collidedz = true end end end + if collidedz then e.vz = -e.vz * bounce end + -- X MOVEMENT local boxx = expandb(lume.clone(box), dx, 0, dz) boxx.x = boxx.x + x boxx.y = boxx.y + y @@ -54,6 +69,7 @@ function moveandcollide(world, e, box, dt) end end + -- Y MOVEMENT local boxy = expandb(lume.clone(box), dx, dy, dz) boxy.x = boxy.x + x boxy.y = boxy.y + y @@ -81,3 +97,4 @@ function moveandcollide(world, e, box, dt) return onground end +return physics diff --git a/common/services/snowball.lua b/common/services/snowball.lua new file mode 100644 index 0000000..3ef501d --- /dev/null +++ b/common/services/snowball.lua @@ -0,0 +1,64 @@ +local physics = loex.physics +local lg = love.graphics + +local snowball = {} + +function snowball.init(g) + g.onupdate:catch(snowball.onupdate) -- client side prediction also + if g.master then + g.socket.onreceive:catch(snowball.onreceive_client, g) + g.ondraw:catch(snowball.ondraw) + end +end + +function snowball.onreceive_client(g, peer, packet) + if packet.type == "entityadd" and packet.entity_type == "snowball" then + print("SNOWBALL ADDED") + snowball.entity(g, packet.id, packet.x, packet.y, packet.z, packet.vx, packet.vy, packet.vz) + end +end + +function snowball.ondraw(g) + local snowball_model = g.gamescreen.snowball_model + local camera = g3d.camera.position + + lg.setMeshCullMode("none") + lg.setColor(1, 1, 1) + + for _, e in pairs(g.world:query("snowball")) do + if e ~= g.gamescreen.player then + local dx, dy, dz = e.x - camera[1], e.y - camera[2], e.z - camera[3] + snowball_model:setTranslation(e.x, e.y, e.z) + snowball_model:setRotation( + 0, + -math.atan2(dz, math.sqrt(dx * dx + dy * dy)), + math.atan2(e.y - camera[2], e.x - camera[1]) + ) + snowball_model:setScale(0.4, 0.4, 0.4) + snowball_model:draw() + end + end +end + +function snowball.onupdate(g, dt) + for _, e in pairs(g.world:query("snowball")) do + e.vz = e.vz - g.gravity * dt + e.x = e.x + e.vx * dt + e.y = e.y + e.vy * dt + e.z = e.z + e.vz * dt + + if g.master == nil and physics.intersect_point_world(g.world, e.x, e.y, e.z) then g.world:tag(e, "destroyed") end + end +end + +function snowball.entity(g, id, x, y, z, vx, vy, vz) + local e = { id = id } + e.x, e.y, e.z = x, y, z + e.vx, e.vy, e.vz = vx, vy, vz + + g.world:insert(e) + g.world:tag(e, "snowball") + return e +end + +return snowball diff --git a/common/signal.lua b/common/signal.lua index c502b09..5aa6320 100644 --- a/common/signal.lua +++ b/common/signal.lua @@ -1,22 +1,40 @@ local signal = {} signal.__index = signal +local signal_handle = {} +signal_handle.__index = signal_handle + +function signal_handle:destroy() self.signal.subs[self.id] = nil end + function signal.new() local new = {} new.subs = {} + new.idc = 0 setmetatable(new, signal) return new end -function signal:catch(handle, ...) table.insert(self.subs, { handle = handle, opt = { ... } }) end +function signal:catch(f, ...) + assert(f) + local handle = { signal = self, id = self.idc } + setmetatable(handle, signal_handle) + self.idc = self.idc + 1 + + self.subs[handle.id] = { f = f, opt = { ... } } + return handle +end function signal:emit(...) - for i = 1, #self.subs do - local sub = self.subs[i] + for id, sub in pairs(self.subs) do if #sub.opt ~= 0 then - sub.handle(unpack(sub.opt), ...) + local args = { ... } + for n = #sub.opt, 1, -1 do + table.insert(args, 1, sub.opt[n]) + end + + sub.f(unpack(args)) else - sub.handle(...) + sub.f(...) end end end diff --git a/common/socket.lua b/common/socket.lua index 1dc93c4..c366aa6 100644 --- a/common/socket.lua +++ b/common/socket.lua @@ -35,6 +35,19 @@ function socket.connect(address) return socket.new(enet) end +function socket.encode(t) + local p + if t.bin then + local bin = t.bin + t.bin = nil + p = json.encode(t) + p = p .. bin + else + p = json.encode(t) + end + return p +end + -- function socket:broadcast(data, channel, mode, dest) -- local dest = dest or self.peers -- for _, peer in pairs(dest) do @@ -42,36 +55,42 @@ end -- end -- end -local function decode(packet) - local bytedata = love.data.newByteData(packet) - local data = ffi.cast("uint8_t *", bytedata:getFFIPointer()) - - assert(data[0] == 91) -- [ - local entry = { {}, {} } - local entryi = 1 - local out = {} - local headersize = nil - - for i = 1, bytedata:getSize() do - local char = string.char(data[i]) - if char == "]" then - headersize = i + 1 - break - elseif char == "=" then - entryi = 2 - elseif char == ";" then - out[table.concat(entry[1])] = table.concat(entry[2]) - entry = { {}, {} } - entryi = 1 - else - table.insert(entry[entryi], char) - end - end - - local binsize = bytedata:getSize() - headersize - if binsize ~= 0 then out.bin = love.data.newDataView(bytedata, headersize, binsize) end - - return out +--local function decode(packet) +-- local bytedata = love.data.newByteData(packet) +-- local data = ffi.cast("uint8_t *", bytedata:getFFIPointer()) +-- +-- assert(data[0] == 91) -- [ +-- local entry = { {}, {} } +-- local entryi = 1 +-- local out = {} +-- local headersize = nil +-- +-- for i = 1, bytedata:getSize() do +-- local char = string.char(data[i]) +-- if char == "]" then +-- headersize = i + 1 +-- break +-- elseif char == "=" then +-- entryi = 2 +-- elseif char == ";" then +-- out[table.concat(entry[1])] = table.concat(entry[2]) +-- entry = { {}, {} } +-- entryi = 1 +-- else +-- table.insert(entry[entryi], char) +-- end +-- end +-- +-- local binsize = bytedata:getSize() - headersize +-- if binsize ~= 0 then out.bin = love.data.newDataView(bytedata, headersize, binsize) end +-- +-- return out +--end +-- +function socket.decode(p) + local t, idx = json.decode(p) + if idx <= #p then t.bin = string.sub(p, idx, #p) end + return t end function socket:peerdata(peer) return self.peerdatas[peer:index()] end @@ -83,7 +102,7 @@ function socket:service() local success, result = pcall(function() if event.type == "receive" then - local packet = decode(event.data) + local packet = socket.decode(event.data) self.onreceive:emit(event.peer, packet) elseif event.type == "connect" then self.peerdatas[peerid] = {} diff --git a/common/tiles.lua b/common/tiles.lua index 7ec5b7d..88c5bbf 100644 --- a/common/tiles.lua +++ b/common/tiles.lua @@ -9,6 +9,7 @@ tiles.tiles = { bricks = { id = 5, tex = 3 }, leaves = { id = 6, tex = 16 }, log = { id = 7, tex = { 22, 22, 21, 21, 21, 21 } }, + slime = { id = 8, tex = 5, bounce = 0.8 }, } local id = {} diff --git a/common/world.lua b/common/world.lua index e19ab23..ea30126 100644 --- a/common/world.lua +++ b/common/world.lua @@ -9,7 +9,9 @@ world.__index = world function world.new() local new = {} new.chunks = {} + new.entities = {} + new.tagtables = {} new.ontilemodified = loex.signal.new() new.onentityinserted = loex.signal.new() @@ -28,34 +30,62 @@ function world:insert(e) self.onentityinserted:emit(e) end +function world:tag(e, tag) + local tagtable = self.tagtables[tag] + if not tagtable then + tagtable = {} + self.tagtables[tag] = tagtable + end + tagtable[e.id] = e + + return tags +end + +function world:untag(e, tag) + local tagtable = self.tagtables[tag] + if tagtable then tagtable[e.id] = nil end +end + +function world:tagged(e, tag) + local tagtable = self.tagtables[tag] + return tagtable and tagtable[e.id] +end + function world:remove(e) - assert(e) - local id - if type(e) == "table" then - id = e.id - else - id = e - e = self.entities[id] + assert(self.entities[e.id], "entity does not exist") + local e = self.entities[e.id] + self.entities[e.id] = nil + + for _, tagtable in pairs(self.tagtables) do + tagtable[e.id] = nil end - assert(self.entities[id]) - self.entities[id] = nil self.onentityremoved:emit(e) end +local function intersect(a, b) + local t = {} + if a == nil or b == nil then return t end + + for k, _ in pairs(a) do + if b[k] ~= nil then t[k] = true end + end + return t +end + function world:query(...) - local res = {} local tags = { ... } - for _, e in pairs(self.entities) do - local hastags = true - for _, tag in ipairs(tags) do - hastags = hastags and e:has(tag) - end - if hastags then insert(res, e) end + if #tags == 0 then return self.entities end + + local query = self.tagtables[tags[1]] + + for i = 2, #tags do + query = intersect(query, self.tagtables[tags[i]]) end - return res + return query or {} end function world:entity(id) return self.entities[id] end + function world:chunk(hash) return self.chunks[hash] end function world:insertchunk(c) diff --git a/icon.svg b/icon.svg new file mode 100644 index 0000000..f1c440a --- /dev/null +++ b/icon.svg @@ -0,0 +1,504 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Ö + + V + + X + + + diff --git a/server/game.lua b/server/game.lua new file mode 100644 index 0000000..6be4523 --- /dev/null +++ b/server/game.lua @@ -0,0 +1,52 @@ +local gen = require("gen") +local overworld = require("gen.overworld") + +local floor = math.floor +local tiles = loex.tiles +local size = loex.chunk.size + +local game = {} + +function game.new() + local new = {} + setmetatable(new, { __index = game }) + return new +end + +function game:init(socket) + self.socket = socket + + self.world = loex.world.new() + self.world.onentityinserted:catch(self.world_onentityinserted, self) + self.world.onentityremoved:catch(self.world_onentityremoved, self) + + self.genstate = gen.state.new(overworld.layers, 43242) + + self.gravity = 42 + + self.onupdate = loex.signal.new() + self.onshutdown = loex.signal.new() + + require("services.connection_manager").init(self) + require("services.player").init(self) + require("services.sync").init(self) + require("common.services.snowball").init(self) +end + +function game:update(dt) + self.socket:service() + + self.onupdate:emit(self, dt) +end + +function game:world_onentityinserted(e) print(e.id .. " added") end + +function game:world_onentityremoved(e) print(e.id .. " removed") end + +function game:shutdown() + print("shutting down...") + self.socket:disconnect() + self.onshutdown:emit(self) +end + +return game diff --git a/server/main.lua b/server/main.lua index 857a699..3a8514b 100644 --- a/server/main.lua +++ b/server/main.lua @@ -1,30 +1,15 @@ if arg[#arg] == "vsc_debug" then require("lldebugger").start() end package.path = package.path .. ";?/init.lua" +require("common") -IS_SERVER = true +-- IS_SERVER = true CHANNEL_ONE = 0 CHANNEL_CHUNKS = 1 CHANNEL_EVENTS = 3 CHANNEL_UPDATES = 4 -common = require("common") -packets = require("packets") -remote = require("remote") -gen = require("gen") -local player = require("player") - -banlist = {} -takenusernames = {} - -local socket -local world -local genstate - -local floor = math.floor -local tiles = loex.tiles -local size = loex.chunk.size -local overworld = require("gen.overworld") +local game function love.load(args) if #args < 1 then @@ -32,174 +17,14 @@ function love.load(args) return love.event.quit(-1) end - port = tonumber(args[1]) + local port = tonumber(args[1]) print("starting server on port " .. tostring(port) .. "...") + local socket = loex.socket.host(port, 64) - socket = loex.socket.host(port, 64) - socket.onconnect:catch(onconnect) - socket.ondisconnect:catch(ondisconnect) - socket.onreceive:catch(onreceive) - - world = loex.world.new() - world.onentityinserted:catch(world_onentityinserted) - world.onentityremoved:catch(world_onentityremoved) - world.ontilemodified:catch(world_ontilemodified) - - genstate = gen.state.new(overworld.layers, 43242) -end - -function world_onentityinserted(e) print(e.id .. " added") end - -function world_onentityremoved(e) - print(e.id .. " removed") - - for _, p in ipairs(world:query("player")) do - if p.view:entity(e.id) then p.view:remove(e.id) end - end + game = require("game").new() + game:init(socket) end -function world_ontilemodified(x, y, z, t) - local packet - if t == loex.tiles.air.id then - packet = packets.broken(x, y, z) - else - packet = packets.placed(x, y, z, t) - end - - for _, p in ipairs(world:query("player")) do - if p.view:tile(x, y, z) >= 0 then p.master:send(packet) end - end -end - -function onconnect(peer) print("Connected!") end - -function ondisconnect(peer) - local peerdata = socket:peerdata(peer) - if peerdata.playerentity then - print(peerdata.playerentity.username .. " left the game :<") - world:remove(peerdata.playerentity) - takenusernames[peerdata.playerentity.username] = nil - end -end - -function handle_player_packet(player, packet) - local handles = { - ["move"] = function(p, d) - local x, y, z = tonumber(d.x), tonumber(d.y), tonumber(d.z) - p.x = x - p.y = y - p.z = z - end, - ["place"] = function(p, d) - local x, y, z, t = tonumber(d.x), tonumber(d.y), tonumber(d.z), tonumber(d.t) - world:tile(x, y, z, t) - end, - ["breaktile"] = function(p, d) - local x, y, z = tonumber(d.x), tonumber(d.y), tonumber(d.z) - world:tile(x, y, z, tiles.air.id) - end, - } - handles[packet.type](player, packet) -end - -function broadcast(packet) - for _, e in pairs(world.entities) do - if e:has("player") then e.master:send(packet) end - end -end +function love.update(dt) game:update(dt) end -function onreceive(peer, packet) - print("Received " .. packet.type) - local peerdata = socket:peerdata(peer) - - if peerdata.playerentity == nil then - if packet.type == "join" then - local err = verify(packet.username) - if err then - peer:send(packets.joinfailure(err), CHANNEL_ONE) - peer:disconnect_later() - return - end - local p = player.entity(0, 0, 180, nil, packet.username, peer) - - peer:send(packets.joinsuccess(p.id, p.x, p.y, p.z), CHANNEL_ONE) - - world:insert(p) - peerdata.playerentity = p - - takenusernames[p.username] = true - - print(p.username .. " joined the game :>") - else - error("invalid packet for ghost peer") - end - else - handle_player_packet(peerdata.playerentity, packet) - end -end - -function love.update(dt) - socket:service() - local gendistance = 5 - for _, e in pairs(world.entities) do - if e:has("remote") then - e.remote.x = e.x - e.remote.y = e.y - e.remote.z = e.z - end - - if e:has("player") then - for i = -gendistance + floor(e.x / size), gendistance + floor(e.x / size) do - for j = -gendistance + floor(e.y / size), gendistance + floor(e.y / size) do - for k = 0, overworld.columnheight - 1 do - if not world:chunk(loex.hash.spatial(i, j, k)) then - local c = overworld:generate(genstate, i, j, k) - world:insertchunk(c) - end - end - end - end - - for _, c in pairs(world.chunks) do - if not player.inview(e, c.x * size, c.y * size, c.z * size) then - if e.view.chunks[c.hash] then e.view:removechunk(c.hash) end - else - if not e.view.chunks[c.hash] then e.view:insertchunk(c) end - end - end - for _, entity in pairs(world.entities) do - if not player.inview(e, entity.x, entity.y, entity.z) then - if e.view:entity(entity.id) then e.view:remove(entity) end - else - if not e.view:entity(entity.id) then e.view:insert(entity) end - end - end - end - end - - for _, e in pairs(world:query("remote")) do - for property, _ in pairs(e.remote.edits) do - local packet = packets.entityremoteset(e.id, property, e.remote[property]) - e.remote.edits[property] = nil - for _, p in ipairs(world:query("player")) do - if p.view:entity(e.id) then p.master:send(packet) end - end - end - end -end - -function love.quit() socket:disconnect() end - -function verify(username) - local validUsername = "^[a-zA-Z_]+$" - - if banlist[username] then return "You're banned from this server. Cry about it :-(" end - - if #username < 3 then return "Username too short" end - - if #username > 15 then return "Username too long" end - - if not username:match(validUsername) then return "Invalid username (It has to be good)" end - - if takenusernames[username] then return "Username already taken. Try again :)" end -end +function love.quit() game:shutdown() end diff --git a/server/packets.lua b/server/packets.lua index 85a7c61..4fc299f 100644 --- a/server/packets.lua +++ b/server/packets.lua @@ -1,29 +1,47 @@ local packets = {} +local encode = loex.socket.encode + +function packets.joinsuccess(id, x, y, z) + return encode { + type = "joinsuccess", + x = x, + y = y, + z = z, + id = id, + } +end -function packets.joinsuccess(id, x, y, z) return ("[type=joinsuccess;x=%d;y=%d;z=%d;id=%s;]"):format(x, y, z, id) end - -function packets.joinfailure(cause) return ("[type=joinfailure;cause=%s;]"):format(cause) end - -function packets.chunkadd(tiles, cx, cy, cz) - return table.concat { ("[type=chunkadd;cx=%d;cy=%d;cz=%d;]"):format(cx, cy, cz), tiles } +function packets.joinfailure(cause) + return encode { + type = "joinfailure", + cause = cause, + } end -function packets.chunkremove(cx, cy, cz) return ("[type=chunkremove;cx=%d;cy=%d;cz=%d;]"):format(cx, cy, cz) end +function packets.chunkadd(tiles, cx, cy, cz) return encode { type = "chunkadd", cx = cx, cy = cy, cz = cz, bin = tiles } end -function packets.placed(x, y, z, t) return ("[type=placed;x=%d;y=%d;z=%d;t=%d;]"):format(x, y, z, t) end +function packets.chunkremove(cx, cy, cz) return encode { type = "chunkremove", cx = cx, cy = cy, cz = cz } end -function packets.broken(x, y, z) return ("[type=broken;x=%d;y=%d;z=%d;]"):format(x, y, z) end +function packets.placed(x, y, z, t) return encode { type = "placed", x = x, y = y, z = z, t = t } end -function packets.entityadd(id, x, y, z) return ("[type=entityadd;x=%f;y=%f;z=%f;id=%s;]"):format(x, y, z, id) end +function packets.broken(x, y, z) return encode { type = "broken", x = x, y = y, z = z } end -function packets.entityremove(id) return ("[type=entityremove;id=%s;]"):format(id) end +function packets.entityadd(id) return encode { type = "entityadd", id = id } end + +function packets.entityremove(id) -- TODO: get rid of this packet + return encode { type = "entityremove", id = id } +end -- function packets.entitymove(id, x, y, z) -- return ("[type=entitymove;x=%f;y=%f;z=%f;id=%s;]"):format(x, y, z, id) -- end -function packets.entityremoteset(id, k, v) - return ("[type=entityremoteset;id=%s;property=%s;value=%f;]"):format(id, k, v) +function packets.entityremoteset(id, properties) + return encode { + type = "entityremoteset", + id = id, + properties = properties, + } end return packets diff --git a/server/player.lua b/server/player.lua deleted file mode 100644 index ed9ab8b..0000000 --- a/server/player.lua +++ /dev/null @@ -1,34 +0,0 @@ -local player = {} -local floor = math.floor -local size = loex.chunk.size -local remote = require("remote") - -function player.view_onchunkinserted(e, c) e.master:send(packets.chunkadd(c:dump(), c.x, c.y, c.z)) end - -function player.view_onchunkremoved(e, c) e.master:send(packets.chunkremove(c.x, c.y, c.z)) end - -function player.view_onentityinserted(p, e) p.master:send(packets.entityadd(e.id, e.x, e.y, e.z)) end - -function player.view_onentityremoved(p, e) p.master:send(packets.entityremove(e.id)) end - -function player.entity(x, y, z, id, username, master) - local new = remote(x, y, z, id) - new:tag("player") - new.username = username - new.master = master - new.view = loex.world.new() - new.view.onchunkinserted:catch(player.view_onchunkinserted, new) - new.view.onchunkremoved:catch(player.view_onchunkremoved, new) - new.view.onentityinserted:catch(player.view_onentityinserted, new) - new.view.onentityremoved:catch(player.view_onentityremoved, new) - return new -end - -function player.inview(e, x, y, z) - local range = 12 -- chunks - x, y, z = floor(x / size), floor(y / size), floor(z / size) - local px, py, pz = floor(e.x / size), floor(e.y / size), floor(e.z / size) - return loex.utils.distance3d(px, py, 0, x, y, 0, true) <= range * range -end - -return player diff --git a/server/remote.lua b/server/remote.lua deleted file mode 100644 index 12a24eb..0000000 --- a/server/remote.lua +++ /dev/null @@ -1,17 +0,0 @@ -local remote_mt = { - __index = function(t, k) return t.inner[k] end, - __newindex = function(t, k, v) - if t.inner[k] ~= v then - t.inner[k] = v - t.edits[k] = true - end - end, -} - -return function(x, y, z, id) - local e = loex.entity.new(x, y, z, id) - e:tag("remote") - e.remote = { inner = {}, edits = {} } - setmetatable(e.remote, remote_mt) - return e -end diff --git a/server/services/connection_manager.lua b/server/services/connection_manager.lua new file mode 100644 index 0000000..7382019 --- /dev/null +++ b/server/services/connection_manager.lua @@ -0,0 +1,75 @@ +local player = require("services.player") +local packets = require("packets") + +local connection_manager = {} + +function connection_manager.verify_username(g, username) + local validUsername = "^[a-zA-Z_]+$" + + if g.banlist[username] then return "You're banned from this server. Cry about it :-(" end + + if #username < 3 then return "Username too short" end + + if #username > 15 then return "Username too long" end + + if not username:match(validUsername) then return "Invalid username (It has to be good)" end + + if g.taken_usernames[username] then return "Username already taken. Try again :)" end +end + +function connection_manager.init(g) + g.banlist = {} -- TODO: load from list + g.taken_usernames = {} + + g.socket.onconnect:catch(connection_manager.socket_onconnect, g) + g.socket.ondisconnect:catch(connection_manager.socket_ondisconnect, g) + g.socket.onreceive:catch(connection_manager.socket_onreceive, g) +end + +function connection_manager.socket_onconnect(g, peer) print("new connection!") end + +function connection_manager.socket_ondisconnect(g, peer) + local socket = g.socket + + local peerdata = socket:peerdata(peer) + if peerdata.playerentity then + print(peerdata.playerentity.username .. " left the game :<") + g.world:tag(peerdata.playerentity, "destroyed") + g.taken_usernames[peerdata.playerentity.username] = nil + end +end + +function connection_manager.socket_onreceive(g, peer, packet) + print("received " .. packet.type) + + local socket = g.socket + local world = g.world + + local peerdata = socket:peerdata(peer) + + if peerdata.playerentity == nil then + if packet.type == "join" then + local err = connection_manager.verify_username(g, packet.username) + if err then + peer:send(packets.joinfailure(err), CHANNEL_ONE) + peer:disconnect_later() + return + end + + -- spawn player + local p = player.entity(g, world, lume.uuid(), 0, 0, 180, packet.username, peer) + + peer:send(packets.joinsuccess(p.id, p.x, p.y, p.z), CHANNEL_ONE) + + peerdata.playerentity = p + + g.taken_usernames[p.username] = true + + print(p.username .. " joined the game :>") + else + error("invalid packet for ghost peer") + end + end +end + +return connection_manager diff --git a/server/services/player.lua b/server/services/player.lua new file mode 100644 index 0000000..73d0bcf --- /dev/null +++ b/server/services/player.lua @@ -0,0 +1,205 @@ +local overworld = require("gen.overworld") +local packets = require("packets") +local snowball = require("common.services.snowball") + +local socket = loex.socket +local size = loex.chunk.size +local entity = loex.entity +local floor = math.floor + +local player = {} + +function player.init(g) + assert(g.master == nil) + g.onupdate:catch(player.update) + g.socket.onreceive:catch(player.socket_onreceive, g) + g.world.ontilemodified:catch(player.world_ontilemodified, g) + g.world.onentityremoved:catch(player.world_onentityremoved, g) +end + +function player.view_onchunkinserted(g, p, c) p.master:send(packets.chunkadd(c:dump(), c.x, c.y, c.z)) end + +function player.view_onchunkremoved(g, p, c) p.master:send(packets.chunkremove(c.x, c.y, c.z)) end + +function player.view_onentityinserted(g, p, e) + -- TODO: need abstraction for this + if g.world:tagged(e, "player") then + p.master:send(socket.encode { + type = "entityadd", + id = e.id, + x = e.x, + y = e.y, + z = e.z, + username = e.username, + entity_type = "player", + }) + elseif g.world:tagged(e, "snowball") then + p.master:send(socket.encode { + type = "entityadd", + id = e.id, + x = e.x, + y = e.y, + z = e.z, + vx = e.vx, + vy = e.vy, + vz = e.vz, + entity_type = "snowball", + }) + end +end + +function player.view_onentityremoved(g, p, e) p.master:send(packets.entityremove(e.id)) end + +function player.entity(g, w, id, x, y, z, username, master) + local e = { id = id } + + e.last_throw = os.time() + e.x, e.y, e.z = x, y, z + e.vx, e.vy, e.vz = 0, 0, 0 + e.box = { + x = 0, + y = 0, + z = 0, + w = 0.3, + d = 0.3, + h = 0.9, + } + e.username = username + e.master = master + + e.view = loex.world.new() + e.view.onchunkinserted:catch(player.view_onchunkinserted, g, e) + e.view.onchunkremoved:catch(player.view_onchunkremoved, g, e) + e.view.onentityinserted:catch(player.view_onentityinserted, g, e) + e.view.onentityremoved:catch(player.view_onentityremoved, g, e) + + e.remote = {} + + w:insert(e) + w:tag(e, "box") + w:tag(e, "player") + + return e +end + +function player.inview(e, x, y, z) + local range = 12 -- chunks + x, y, z = floor(x / size), floor(y / size), floor(z / size) + local px, py, pz = floor(e.x / size), floor(e.y / size), floor(e.z / size) + return loex.utils.distance3d(px, py, 0, x, y, 0, true) <= range * range +end + +function player.socket_onreceive(g, peer, d) + if d.type == "join" then return end -- FIXME + local p = g.socket:peerdata(peer).playerentity + if not p then return end + + local world = g.world + + local handles = { + ["move"] = function(p, d) + local x, y, z = tonumber(d.x), tonumber(d.y), tonumber(d.z) + p.x = x + p.y = y + p.z = z + end, + ["place"] = function(p, d) + local x, y, z, t = tonumber(d.x), tonumber(d.y), tonumber(d.z), tonumber(d.t) + world:tile(x, y, z, t) + end, + ["breaktile"] = function(p, d) + local x, y, z = tonumber(d.x), tonumber(d.y), tonumber(d.z) + world:tile(x, y, z, loex.tiles.air.id) + end, + ["snowball_throw"] = function(p, d) + -- throw snowballs + --if os.time() - p.last_throw > 0.05 then + print("thrown snowball") + p.last_throw = os.time() + + local e = snowball.entity(g, lume.uuid(), d.x, d.y, d.z, d.vx, d.vy, d.vz) + e.remote = {} + --end + end, + } + handles[d.type](p, d) +end + +function player.world_onentityremoved(g, e) + local world = g.world + for _, p in pairs(world:query("player")) do + if p.view:entity(e) then p.view:remove(e) end + end +end + +function player.world_ontilemodified(g, x, y, z, t) + local world = g.world + local packet + if t == loex.tiles.air.id then + packet = packets.broken(x, y, z) + else + packet = packets.placed(x, y, z, t) + end + + for _, p in pairs(world:query("player")) do + if p.view:tile(x, y, z) >= 0 then p.master:send(packet) end + end +end + +function player.update(g, dt) + local world = g.world + local genstate = g.genstate + local gendistance = 5 + + for _, p in pairs(g.world:query("player")) do + -- generate world + for i = -gendistance + floor(p.x / size), gendistance + floor(p.x / size) do + for j = -gendistance + floor(p.y / size), gendistance + floor(p.y / size) do + for k = 0, overworld.columnheight - 1 do + if not world:chunk(loex.hash.spatial(i, j, k)) then + local c = overworld:generate(genstate, i, j, k) + world:insertchunk(c) + end + end + end + end + + -- compute chunks in player view + for _, c in pairs(world.chunks) do + if not player.inview(p, c.x * size, c.y * size, c.z * size) then + if p.view.chunks[c.hash] then p.view:removechunk(c.hash) end + else + if not p.view.chunks[c.hash] then p.view:insertchunk(c) end + end + end + + -- compute entities in player view + for _, e in pairs(world.entities) do + if e ~= p then + if not player.inview(p, e.x, e.y, e.z) then + if p.view:entity(e.id) then p.view:remove(e) end + else + if not p.view:entity(e.id) then + p.view:insert(e) + else + -- TODO: this is bad, really bad + if not (e.remote.x == e.x and e.remote.y == e.y and e.remote.z == e.z) then + p.master:send(socket.encode { + type = "entityremoteset", + id = e.id, + properties = { x = e.x, y = e.y, z = e.z }, + }) + end + end + end + end + end + end + for _, e in pairs(world.entities) do + if e.remote then + e.remote.x, e.remote.y, e.remote.z = e.x, e.y, e.z + end + end +end + +return player diff --git a/server/services/sync.lua b/server/services/sync.lua new file mode 100644 index 0000000..fa09986 --- /dev/null +++ b/server/services/sync.lua @@ -0,0 +1,41 @@ +local sync = {} +local packets = require("packets") + +function sync.init(g) g.onupdate:catch(sync.update) end + +function sync.update(g, dt) + local world = g.world + local socket = g.socket + + local genstate = g.genstate + + for _, e in pairs(world.entities) do + -- -- sync edits + -- local p_edits, p_edits_count = e:property_edits() + -- local t_edits, t_edits_count = e:tag_edits() + -- + -- if p_edits_count > 0 or t_edits_count > 0 then + -- local packet = packets.entityremoteset(e.id, p_edits, t_edits) + -- e:clear_tag_edits() + -- e:clear_property_edits() + -- + -- for _, p in ipairs(world:query("player")) do + -- if p.view:entity(e.id) then p.master:send(packet) end + -- end + -- end + + -- sync destroys + if g.world:tagged(e, "destroyed") then + for _, p in pairs(world:query("player")) do + if p.view:entity(e.id) then p.view:remove(e) end + end + end + end + + -- remove destroyed entities + for _, e in pairs(world:query("destroyed")) do + world:remove(e) + end +end + +return sync