🐳chore(工具): 调整docker 文件
parent
f2cbf8041b
commit
f782003d1e
@ -1,15 +0,0 @@
|
|||||||
kind: pipeline
|
|
||||||
type: docker
|
|
||||||
name: hello-world
|
|
||||||
|
|
||||||
trigger:
|
|
||||||
branch:
|
|
||||||
- master
|
|
||||||
event:
|
|
||||||
- push
|
|
||||||
|
|
||||||
steps:
|
|
||||||
- name: say-hello
|
|
||||||
image: busybox
|
|
||||||
commands:
|
|
||||||
- echo hello-world
|
|
||||||
@ -1,95 +0,0 @@
|
|||||||
# drone-gitea-on-docker
|
|
||||||
DroneCI and Gitea on Docker
|
|
||||||
|
|
||||||
## Usage
|
|
||||||
|
|
||||||
Set the following in your `boot.sh`:
|
|
||||||
|
|
||||||
```
|
|
||||||
IP_ADDRESS=192.168.0.6 -> either reachable dns or ip address which will be your clone address and ui addresses.
|
|
||||||
GITEA_ADMIN_USER="giteauser" -> will be the user you register with in drone
|
|
||||||
```
|
|
||||||
|
|
||||||
Now boot the stack:
|
|
||||||
|
|
||||||
```
|
|
||||||
$ bash boot.sh
|
|
||||||
```
|
|
||||||
|
|
||||||
*Note*: Theres a [current issue](https://github.com/go-gitea/gitea/issues/7702) where webhooks get fired twice, if you see that just restart gitea with `docker restart gitea`.
|
|
||||||
|
|
||||||
- Head over to: `http://${IP_ADDRESS}:3000/user/settings/applications` and create a new OAuth2 Application and set the Redirect URI to `http://${IP_ADDRESS}:3001/login`
|
|
||||||
|
|
||||||
- Capture the client id and client secret and populate them in the `boot.sh` in `DRONE_GITEA_CLIENT_ID` and `DRONE_GITEA_CLIENT_SECRET` and run `bash boot.sh` again. This will give drone the correct credentials in order to authenticate with gitea.
|
|
||||||
|
|
||||||
- Now when you head over to `http://${IP_ADDRESS}:3001/` you will be asked to authorize the application and you should be able to access drone.
|
|
||||||
|
|
||||||
## Drone CLI
|
|
||||||
|
|
||||||
Install Drone CLI:
|
|
||||||
- https://docs.drone.io/cli/install/
|
|
||||||
|
|
||||||
```
|
|
||||||
$ curl -L https://github.com/drone/drone-cli/releases/latest/download/drone_darwin_amd64.tar.gz | tar zx
|
|
||||||
$ sudo mv drone /usr/local/bin/drone
|
|
||||||
$ chmod +x /usr/local/bin/drone
|
|
||||||
```
|
|
||||||
|
|
||||||
Get your Drone Token:
|
|
||||||
- http://${IP_ADDRESS}:3001/account
|
|
||||||
|
|
||||||
```
|
|
||||||
$ export DRONE_SERVER=http://${IP_ADDRESS}:3001
|
|
||||||
$ export DRONE_TOKEN=one-from-the-account-page
|
|
||||||
drone info
|
|
||||||
```
|
|
||||||
|
|
||||||
## Build your first pipeline
|
|
||||||
|
|
||||||
Create a test repo in gitea:
|
|
||||||
|
|
||||||

|
|
||||||
|
|
||||||
Commit a `.drone.yml` file for drone:
|
|
||||||
|
|
||||||
```
|
|
||||||
$ cat .drone.yml
|
|
||||||
kind: pipeline
|
|
||||||
type: docker
|
|
||||||
name: hello-world
|
|
||||||
|
|
||||||
trigger:
|
|
||||||
branch:
|
|
||||||
- master
|
|
||||||
event:
|
|
||||||
- push
|
|
||||||
|
|
||||||
steps:
|
|
||||||
- name: say-hello
|
|
||||||
image: busybox
|
|
||||||
commands:
|
|
||||||
- echo hello-world
|
|
||||||
```
|
|
||||||
|
|
||||||
Head over to drone and sync your repositories:
|
|
||||||
|
|
||||||

|
|
||||||
|
|
||||||
Activate your repository:
|
|
||||||
|
|
||||||

|
|
||||||
|
|
||||||
Push a commit to master and see your pipeline running:
|
|
||||||
|
|
||||||

|
|
||||||
|
|
||||||
## More Examples
|
|
||||||
|
|
||||||
- https://github.com/ruanbekker/drone-ci-testing
|
|
||||||
- https://github.com/ruanbekker/drone-demo-python-flask
|
|
||||||
- https://github.com/ruanbekker/drone-with-go
|
|
||||||
- https://github.com/ruanbekker/demo-drone-mongodb-tests
|
|
||||||
- https://github.com/ruanbekker/drone-multi-pipeline
|
|
||||||
- https://github.com/ruanbekker/docker-jekyll-drone
|
|
||||||
- [Localstack with Drone and Gitea](https://gist.github.com/ruanbekker/84cb9f0c2a21434ca8381a0c74842d84)
|
|
||||||
- [Drone, Minio, Gitea, Sqlite on Docker Compose](https://gist.github.com/ruanbekker/3847bbf1b961efc568b93ccbf5c6f9f6)
|
|
||||||
@ -1,19 +0,0 @@
|
|||||||
#!/usr/bin/env bash
|
|
||||||
|
|
||||||
export HOSTNAME=$(hostname)
|
|
||||||
export DRONE_VERSION=1.10.1
|
|
||||||
export DRONE_RUNNER_VERSION=1.6.3
|
|
||||||
export GITEA_VERSION=1.13
|
|
||||||
export IP_ADDRESS=127.0.0.1
|
|
||||||
export MINIO_ACCESS_KEY="EXAMPLEKEY"
|
|
||||||
export MINIO_SECRET_KEY="EXAMPLESECRET"
|
|
||||||
export GITEA_ADMIN_USER="example"
|
|
||||||
export DRONE_RPC_SECRET="$(echo ${HOSTNAME} | openssl dgst -md5 -hex)"
|
|
||||||
export DRONE_USER_CREATE="username:${GITEA_ADMIN_USER},machine:false,admin:true,token:${DRONE_RPC_SECRET}"
|
|
||||||
export DRONE_GITEA_CLIENT_ID=""
|
|
||||||
export DRONE_GITEA_CLIENT_SECRET=""
|
|
||||||
docker-compose up -d
|
|
||||||
|
|
||||||
echo ""
|
|
||||||
echo "Gitea: http://${IP_ADDRESS}:3000/"
|
|
||||||
echo "Drone: http://${IP_ADDRESS}:3001/"
|
|
||||||
@ -1,83 +0,0 @@
|
|||||||
version: '3.6'
|
|
||||||
|
|
||||||
services:
|
|
||||||
gitea:
|
|
||||||
container_name: gitea
|
|
||||||
image: gitea/gitea:${GITEA_VERSION:-1.10.6}
|
|
||||||
restart: unless-stopped
|
|
||||||
environment:
|
|
||||||
# https://docs.gitea.io/en-us/install-with-docker/#environments-variables
|
|
||||||
- APP_NAME="Gitea"
|
|
||||||
- USER_UID=1000
|
|
||||||
- USER_GID=1000
|
|
||||||
- RUN_MODE=prod
|
|
||||||
- DOMAIN=${IP_ADDRESS}
|
|
||||||
- SSH_DOMAIN=${IP_ADDRESS}
|
|
||||||
- HTTP_PORT=3000
|
|
||||||
- ROOT_URL=http://${IP_ADDRESS}:3000
|
|
||||||
- SSH_PORT=222
|
|
||||||
- SSH_LISTEN_PORT=22
|
|
||||||
- DB_TYPE=sqlite3
|
|
||||||
ports:
|
|
||||||
- "3000:3000"
|
|
||||||
- "222:22"
|
|
||||||
networks:
|
|
||||||
- cicd_net
|
|
||||||
volumes:
|
|
||||||
- ./gitea:/data
|
|
||||||
|
|
||||||
drone:
|
|
||||||
container_name: drone
|
|
||||||
image: drone/drone:${DRONE_VERSION:-1.6.4}
|
|
||||||
restart: unless-stopped
|
|
||||||
depends_on:
|
|
||||||
- gitea
|
|
||||||
environment:
|
|
||||||
# https://docs.drone.io/server/provider/gitea/
|
|
||||||
- DRONE_DATABASE_DRIVER=sqlite3
|
|
||||||
- DRONE_DATABASE_DATASOURCE=/data/database.sqlite
|
|
||||||
- DRONE_GITEA_SERVER=http://${IP_ADDRESS}:3000/
|
|
||||||
- DRONE_GIT_ALWAYS_AUTH=false
|
|
||||||
- DRONE_RPC_SECRET=${DRONE_RPC_SECRET}
|
|
||||||
- DRONE_SERVER_PROTO=http
|
|
||||||
- DRONE_SERVER_HOST=${IP_ADDRESS}:3001
|
|
||||||
- DRONE_TLS_AUTOCERT=false
|
|
||||||
- DRONE_USER_CREATE=${DRONE_USER_CREATE}
|
|
||||||
- DRONE_GITEA_CLIENT_ID=${DRONE_GITEA_CLIENT_ID}
|
|
||||||
- DRONE_GITEA_CLIENT_SECRET=${DRONE_GITEA_CLIENT_SECRET}
|
|
||||||
ports:
|
|
||||||
- "3001:80"
|
|
||||||
- "9001:9000"
|
|
||||||
networks:
|
|
||||||
- cicd_net
|
|
||||||
volumes:
|
|
||||||
- /var/run/docker.sock:/var/run/docker.sock
|
|
||||||
- ./drone:/data
|
|
||||||
|
|
||||||
drone-runner:
|
|
||||||
container_name: drone-runner
|
|
||||||
image: drone/drone-runner-docker:${DRONE_RUNNER_VERSION:-1}
|
|
||||||
restart: unless-stopped
|
|
||||||
depends_on:
|
|
||||||
- drone
|
|
||||||
environment:
|
|
||||||
# https://docs.drone.io/runner/docker/installation/linux/
|
|
||||||
# https://docs.drone.io/server/metrics/
|
|
||||||
- DRONE_RPC_PROTO=http
|
|
||||||
- DRONE_RPC_HOST=drone
|
|
||||||
- DRONE_RPC_SECRET=${DRONE_RPC_SECRET}
|
|
||||||
- DRONE_RUNNER_NAME="${HOSTNAME}-runner"
|
|
||||||
- DRONE_RUNNER_CAPACITY=2
|
|
||||||
- DRONE_RUNNER_NETWORKS=cicd_net
|
|
||||||
- DRONE_DEBUG=false
|
|
||||||
- DRONE_TRACE=false
|
|
||||||
ports:
|
|
||||||
- "3002:3000"
|
|
||||||
networks:
|
|
||||||
- cicd_net
|
|
||||||
volumes:
|
|
||||||
- /var/run/docker.sock:/var/run/docker.sock
|
|
||||||
|
|
||||||
networks:
|
|
||||||
cicd_net:
|
|
||||||
name: cicd_net
|
|
||||||
@ -0,0 +1,824 @@
|
|||||||
|
local bump = {
|
||||||
|
_VERSION = 'bump v3.1.7',
|
||||||
|
_URL = 'https://github.com/kikito/bump.lua',
|
||||||
|
_DESCRIPTION = 'A collision detection library for Lua',
|
||||||
|
}
|
||||||
|
|
||||||
|
------------------------------------------
|
||||||
|
-- Auxiliary functions
|
||||||
|
------------------------------------------
|
||||||
|
local DELTA = 1e-10 -- floating-point margin of error
|
||||||
|
|
||||||
|
local abs, floor, ceil, min, max = math.abs, math.floor, math.ceil, math.min, math.max
|
||||||
|
|
||||||
|
local function sign(x)
|
||||||
|
if x > 0 then
|
||||||
|
return 1
|
||||||
|
end
|
||||||
|
if x == 0 then
|
||||||
|
return 0
|
||||||
|
end
|
||||||
|
return -1
|
||||||
|
end
|
||||||
|
|
||||||
|
local function nearest(x, a, b)
|
||||||
|
if abs(a - x) < abs(b - x) then
|
||||||
|
return a
|
||||||
|
else
|
||||||
|
return b
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
local function assertType(desiredType, value, name)
|
||||||
|
if type(value) ~= desiredType then
|
||||||
|
error(name .. ' must be a ' .. desiredType .. ', but was ' .. tostring(value) .. '(a ' .. type(value) .. ')')
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
local function assertIsPositiveNumber(value, name)
|
||||||
|
if type(value) ~= 'number' or value <= 0 then
|
||||||
|
error(name .. ' must be a positive integer, but was ' .. tostring(value) .. '(' .. type(value) .. ')')
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
local function assertIsRect(x, y, w, h)
|
||||||
|
assertType('number', x, 'x')
|
||||||
|
assertType('number', y, 'y')
|
||||||
|
assertIsPositiveNumber(w, 'w')
|
||||||
|
assertIsPositiveNumber(h, 'h')
|
||||||
|
end
|
||||||
|
|
||||||
|
local defaultFilter = function()
|
||||||
|
return 'slide'
|
||||||
|
end
|
||||||
|
|
||||||
|
------------------------------------------
|
||||||
|
-- Rectangle functions
|
||||||
|
------------------------------------------
|
||||||
|
|
||||||
|
local function rect_getNearestCorner(x, y, w, h, px, py)
|
||||||
|
return nearest(px, x, x + w), nearest(py, y, y + h)
|
||||||
|
end
|
||||||
|
|
||||||
|
-- This is a generalized implementation of the liang-barsky algorithm, which also returns
|
||||||
|
-- the normals of the sides where the segment intersects.
|
||||||
|
-- Returns nil if the segment never touches the rect
|
||||||
|
-- Notice that normals are only guaranteed to be accurate when initially ti1, ti2 == -math.huge, math.huge
|
||||||
|
local function rect_getSegmentIntersectionIndices(x, y, w, h, x1, y1, x2, y2, ti1, ti2)
|
||||||
|
ti1, ti2 = ti1 or 0, ti2 or 1
|
||||||
|
local dx, dy = x2 - x1, y2 - y1
|
||||||
|
local nx, ny
|
||||||
|
local nx1, ny1, nx2, ny2 = 0, 0, 0, 0
|
||||||
|
local p, q, r
|
||||||
|
|
||||||
|
for side = 1, 4 do
|
||||||
|
if side == 1 then
|
||||||
|
nx, ny, p, q = -1, 0, -dx, x1 - x -- left
|
||||||
|
elseif side == 2 then
|
||||||
|
nx, ny, p, q = 1, 0, dx, x + w - x1 -- right
|
||||||
|
elseif side == 3 then
|
||||||
|
nx, ny, p, q = 0, -1, -dy, y1 - y -- top
|
||||||
|
else
|
||||||
|
nx, ny, p, q = 0, 1, dy, y + h - y1 -- bottom
|
||||||
|
end
|
||||||
|
|
||||||
|
if p == 0 then
|
||||||
|
if q <= 0 then
|
||||||
|
return nil
|
||||||
|
end
|
||||||
|
else
|
||||||
|
r = q / p
|
||||||
|
if p < 0 then
|
||||||
|
if r > ti2 then
|
||||||
|
return nil
|
||||||
|
elseif r > ti1 then
|
||||||
|
ti1, nx1, ny1 = r, nx, ny
|
||||||
|
end
|
||||||
|
else -- p > 0
|
||||||
|
if r < ti1 then
|
||||||
|
return nil
|
||||||
|
elseif r < ti2 then
|
||||||
|
ti2, nx2, ny2 = r, nx, ny
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
return ti1, ti2, nx1, ny1, nx2, ny2
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Calculates the minkowsky difference between 2 rects, which is another rect
|
||||||
|
local function rect_getDiff(x1, y1, w1, h1, x2, y2, w2, h2)
|
||||||
|
return x2 - x1 - w1, y2 - y1 - h1, w1 + w2, h1 + h2
|
||||||
|
end
|
||||||
|
|
||||||
|
local function rect_containsPoint(x, y, w, h, px, py)
|
||||||
|
return px - x > DELTA and py - y > DELTA and x + w - px > DELTA and y + h - py > DELTA
|
||||||
|
end
|
||||||
|
|
||||||
|
local function rect_isIntersecting(x1, y1, w1, h1, x2, y2, w2, h2)
|
||||||
|
return x1 < x2 + w2 and x2 < x1 + w1 and y1 < y2 + h2 and y2 < y1 + h1
|
||||||
|
end
|
||||||
|
|
||||||
|
local function rect_getSquareDistance(x1, y1, w1, h1, x2, y2, w2, h2)
|
||||||
|
local dx = x1 - x2 + (w1 - w2) / 2
|
||||||
|
local dy = y1 - y2 + (h1 - h2) / 2
|
||||||
|
return dx * dx + dy * dy
|
||||||
|
end
|
||||||
|
|
||||||
|
local function rect_detectCollision(x1, y1, w1, h1, x2, y2, w2, h2, goalX, goalY)
|
||||||
|
goalX = goalX or x1
|
||||||
|
goalY = goalY or y1
|
||||||
|
|
||||||
|
local dx, dy = goalX - x1, goalY - y1
|
||||||
|
local x, y, w, h = rect_getDiff(x1, y1, w1, h1, x2, y2, w2, h2)
|
||||||
|
|
||||||
|
local overlaps, ti, nx, ny
|
||||||
|
|
||||||
|
if rect_containsPoint(x, y, w, h, 0, 0) then -- item was intersecting other
|
||||||
|
local px, py = rect_getNearestCorner(x, y, w, h, 0, 0)
|
||||||
|
local wi, hi = min(w1, abs(px)), min(h1, abs(py)) -- area of intersection
|
||||||
|
ti = -wi * hi -- ti is the negative area of intersection
|
||||||
|
overlaps = true
|
||||||
|
else
|
||||||
|
local ti1, ti2, nx1, ny1 = rect_getSegmentIntersectionIndices(x, y, w, h, 0, 0, dx, dy, -math.huge, math.huge)
|
||||||
|
|
||||||
|
-- item tunnels into other
|
||||||
|
if ti1 and ti1 < 1 and (abs(ti1 - ti2) >= DELTA) -- special case for rect going through another rect's corner
|
||||||
|
and (0 < ti1 + DELTA or 0 == ti1 and ti2 > 0) then
|
||||||
|
ti, nx, ny = ti1, nx1, ny1
|
||||||
|
overlaps = false
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
if not ti then
|
||||||
|
return
|
||||||
|
end
|
||||||
|
|
||||||
|
local tx, ty
|
||||||
|
|
||||||
|
if overlaps then
|
||||||
|
if dx == 0 and dy == 0 then
|
||||||
|
-- intersecting and not moving - use minimum displacement vector
|
||||||
|
local px, py = rect_getNearestCorner(x, y, w, h, 0, 0)
|
||||||
|
if abs(px) < abs(py) then
|
||||||
|
py = 0
|
||||||
|
else
|
||||||
|
px = 0
|
||||||
|
end
|
||||||
|
nx, ny = sign(px), sign(py)
|
||||||
|
tx, ty = x1 + px, y1 + py
|
||||||
|
else
|
||||||
|
-- intersecting and moving - move in the opposite direction
|
||||||
|
local ti1, _
|
||||||
|
ti1, _, nx, ny = rect_getSegmentIntersectionIndices(x, y, w, h, 0, 0, dx, dy, -math.huge, 1)
|
||||||
|
if not ti1 then
|
||||||
|
return
|
||||||
|
end
|
||||||
|
tx, ty = x1 + dx * ti1, y1 + dy * ti1
|
||||||
|
end
|
||||||
|
else -- tunnel
|
||||||
|
tx, ty = x1 + dx * ti, y1 + dy * ti
|
||||||
|
end
|
||||||
|
|
||||||
|
return {
|
||||||
|
overlaps = overlaps,
|
||||||
|
ti = ti,
|
||||||
|
move = {
|
||||||
|
x = dx,
|
||||||
|
y = dy,
|
||||||
|
},
|
||||||
|
normal = {
|
||||||
|
x = nx,
|
||||||
|
y = ny,
|
||||||
|
},
|
||||||
|
touch = {
|
||||||
|
x = tx,
|
||||||
|
y = ty,
|
||||||
|
},
|
||||||
|
itemRect = {
|
||||||
|
x = x1,
|
||||||
|
y = y1,
|
||||||
|
w = w1,
|
||||||
|
h = h1,
|
||||||
|
},
|
||||||
|
otherRect = {
|
||||||
|
x = x2,
|
||||||
|
y = y2,
|
||||||
|
w = w2,
|
||||||
|
h = h2,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
end
|
||||||
|
|
||||||
|
------------------------------------------
|
||||||
|
-- Grid functions
|
||||||
|
------------------------------------------
|
||||||
|
|
||||||
|
local function grid_toWorld(cellSize, cx, cy)
|
||||||
|
return (cx - 1) * cellSize, (cy - 1) * cellSize
|
||||||
|
end
|
||||||
|
|
||||||
|
local function grid_toCell(cellSize, x, y)
|
||||||
|
return floor(x / cellSize) + 1, floor(y / cellSize) + 1
|
||||||
|
end
|
||||||
|
|
||||||
|
-- grid_traverse* functions are based on "A Fast Voxel Traversal Algorithm for Ray Tracing",
|
||||||
|
-- by John Amanides and Andrew Woo - http://www.cse.yorku.ca/~amana/research/grid.pdf
|
||||||
|
-- It has been modified to include both cells when the ray "touches a grid corner",
|
||||||
|
-- and with a different exit condition
|
||||||
|
|
||||||
|
local function grid_traverse_initStep(cellSize, ct, t1, t2)
|
||||||
|
local v = t2 - t1
|
||||||
|
if v > 0 then
|
||||||
|
return 1, cellSize / v, ((ct + v) * cellSize - t1) / v
|
||||||
|
elseif v < 0 then
|
||||||
|
return -1, -cellSize / v, ((ct + v - 1) * cellSize - t1) / v
|
||||||
|
else
|
||||||
|
return 0, math.huge, math.huge
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
local function grid_traverse(cellSize, x1, y1, x2, y2, f)
|
||||||
|
local cx1, cy1 = grid_toCell(cellSize, x1, y1)
|
||||||
|
local cx2, cy2 = grid_toCell(cellSize, x2, y2)
|
||||||
|
local stepX, dx, tx = grid_traverse_initStep(cellSize, cx1, x1, x2)
|
||||||
|
local stepY, dy, ty = grid_traverse_initStep(cellSize, cy1, y1, y2)
|
||||||
|
local cx, cy = cx1, cy1
|
||||||
|
|
||||||
|
f(cx, cy)
|
||||||
|
|
||||||
|
-- The default implementation had an infinite loop problem when
|
||||||
|
-- approaching the last cell in some occassions. We finish iterating
|
||||||
|
-- when we are *next* to the last cell
|
||||||
|
while abs(cx - cx2) + abs(cy - cy2) > 1 do
|
||||||
|
if tx < ty then
|
||||||
|
tx, cx = tx + dx, cx + stepX
|
||||||
|
f(cx, cy)
|
||||||
|
else
|
||||||
|
-- Addition: include both cells when going through corners
|
||||||
|
if tx == ty then
|
||||||
|
f(cx + stepX, cy)
|
||||||
|
end
|
||||||
|
ty, cy = ty + dy, cy + stepY
|
||||||
|
f(cx, cy)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
-- If we have not arrived to the last cell, use it
|
||||||
|
if cx ~= cx2 or cy ~= cy2 then
|
||||||
|
f(cx2, cy2)
|
||||||
|
end
|
||||||
|
|
||||||
|
end
|
||||||
|
|
||||||
|
local function grid_toCellRect(cellSize, x, y, w, h)
|
||||||
|
local cx, cy = grid_toCell(cellSize, x, y)
|
||||||
|
local cr, cb = ceil((x + w) / cellSize), ceil((y + h) / cellSize)
|
||||||
|
return cx, cy, cr - cx + 1, cb - cy + 1
|
||||||
|
end
|
||||||
|
|
||||||
|
------------------------------------------
|
||||||
|
-- Responses
|
||||||
|
------------------------------------------
|
||||||
|
|
||||||
|
local touch = function(world, col, x, y, w, h, goalX, goalY, filter)
|
||||||
|
return col.touch.x, col.touch.y, {}, 0
|
||||||
|
end
|
||||||
|
|
||||||
|
local cross = function(world, col, x, y, w, h, goalX, goalY, filter)
|
||||||
|
local cols, len = world:project(col.item, x, y, w, h, goalX, goalY, filter)
|
||||||
|
return goalX, goalY, cols, len
|
||||||
|
end
|
||||||
|
|
||||||
|
local slide = function(world, col, x, y, w, h, goalX, goalY, filter)
|
||||||
|
goalX = goalX or x
|
||||||
|
goalY = goalY or y
|
||||||
|
|
||||||
|
local tch, move = col.touch, col.move
|
||||||
|
if move.x ~= 0 or move.y ~= 0 then
|
||||||
|
if col.normal.x ~= 0 then
|
||||||
|
goalX = tch.x
|
||||||
|
else
|
||||||
|
goalY = tch.y
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
col.slide = {
|
||||||
|
x = goalX,
|
||||||
|
y = goalY,
|
||||||
|
}
|
||||||
|
|
||||||
|
x, y = tch.x, tch.y
|
||||||
|
local cols, len = world:project(col.item, x, y, w, h, goalX, goalY, filter)
|
||||||
|
return goalX, goalY, cols, len
|
||||||
|
end
|
||||||
|
|
||||||
|
local bounce = function(world, col, x, y, w, h, goalX, goalY, filter)
|
||||||
|
goalX = goalX or x
|
||||||
|
goalY = goalY or y
|
||||||
|
|
||||||
|
local tch, move = col.touch, col.move
|
||||||
|
local tx, ty = tch.x, tch.y
|
||||||
|
|
||||||
|
local bx, by = tx, ty
|
||||||
|
|
||||||
|
if move.x ~= 0 or move.y ~= 0 then
|
||||||
|
local bnx, bny = goalX - tx, goalY - ty
|
||||||
|
if col.normal.x == 0 then
|
||||||
|
bny = -bny
|
||||||
|
else
|
||||||
|
bnx = -bnx
|
||||||
|
end
|
||||||
|
bx, by = tx + bnx, ty + bny
|
||||||
|
end
|
||||||
|
|
||||||
|
col.bounce = {
|
||||||
|
x = bx,
|
||||||
|
y = by,
|
||||||
|
}
|
||||||
|
x, y = tch.x, tch.y
|
||||||
|
goalX, goalY = bx, by
|
||||||
|
|
||||||
|
local cols, len = world:project(col.item, x, y, w, h, goalX, goalY, filter)
|
||||||
|
return goalX, goalY, cols, len
|
||||||
|
end
|
||||||
|
|
||||||
|
------------------------------------------
|
||||||
|
-- World
|
||||||
|
------------------------------------------
|
||||||
|
|
||||||
|
local World = {}
|
||||||
|
local World_mt = {
|
||||||
|
__index = World,
|
||||||
|
}
|
||||||
|
|
||||||
|
-- Private functions and methods
|
||||||
|
|
||||||
|
local function sortByWeight(a, b)
|
||||||
|
return a.weight < b.weight
|
||||||
|
end
|
||||||
|
|
||||||
|
local function sortByTiAndDistance(a, b)
|
||||||
|
if a.ti == b.ti then
|
||||||
|
local ir, ar, br = a.itemRect, a.otherRect, b.otherRect
|
||||||
|
local ad = rect_getSquareDistance(ir.x, ir.y, ir.w, ir.h, ar.x, ar.y, ar.w, ar.h)
|
||||||
|
local bd = rect_getSquareDistance(ir.x, ir.y, ir.w, ir.h, br.x, br.y, br.w, br.h)
|
||||||
|
return ad < bd
|
||||||
|
end
|
||||||
|
return a.ti < b.ti
|
||||||
|
end
|
||||||
|
|
||||||
|
local function addItemToCell(self, item, cx, cy)
|
||||||
|
self.rows[cy] = self.rows[cy] or setmetatable({}, {
|
||||||
|
__mode = 'v',
|
||||||
|
})
|
||||||
|
local row = self.rows[cy]
|
||||||
|
row[cx] = row[cx] or {
|
||||||
|
itemCount = 0,
|
||||||
|
x = cx,
|
||||||
|
y = cy,
|
||||||
|
items = setmetatable({}, {
|
||||||
|
__mode = 'k',
|
||||||
|
}),
|
||||||
|
}
|
||||||
|
local cell = row[cx]
|
||||||
|
self.nonEmptyCells[cell] = true
|
||||||
|
if not cell.items[item] then
|
||||||
|
cell.items[item] = true
|
||||||
|
cell.itemCount = cell.itemCount + 1
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
local function removeItemFromCell(self, item, cx, cy)
|
||||||
|
local row = self.rows[cy]
|
||||||
|
if not row or not row[cx] or not row[cx].items[item] then
|
||||||
|
return false
|
||||||
|
end
|
||||||
|
|
||||||
|
local cell = row[cx]
|
||||||
|
cell.items[item] = nil
|
||||||
|
cell.itemCount = cell.itemCount - 1
|
||||||
|
if cell.itemCount == 0 then
|
||||||
|
self.nonEmptyCells[cell] = nil
|
||||||
|
end
|
||||||
|
return true
|
||||||
|
end
|
||||||
|
|
||||||
|
local function getDictItemsInCellRect(self, cl, ct, cw, ch)
|
||||||
|
local items_dict = {}
|
||||||
|
for cy = ct, ct + ch - 1 do
|
||||||
|
local row = self.rows[cy]
|
||||||
|
if row then
|
||||||
|
for cx = cl, cl + cw - 1 do
|
||||||
|
local cell = row[cx]
|
||||||
|
if cell and cell.itemCount > 0 then -- no cell.itemCount > 1 because tunneling
|
||||||
|
for item, _ in pairs(cell.items) do
|
||||||
|
items_dict[item] = true
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
return items_dict
|
||||||
|
end
|
||||||
|
|
||||||
|
local function getCellsTouchedBySegment(self, x1, y1, x2, y2)
|
||||||
|
|
||||||
|
local cells, cellsLen, visited = {}, 0, {}
|
||||||
|
|
||||||
|
grid_traverse(self.cellSize, x1, y1, x2, y2, function(cx, cy)
|
||||||
|
local row = self.rows[cy]
|
||||||
|
if not row then
|
||||||
|
return
|
||||||
|
end
|
||||||
|
local cell = row[cx]
|
||||||
|
if not cell or visited[cell] then
|
||||||
|
return
|
||||||
|
end
|
||||||
|
|
||||||
|
visited[cell] = true
|
||||||
|
cellsLen = cellsLen + 1
|
||||||
|
cells[cellsLen] = cell
|
||||||
|
end)
|
||||||
|
|
||||||
|
return cells, cellsLen
|
||||||
|
end
|
||||||
|
|
||||||
|
local function getInfoAboutItemsTouchedBySegment(self, x1, y1, x2, y2, filter)
|
||||||
|
local cells, len = getCellsTouchedBySegment(self, x1, y1, x2, y2)
|
||||||
|
local cell, rect, l, t, w, h, ti1, ti2, tii0, tii1
|
||||||
|
local visited, itemInfo, itemInfoLen = {}, {}, 0
|
||||||
|
for i = 1, len do
|
||||||
|
cell = cells[i]
|
||||||
|
for item in pairs(cell.items) do
|
||||||
|
if not visited[item] then
|
||||||
|
visited[item] = true
|
||||||
|
if (not filter or filter(item)) then
|
||||||
|
rect = self.rects[item]
|
||||||
|
l, t, w, h = rect.x, rect.y, rect.w, rect.h
|
||||||
|
|
||||||
|
ti1, ti2 = rect_getSegmentIntersectionIndices(l, t, w, h, x1, y1, x2, y2, 0, 1)
|
||||||
|
if ti1 and ((0 < ti1 and ti1 < 1) or (0 < ti2 and ti2 < 1)) then
|
||||||
|
-- the sorting is according to the t of an infinite line, not the segment
|
||||||
|
tii0, tii1 = rect_getSegmentIntersectionIndices(l, t, w, h, x1, y1, x2, y2, -math.huge,
|
||||||
|
math.huge)
|
||||||
|
itemInfoLen = itemInfoLen + 1
|
||||||
|
itemInfo[itemInfoLen] = {
|
||||||
|
item = item,
|
||||||
|
ti1 = ti1,
|
||||||
|
ti2 = ti2,
|
||||||
|
weight = min(tii0, tii1),
|
||||||
|
}
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
table.sort(itemInfo, sortByWeight)
|
||||||
|
return itemInfo, itemInfoLen
|
||||||
|
end
|
||||||
|
|
||||||
|
local function getResponseByName(self, name)
|
||||||
|
local response = self.responses[name]
|
||||||
|
if not response then
|
||||||
|
error(('Unknown collision type: %s (%s)'):format(name, type(name)))
|
||||||
|
end
|
||||||
|
return response
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Misc Public Methods
|
||||||
|
|
||||||
|
function World:addResponse(name, response)
|
||||||
|
self.responses[name] = response
|
||||||
|
end
|
||||||
|
|
||||||
|
function World:project(item, x, y, w, h, goalX, goalY, filter)
|
||||||
|
assertIsRect(x, y, w, h)
|
||||||
|
|
||||||
|
goalX = goalX or x
|
||||||
|
goalY = goalY or y
|
||||||
|
filter = filter or defaultFilter
|
||||||
|
|
||||||
|
local collisions, len = {}, 0
|
||||||
|
|
||||||
|
local visited = {}
|
||||||
|
if item ~= nil then
|
||||||
|
visited[item] = true
|
||||||
|
end
|
||||||
|
|
||||||
|
-- This could probably be done with less cells using a polygon raster over the cells instead of a
|
||||||
|
-- bounding rect of the whole movement. Conditional to building a queryPolygon method
|
||||||
|
local tl, tt = min(goalX, x), min(goalY, y)
|
||||||
|
local tr, tb = max(goalX + w, x + w), max(goalY + h, y + h)
|
||||||
|
local tw, th = tr - tl, tb - tt
|
||||||
|
|
||||||
|
local cl, ct, cw, ch = grid_toCellRect(self.cellSize, tl, tt, tw, th)
|
||||||
|
|
||||||
|
local dictItemsInCellRect = getDictItemsInCellRect(self, cl, ct, cw, ch)
|
||||||
|
|
||||||
|
for other, _ in pairs(dictItemsInCellRect) do
|
||||||
|
if not visited[other] then
|
||||||
|
visited[other] = true
|
||||||
|
|
||||||
|
local responseName = filter(item, other)
|
||||||
|
if responseName then
|
||||||
|
local ox, oy, ow, oh = self:getRect(other)
|
||||||
|
local col = rect_detectCollision(x, y, w, h, ox, oy, ow, oh, goalX, goalY)
|
||||||
|
|
||||||
|
if col then
|
||||||
|
col.other = other
|
||||||
|
col.item = item
|
||||||
|
col.type = responseName
|
||||||
|
|
||||||
|
len = len + 1
|
||||||
|
collisions[len] = col
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
table.sort(collisions, sortByTiAndDistance)
|
||||||
|
|
||||||
|
return collisions, len
|
||||||
|
end
|
||||||
|
|
||||||
|
function World:countCells()
|
||||||
|
local count = 0
|
||||||
|
for _, row in pairs(self.rows) do
|
||||||
|
for _, _ in pairs(row) do
|
||||||
|
count = count + 1
|
||||||
|
end
|
||||||
|
end
|
||||||
|
return count
|
||||||
|
end
|
||||||
|
|
||||||
|
function World:hasItem(item)
|
||||||
|
return not not self.rects[item]
|
||||||
|
end
|
||||||
|
|
||||||
|
function World:getItems()
|
||||||
|
local items, len = {}, 0
|
||||||
|
for item, _ in pairs(self.rects) do
|
||||||
|
len = len + 1
|
||||||
|
items[len] = item
|
||||||
|
end
|
||||||
|
return items, len
|
||||||
|
end
|
||||||
|
|
||||||
|
function World:countItems()
|
||||||
|
local len = 0
|
||||||
|
for _ in pairs(self.rects) do
|
||||||
|
len = len + 1
|
||||||
|
end
|
||||||
|
return len
|
||||||
|
end
|
||||||
|
|
||||||
|
function World:getRect(item)
|
||||||
|
local rect = self.rects[item]
|
||||||
|
if not rect then
|
||||||
|
error('Item ' .. tostring(item) ..
|
||||||
|
' must be added to the world before getting its rect. Use world:add(item, x,y,w,h) to add it first.')
|
||||||
|
end
|
||||||
|
return rect.x, rect.y, rect.w, rect.h
|
||||||
|
end
|
||||||
|
|
||||||
|
function World:toWorld(cx, cy)
|
||||||
|
return grid_toWorld(self.cellSize, cx, cy)
|
||||||
|
end
|
||||||
|
|
||||||
|
function World:toCell(x, y)
|
||||||
|
return grid_toCell(self.cellSize, x, y)
|
||||||
|
end
|
||||||
|
|
||||||
|
--- Query methods
|
||||||
|
|
||||||
|
function World:queryRect(x, y, w, h, filter)
|
||||||
|
|
||||||
|
assertIsRect(x, y, w, h)
|
||||||
|
|
||||||
|
local cl, ct, cw, ch = grid_toCellRect(self.cellSize, x, y, w, h)
|
||||||
|
local dictItemsInCellRect = getDictItemsInCellRect(self, cl, ct, cw, ch)
|
||||||
|
|
||||||
|
local items, len = {}, 0
|
||||||
|
|
||||||
|
local rect
|
||||||
|
for item, _ in pairs(dictItemsInCellRect) do
|
||||||
|
rect = self.rects[item]
|
||||||
|
if (not filter or filter(item)) and rect_isIntersecting(x, y, w, h, rect.x, rect.y, rect.w, rect.h) then
|
||||||
|
len = len + 1
|
||||||
|
items[len] = item
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
return items, len
|
||||||
|
end
|
||||||
|
|
||||||
|
function World:queryPoint(x, y, filter)
|
||||||
|
local cx, cy = self:toCell(x, y)
|
||||||
|
local dictItemsInCellRect = getDictItemsInCellRect(self, cx, cy, 1, 1)
|
||||||
|
|
||||||
|
local items, len = {}, 0
|
||||||
|
|
||||||
|
local rect
|
||||||
|
for item, _ in pairs(dictItemsInCellRect) do
|
||||||
|
rect = self.rects[item]
|
||||||
|
if (not filter or filter(item)) and rect_containsPoint(rect.x, rect.y, rect.w, rect.h, x, y) then
|
||||||
|
len = len + 1
|
||||||
|
items[len] = item
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
return items, len
|
||||||
|
end
|
||||||
|
|
||||||
|
function World:querySegment(x1, y1, x2, y2, filter)
|
||||||
|
local itemInfo, len = getInfoAboutItemsTouchedBySegment(self, x1, y1, x2, y2, filter)
|
||||||
|
local items = {}
|
||||||
|
for i = 1, len do
|
||||||
|
items[i] = itemInfo[i].item
|
||||||
|
end
|
||||||
|
return items, len
|
||||||
|
end
|
||||||
|
|
||||||
|
function World:querySegmentWithCoords(x1, y1, x2, y2, filter)
|
||||||
|
local itemInfo, len = getInfoAboutItemsTouchedBySegment(self, x1, y1, x2, y2, filter)
|
||||||
|
local dx, dy = x2 - x1, y2 - y1
|
||||||
|
local info, ti1, ti2
|
||||||
|
for i = 1, len do
|
||||||
|
info = itemInfo[i]
|
||||||
|
ti1 = info.ti1
|
||||||
|
ti2 = info.ti2
|
||||||
|
|
||||||
|
info.weight = nil
|
||||||
|
info.x1 = x1 + dx * ti1
|
||||||
|
info.y1 = y1 + dy * ti1
|
||||||
|
info.x2 = x1 + dx * ti2
|
||||||
|
info.y2 = y1 + dy * ti2
|
||||||
|
end
|
||||||
|
return itemInfo, len
|
||||||
|
end
|
||||||
|
|
||||||
|
--- Main methods
|
||||||
|
|
||||||
|
function World:add(item, x, y, w, h)
|
||||||
|
local rect = self.rects[item]
|
||||||
|
if rect then
|
||||||
|
error('Item ' .. tostring(item) .. ' added to the world twice.')
|
||||||
|
end
|
||||||
|
assertIsRect(x, y, w, h)
|
||||||
|
|
||||||
|
self.rects[item] = {
|
||||||
|
x = x,
|
||||||
|
y = y,
|
||||||
|
w = w,
|
||||||
|
h = h,
|
||||||
|
}
|
||||||
|
|
||||||
|
local cl, ct, cw, ch = grid_toCellRect(self.cellSize, x, y, w, h)
|
||||||
|
for cy = ct, ct + ch - 1 do
|
||||||
|
for cx = cl, cl + cw - 1 do
|
||||||
|
addItemToCell(self, item, cx, cy)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
return item
|
||||||
|
end
|
||||||
|
|
||||||
|
function World:remove(item)
|
||||||
|
local x, y, w, h = self:getRect(item)
|
||||||
|
|
||||||
|
self.rects[item] = nil
|
||||||
|
local cl, ct, cw, ch = grid_toCellRect(self.cellSize, x, y, w, h)
|
||||||
|
for cy = ct, ct + ch - 1 do
|
||||||
|
for cx = cl, cl + cw - 1 do
|
||||||
|
removeItemFromCell(self, item, cx, cy)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
function World:update(item, x2, y2, w2, h2)
|
||||||
|
local x1, y1, w1, h1 = self:getRect(item)
|
||||||
|
w2, h2 = w2 or w1, h2 or h1
|
||||||
|
assertIsRect(x2, y2, w2, h2)
|
||||||
|
|
||||||
|
if x1 ~= x2 or y1 ~= y2 or w1 ~= w2 or h1 ~= h2 then
|
||||||
|
|
||||||
|
local cellSize = self.cellSize
|
||||||
|
local cl1, ct1, cw1, ch1 = grid_toCellRect(cellSize, x1, y1, w1, h1)
|
||||||
|
local cl2, ct2, cw2, ch2 = grid_toCellRect(cellSize, x2, y2, w2, h2)
|
||||||
|
|
||||||
|
if cl1 ~= cl2 or ct1 ~= ct2 or cw1 ~= cw2 or ch1 ~= ch2 then
|
||||||
|
|
||||||
|
local cr1, cb1 = cl1 + cw1 - 1, ct1 + ch1 - 1
|
||||||
|
local cr2, cb2 = cl2 + cw2 - 1, ct2 + ch2 - 1
|
||||||
|
local cyOut
|
||||||
|
|
||||||
|
for cy = ct1, cb1 do
|
||||||
|
cyOut = cy < ct2 or cy > cb2
|
||||||
|
for cx = cl1, cr1 do
|
||||||
|
if cyOut or cx < cl2 or cx > cr2 then
|
||||||
|
removeItemFromCell(self, item, cx, cy)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
for cy = ct2, cb2 do
|
||||||
|
cyOut = cy < ct1 or cy > cb1
|
||||||
|
for cx = cl2, cr2 do
|
||||||
|
if cyOut or cx < cl1 or cx > cr1 then
|
||||||
|
addItemToCell(self, item, cx, cy)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
end
|
||||||
|
|
||||||
|
local rect = self.rects[item]
|
||||||
|
rect.x, rect.y, rect.w, rect.h = x2, y2, w2, h2
|
||||||
|
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
function World:move(item, goalX, goalY, filter)
|
||||||
|
local actualX, actualY, cols, len = self:check(item, goalX, goalY, filter)
|
||||||
|
|
||||||
|
self:update(item, actualX, actualY)
|
||||||
|
|
||||||
|
return actualX, actualY, cols, len
|
||||||
|
end
|
||||||
|
|
||||||
|
function World:check(item, goalX, goalY, filter)
|
||||||
|
filter = filter or defaultFilter
|
||||||
|
|
||||||
|
local visited = {
|
||||||
|
[item] = true,
|
||||||
|
}
|
||||||
|
local visitedFilter = function(itm, other)
|
||||||
|
if visited[other] then
|
||||||
|
return false
|
||||||
|
end
|
||||||
|
return filter(itm, other)
|
||||||
|
end
|
||||||
|
|
||||||
|
local cols, len = {}, 0
|
||||||
|
|
||||||
|
local x, y, w, h = self:getRect(item)
|
||||||
|
|
||||||
|
local projected_cols, projected_len = self:project(item, x, y, w, h, goalX, goalY, visitedFilter)
|
||||||
|
|
||||||
|
while projected_len > 0 do
|
||||||
|
local col = projected_cols[1]
|
||||||
|
len = len + 1
|
||||||
|
cols[len] = col
|
||||||
|
|
||||||
|
visited[col.other] = true
|
||||||
|
|
||||||
|
local response = getResponseByName(self, col.type)
|
||||||
|
|
||||||
|
goalX, goalY, projected_cols, projected_len = response(self, col, x, y, w, h, goalX, goalY, visitedFilter)
|
||||||
|
end
|
||||||
|
|
||||||
|
return goalX, goalY, cols, len
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Public library functions
|
||||||
|
|
||||||
|
bump.newWorld = function(cellSize)
|
||||||
|
cellSize = cellSize or 64
|
||||||
|
assertIsPositiveNumber(cellSize, 'cellSize')
|
||||||
|
local world = setmetatable({
|
||||||
|
cellSize = cellSize,
|
||||||
|
rects = {},
|
||||||
|
rows = {},
|
||||||
|
nonEmptyCells = {},
|
||||||
|
responses = {},
|
||||||
|
}, World_mt)
|
||||||
|
|
||||||
|
world:addResponse('touch', touch)
|
||||||
|
world:addResponse('cross', cross)
|
||||||
|
world:addResponse('slide', slide)
|
||||||
|
world:addResponse('bounce', bounce)
|
||||||
|
|
||||||
|
return world
|
||||||
|
end
|
||||||
|
|
||||||
|
bump.rect = {
|
||||||
|
getNearestCorner = rect_getNearestCorner,
|
||||||
|
getSegmentIntersectionIndices = rect_getSegmentIntersectionIndices,
|
||||||
|
getDiff = rect_getDiff,
|
||||||
|
containsPoint = rect_containsPoint,
|
||||||
|
isIntersecting = rect_isIntersecting,
|
||||||
|
getSquareDistance = rect_getSquareDistance,
|
||||||
|
detectCollision = rect_detectCollision,
|
||||||
|
}
|
||||||
|
|
||||||
|
bump.responses = {
|
||||||
|
touch = touch,
|
||||||
|
cross = cross,
|
||||||
|
slide = slide,
|
||||||
|
bounce = bounce,
|
||||||
|
}
|
||||||
|
|
||||||
|
return bump
|
||||||
@ -0,0 +1,326 @@
|
|||||||
|
-- https://github.com/bakpakin/corope
|
||||||
|
local setmetatable = setmetatable
|
||||||
|
local create = coroutine.create
|
||||||
|
local status = coroutine.status
|
||||||
|
local resume = coroutine.resume
|
||||||
|
local yield = coroutine.yield
|
||||||
|
local running = coroutine.running
|
||||||
|
local type = type
|
||||||
|
local select = select
|
||||||
|
local assert = assert
|
||||||
|
local unpack = unpack or table.unpack
|
||||||
|
|
||||||
|
-- Object definitions
|
||||||
|
|
||||||
|
local Bundle = {}
|
||||||
|
local Bundle_mt = {
|
||||||
|
__index = Bundle,
|
||||||
|
}
|
||||||
|
|
||||||
|
local Rope = {}
|
||||||
|
local Rope_mt = {
|
||||||
|
__index = Rope,
|
||||||
|
}
|
||||||
|
|
||||||
|
local function newBundle(options)
|
||||||
|
options = options or {}
|
||||||
|
return setmetatable({
|
||||||
|
ropes = {}, -- Active threads
|
||||||
|
signals = {}, -- Signal listeners
|
||||||
|
time = 0,
|
||||||
|
errhand = options.errhand or print,
|
||||||
|
}, Bundle_mt)
|
||||||
|
end
|
||||||
|
|
||||||
|
local function newRope(fn, ...)
|
||||||
|
return setmetatable({
|
||||||
|
thread = create(fn),
|
||||||
|
primer = {
|
||||||
|
args = {...},
|
||||||
|
n = select('#', ...),
|
||||||
|
},
|
||||||
|
}, Rope_mt)
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Bundle implementation
|
||||||
|
|
||||||
|
-- TODO: Should ropes be allowed to change bundles?
|
||||||
|
function Bundle:suspend(rope)
|
||||||
|
assert(rope.bundle == self, 'rope does not belong to this bundle')
|
||||||
|
assert(not rope.paused, 'cannot suspend already suspended rope')
|
||||||
|
local ts = self.ropes
|
||||||
|
local index = rope.index
|
||||||
|
rope.paused = true
|
||||||
|
ts[index] = ts[#ts]
|
||||||
|
ts[index].index = index
|
||||||
|
ts[#ts] = nil
|
||||||
|
end
|
||||||
|
|
||||||
|
function Bundle:resume(rope)
|
||||||
|
assert(rope.bundle == self, 'rope does not belong to this bundle')
|
||||||
|
assert(rope.paused, 'cannot resume active rope')
|
||||||
|
local ts = self.ropes
|
||||||
|
local newIndex = #ts + 1
|
||||||
|
ts[newIndex] = rope
|
||||||
|
rope.index = newIndex
|
||||||
|
rope.paused = nil
|
||||||
|
end
|
||||||
|
|
||||||
|
function Bundle:update(dseconds)
|
||||||
|
local ropes = self.ropes
|
||||||
|
local i = 1
|
||||||
|
while i <= #ropes do
|
||||||
|
local rope = ropes[i]
|
||||||
|
local t = rope.thread
|
||||||
|
local stat, err
|
||||||
|
if rope.primer then
|
||||||
|
local n = rope.primer.n
|
||||||
|
local args = rope.primer.args
|
||||||
|
rope.primer = nil
|
||||||
|
stat, err = resume(t, rope, unpack(args, 1, n))
|
||||||
|
else
|
||||||
|
stat, err = resume(t, dseconds, rope.signal)
|
||||||
|
end
|
||||||
|
if not stat then
|
||||||
|
local errhand = rope.errhand or self.errhand
|
||||||
|
errhand(err)
|
||||||
|
end
|
||||||
|
if status(t) == 'dead' then -- rope has finished
|
||||||
|
ropes[i] = ropes[#ropes]
|
||||||
|
ropes[i].index = i
|
||||||
|
ropes[#ropes] = nil
|
||||||
|
else
|
||||||
|
i = i + 1
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
function Bundle:rope(fn, ...)
|
||||||
|
local ropes = self.ropes
|
||||||
|
local index = #ropes + 1
|
||||||
|
local rope = newRope(fn, ...)
|
||||||
|
rope.index = index
|
||||||
|
rope.bundle = self
|
||||||
|
ropes[index] = rope
|
||||||
|
return rope
|
||||||
|
end
|
||||||
|
Bundle_mt.__call = Bundle.rope
|
||||||
|
|
||||||
|
-- Rope implmentation
|
||||||
|
|
||||||
|
local timeUnitToSeconds = {
|
||||||
|
s = 1,
|
||||||
|
sec = 1,
|
||||||
|
seconds = 1,
|
||||||
|
second = 1,
|
||||||
|
ms = 0.001,
|
||||||
|
milliseconds = 0.0001,
|
||||||
|
millisecond = 0.0001,
|
||||||
|
min = 60,
|
||||||
|
min = 60,
|
||||||
|
mn = 60,
|
||||||
|
minutes = 60,
|
||||||
|
minute = 60,
|
||||||
|
h = 3600,
|
||||||
|
hs = 3600,
|
||||||
|
hours = 3600,
|
||||||
|
hour = 3600,
|
||||||
|
}
|
||||||
|
|
||||||
|
local timeUnitFrames = {
|
||||||
|
f = true,
|
||||||
|
fs = true,
|
||||||
|
frames = true,
|
||||||
|
frame = true,
|
||||||
|
}
|
||||||
|
|
||||||
|
local function checkCorrectCoroutine(rope)
|
||||||
|
if running() ~= rope.thread then
|
||||||
|
error('rope function called outside of dispatch function or inside coroutine', 3)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
function Rope:wait(time)
|
||||||
|
checkCorrectCoroutine(self)
|
||||||
|
if time == nil then
|
||||||
|
return yield()
|
||||||
|
end
|
||||||
|
local tp = type(time)
|
||||||
|
if tp == 'number' then
|
||||||
|
while (time > 0) do
|
||||||
|
time = time - yield()
|
||||||
|
end
|
||||||
|
elseif tp == 'string' then
|
||||||
|
local numstr, unit = time:match('^(.-)(%a*)$')
|
||||||
|
local num = tonumber(numstr) or 1
|
||||||
|
if timeUnitFrames[unit] then
|
||||||
|
for i = 1, num do
|
||||||
|
yield()
|
||||||
|
end
|
||||||
|
else
|
||||||
|
local time = num * (timeUnitToSeconds[unit] or 1)
|
||||||
|
while (time > 0) do
|
||||||
|
time = time - yield()
|
||||||
|
end
|
||||||
|
end
|
||||||
|
else
|
||||||
|
local f = time
|
||||||
|
local time = 0
|
||||||
|
while not f(time) do
|
||||||
|
time = time + yield()
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Generate ease functions - https://github.com/rxi/flux/
|
||||||
|
local easeFunctions = {}
|
||||||
|
do
|
||||||
|
local expressions = {
|
||||||
|
quad = "p * p",
|
||||||
|
cubic = "p * p * p",
|
||||||
|
quart = "p * p * p * p",
|
||||||
|
quint = "p * p * p * p * p",
|
||||||
|
expo = "2 ^ (10 * (p - 1))",
|
||||||
|
sine = "-math.cos(p * (math.pi * .5)) + 1",
|
||||||
|
circ = "-(math.sqrt(1 - (p * p)) - 1)",
|
||||||
|
back = "p * p * (2.7 * p - 1.7)",
|
||||||
|
elastic = "-(2^(10 * (p - 1)) * math.sin((p - 1.075) * (math.pi * 2) / .3))",
|
||||||
|
}
|
||||||
|
|
||||||
|
local function makeEaseFunction(str, expr)
|
||||||
|
local load = loadstring or load
|
||||||
|
return load("return function(p) " .. str:gsub("%$e", expr) .. " end")()
|
||||||
|
end
|
||||||
|
|
||||||
|
local function generateEase(name, expression)
|
||||||
|
easeFunctions[name] = makeEaseFunction("return $e", expression)
|
||||||
|
easeFunctions[name .. "in"] = easeFunctions[name]
|
||||||
|
easeFunctions[name .. "out"] = makeEaseFunction([[
|
||||||
|
p = 1 - p
|
||||||
|
return 1 - ($e)
|
||||||
|
]], expression)
|
||||||
|
easeFunctions[name .. "inout"] = makeEaseFunction([[
|
||||||
|
p = p * 2
|
||||||
|
if p < 1 then
|
||||||
|
return .5 * ($e)
|
||||||
|
else
|
||||||
|
p = 2 - p
|
||||||
|
return .5 * (1 - ($e)) + .5
|
||||||
|
end
|
||||||
|
]], expression)
|
||||||
|
end
|
||||||
|
|
||||||
|
for k, v in pairs(expressions) do
|
||||||
|
generateEase(k, v)
|
||||||
|
end
|
||||||
|
easeFunctions['linear'] = makeEaseFunction('return $e', 'p')
|
||||||
|
end
|
||||||
|
|
||||||
|
function Rope:tween(options)
|
||||||
|
checkCorrectCoroutine(self)
|
||||||
|
local object = options.object
|
||||||
|
local key = options.key
|
||||||
|
local to = options.to
|
||||||
|
local ease = options.ease or 'linear'
|
||||||
|
local timenumstr, timeunit = options.time:match('^(.-)(%a*)$')
|
||||||
|
local timenum = tonumber(timenumstr) or 1
|
||||||
|
local from = options.from or object[key]
|
||||||
|
local scale = to - from
|
||||||
|
ease = easeFunctions[ease] or ease
|
||||||
|
assert(type(ease) == 'function' or type(ease) == 'table',
|
||||||
|
'expected valid name of callable object for easing function')
|
||||||
|
if timeUnitFrames[timeunit] then
|
||||||
|
local t = 0
|
||||||
|
local dt = 1 / timenum
|
||||||
|
for frame = 1, timenum do
|
||||||
|
object[key] = from + scale * ease(t)
|
||||||
|
t = t + dt
|
||||||
|
yield()
|
||||||
|
end
|
||||||
|
else
|
||||||
|
object[key] = to
|
||||||
|
local tscale = 1 / (timenum * (timeUnitToSeconds[timeunit] or 1))
|
||||||
|
local t = 0
|
||||||
|
while t < 1 do
|
||||||
|
object[key] = from + scale * ease(t)
|
||||||
|
local dt = yield()
|
||||||
|
t = t + tscale * dt
|
||||||
|
end
|
||||||
|
end
|
||||||
|
object[key] = to
|
||||||
|
end
|
||||||
|
|
||||||
|
function Rope:tweenFork(options)
|
||||||
|
return self.bundle(Rope.tween, options)
|
||||||
|
end
|
||||||
|
|
||||||
|
function Rope:fork(fn, ...)
|
||||||
|
checkCorrectCoroutine(self)
|
||||||
|
return self.bundle(fn, ...)
|
||||||
|
end
|
||||||
|
|
||||||
|
function Rope:listen(name)
|
||||||
|
checkCorrectCoroutine(self)
|
||||||
|
local bundle = self.bundle
|
||||||
|
local signals = bundle.signals
|
||||||
|
local slist = signals[name]
|
||||||
|
bundle:suspend(self)
|
||||||
|
if not slist then
|
||||||
|
slist = {}
|
||||||
|
signals[name] = slist
|
||||||
|
end
|
||||||
|
slist[#slist + 1] = self
|
||||||
|
local dt, sig = yield()
|
||||||
|
return sig
|
||||||
|
end
|
||||||
|
|
||||||
|
function Rope:signal(name, data)
|
||||||
|
checkCorrectCoroutine(self)
|
||||||
|
local bundle = self.bundle
|
||||||
|
local signals = bundle.signals
|
||||||
|
local slist = signals[name]
|
||||||
|
if slist then
|
||||||
|
for i = 1, #slist do
|
||||||
|
local rope = slist[i]
|
||||||
|
bundle:resume(rope)
|
||||||
|
rope.signal = data
|
||||||
|
end
|
||||||
|
signals[name] = nil
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
function Rope:parallel(...)
|
||||||
|
checkCorrectCoroutine(self)
|
||||||
|
local bundle = self.bundle
|
||||||
|
local n = select('#', ...)
|
||||||
|
local ropes = {} -- also used as signal
|
||||||
|
local function onDone(rope)
|
||||||
|
local pindex = rope.pindex
|
||||||
|
ropes[pindex] = ropes[#ropes]
|
||||||
|
ropes[pindex].pindex = pindex
|
||||||
|
ropes[#ropes] = nil
|
||||||
|
if #ropes == 0 then
|
||||||
|
rope:signal(ropes, false) -- no error
|
||||||
|
end
|
||||||
|
end
|
||||||
|
for i = 1, n do
|
||||||
|
local fn = select(i, ...)
|
||||||
|
local function wrappedfn(r)
|
||||||
|
fn(r)
|
||||||
|
onDone(r)
|
||||||
|
end
|
||||||
|
local rope = bundle(wrappedfn)
|
||||||
|
local function errhand(err)
|
||||||
|
for i = 1, #ropes do
|
||||||
|
bundle:suspend(ropes[i])
|
||||||
|
end
|
||||||
|
rope:signal(ropes, err) -- we errored out
|
||||||
|
end
|
||||||
|
rope.errhand = errhand
|
||||||
|
rope.pindex = i
|
||||||
|
ropes[#ropes + 1] = rope
|
||||||
|
end
|
||||||
|
return self:listen(ropes)
|
||||||
|
end
|
||||||
|
|
||||||
|
return newBundle
|
||||||
@ -0,0 +1,725 @@
|
|||||||
|
-- https://github.com/nenofite/mm
|
||||||
|
|
||||||
|
|
||||||
|
-- Terminal color (and formatting) codes.
|
||||||
|
local C = {
|
||||||
|
e = '\27[0m', -- reset
|
||||||
|
|
||||||
|
-- Text attributes.
|
||||||
|
br = '\27[1m', -- bright
|
||||||
|
di = '\27[2m', -- dim
|
||||||
|
it = '\27[3m', -- italics
|
||||||
|
un = '\27[4m', -- underscore
|
||||||
|
bl = '\27[5m', -- blink
|
||||||
|
re = '\27[7m', -- reverse
|
||||||
|
hi = '\27[8m', -- hidden
|
||||||
|
|
||||||
|
-- Text colors.
|
||||||
|
k = '\27[30m', -- black
|
||||||
|
r = '\27[31m', -- red
|
||||||
|
g = '\27[32m', -- green
|
||||||
|
y = '\27[33m', -- yellow
|
||||||
|
b = '\27[34m', -- blue
|
||||||
|
m = '\27[35m', -- magenta
|
||||||
|
c = '\27[36m', -- cyan
|
||||||
|
w = '\27[37m', -- white
|
||||||
|
|
||||||
|
-- Background colors.
|
||||||
|
_k = '\27[40m', -- black
|
||||||
|
_r = '\27[41m', -- red
|
||||||
|
_g = '\27[42m', -- green
|
||||||
|
_y = '\27[43m', -- yellow
|
||||||
|
_b = '\27[44m', -- blue
|
||||||
|
_m = '\27[45m', -- magenta
|
||||||
|
_c = '\27[46m', -- cyan
|
||||||
|
_w = '\27[47m', -- white
|
||||||
|
}
|
||||||
|
|
||||||
|
-- If we're on Windows, set all colors to empty strings so we don't spam the
|
||||||
|
-- output with meaningless escape codes.
|
||||||
|
local ON_WINDOWS = string.find(package.path, '\\') ~= nil
|
||||||
|
if ON_WINDOWS then
|
||||||
|
for k, v in pairs(C) do
|
||||||
|
C[k] = ''
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
local METATABLE = {
|
||||||
|
"<metatable>",
|
||||||
|
colors = C.it .. C.y,
|
||||||
|
}
|
||||||
|
local INDENT = " "
|
||||||
|
|
||||||
|
-- The default sequence separator.
|
||||||
|
local SEP = " "
|
||||||
|
|
||||||
|
-- The open and close brackets can be any piece (notably, a sequence with
|
||||||
|
-- colors). The separator must be a plain string.
|
||||||
|
local BOPEN, BSEP, BCLOSE = 1, 2, 3
|
||||||
|
|
||||||
|
-- The default frame brackets and separator.
|
||||||
|
local BRACKETS = {{
|
||||||
|
"{",
|
||||||
|
colors = C.br,
|
||||||
|
}, ",", {
|
||||||
|
"}",
|
||||||
|
colors = C.br,
|
||||||
|
}}
|
||||||
|
|
||||||
|
local STR_HALF = 30
|
||||||
|
local MAX_STR_LEN = STR_HALF * 2
|
||||||
|
|
||||||
|
-- Names to use for named references. The order is important; these are aligned
|
||||||
|
-- with the colors in `NAME_COLORS`.
|
||||||
|
local NAMES = {"Cherry", "Apple", "Lemon", "Blueberry", "Jam", "Cream", "Rhubarb", "Lime", "Butter", "Grape",
|
||||||
|
"Pomegranate", "Sugar", "Cinnamon", "Avocado", "Honey"}
|
||||||
|
|
||||||
|
-- Colors to use for named references. Don't use black nor white.
|
||||||
|
local NAME_COLORS = {C.r, C.g, C.y, C.b, C.m, C.c}
|
||||||
|
|
||||||
|
-- Reserved Lua keywords as a convenient look-up table.
|
||||||
|
local RESERVED = {
|
||||||
|
['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,
|
||||||
|
}
|
||||||
|
|
||||||
|
--
|
||||||
|
-- Namers
|
||||||
|
--
|
||||||
|
|
||||||
|
local function new_namer()
|
||||||
|
local index = 1
|
||||||
|
local suffix = 1
|
||||||
|
local color_index = 1
|
||||||
|
|
||||||
|
return function()
|
||||||
|
-- Pick the name.
|
||||||
|
local result = NAMES[index]
|
||||||
|
if suffix > 1 then
|
||||||
|
result = result .. " " .. tostring(suffix)
|
||||||
|
end
|
||||||
|
|
||||||
|
index = index + 1
|
||||||
|
if index > #NAMES then
|
||||||
|
index = 1
|
||||||
|
suffix = suffix + 1
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Pick the color.
|
||||||
|
local color = NAME_COLORS[color_index]
|
||||||
|
|
||||||
|
color_index = color_index + 1
|
||||||
|
if color_index > #NAME_COLORS then
|
||||||
|
color_index = 1
|
||||||
|
end
|
||||||
|
|
||||||
|
return {
|
||||||
|
result,
|
||||||
|
colors = C.un .. color,
|
||||||
|
}
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
--
|
||||||
|
-- Context
|
||||||
|
--
|
||||||
|
|
||||||
|
local function new_context()
|
||||||
|
return {
|
||||||
|
occur = {},
|
||||||
|
named = {},
|
||||||
|
next_name = new_namer(),
|
||||||
|
|
||||||
|
prev_indent = '',
|
||||||
|
next_indent = INDENT,
|
||||||
|
line_len = 0,
|
||||||
|
max_width = 78,
|
||||||
|
|
||||||
|
result = '',
|
||||||
|
}
|
||||||
|
end
|
||||||
|
|
||||||
|
--
|
||||||
|
-- Translating into pieces
|
||||||
|
--
|
||||||
|
|
||||||
|
-- Translaters take any Lua value and create pieces to represent them.
|
||||||
|
--
|
||||||
|
-- Some values should only be serialized once, both to prevent cycles and to
|
||||||
|
-- prevent redundancy. Or in other cases, these values cannot be serialized
|
||||||
|
-- (such as functions) but if they appear multiple times we want to express
|
||||||
|
-- that they are the same.
|
||||||
|
--
|
||||||
|
-- When a translater encounters such a value for the first time, it is
|
||||||
|
-- registered in the context in `occur`. The value is wrapped in a plain table
|
||||||
|
-- with the `id` field pointing to the original value. If the value is
|
||||||
|
-- serializable, such as a table, then the the `def` field contains the piece
|
||||||
|
-- to display. If it is unserializable or it is not the first time this value
|
||||||
|
-- has occurred, the `def` field is nil.
|
||||||
|
--
|
||||||
|
-- In the cleaning stage, these `id` fields are replaced with their names. If a
|
||||||
|
-- `def` field is present, then a sequence is generated to define the name with
|
||||||
|
-- the piece.
|
||||||
|
|
||||||
|
local translaters = {}
|
||||||
|
local translate, ident_friendly
|
||||||
|
|
||||||
|
function translate(val, ctx)
|
||||||
|
-- Try to find a type-specific translater.
|
||||||
|
local by_type = translaters[type(val)]
|
||||||
|
|
||||||
|
if by_type then
|
||||||
|
-- If there is a type-specific translater, call it.
|
||||||
|
return by_type(val, ctx)
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Otherwise perform the default translation.
|
||||||
|
|
||||||
|
-- Check whether we've already encountered this value.
|
||||||
|
if ctx.occur[val] then
|
||||||
|
-- We have; give it a name if we haven't already.
|
||||||
|
if not ctx.named[val] then
|
||||||
|
ctx.named[val] = ctx.next_name()
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Return the value as a reference.
|
||||||
|
return {
|
||||||
|
id = val,
|
||||||
|
}
|
||||||
|
else
|
||||||
|
-- We haven't; mark it as encountered.
|
||||||
|
ctx.occur[val] = true
|
||||||
|
|
||||||
|
-- Return the value as a definition.
|
||||||
|
return {
|
||||||
|
id = val,
|
||||||
|
def = tostring(val),
|
||||||
|
}
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
translaters['function'] = function(val, ctx)
|
||||||
|
-- Check whether we've already encountered this function.
|
||||||
|
if ctx.occur[val] then
|
||||||
|
-- We have; give it a name if we haven't already.
|
||||||
|
if not ctx.named[val] then
|
||||||
|
ctx.named[val] = ctx.next_name()
|
||||||
|
end
|
||||||
|
else
|
||||||
|
-- We haven't; mark it as encountered.
|
||||||
|
ctx.occur[val] = true
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Return the unserialized function.
|
||||||
|
return {
|
||||||
|
id = val,
|
||||||
|
}
|
||||||
|
end
|
||||||
|
|
||||||
|
function translaters.table(val, ctx)
|
||||||
|
-- Check whether we've already encountered this table.
|
||||||
|
if ctx.occur[val] then
|
||||||
|
-- We have; give it a name if we haven't already.
|
||||||
|
if not ctx.named[val] then
|
||||||
|
ctx.named[val] = ctx.next_name()
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Return the unserialized table.
|
||||||
|
return {
|
||||||
|
id = val,
|
||||||
|
}
|
||||||
|
else
|
||||||
|
-- We haven't; mark it as encountered.
|
||||||
|
ctx.occur[val] = true
|
||||||
|
|
||||||
|
-- Construct the frame for this table.
|
||||||
|
local result = {
|
||||||
|
bracket = BRACKETS,
|
||||||
|
}
|
||||||
|
|
||||||
|
-- The equals-sign between key and value.
|
||||||
|
local eq = {
|
||||||
|
"=",
|
||||||
|
colors = C.di,
|
||||||
|
}
|
||||||
|
|
||||||
|
-- Represent the metatable, if present.
|
||||||
|
local mt = getmetatable(val)
|
||||||
|
if mt then
|
||||||
|
-- Translate the metatable.
|
||||||
|
mt = translate(mt, ctx)
|
||||||
|
table.insert(result, {METATABLE, eq, mt})
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Represent the contents.
|
||||||
|
for k, v in pairs(val) do
|
||||||
|
-- If it is a string key which can be represented without quotes, leave
|
||||||
|
-- it plain.
|
||||||
|
if ident_friendly(k) then
|
||||||
|
-- Leave the key as it is.
|
||||||
|
k = {
|
||||||
|
k,
|
||||||
|
colors = C.m,
|
||||||
|
}
|
||||||
|
else
|
||||||
|
-- Otherwise translate the key.
|
||||||
|
k = translate(k, ctx)
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Translate the value.
|
||||||
|
v = translate(v, ctx)
|
||||||
|
|
||||||
|
table.insert(result, {k, eq, v})
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Wrap the result with its id.
|
||||||
|
return {
|
||||||
|
id = val,
|
||||||
|
def = result,
|
||||||
|
}
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
function translaters.string(val, ctx)
|
||||||
|
if #val <= MAX_STR_LEN then
|
||||||
|
-- The string is short enough; display it all.
|
||||||
|
local a = string.format('%q', val)
|
||||||
|
a = string.gsub(a, '\n', 'n')
|
||||||
|
|
||||||
|
return {
|
||||||
|
a,
|
||||||
|
colors = C.g,
|
||||||
|
}
|
||||||
|
else
|
||||||
|
-- The string is too long. Only show the start and end.
|
||||||
|
local a = string.format('%q', string.sub(val, 1, STR_HALF))
|
||||||
|
a = string.gsub(a, '\n', 'n')
|
||||||
|
local b = string.format('%q', string.sub(val, -STR_HALF))
|
||||||
|
b = string.gsub(b, '\n', 'n')
|
||||||
|
|
||||||
|
return {
|
||||||
|
a,
|
||||||
|
{
|
||||||
|
"...",
|
||||||
|
colors = C.di,
|
||||||
|
},
|
||||||
|
b,
|
||||||
|
colors = C.g,
|
||||||
|
sep = '',
|
||||||
|
tight = true,
|
||||||
|
}
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
function translaters.number(val, ctx)
|
||||||
|
return {
|
||||||
|
tostring(val),
|
||||||
|
colors = C.m .. C.br,
|
||||||
|
}
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Check whether a value can be represented as a Lua identifier, without the
|
||||||
|
-- need for quotes or translation.
|
||||||
|
--
|
||||||
|
-- If the value is not a string, this immediately returns false. Otherwise, the
|
||||||
|
-- string must be a valid Lua name: a sequence of letters, digits, and
|
||||||
|
-- underscores that doesn't start with a digit and isn't a reserved keyword.
|
||||||
|
--
|
||||||
|
-- See http://www.lua.org/manual/5.3/manual.html#3.1
|
||||||
|
function ident_friendly(val)
|
||||||
|
-- The value must be a string.
|
||||||
|
if type(val) ~= 'string' then
|
||||||
|
return false
|
||||||
|
end
|
||||||
|
|
||||||
|
if string.find(val, '^[_%a][_%a%d]*$') then
|
||||||
|
-- The value is a Lua name; check if it is reserved.
|
||||||
|
if RESERVED[val] then
|
||||||
|
-- The value is a resreved keyword.
|
||||||
|
return false
|
||||||
|
else
|
||||||
|
-- The value is a valid name.
|
||||||
|
return true
|
||||||
|
end
|
||||||
|
else
|
||||||
|
-- The value is not a Lua name.
|
||||||
|
return false
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
--
|
||||||
|
-- Cleaning pieces
|
||||||
|
--
|
||||||
|
|
||||||
|
local function clean(piece, ctx)
|
||||||
|
if type(piece) == 'table' then
|
||||||
|
-- Check if it's an id reference.
|
||||||
|
if piece.id then
|
||||||
|
local name = ctx.named[piece.id]
|
||||||
|
local def = piece.def
|
||||||
|
|
||||||
|
-- Check whether it has been given a name.
|
||||||
|
if name then
|
||||||
|
local header = {
|
||||||
|
"<",
|
||||||
|
type(piece.id),
|
||||||
|
" ",
|
||||||
|
name,
|
||||||
|
">",
|
||||||
|
colors = C.it,
|
||||||
|
sep = '',
|
||||||
|
tight = true,
|
||||||
|
}
|
||||||
|
-- Named. Check whether the reference has a definition.
|
||||||
|
if def then
|
||||||
|
-- Create a sequence defining the name to the definition.
|
||||||
|
return {header, {
|
||||||
|
"is",
|
||||||
|
colors = C.di,
|
||||||
|
}, clean(piece.def, ctx)}
|
||||||
|
else
|
||||||
|
-- Show just the name.
|
||||||
|
return header
|
||||||
|
end
|
||||||
|
else
|
||||||
|
-- No name. Check whether the reference has a definition.
|
||||||
|
if def then
|
||||||
|
-- Display the definition without any header.
|
||||||
|
return clean(piece.def, ctx)
|
||||||
|
else
|
||||||
|
-- Display just the type.
|
||||||
|
return {
|
||||||
|
"<",
|
||||||
|
type(piece.id),
|
||||||
|
">",
|
||||||
|
colors = C.it,
|
||||||
|
sep = '',
|
||||||
|
tight = true,
|
||||||
|
}
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Check if it's a frame.
|
||||||
|
elseif piece.bracket then
|
||||||
|
-- Clean each child.
|
||||||
|
for i, child in ipairs(piece) do
|
||||||
|
piece[i] = clean(child, ctx)
|
||||||
|
end
|
||||||
|
return piece
|
||||||
|
|
||||||
|
-- Otherwise it's a sequence.
|
||||||
|
else
|
||||||
|
-- Clean each child.
|
||||||
|
for i, child in ipairs(piece) do
|
||||||
|
piece[i] = clean(child, ctx)
|
||||||
|
end
|
||||||
|
return piece
|
||||||
|
end
|
||||||
|
else
|
||||||
|
-- It's a plain value, not a table; no cleaning is needed.
|
||||||
|
return piece
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
--
|
||||||
|
-- Displaying pieces
|
||||||
|
--
|
||||||
|
|
||||||
|
-- Pieces are either frames (with brackets), sequences (no brackets), or
|
||||||
|
-- strings.
|
||||||
|
|
||||||
|
-- Frames are displayed either short-form as { a = 1 } or long-form as
|
||||||
|
-- {
|
||||||
|
-- a = 1
|
||||||
|
-- }.
|
||||||
|
|
||||||
|
-- Declare all the local functions first, so they can refer to each other.
|
||||||
|
local min_len, display, display_frame, display_sequence, display_string, display_frame_short, display_frame_long,
|
||||||
|
newline, newline_no_indent, write, write_nolength, space_here, space_newline
|
||||||
|
|
||||||
|
-- Dispatch based on the piece's type.
|
||||||
|
function display(piece, ctx)
|
||||||
|
if type(piece) == 'string' then
|
||||||
|
-- String.
|
||||||
|
return display_string(piece, ctx)
|
||||||
|
elseif piece.bracket then
|
||||||
|
-- Frame.
|
||||||
|
return display_frame(piece, ctx)
|
||||||
|
else
|
||||||
|
-- Sequence.
|
||||||
|
return display_sequence(piece, ctx)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Display a frame.
|
||||||
|
function display_frame(frame, ctx)
|
||||||
|
if #frame == 0 then
|
||||||
|
-- If the frame is empty, just display the brackets.
|
||||||
|
local str = {
|
||||||
|
frame.bracket[BOPEN],
|
||||||
|
frame.bracket[BCLOSE],
|
||||||
|
sep = '',
|
||||||
|
tight = true,
|
||||||
|
}
|
||||||
|
return display(str, ctx)
|
||||||
|
end
|
||||||
|
|
||||||
|
local ml = min_len(frame)
|
||||||
|
|
||||||
|
-- Try to fit the frame short-form on this line.
|
||||||
|
if ml <= space_here(ctx) then
|
||||||
|
return display_frame_short(frame, ctx)
|
||||||
|
|
||||||
|
-- Otherwise try to fit it short-form on the next line.
|
||||||
|
elseif ml <= space_newline(ctx) then
|
||||||
|
newline(ctx)
|
||||||
|
return display_frame_short(frame, ctx)
|
||||||
|
|
||||||
|
-- Otherwise display it long-form.
|
||||||
|
else
|
||||||
|
return display_frame_long(frame, ctx)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
function display_frame_short(frame, ctx)
|
||||||
|
-- Short-form frames never wrap onto new lines, so we don't need to do any
|
||||||
|
-- length checking (it's already been done for us).
|
||||||
|
|
||||||
|
-- Write the open bracket.
|
||||||
|
display(frame.bracket[BOPEN], ctx)
|
||||||
|
write(" ", ctx)
|
||||||
|
|
||||||
|
-- Display the first child.
|
||||||
|
display(frame[1], ctx)
|
||||||
|
|
||||||
|
-- Display the remaining children.
|
||||||
|
for i = 2, #frame do
|
||||||
|
local child = frame[i]
|
||||||
|
|
||||||
|
-- Write the separator.
|
||||||
|
write(frame.bracket[BSEP], ctx)
|
||||||
|
write(" ", ctx)
|
||||||
|
|
||||||
|
-- Display the child.
|
||||||
|
display(child, ctx)
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Write the close bracket.
|
||||||
|
write(" ", ctx)
|
||||||
|
display(frame.bracket[BCLOSE], ctx)
|
||||||
|
end
|
||||||
|
|
||||||
|
function display_frame_long(frame, ctx)
|
||||||
|
-- Remember the original value of next_indent.
|
||||||
|
local old_old_indent = ctx.prev_indent
|
||||||
|
local old_indent = ctx.next_indent
|
||||||
|
|
||||||
|
-- Display the open bracket.
|
||||||
|
display(frame.bracket[BOPEN], ctx)
|
||||||
|
|
||||||
|
-- Increase the indentation.
|
||||||
|
ctx.prev_indent = old_indent
|
||||||
|
ctx.next_indent = old_indent .. INDENT
|
||||||
|
|
||||||
|
-- For all but the last child...
|
||||||
|
for i = 1, #frame - 1 do
|
||||||
|
local child = frame[i]
|
||||||
|
|
||||||
|
-- Start a new line with old indentation.
|
||||||
|
newline_no_indent(ctx)
|
||||||
|
write(old_indent, ctx)
|
||||||
|
|
||||||
|
-- Display the child.
|
||||||
|
display(child, ctx)
|
||||||
|
|
||||||
|
-- Write the separator.
|
||||||
|
write(frame.bracket[BSEP], ctx)
|
||||||
|
end
|
||||||
|
|
||||||
|
-- For the last child...
|
||||||
|
do
|
||||||
|
local child = frame[#frame]
|
||||||
|
|
||||||
|
-- Start a new line with old indentation.
|
||||||
|
newline_no_indent(ctx)
|
||||||
|
write(old_indent, ctx)
|
||||||
|
|
||||||
|
-- Display the child.
|
||||||
|
display(child, ctx)
|
||||||
|
-- No separator.
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Write the close bracket.
|
||||||
|
newline_no_indent(ctx)
|
||||||
|
write(old_old_indent, ctx)
|
||||||
|
display(frame.bracket[BCLOSE], ctx)
|
||||||
|
|
||||||
|
-- Return to the old indentation.
|
||||||
|
ctx.prev_indent = old_old_indent
|
||||||
|
ctx.next_indent = old_indent
|
||||||
|
end
|
||||||
|
|
||||||
|
function display_sequence(piece, ctx)
|
||||||
|
if #piece > 0 then
|
||||||
|
-- Check if this is a tight sequence.
|
||||||
|
if piece.tight then
|
||||||
|
-- Try to fit the entire sequence on one line.
|
||||||
|
local ml = min_len(piece, ctx)
|
||||||
|
|
||||||
|
-- If it won't fit here, but it would fit on the next line, then write it
|
||||||
|
-- on the next line; otherwise, write it here.
|
||||||
|
if ml > space_here(ctx) and ml <= space_newline(ctx) then
|
||||||
|
newline(ctx)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Apply the colors, if given.
|
||||||
|
if piece.colors then
|
||||||
|
write_nolength(piece.colors, ctx)
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Display the first child.
|
||||||
|
display(piece[1], ctx)
|
||||||
|
|
||||||
|
-- For each following children:
|
||||||
|
for i = 2, #piece do
|
||||||
|
local child = piece[i]
|
||||||
|
|
||||||
|
-- Apply the colors, if given.
|
||||||
|
if piece.colors then
|
||||||
|
write_nolength(piece.colors, ctx)
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Write a separator.
|
||||||
|
write(piece.sep or SEP, ctx)
|
||||||
|
|
||||||
|
-- Then display the child.
|
||||||
|
display(child, ctx)
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Reset the colors.
|
||||||
|
if piece.colors then
|
||||||
|
write_nolength(C.e, ctx)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
function display_string(piece, ctx)
|
||||||
|
local ml = min_len(piece)
|
||||||
|
|
||||||
|
-- If it won't fit here, but it would fit on the next line, then write it on
|
||||||
|
-- the next line; otherwise, write it here.
|
||||||
|
if ml > space_here(ctx) and ml <= space_newline(ctx) then
|
||||||
|
newline(ctx)
|
||||||
|
end
|
||||||
|
|
||||||
|
write(piece, ctx)
|
||||||
|
end
|
||||||
|
|
||||||
|
-- The minimum length to display this piece, if it is placed all on one line.
|
||||||
|
function min_len(piece, ctx)
|
||||||
|
-- For strings, simply return their length.
|
||||||
|
if type(piece) == 'string' then
|
||||||
|
return #piece
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Otherwise, we have some calculations to do.
|
||||||
|
local result = 0
|
||||||
|
|
||||||
|
if piece.bracket then
|
||||||
|
-- This is a frame.
|
||||||
|
|
||||||
|
-- If it's an empty frame, just the open and close brackets.
|
||||||
|
if #piece == 0 then
|
||||||
|
return min_len(piece.bracket[BOPEN]) + min_len(piece.bracket[BCLOSE])
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Open and close brackets, plus a space for each.
|
||||||
|
result = result + min_len(piece.bracket[BOPEN]) + min_len(piece.bracket[BCLOSE]) + 2
|
||||||
|
|
||||||
|
-- A separator between each item, plus a space for each.
|
||||||
|
result = result + (#piece - 1) * (#piece.bracket[BSEP] + 1)
|
||||||
|
else
|
||||||
|
-- This is a sequence.
|
||||||
|
|
||||||
|
-- If it's an empty sequence, then nothing.
|
||||||
|
if #piece == 0 then
|
||||||
|
return 0
|
||||||
|
end
|
||||||
|
|
||||||
|
-- A single separator between each item.
|
||||||
|
result = result + (#piece - 1) * #(piece.sep or SEP)
|
||||||
|
end
|
||||||
|
|
||||||
|
-- For both frames and sequences:
|
||||||
|
-- Find the minimum length of each child.
|
||||||
|
for _, child in ipairs(piece) do
|
||||||
|
result = result + min_len(child, ctx)
|
||||||
|
end
|
||||||
|
|
||||||
|
return result
|
||||||
|
end
|
||||||
|
|
||||||
|
function newline(ctx)
|
||||||
|
ctx.result = ctx.result .. "\n"
|
||||||
|
ctx.line_len = 0
|
||||||
|
write(ctx.next_indent, ctx)
|
||||||
|
end
|
||||||
|
|
||||||
|
function newline_no_indent(ctx)
|
||||||
|
ctx.result = ctx.result .. "\n"
|
||||||
|
ctx.line_len = 0
|
||||||
|
end
|
||||||
|
|
||||||
|
function write(str, ctx)
|
||||||
|
ctx.result = ctx.result .. str
|
||||||
|
ctx.line_len = ctx.line_len + #str
|
||||||
|
end
|
||||||
|
|
||||||
|
function write_nolength(str, ctx)
|
||||||
|
ctx.result = ctx.result .. str
|
||||||
|
end
|
||||||
|
|
||||||
|
function space_here(ctx)
|
||||||
|
return math.max(0, ctx.max_width - ctx.line_len)
|
||||||
|
end
|
||||||
|
|
||||||
|
function space_newline(ctx)
|
||||||
|
return math.max(0, ctx.max_width - #ctx.next_indent)
|
||||||
|
end
|
||||||
|
|
||||||
|
--
|
||||||
|
-- Main function
|
||||||
|
--
|
||||||
|
|
||||||
|
return function(val)
|
||||||
|
if val == nil then
|
||||||
|
print(nil)
|
||||||
|
else
|
||||||
|
local ctx = new_context()
|
||||||
|
local piece = translate(val, ctx)
|
||||||
|
piece = clean(piece, ctx)
|
||||||
|
display(piece, ctx)
|
||||||
|
print(C.e .. ctx.result .. C.e)
|
||||||
|
end
|
||||||
|
end
|
||||||
@ -0,0 +1,177 @@
|
|||||||
|
--
|
||||||
|
-- https://github.com/rxi/shash
|
||||||
|
--
|
||||||
|
--
|
||||||
|
local shash = {
|
||||||
|
_version = "0.1.1",
|
||||||
|
}
|
||||||
|
shash.__index = shash
|
||||||
|
|
||||||
|
function shash.new(cellsize)
|
||||||
|
local self = setmetatable({}, shash)
|
||||||
|
cellsize = cellsize or 64
|
||||||
|
self.cellsize = cellsize
|
||||||
|
self.tablepool = {}
|
||||||
|
self.cells = {}
|
||||||
|
self.entities = {}
|
||||||
|
return self
|
||||||
|
end
|
||||||
|
|
||||||
|
local function coord_to_key(x, y)
|
||||||
|
return x + y * 1e7
|
||||||
|
end
|
||||||
|
|
||||||
|
local function cell_position(cellsize, x, y)
|
||||||
|
return math.floor(x / cellsize), math.floor(y / cellsize)
|
||||||
|
end
|
||||||
|
|
||||||
|
local function each_overlapping_cell(self, e, fn, ...)
|
||||||
|
local cellsize = self.cellsize
|
||||||
|
local sx, sy = cell_position(cellsize, e[1], e[2])
|
||||||
|
local ex, ey = cell_position(cellsize, e[3], e[4])
|
||||||
|
for y = sy, ey do
|
||||||
|
for x = sx, ex do
|
||||||
|
local idx = coord_to_key(x, y)
|
||||||
|
fn(self, idx, ...)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
local function add_entity_to_cell(self, idx, e)
|
||||||
|
if not self.cells[idx] then
|
||||||
|
self.cells[idx] = {e}
|
||||||
|
else
|
||||||
|
table.insert(self.cells[idx], e)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
local function remove_entity_from_cell(self, idx, e)
|
||||||
|
local t = self.cells[idx]
|
||||||
|
local n = #t
|
||||||
|
-- Only one entity? Remove entity from cell and remove cell
|
||||||
|
if n == 1 then
|
||||||
|
self.cells[idx] = nil
|
||||||
|
return
|
||||||
|
end
|
||||||
|
-- Find and swap-remove entity
|
||||||
|
for i, v in ipairs(t) do
|
||||||
|
if v == e then
|
||||||
|
t[i] = t[n]
|
||||||
|
t[n] = nil
|
||||||
|
return
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
function shash:add(obj, x, y, w, h)
|
||||||
|
-- Create entity. The table is used as an array as this offers a noticable
|
||||||
|
-- performance increase on LuaJIT; the indices are as follows:
|
||||||
|
-- [1] = left, [2] = top, [3] = right, [4] = bottom, [5] = object
|
||||||
|
local e = {x, y, x + w, y + h, obj}
|
||||||
|
-- Add to main entities table
|
||||||
|
self.entities[obj] = e
|
||||||
|
-- Add to cells
|
||||||
|
each_overlapping_cell(self, e, add_entity_to_cell, e)
|
||||||
|
end
|
||||||
|
|
||||||
|
function shash:remove(obj)
|
||||||
|
-- Get entity of obj
|
||||||
|
local e = self.entities[obj]
|
||||||
|
-- Remove from main entities table
|
||||||
|
self.entities[obj] = nil
|
||||||
|
-- Remove from cells
|
||||||
|
each_overlapping_cell(self, e, remove_entity_from_cell, e)
|
||||||
|
end
|
||||||
|
|
||||||
|
function shash:update(obj, x, y, w, h)
|
||||||
|
-- Get entity from obj
|
||||||
|
local e = self.entities[obj]
|
||||||
|
-- No width/height specified? Get width/height from existing bounding box
|
||||||
|
w = w or e[3] - e[1]
|
||||||
|
h = h or e[4] - e[2]
|
||||||
|
-- Check the entity has actually changed cell-position, if it hasn't we don't
|
||||||
|
-- need to touch the cells at all
|
||||||
|
local cellsize = self.cellsize
|
||||||
|
local ax1, ay1 = cell_position(cellsize, e[1], e[2])
|
||||||
|
local ax2, ay2 = cell_position(cellsize, e[3], e[4])
|
||||||
|
local bx1, by1 = cell_position(cellsize, x, y)
|
||||||
|
local bx2, by2 = cell_position(cellsize, x + w, y + h)
|
||||||
|
local dirty = ax1 ~= bx1 or ay1 ~= by1 or ax2 ~= bx2 or ay2 ~= by2
|
||||||
|
-- Remove from old cells
|
||||||
|
if dirty then
|
||||||
|
each_overlapping_cell(self, e, remove_entity_from_cell, e)
|
||||||
|
end
|
||||||
|
-- Update entity
|
||||||
|
e[1], e[2], e[3], e[4] = x, y, x + w, y + h
|
||||||
|
-- Add to new cells
|
||||||
|
if dirty then
|
||||||
|
each_overlapping_cell(self, e, add_entity_to_cell, e)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
function shash:clear()
|
||||||
|
-- Clear all cells and entities
|
||||||
|
for k in pairs(self.cells) do
|
||||||
|
self.cells[k] = nil
|
||||||
|
end
|
||||||
|
for k in pairs(self.entities) do
|
||||||
|
self.entities[k] = nil
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
local function overlaps(e1, e2)
|
||||||
|
return e1[3] > e2[1] and e1[1] < e2[3] and e1[4] > e2[2] and e1[2] < e2[4]
|
||||||
|
end
|
||||||
|
|
||||||
|
local function each_overlapping_in_cell(self, idx, e, set, fn, ...)
|
||||||
|
local t = self.cells[idx]
|
||||||
|
if not t then
|
||||||
|
return
|
||||||
|
end
|
||||||
|
for i, v in ipairs(t) do
|
||||||
|
if e ~= v and overlaps(e, v) and not set[v] then
|
||||||
|
fn(v[5], ...)
|
||||||
|
set[v] = true
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
local function each_overlapping_entity(self, e, fn, ...)
|
||||||
|
-- Init set for keeping track of which entities have already been handled
|
||||||
|
local set = table.remove(self.tablepool) or {}
|
||||||
|
-- Do overlap checks
|
||||||
|
each_overlapping_cell(self, e, each_overlapping_in_cell, e, set, fn, ...)
|
||||||
|
-- Clear set and return to pool
|
||||||
|
for v in pairs(set) do
|
||||||
|
set[v] = nil
|
||||||
|
end
|
||||||
|
table.insert(self.tablepool, set)
|
||||||
|
end
|
||||||
|
|
||||||
|
function shash:each(x, y, w, h, fn, ...)
|
||||||
|
local e = self.entities[x]
|
||||||
|
if e then
|
||||||
|
-- Got object, use its entity
|
||||||
|
each_overlapping_entity(self, e, y, w, h, fn, ...)
|
||||||
|
else
|
||||||
|
-- Got bounding box, make temporary entity
|
||||||
|
each_overlapping_entity(self, {x, y, x + w, y + h}, fn, ...)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
function shash:info(opt, ...)
|
||||||
|
if opt == "cells" or opt == "entities" then
|
||||||
|
local n = 0
|
||||||
|
for _ in pairs(self[opt]) do
|
||||||
|
n = n + 1
|
||||||
|
end
|
||||||
|
return n
|
||||||
|
end
|
||||||
|
if opt == "cell" then
|
||||||
|
local t = self.cells[coord_to_key(...)]
|
||||||
|
return t and #t or 0
|
||||||
|
end
|
||||||
|
error(string.format("invalid opt '%s'", opt))
|
||||||
|
end
|
||||||
|
|
||||||
|
return shash
|
||||||
@ -0,0 +1,138 @@
|
|||||||
|
------------------
|
||||||
|
-- *treap.lua*, a simple treap data structure implemented in Lua.
|
||||||
|
-- Source on [Github](http://github.com/Yonaba/treap.lua)
|
||||||
|
-- Private definitions
|
||||||
|
local random = math.random
|
||||||
|
|
||||||
|
-- Performs right rotation on node y
|
||||||
|
local function rightRotate(y)
|
||||||
|
local x = y.left
|
||||||
|
local tail = x.right
|
||||||
|
x.right = y
|
||||||
|
y.left = tail
|
||||||
|
return x
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Performs left rotation on node x
|
||||||
|
local function leftRotate(x)
|
||||||
|
local y = x.right
|
||||||
|
local tail = y.left
|
||||||
|
y.left = x
|
||||||
|
x.right = tail
|
||||||
|
return y
|
||||||
|
end
|
||||||
|
|
||||||
|
------------------------------- Module functions ------------------------------
|
||||||
|
|
||||||
|
--- Creates a treap node.
|
||||||
|
-- @name node
|
||||||
|
-- @param key a key
|
||||||
|
-- @param[opt] priority a numeric priority for the given key. Defaults to random value.
|
||||||
|
-- @return a node
|
||||||
|
local function newTreap(key, priority)
|
||||||
|
return {
|
||||||
|
key = key,
|
||||||
|
priority = priority or random(),
|
||||||
|
left = nil,
|
||||||
|
right = nil,
|
||||||
|
}
|
||||||
|
end
|
||||||
|
|
||||||
|
--- Finds a key in the treap
|
||||||
|
-- @name find
|
||||||
|
-- @param root a root node in the treap
|
||||||
|
-- @param key a key
|
||||||
|
-- @return a node (or nil)
|
||||||
|
local function find(root, key)
|
||||||
|
if root == nil or root.key == key then
|
||||||
|
return root
|
||||||
|
end
|
||||||
|
if root.key < key then
|
||||||
|
return find(root.right, key)
|
||||||
|
end
|
||||||
|
return find(root.left, key)
|
||||||
|
end
|
||||||
|
|
||||||
|
--- Inserts a key in the treap
|
||||||
|
-- @name insert
|
||||||
|
-- @param root a root node in the treap
|
||||||
|
-- @param key a key
|
||||||
|
-- @return the root node
|
||||||
|
local function insert(root, key, priority)
|
||||||
|
if root == nil then
|
||||||
|
return newTreap(key, priority)
|
||||||
|
end
|
||||||
|
if key <= root.key then
|
||||||
|
root.left = insert(root.left, key)
|
||||||
|
if root.left.priority > root.priority then
|
||||||
|
root = rightRotate(root)
|
||||||
|
end
|
||||||
|
else
|
||||||
|
root.right = insert(root.right, key)
|
||||||
|
if root.right.priority > root.priority then
|
||||||
|
root = leftRotate(root)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
return root
|
||||||
|
end
|
||||||
|
|
||||||
|
--- Deletes a key in the treap
|
||||||
|
-- @name delete
|
||||||
|
-- @param root a root node in the treap
|
||||||
|
-- @param key a key
|
||||||
|
-- @return the root node
|
||||||
|
local function delete(root, key)
|
||||||
|
if root == nil then
|
||||||
|
return root
|
||||||
|
end
|
||||||
|
if key < root.key then
|
||||||
|
root.left = delete(root.left, key)
|
||||||
|
elseif key > root.key then
|
||||||
|
root.right = delete(root.right, key)
|
||||||
|
elseif root.left == nil then
|
||||||
|
root = root.right
|
||||||
|
elseif root.right == nil then
|
||||||
|
root = root.left
|
||||||
|
elseif root.left.priority < root.right.priority then
|
||||||
|
root = leftRotate(root)
|
||||||
|
root.left = delete(root.left, key)
|
||||||
|
else
|
||||||
|
root = rightRotate(root)
|
||||||
|
root.right = delete(root.right, key)
|
||||||
|
end
|
||||||
|
return root
|
||||||
|
end
|
||||||
|
|
||||||
|
--- In-order traversal. It maps `f (node, ...)` on every node along the traversal.
|
||||||
|
-- @name inorder
|
||||||
|
-- @param root a root node in the treap
|
||||||
|
-- @param f a function, defined as `f (node, ...)`
|
||||||
|
-- @param[opt] ... optional arguments to `f`
|
||||||
|
local function inorder(root, f, ...)
|
||||||
|
if root == nil then
|
||||||
|
return
|
||||||
|
end
|
||||||
|
inorder(root.left, f, ...)
|
||||||
|
f(root, ...)
|
||||||
|
inorder(root.right, f, ...)
|
||||||
|
end
|
||||||
|
|
||||||
|
--- Returns the treap size.
|
||||||
|
-- @name size
|
||||||
|
-- @param root a root node in the treap
|
||||||
|
local function size(root)
|
||||||
|
local n = 0
|
||||||
|
inorder(root, function()
|
||||||
|
n = n + 1
|
||||||
|
end)
|
||||||
|
return n
|
||||||
|
end
|
||||||
|
|
||||||
|
return {
|
||||||
|
new = newTreap,
|
||||||
|
insert = insert,
|
||||||
|
delete = delete,
|
||||||
|
find = find,
|
||||||
|
inorder = inorder,
|
||||||
|
size = size,
|
||||||
|
}
|
||||||
Loading…
Reference in New Issue