Tangent 4 years ago
commit
d54a271f55
4 changed files with 1218 additions and 0 deletions
  1. 60
    0
      notes.txt
  2. 400
    0
      src/json.lua
  3. 737
    0
      src/lovebird.lua
  4. 21
    0
      src/main.moon

+ 60
- 0
notes.txt View File

@@ -0,0 +1,60 @@
1
+gold
2
+hp
3
+level
4
+strength
5
+armor
6
+xp
7
+
8
+rooms with period flooring, +/- doors, and hallways?
9
+- more like original
10
+kobolds as a basic dumb enemy
11
+i inventory listing, somewhere to display last x messages about events
12
+
13
+Amulet of Yendor: goal, lowest level
14
+"Rodney" spelled backwards, which they envisioned as renowned wizard
15
+weapons, armor, potions, scrolls, and other magical items
16
+q to quaff a potion, w to wield a weapon, e to eat some food
17
+scoreboard based on time survived + depth traveled + what found/have
18
+☺
19
+
20
+alt idea: text descriptions in natural language of Adventure/Colossal Cave and
21
+  the original Zork games
22
+
23
+grid of three rooms by three rooms (potentially); dead end hallways sometimes
24
+appear where rooms would be expected. Lower levels can also include a maze in
25
+the place of a room
26
+
27
+- https://en.wikipedia.org/wiki/Code_page_437
28
+- https://en.wikipedia.org/wiki/Hack_(video_game)
29
+- https://en.wikipedia.org/wiki/NetHack
30
+- https://en.wikipedia.org/wiki/Moria_(video_game)
31
+- https://en.wikipedia.org/wiki/Angband_(video_game)
32
+- https://en.wikipedia.org/wiki/Roguelike
33
+- https://en.wikipedia.org/wiki/Dungeon_crawl
34
+- https://www.reddit.com/r/roguelikes/
35
+- http://roguebasin.com/index.php?title=Main_Page
36
+- http://roguebasin.com/index.php?title=Roguelike_Dev_FAQ
37
+- http://roguebasin.com/index.php?title=How_to_Write_a_Roguelike_in_15_Steps
38
+- http://roguebasin.com/index.php?title=Articles
39
+
40
+spells that can take effect after a delay
41
+lasting enchantments or permanent effects
42
+one-use items vs continually useful
43
+generation of dungeon
44
+idea: games playable within each others...
45
+terrain, wilderness, dungeons, etc
46
+scheduling: how turns / speed work, whether actions can be interrupted or not
47
+food requirement? items, etc
48
+item types, effects, uses
49
+spell costs
50
+shopping, theft, crafting
51
+interactions besides fighting
52
+classes, skills, complexity and freedom
53
+simple controls, easy to start, hard to master
54
+books! guides! writing!
55
+quests, jobs, daily cycles
56
+
57
+unlocking abilities through picking up items hmmm
58
+deleting, re-generating vessels ? save/load ??
59
+
60
+You are trapped in an out-of-control computer, and it's making deadly monsters. The goal is to stay alive and find a way out.

+ 400
- 0
src/json.lua View File

@@ -0,0 +1,400 @@
1
+--
2
+-- json.lua
3
+--
4
+-- Copyright (c) 2019 rxi
5
+--
6
+-- Permission is hereby granted, free of charge, to any person obtaining a copy of
7
+-- this software and associated documentation files (the "Software"), to deal in
8
+-- the Software without restriction, including without limitation the rights to
9
+-- use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
10
+-- of the Software, and to permit persons to whom the Software is furnished to do
11
+-- so, subject to the following conditions:
12
+--
13
+-- The above copyright notice and this permission notice shall be included in all
14
+-- copies or substantial portions of the Software.
15
+--
16
+-- THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
17
+-- IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
18
+-- FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
19
+-- AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
20
+-- LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
21
+-- OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
22
+-- SOFTWARE.
23
+--
24
+
25
+local json = { _version = "0.1.2" }
26
+
27
+-------------------------------------------------------------------------------
28
+-- Encode
29
+-------------------------------------------------------------------------------
30
+
31
+local encode
32
+
33
+local escape_char_map = {
34
+  [ "\\" ] = "\\\\",
35
+  [ "\"" ] = "\\\"",
36
+  [ "\b" ] = "\\b",
37
+  [ "\f" ] = "\\f",
38
+  [ "\n" ] = "\\n",
39
+  [ "\r" ] = "\\r",
40
+  [ "\t" ] = "\\t",
41
+}
42
+
43
+local escape_char_map_inv = { [ "\\/" ] = "/" }
44
+for k, v in pairs(escape_char_map) do
45
+  escape_char_map_inv[v] = k
46
+end
47
+
48
+
49
+local function escape_char(c)
50
+  return escape_char_map[c] or string.format("\\u%04x", c:byte())
51
+end
52
+
53
+
54
+local function encode_nil(val)
55
+  return "null"
56
+end
57
+
58
+
59
+local function encode_table(val, stack)
60
+  local res = {}
61
+  stack = stack or {}
62
+
63
+  -- Circular reference?
64
+  if stack[val] then error("circular reference") end
65
+
66
+  stack[val] = true
67
+
68
+  if rawget(val, 1) ~= nil or next(val) == nil then
69
+    -- Treat as array -- check keys are valid and it is not sparse
70
+    local n = 0
71
+    for k in pairs(val) do
72
+      if type(k) ~= "number" then
73
+        error("invalid table: mixed or invalid key types")
74
+      end
75
+      n = n + 1
76
+    end
77
+    if n ~= #val then
78
+      error("invalid table: sparse array")
79
+    end
80
+    -- Encode
81
+    for i, v in ipairs(val) do
82
+      table.insert(res, encode(v, stack))
83
+    end
84
+    stack[val] = nil
85
+    return "[" .. table.concat(res, ",") .. "]"
86
+
87
+  else
88
+    -- Treat as an object
89
+    for k, v in pairs(val) do
90
+      if type(k) ~= "string" then
91
+        error("invalid table: mixed or invalid key types")
92
+      end
93
+      table.insert(res, encode(k, stack) .. ":" .. encode(v, stack))
94
+    end
95
+    stack[val] = nil
96
+    return "{" .. table.concat(res, ",") .. "}"
97
+  end
98
+end
99
+
100
+
101
+local function encode_string(val)
102
+  return '"' .. val:gsub('[%z\1-\31\\"]', escape_char) .. '"'
103
+end
104
+
105
+
106
+local function encode_number(val)
107
+  -- Check for NaN, -inf and inf
108
+  if val ~= val or val <= -math.huge or val >= math.huge then
109
+    error("unexpected number value '" .. tostring(val) .. "'")
110
+  end
111
+  return string.format("%.14g", val)
112
+end
113
+
114
+
115
+local type_func_map = {
116
+  [ "nil"     ] = encode_nil,
117
+  [ "table"   ] = encode_table,
118
+  [ "string"  ] = encode_string,
119
+  [ "number"  ] = encode_number,
120
+  [ "boolean" ] = tostring,
121
+}
122
+
123
+
124
+encode = function(val, stack)
125
+  local t = type(val)
126
+  local f = type_func_map[t]
127
+  if f then
128
+    return f(val, stack)
129
+  end
130
+  error("unexpected type '" .. t .. "'")
131
+end
132
+
133
+
134
+function json.encode(val)
135
+  return ( encode(val) )
136
+end
137
+
138
+
139
+-------------------------------------------------------------------------------
140
+-- Decode
141
+-------------------------------------------------------------------------------
142
+
143
+local parse
144
+
145
+local function create_set(...)
146
+  local res = {}
147
+  for i = 1, select("#", ...) do
148
+    res[ select(i, ...) ] = true
149
+  end
150
+  return res
151
+end
152
+
153
+local space_chars   = create_set(" ", "\t", "\r", "\n")
154
+local delim_chars   = create_set(" ", "\t", "\r", "\n", "]", "}", ",")
155
+local escape_chars  = create_set("\\", "/", '"', "b", "f", "n", "r", "t", "u")
156
+local literals      = create_set("true", "false", "null")
157
+
158
+local literal_map = {
159
+  [ "true"  ] = true,
160
+  [ "false" ] = false,
161
+  [ "null"  ] = nil,
162
+}
163
+
164
+
165
+local function next_char(str, idx, set, negate)
166
+  for i = idx, #str do
167
+    if set[str:sub(i, i)] ~= negate then
168
+      return i
169
+    end
170
+  end
171
+  return #str + 1
172
+end
173
+
174
+
175
+local function decode_error(str, idx, msg)
176
+  local line_count = 1
177
+  local col_count = 1
178
+  for i = 1, idx - 1 do
179
+    col_count = col_count + 1
180
+    if str:sub(i, i) == "\n" then
181
+      line_count = line_count + 1
182
+      col_count = 1
183
+    end
184
+  end
185
+  error( string.format("%s at line %d col %d", msg, line_count, col_count) )
186
+end
187
+
188
+
189
+local function codepoint_to_utf8(n)
190
+  -- http://scripts.sil.org/cms/scripts/page.php?site_id=nrsi&id=iws-appendixa
191
+  local f = math.floor
192
+  if n <= 0x7f then
193
+    return string.char(n)
194
+  elseif n <= 0x7ff then
195
+    return string.char(f(n / 64) + 192, n % 64 + 128)
196
+  elseif n <= 0xffff then
197
+    return string.char(f(n / 4096) + 224, f(n % 4096 / 64) + 128, n % 64 + 128)
198
+  elseif n <= 0x10ffff then
199
+    return string.char(f(n / 262144) + 240, f(n % 262144 / 4096) + 128,
200
+                       f(n % 4096 / 64) + 128, n % 64 + 128)
201
+  end
202
+  error( string.format("invalid unicode codepoint '%x'", n) )
203
+end
204
+
205
+
206
+local function parse_unicode_escape(s)
207
+  local n1 = tonumber( s:sub(3, 6),  16 )
208
+  local n2 = tonumber( s:sub(9, 12), 16 )
209
+  -- Surrogate pair?
210
+  if n2 then
211
+    return codepoint_to_utf8((n1 - 0xd800) * 0x400 + (n2 - 0xdc00) + 0x10000)
212
+  else
213
+    return codepoint_to_utf8(n1)
214
+  end
215
+end
216
+
217
+
218
+local function parse_string(str, i)
219
+  local has_unicode_escape = false
220
+  local has_surrogate_escape = false
221
+  local has_escape = false
222
+  local last
223
+  for j = i + 1, #str do
224
+    local x = str:byte(j)
225
+
226
+    if x < 32 then
227
+      decode_error(str, j, "control character in string")
228
+    end
229
+
230
+    if last == 92 then -- "\\" (escape char)
231
+      if x == 117 then -- "u" (unicode escape sequence)
232
+        local hex = str:sub(j + 1, j + 5)
233
+        if not hex:find("%x%x%x%x") then
234
+          decode_error(str, j, "invalid unicode escape in string")
235
+        end
236
+        if hex:find("^[dD][89aAbB]") then
237
+          has_surrogate_escape = true
238
+        else
239
+          has_unicode_escape = true
240
+        end
241
+      else
242
+        local c = string.char(x)
243
+        if not escape_chars[c] then
244
+          decode_error(str, j, "invalid escape char '" .. c .. "' in string")
245
+        end
246
+        has_escape = true
247
+      end
248
+      last = nil
249
+
250
+    elseif x == 34 then -- '"' (end of string)
251
+      local s = str:sub(i + 1, j - 1)
252
+      if has_surrogate_escape then
253
+        s = s:gsub("\\u[dD][89aAbB]..\\u....", parse_unicode_escape)
254
+      end
255
+      if has_unicode_escape then
256
+        s = s:gsub("\\u....", parse_unicode_escape)
257
+      end
258
+      if has_escape then
259
+        s = s:gsub("\\.", escape_char_map_inv)
260
+      end
261
+      return s, j + 1
262
+
263
+    else
264
+      last = x
265
+    end
266
+  end
267
+  decode_error(str, i, "expected closing quote for string")
268
+end
269
+
270
+
271
+local function parse_number(str, i)
272
+  local x = next_char(str, i, delim_chars)
273
+  local s = str:sub(i, x - 1)
274
+  local n = tonumber(s)
275
+  if not n then
276
+    decode_error(str, i, "invalid number '" .. s .. "'")
277
+  end
278
+  return n, x
279
+end
280
+
281
+
282
+local function parse_literal(str, i)
283
+  local x = next_char(str, i, delim_chars)
284
+  local word = str:sub(i, x - 1)
285
+  if not literals[word] then
286
+    decode_error(str, i, "invalid literal '" .. word .. "'")
287
+  end
288
+  return literal_map[word], x
289
+end
290
+
291
+
292
+local function parse_array(str, i)
293
+  local res = {}
294
+  local n = 1
295
+  i = i + 1
296
+  while 1 do
297
+    local x
298
+    i = next_char(str, i, space_chars, true)
299
+    -- Empty / end of array?
300
+    if str:sub(i, i) == "]" then
301
+      i = i + 1
302
+      break
303
+    end
304
+    -- Read token
305
+    x, i = parse(str, i)
306
+    res[n] = x
307
+    n = n + 1
308
+    -- Next token
309
+    i = next_char(str, i, space_chars, true)
310
+    local chr = str:sub(i, i)
311
+    i = i + 1
312
+    if chr == "]" then break end
313
+    if chr ~= "," then decode_error(str, i, "expected ']' or ','") end
314
+  end
315
+  return res, i
316
+end
317
+
318
+
319
+local function parse_object(str, i)
320
+  local res = {}
321
+  i = i + 1
322
+  while 1 do
323
+    local key, val
324
+    i = next_char(str, i, space_chars, true)
325
+    -- Empty / end of object?
326
+    if str:sub(i, i) == "}" then
327
+      i = i + 1
328
+      break
329
+    end
330
+    -- Read key
331
+    if str:sub(i, i) ~= '"' then
332
+      decode_error(str, i, "expected string for key")
333
+    end
334
+    key, i = parse(str, i)
335
+    -- Read ':' delimiter
336
+    i = next_char(str, i, space_chars, true)
337
+    if str:sub(i, i) ~= ":" then
338
+      decode_error(str, i, "expected ':' after key")
339
+    end
340
+    i = next_char(str, i + 1, space_chars, true)
341
+    -- Read value
342
+    val, i = parse(str, i)
343
+    -- Set
344
+    res[key] = val
345
+    -- Next token
346
+    i = next_char(str, i, space_chars, true)
347
+    local chr = str:sub(i, i)
348
+    i = i + 1
349
+    if chr == "}" then break end
350
+    if chr ~= "," then decode_error(str, i, "expected '}' or ','") end
351
+  end
352
+  return res, i
353
+end
354
+
355
+
356
+local char_func_map = {
357
+  [ '"' ] = parse_string,
358
+  [ "0" ] = parse_number,
359
+  [ "1" ] = parse_number,
360
+  [ "2" ] = parse_number,
361
+  [ "3" ] = parse_number,
362
+  [ "4" ] = parse_number,
363
+  [ "5" ] = parse_number,
364
+  [ "6" ] = parse_number,
365
+  [ "7" ] = parse_number,
366
+  [ "8" ] = parse_number,
367
+  [ "9" ] = parse_number,
368
+  [ "-" ] = parse_number,
369
+  [ "t" ] = parse_literal,
370
+  [ "f" ] = parse_literal,
371
+  [ "n" ] = parse_literal,
372
+  [ "[" ] = parse_array,
373
+  [ "{" ] = parse_object,
374
+}
375
+
376
+
377
+parse = function(str, idx)
378
+  local chr = str:sub(idx, idx)
379
+  local f = char_func_map[chr]
380
+  if f then
381
+    return f(str, idx)
382
+  end
383
+  decode_error(str, idx, "unexpected character '" .. chr .. "'")
384
+end
385
+
386
+
387
+function json.decode(str)
388
+  if type(str) ~= "string" then
389
+    error("expected argument of type string, got " .. type(str))
390
+  end
391
+  local res, idx = parse(str, next_char(str, 1, space_chars, true))
392
+  idx = next_char(str, idx, space_chars, true)
393
+  if idx <= #str then
394
+    decode_error(str, idx, "trailing garbage")
395
+  end
396
+  return res
397
+end
398
+
399
+
400
+return json

+ 737
- 0
src/lovebird.lua View File

@@ -0,0 +1,737 @@
1
+--
2
+-- lovebird
3
+--
4
+-- Copyright (c) 2017 rxi
5
+--
6
+-- This library is free software; you can redistribute it and/or modify it
7
+-- under the terms of the MIT license. See LICENSE for details.
8
+--
9
+
10
+local socket = require "socket"
11
+
12
+local lovebird = { _version = "0.4.3" }
13
+
14
+lovebird.loadstring = loadstring or load
15
+lovebird.inited = false
16
+lovebird.host = "*"
17
+lovebird.buffer = ""
18
+lovebird.lines = {}
19
+lovebird.connections = {}
20
+lovebird.pages = {}
21
+
22
+lovebird.wrapprint = true
23
+lovebird.timestamp = true
24
+lovebird.allowhtml = false
25
+lovebird.echoinput = true
26
+lovebird.port = 8000
27
+lovebird.whitelist = { "127.0.0.1" }
28
+lovebird.maxlines = 200
29
+lovebird.updateinterval = .5
30
+
31
+
32
+lovebird.pages["index"] = [[
33
+<?lua
34
+-- Handle console input
35
+if req.parsedbody.input then
36
+  local str = req.parsedbody.input
37
+  if lovebird.echoinput then
38
+    lovebird.pushline({ type = 'input', str = str })
39
+  end
40
+  if str:find("^=") then
41
+    str = "print(" .. str:sub(2) .. ")"
42
+  end
43
+  xpcall(function() assert(lovebird.loadstring(str, "input"))() end,
44
+         lovebird.onerror)
45
+end
46
+?>
47
+
48
+<!doctype html>
49
+<html>
50
+  <head>
51
+  <meta http-equiv="x-ua-compatible" content="IE=Edge"/>
52
+  <meta charset="utf-8">
53
+  <title>lovebird</title>
54
+  <style>
55
+    body {
56
+      margin: 0px;
57
+      font-size: 14px;
58
+      font-family: helvetica, verdana, sans;
59
+      background: #FFFFFF;
60
+    }
61
+    form {
62
+      margin-bottom: 0px;
63
+    }
64
+    .timestamp {
65
+      color: #909090;
66
+      padding-right: 4px;
67
+    }
68
+    .repeatcount {
69
+      color: #F0F0F0;
70
+      background: #505050;
71
+      font-size: 11px;
72
+      font-weight: bold;
73
+      text-align: center;
74
+      padding-left: 4px;
75
+      padding-right: 4px;
76
+      padding-top: 0px;
77
+      padding-bottom: 0px;
78
+      border-radius: 7px;
79
+      display: inline-block;
80
+    }
81
+    .errormarker {
82
+      color: #F0F0F0;
83
+      background: #8E0000;
84
+      font-size: 11px;
85
+      font-weight: bold;
86
+      text-align: center;
87
+      border-radius: 8px;
88
+      width: 17px;
89
+      padding-top: 0px;
90
+      padding-bottom: 0px;
91
+      display: inline-block;
92
+    }
93
+    .greybordered {
94
+      margin: 12px;
95
+      background: #F0F0F0;
96
+      border: 1px solid #E0E0E0;
97
+      border-radius: 3px;
98
+    }
99
+    .inputline {
100
+      font-family: mono, courier;
101
+      font-size: 13px;
102
+      color: #606060;
103
+    }
104
+    .inputline:before {
105
+      content: '\00B7\00B7\00B7';
106
+      padding-right: 5px;
107
+    }
108
+    .errorline {
109
+      color: #8E0000;
110
+    }
111
+    #header {
112
+      background: #101010;
113
+      height: 25px;
114
+      color: #F0F0F0;
115
+      padding: 9px
116
+    }
117
+    #title {
118
+      float: left;
119
+      font-size: 20px;
120
+    }
121
+    #title a {
122
+      color: #F0F0F0;
123
+      text-decoration: none;
124
+    }
125
+    #title a:hover {
126
+      color: #FFFFFF;
127
+    }
128
+    #version {
129
+      font-size: 10px;
130
+    }
131
+    #status {
132
+      float: right;
133
+      font-size: 14px;
134
+      padding-top: 4px;
135
+    }
136
+    #main a {
137
+      color: #000000;
138
+      text-decoration: none;
139
+      background: #E0E0E0;
140
+      border: 1px solid #D0D0D0;
141
+      border-radius: 3px;
142
+      padding-left: 2px;
143
+      padding-right: 2px;
144
+      display: inline-block;
145
+    }
146
+    #main a:hover {
147
+      background: #D0D0D0;
148
+      border: 1px solid #C0C0C0;
149
+    }
150
+    #console {
151
+      position: absolute;
152
+      top: 40px; bottom: 0px; left: 0px; right: 312px;
153
+    }
154
+    #input {
155
+      position: absolute;
156
+      margin: 10px;
157
+      bottom: 0px; left: 0px; right: 0px;
158
+    }
159
+    #inputbox {
160
+      width: 100%;
161
+      font-family: mono, courier;
162
+      font-size: 13px;
163
+    }
164
+    #output {
165
+      overflow-y: scroll;
166
+      position: absolute;
167
+      margin: 10px;
168
+      line-height: 17px;
169
+      top: 0px; bottom: 36px; left: 0px; right: 0px;
170
+    }
171
+    #env {
172
+      position: absolute;
173
+      top: 40px; bottom: 0px; right: 0px;
174
+      width: 300px;
175
+    }
176
+    #envheader {
177
+      padding: 5px;
178
+      background: #E0E0E0;
179
+    }
180
+    #envvars {
181
+      position: absolute;
182
+      left: 0px; right: 0px; top: 25px; bottom: 0px;
183
+      margin: 10px;
184
+      overflow-y: scroll;
185
+      font-size: 12px;
186
+    }
187
+  </style>
188
+  </head>
189
+  <body>
190
+    <div id="header">
191
+      <div id="title">
192
+        <a href="https://github.com/rxi/lovebird">lovebird</a>
193
+        <span id="version"><?lua echo(lovebird._version) ?></span>
194
+      </div>
195
+      <div id="status"></div>
196
+    </div>
197
+    <div id="main">
198
+      <div id="console" class="greybordered">
199
+        <div id="output"> <?lua echo(lovebird.buffer) ?> </div>
200
+        <div id="input">
201
+          <form method="post"
202
+                onkeydown="return onInputKeyDown(event);"
203
+                onsubmit="onInputSubmit(); return false;">
204
+            <input id="inputbox" name="input" type="text"
205
+                autocomplete="off"></input>
206
+          </form>
207
+        </div>
208
+      </div>
209
+      <div id="env" class="greybordered">
210
+        <div id="envheader"></div>
211
+        <div id="envvars"></div>
212
+      </div>
213
+    </div>
214
+    <script>
215
+      document.getElementById("inputbox").focus();
216
+
217
+      var changeFavicon = function(href) {
218
+        var old = document.getElementById("favicon");
219
+        if (old) document.head.removeChild(old);
220
+        var link = document.createElement("link");
221
+        link.id = "favicon";
222
+        link.rel = "shortcut icon";
223
+        link.href = href;
224
+        document.head.appendChild(link);
225
+      }
226
+
227
+      var truncate = function(str, len) {
228
+        if (str.length <= len) return str;
229
+        return str.substring(0, len - 3) + "...";
230
+      }
231
+
232
+      var geturl = function(url, onComplete, onFail) {
233
+        var req = new XMLHttpRequest();
234
+        req.onreadystatechange = function() {
235
+          if (req.readyState != 4) return;
236
+          if (req.status == 200) {
237
+            if (onComplete) onComplete(req.responseText);
238
+          } else {
239
+            if (onFail) onFail(req.responseText);
240
+          }
241
+        }
242
+        url += (url.indexOf("?") > -1 ? "&_=" : "?_=") + Math.random();
243
+        req.open("GET", url, true);
244
+        req.send();
245
+      }
246
+
247
+      var divContentCache = {}
248
+      var updateDivContent = function(id, content) {
249
+        if (divContentCache[id] != content) {
250
+          document.getElementById(id).innerHTML = content;
251
+          divContentCache[id] = content
252
+          return true;
253
+        }
254
+        return false;
255
+      }
256
+
257
+      var onInputSubmit = function() {
258
+        var b = document.getElementById("inputbox");
259
+        var req = new XMLHttpRequest();
260
+        req.open("POST", "/", true);
261
+        req.send("input=" + encodeURIComponent(b.value));
262
+        /* Do input history */
263
+        if (b.value && inputHistory[0] != b.value) {
264
+          inputHistory.unshift(b.value);
265
+        }
266
+        inputHistory.index = -1;
267
+        /* Reset */
268
+        b.value = "";
269
+        refreshOutput();
270
+      }
271
+
272
+      /* Input box history */
273
+      var inputHistory = [];
274
+      inputHistory.index = 0;
275
+      var onInputKeyDown = function(e) {
276
+        var key = e.which || e.keyCode;
277
+        if (key != 38 && key != 40) return true;
278
+        var b = document.getElementById("inputbox");
279
+        if (key == 38 && inputHistory.index < inputHistory.length - 1) {
280
+          /* Up key */
281
+          inputHistory.index++;
282
+        }
283
+        if (key == 40 && inputHistory.index >= 0) {
284
+          /* Down key */
285
+          inputHistory.index--;
286
+        }
287
+        b.value = inputHistory[inputHistory.index] || "";
288
+        b.selectionStart = b.value.length;
289
+        return false;
290
+      }
291
+
292
+      /* Output buffer and status */
293
+      var refreshOutput = function() {
294
+        geturl("/buffer", function(text) {
295
+          updateDivContent("status", "connected &#9679;");
296
+          if (updateDivContent("output", text)) {
297
+            var div = document.getElementById("output");
298
+            div.scrollTop = div.scrollHeight;
299
+          }
300
+          /* Update favicon */
301
+          changeFavicon("data:image/png;base64," +
302
+"iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAMAAAAoLQ9TAAAAP1BMVEUAAAAAAAAAAAD////19fUO"+
303
+"Dg7v7+/h4eGzs7MlJSUeHh7n5+fY2NjJycnGxsa3t7eioqKfn5+QkJCHh4d+fn7zU+b5AAAAAnRS"+
304
+"TlPlAFWaypEAAABRSURBVBjTfc9HDoAwDERRQ+w0ern/WQkZaUBC4e/mrWzppH9VJjbjZg1Ii2rM"+
305
+"DyR1JZ8J0dVWggIGggcEwgbYCRbuPRqgyjHNpzUP+39GPu9fgloC5L9DO0sAAAAASUVORK5CYII="
306
+          );
307
+        },
308
+        function(text) {
309
+          updateDivContent("status", "disconnected &#9675;");
310
+          /* Update favicon */
311
+          changeFavicon("data:image/png;base64," +
312
+"iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAMAAAAoLQ9TAAAAYFBMVEUAAAAAAAAAAADZ2dm4uLgM"+
313
+"DAz29vbz8/Pv7+/h4eHIyMiwsLBtbW0lJSUeHh4QEBDn5+fS0tLDw8O0tLSioqKfn5+QkJCHh4d+"+
314
+"fn5ycnJmZmZgYGBXV1dLS0tFRUUGBgZ0He44AAAAAnRSTlPlAFWaypEAAABeSURBVBjTfY9HDoAw"+
315
+"DAQD6Z3ey/9/iXMxkVDYw0g7F3tJReosUKHnwY4pCM+EtOEVXrb7wVRA0dMbaAcUwiVeDQq1Jp4a"+
316
+"xUg5kE0ooqZu68Di2Tgbs/DiY/9jyGf+AyFKBAK7KD2TAAAAAElFTkSuQmCC"
317
+          );
318
+        });
319
+      }
320
+      setInterval(refreshOutput,
321
+                  <?lua echo(lovebird.updateinterval) ?> * 1000);
322
+
323
+      /* Environment variable view */
324
+      var envPath = "";
325
+      var refreshEnv = function() {
326
+        geturl("/env.json?p=" + envPath, function(text) {
327
+          var json = eval("(" + text + ")");
328
+
329
+          /* Header */
330
+          var html = "<a href='#' onclick=\"setEnvPath('')\">env</a>";
331
+          var acc = "";
332
+          var p = json.path != "" ? json.path.split(".") : [];
333
+          for (var i = 0; i < p.length; i++) {
334
+            acc += "." + p[i];
335
+            html += " <a href='#' onclick=\"setEnvPath('" + acc + "')\">" +
336
+                    truncate(p[i], 10) + "</a>";
337
+          }
338
+          updateDivContent("envheader", html);
339
+
340
+          /* Handle invalid table path */
341
+          if (!json.valid) {
342
+            updateDivContent("envvars", "Bad path");
343
+            return;
344
+          }
345
+
346
+          /* Variables */
347
+          var html = "<table>";
348
+          for (var i = 0; json.vars[i]; i++) {
349
+            var x = json.vars[i];
350
+            var fullpath = (json.path + "." + x.key).replace(/^\./, "");
351
+            var k = truncate(x.key, 15);
352
+            if (x.type == "table") {
353
+              k = "<a href='#' onclick=\"setEnvPath('" + fullpath + "')\">" +
354
+                  k + "</a>";
355
+            }
356
+            var v = "<a href='#' onclick=\"insertVar('" +
357
+                    fullpath.replace(/\.(-?[0-9]+)/g, "[$1]") +
358
+                    "');\">" + x.value + "</a>"
359
+            html += "<tr><td>" + k + "</td><td>" + v + "</td></tr>";
360
+          }
361
+          html += "</table>";
362
+          updateDivContent("envvars", html);
363
+        });
364
+      }
365
+      var setEnvPath = function(p) {
366
+        envPath = p;
367
+        refreshEnv();
368
+      }
369
+      var insertVar = function(p) {
370
+        var b = document.getElementById("inputbox");
371
+        b.value += p;
372
+        b.focus();
373
+      }
374
+      setInterval(refreshEnv, <?lua echo(lovebird.updateinterval) ?> * 1000);
375
+    </script>
376
+  </body>
377
+</html>
378
+]]
379
+
380
+
381
+lovebird.pages["buffer"] = [[ <?lua echo(lovebird.buffer) ?> ]]
382
+
383
+
384
+lovebird.pages["env.json"] = [[
385
+<?lua
386
+  local t = _G
387
+  local p = req.parsedurl.query.p or ""
388
+  p = p:gsub("%.+", "."):match("^[%.]*(.*)[%.]*$")
389
+  if p ~= "" then
390
+    for x in p:gmatch("[^%.]+") do
391
+      t = t[x] or t[tonumber(x)]
392
+      -- Return early if path does not exist
393
+      if type(t) ~= "table" then
394
+        echo('{ "valid": false, "path": ' .. string.format("%q", p) .. ' }')
395
+        return
396
+      end
397
+    end
398
+  end
399
+?>
400
+{
401
+  "valid": true,
402
+  "path": "<?lua echo(p) ?>",
403
+  "vars": [
404
+    <?lua
405
+      local keys = {}
406
+      for k in pairs(t) do
407
+        if type(k) == "number" or type(k) == "string" then
408
+          table.insert(keys, k)
409
+        end
410
+      end
411
+      table.sort(keys, lovebird.compare)
412
+      for _, k in pairs(keys) do
413
+        local v = t[k]
414
+    ?>
415
+      {
416
+        "key": "<?lua echo(k) ?>",
417
+        "value": <?lua echo(
418
+                          string.format("%q",
419
+                            lovebird.truncate(
420
+                              lovebird.htmlescape(
421
+                                tostring(v)), 26))) ?>,
422
+        "type": "<?lua echo(type(v)) ?>",
423
+      },
424
+    <?lua end ?>
425
+  ]
426
+}
427
+]]
428
+
429
+
430
+
431
+function lovebird.init()
432
+  -- Init server
433
+  lovebird.server = assert(socket.bind(lovebird.host, lovebird.port))
434
+  lovebird.addr, lovebird.port = lovebird.server:getsockname()
435
+  lovebird.server:settimeout(0)
436
+  -- Wrap print
437
+  lovebird.origprint = print
438
+  if lovebird.wrapprint then
439
+    local oldprint = print
440
+    print = function(...)
441
+      oldprint(...)
442
+      lovebird.print(...)
443
+    end
444
+  end
445
+  -- Compile page templates
446
+  for k, page in pairs(lovebird.pages) do
447
+    lovebird.pages[k] = lovebird.template(page, "lovebird, req",
448
+                                          "pages." .. k)
449
+  end
450
+  lovebird.inited = true
451
+end
452
+
453
+
454
+function lovebird.template(str, params, chunkname)
455
+  params = params and ("," .. params) or ""
456
+  local f = function(x) return string.format(" echo(%q)", x) end
457
+  str = ("?>"..str.."<?lua"):gsub("%?>(.-)<%?lua", f)
458
+  str = "local echo " .. params .. " = ..." .. str
459
+  local fn = assert(lovebird.loadstring(str, chunkname))
460
+  return function(...)
461
+    local output = {}
462
+    local echo = function(str) table.insert(output, str) end
463
+    fn(echo, ...)
464
+    return table.concat(lovebird.map(output, tostring))
465
+  end
466
+end
467
+
468
+
469
+function lovebird.map(t, fn)
470
+  local res = {}
471
+  for k, v in pairs(t) do res[k] = fn(v) end
472
+  return res
473
+end
474
+
475
+
476
+function lovebird.trace(...)
477
+  local str = "[lovebird] " .. table.concat(lovebird.map({...}, tostring), " ")
478
+  print(str)
479
+  if not lovebird.wrapprint then lovebird.print(str) end
480
+end
481
+
482
+
483
+function lovebird.unescape(str)
484
+  local f = function(x) return string.char(tonumber("0x"..x)) end
485
+  return (str:gsub("%+", " "):gsub("%%(..)", f))
486
+end
487
+
488
+
489
+function lovebird.parseurl(url)
490
+  local res = {}
491
+  res.path, res.search = url:match("/([^%?]*)%??(.*)")
492
+  res.query = {}
493
+  for k, v in res.search:gmatch("([^&^?]-)=([^&^#]*)") do
494
+    res.query[k] = lovebird.unescape(v)
495
+  end
496
+  return res
497
+end
498
+
499
+
500
+local htmlescapemap = {
501
+  ["<"] = "&lt;",
502
+  ["&"] = "&amp;",
503
+  ['"'] = "&quot;",
504
+  ["'"] = "&#039;",
505
+}
506
+
507
+function lovebird.htmlescape(str)
508
+  return ( str:gsub("[<&\"']", htmlescapemap) )
509
+end
510
+
511
+
512
+function lovebird.truncate(str, len)
513
+  if #str <= len then
514
+    return str
515
+  end
516
+  return str:sub(1, len - 3) .. "..."
517
+end
518
+
519
+
520
+function lovebird.compare(a, b)
521
+  local na, nb = tonumber(a), tonumber(b)
522
+  if na then
523
+    if nb then return na < nb end
524
+    return false
525
+  elseif nb then
526
+    return true
527
+  end
528
+  return tostring(a) < tostring(b)
529
+end
530
+
531
+
532
+function lovebird.checkwhitelist(addr)
533
+  if lovebird.whitelist == nil then return true end
534
+  for _, a in pairs(lovebird.whitelist) do
535
+    local ptn = "^" .. a:gsub("%.", "%%."):gsub("%*", "%%d*") .. "$"
536
+    if addr:match(ptn) then return true end
537
+  end
538
+  return false
539
+end
540
+
541
+
542
+function lovebird.clear()
543
+  lovebird.lines = {}
544
+  lovebird.buffer = ""
545
+end
546
+
547
+
548
+function lovebird.pushline(line)
549
+  line.time = os.time()
550
+  line.count = 1
551
+  table.insert(lovebird.lines, line)
552
+  if #lovebird.lines > lovebird.maxlines then
553
+    table.remove(lovebird.lines, 1)
554
+  end
555
+  lovebird.recalcbuffer()
556
+end
557
+
558
+
559
+function lovebird.recalcbuffer()
560
+  local function doline(line)
561
+    local str = line.str
562
+    if not lovebird.allowhtml then
563
+      str = lovebird.htmlescape(line.str):gsub("\n", "<br>")
564
+    end
565
+    if line.type == "input" then
566
+      str = '<span class="inputline">' .. str .. '</span>'
567
+    else
568
+      if line.type == "error" then
569
+        str = '<span class="errormarker">!</span> ' .. str
570
+        str = '<span class="errorline">' .. str .. '</span>'
571
+      end
572
+      if line.count > 1 then
573
+        str = '<span class="repeatcount">' .. line.count .. '</span> ' .. str
574
+      end
575
+      if lovebird.timestamp then
576
+        str = os.date('<span class="timestamp">%H:%M:%S</span> ', line.time) ..
577
+              str
578
+      end
579
+    end
580
+    return str
581
+  end
582
+  lovebird.buffer = table.concat(lovebird.map(lovebird.lines, doline), "<br>")
583
+end
584
+
585
+
586
+function lovebird.print(...)
587
+  local t = {}
588
+  for i = 1, select("#", ...) do
589
+    table.insert(t, tostring(select(i, ...)))
590
+  end
591
+  local str = table.concat(t, " ")
592
+  local last = lovebird.lines[#lovebird.lines]
593
+  if last and str == last.str then
594
+    -- Update last line if this line is a duplicate of it
595
+    last.time = os.time()
596
+    last.count = last.count + 1
597
+    lovebird.recalcbuffer()
598
+  else
599
+    -- Create new line
600
+    lovebird.pushline({ type = "output", str = str })
601
+  end
602
+end
603
+
604
+
605
+function lovebird.onerror(err)
606
+  lovebird.pushline({ type = "error", str = err })
607
+  if lovebird.wrapprint then
608
+    lovebird.origprint("[lovebird] ERROR: " .. err)
609
+  end
610
+end
611
+
612
+
613
+function lovebird.onrequest(req, client)
614
+  local page = req.parsedurl.path
615
+  page = page ~= "" and page or "index"
616
+  -- Handle "page not found"
617
+  if not lovebird.pages[page] then
618
+    return "HTTP/1.1 404\r\nContent-Length: 8\r\n\r\nBad page"
619
+  end
620
+  -- Handle page
621
+  local str
622
+  xpcall(function()
623
+    local data = lovebird.pages[page](lovebird, req)
624
+    local contenttype = "text/html"
625
+    if string.match(page, "%.json$") then
626
+      contenttype = "application/json"
627
+    end
628
+    str = "HTTP/1.1 200 OK\r\n" ..
629
+          "Content-Type: " .. contenttype .. "\r\n" ..
630
+          "Content-Length: " .. #data .. "\r\n" ..
631
+          "\r\n" .. data
632
+  end, lovebird.onerror)
633
+  return str
634
+end
635
+
636
+
637
+function lovebird.receive(client, pattern)
638
+  while 1 do
639
+    local data, msg = client:receive(pattern)
640
+    if not data then
641
+      if msg == "timeout" then
642
+        -- Wait for more data
643
+        coroutine.yield(true)
644
+      else
645
+        -- Disconnected -- yielding nil means we're done
646
+        coroutine.yield(nil)
647
+      end
648
+    else
649
+      return data
650
+    end
651
+  end
652
+end
653
+
654
+
655
+function lovebird.send(client, data)
656
+  local idx = 1
657
+  while idx < #data do
658
+    local res, msg = client:send(data, idx)
659
+    if not res and msg == "closed" then
660
+      -- Handle disconnect
661
+      coroutine.yield(nil)
662
+    else
663
+      idx = idx + res
664
+      coroutine.yield(true)
665
+    end
666
+  end
667
+end
668
+
669
+
670
+function lovebird.onconnect(client)
671
+  -- Create request table
672
+  local requestptn = "(%S*)%s*(%S*)%s*(%S*)"
673
+  local req = {}
674
+  req.socket = client
675
+  req.addr, req.port = client:getsockname()
676
+  req.request = lovebird.receive(client, "*l")
677
+  req.method, req.url, req.proto = req.request:match(requestptn)
678
+  req.headers = {}
679
+  while 1 do
680
+    local line, msg = lovebird.receive(client, "*l")
681
+    if not line or #line == 0 then break end
682
+    local k, v = line:match("(.-):%s*(.*)$")
683
+    req.headers[k] = v
684
+  end
685
+  if req.headers["Content-Length"] then
686
+    req.body = lovebird.receive(client, req.headers["Content-Length"])
687
+  end
688
+  -- Parse body
689
+  req.parsedbody = {}
690
+  if req.body then
691
+    for k, v in req.body:gmatch("([^&]-)=([^&^#]*)") do
692
+      req.parsedbody[k] = lovebird.unescape(v)
693
+    end
694
+  end
695
+  -- Parse request line's url
696
+  req.parsedurl = lovebird.parseurl(req.url)
697
+  -- Handle request; get data to send and send
698
+  local data = lovebird.onrequest(req)
699
+  lovebird.send(client, data)
700
+  -- Clear up
701
+  client:close()
702
+end
703
+
704
+
705
+function lovebird.update()
706
+  if not lovebird.inited then lovebird.init() end
707
+  -- Handle new connections
708
+  while 1 do
709
+    -- Accept new connections
710
+    local client = lovebird.server:accept()
711
+    if not client then break end
712
+    client:settimeout(0)
713
+    local addr = client:getsockname()
714
+    if lovebird.checkwhitelist(addr) then
715
+      -- Connection okay -- create and add coroutine to set
716
+      local conn = coroutine.wrap(function()
717
+        xpcall(function() lovebird.onconnect(client) end, function() end)
718
+      end)
719
+      lovebird.connections[conn] = true
720
+    else
721
+      -- Reject connection not on whitelist
722
+      lovebird.trace("got non-whitelisted connection attempt: ", addr)
723
+      client:close()
724
+    end
725
+  end
726
+  -- Handle existing connections
727
+  for conn in pairs(lovebird.connections) do
728
+    -- Resume coroutine, remove if it has finished
729
+    local status = conn()
730
+    if status == nil then
731
+      lovebird.connections[conn] = nil
732
+    end
733
+  end
734
+end
735
+
736
+
737
+return lovebird

+ 21
- 0
src/main.moon View File

@@ -0,0 +1,21 @@
1
+export lovebird = require "lovebird"
2
+export json = require "json"
3
+
4
+-- export floors = {
5
+--   [0]: {}
6
+-- }
7
+-- export floor = floors[0]
8
+
9
+love.update = ->
10
+  lovebird.update!
11
+
12
+love.directorydropped = (path) ->
13
+  -- open with standard tools
14
+
15
+love.filedropped = (file) ->
16
+  -- if love.filesystem.mount file, "zip"
17
+  --   print love.filesystem.read "zip/test.txt"
18
+
19
+love.keypressed = (key) ->
20
+  if key == "escape"
21
+    love.event.quit! -- TEMP (send to game engine, which can have this function defined or others)