summaryrefslogtreecommitdiff
path: root/server.lua
blob: 9cda5a49f7deea5e9311badc06c6d02879eb0bfb (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
local enet = require("enet")
local socket = require("socket")
local util = require("util")
local common = require("common")
local save_file = require("save_file")

local server = {}

local function migrate_save(save)
    save.players = save.players or {}
end

local function save_data(srv)
    -- TODO: handle failure
    save_file.write(srv.save_file, srv.data)
end

local function get_player(srv, name)
    for _, player in ipairs(srv.data.players) do
        if player.name == name then
            return player
        end
    end
end

local function get_players(srv)
    local players = {}
    for _, player in ipairs(srv.data.players) do
        table.insert(players, {
            name = player.name,
            active = srv.players[player.name] ~= nil,
        })
    end
    return players
end

local function get_info_pkt(srv)
    return util.json_enc({
        type = "client_info",
        players = get_players(srv),
    })
end

local function broadcast_info(srv)
    local pkt = get_info_pkt(srv)
    for _, clt in pairs(srv.clients) do
        clt.peer:send(pkt)
    end
end

local function create_player(srv, name)
    local player = { name = name }
    table.insert(srv.data.players, player)
    print("[server] created player " .. name)
    save_data(srv)
    return player
end

local function disconnect(srv, clt)
    srv.clients[clt.peer] = nil
    if clt.player then
        srv.players[clt.player.name] = nil
    end
    broadcast_info(srv)
end

local function select_player(srv, clt, pkt)
    if #pkt.name > 128 then
        return "name_too_long"
    end
    if srv.players[pkt.name] then
        return nil, "already_active"
    end
    local player = get_player(srv, pkt.name)
    if pkt.create and player then
        return nil, "already_exists"
    end
    if not pkt.create and not player then
        return nil, "not_exists"
    end

    if pkt.create then
        player = create_player(srv, pkt.name)
    end
    return player
end

function server.create(filename, match_addr)
    local srv = {}

    srv.host = enet.host_create()
    srv.secret = util.rand_string(common.secret_len)
    srv.clients = {}
    srv.players = {}

    srv.match = srv.host:connect(match_addr or common.default_match_addr)
    srv.match_req = socket.gettime()

    local save, err = save_file.read(filename)
    if err then
        return nil, err
    end
    srv.save_file = filename
    srv.data = save
    migrate_save(save)

    return srv
end

local function handle_match(srv, pkt)
    if pkt.type == "server_match" then
        local game_id = type(pkt.game_id) == "string" and util.base64_dec(pkt.game_id)
        if not game_id then
            print("[server] server_match: invalid game_id")
            return
        end

        if srv.game_id then
            print("[server] server_match: received while game already running")
            return
        end

        srv.game_id = game_id
        srv.invite = util.base64_enc(srv.game_id .. srv.secret)
    elseif pkt.type == "server_join" then
        if type(pkt.peer_addr) ~= "string" then
            print("[server] server_join: invalid peer_addr")
            return
        end
        srv.host:connect(pkt.peer_addr)
    end
end

local function handle_client(srv, peer, pkt)
    if pkt.type == "server_hi" then
        local secret = type(pkt.secret) == "string" and util.base64_dec(pkt.secret)
        if not secret then
            print("[server] server_hi: invalid secret")
            return
        end

        if srv.clients[peer] then
            print("[server] server_hi: client already connected")
            return
        end

        if secret == srv.secret then
            print("[server] auth success " .. tostring(peer))
            local clt = { peer = peer }
            srv.clients[peer] = clt
            util.send(peer, {
                type = "client_hi",
            })
            peer:send(get_info_pkt(srv))
        else
            print("[server] auth failure " .. tostring(peer))
            util.send(peer, { type = "client_reject" })
            peer:disconnect_later()
       end
    end

    local clt = srv.clients[peer]
    if not clt then
        print("[server] dropping unauthenicated packet from " .. tostring(peer))
        return
    end

    if pkt.type == "server_player" then
        if clt.player then
            print("[server] dropping server_player from already authenticated player")
            return
        end

        if type(pkt.name) ~= "string" or type(pkt.create) ~= "boolean" then
            print("[server] server_player: invalid packet")
            return
        end

        local player, err = select_player(srv, clt, pkt)
        if err then
            print("[server] failed to select player " .. tostring(clt.peer))
            util.send(clt.peer, {
                type = "client_player_fail",
                error = err,
            })
        else
            print("[server] select player " .. tostring(clt.peer) .. ": " .. player.name)
            srv.players[player.name] = clt
            clt.player = player
            util.send(clt.peer, {
                type = "client_player",
                name = player.name,
            })
            broadcast_info(srv)
        end
    end
end

function server.update(srv)
    local event = srv.host:service(20)
    while event do
        if event.type == "receive" then
            local pkt = util.json_dec(event.data)
            if pkt then
                if event.peer == srv.match then
                    handle_match(srv, pkt)
                else
                    handle_client(srv, event.peer, pkt)
                end
            end
        elseif event.type == "connect" then
            if event.peer == srv.match then
                util.send(srv.match, { type = "match_register" })
            end
            print("[server] connect " .. tostring(event.peer))
        elseif event.type == "disconnect" then
            print("[server] disconnect " .. tostring(event.peer))
            if event.peer == srv.match then
                -- TODO
            else
                local clt = srv.clients[event.peer]
                if clt then
                    disconnect(srv, clt)
                end
            end
        end
        event = srv.host:service()
    end
end

function server.match_status(srv)
    if srv.game_id then
        return "active", srv.invite
    elseif srv.match_req + 3 >= socket.gettime() then
        return "wait"
    else
        return "fail"
    end
end

function server.close(srv)
    save_data(srv)
    local peers = srv.host:peer_count()
    for i = 1, peers do
        srv.host:get_peer(i):disconnect()
    end
    srv.host:flush()
    srv.host:destroy()
end

return server