Compare commits
81 commits
recycle-pr
...
master
Author | SHA1 | Date | |
---|---|---|---|
65bdce0140 | |||
707bfb0059 | |||
ed5e64c6b7 | |||
097f1cbfb4 | |||
aae1d30eb9 | |||
ce0639fd40 | |||
3756979b6a | |||
f4229bfc48 | |||
0646e57826 | |||
7ef36d98f2 | |||
8233847f46 | |||
22b41082a7 | |||
6e718eb3b0 | |||
30ada92db7 | |||
d224ba7805 | |||
0d9afa4ced | |||
bd5b9cc4cb | |||
2e53f92179 | |||
32778cf9fd | |||
eb53739618 | |||
7fcb93c83c | |||
55d0bff81c | |||
cf4842e124 | |||
d6cf4c1505 | |||
603dcf502c | |||
3f872c169c | |||
75c3ef6f4b | |||
ca2496f7d5 | |||
5a12540259 | |||
5ed93c28da | |||
0c42d8452c | |||
1c1307a0e2 | |||
0a59ab8b21 | |||
fda82e0cd7 | |||
1370bfd9a1 | |||
80ab0405ec | |||
1cd3618b6f | |||
123d7508d1 | |||
a5b089520b | |||
6c1c4fa0eb | |||
542682e126 | |||
2a96f6d9eb | |||
317578611d | |||
52bdebfc81 | |||
529f5dc67a | |||
aa6f8babc8 | |||
4d078da6f8 | |||
d90c03c3af | |||
91cf9ec489 | |||
b5f3084ddc | |||
478fa59509 | |||
42a220d39d | |||
2fb5c2494c | |||
aa8d27959c | |||
836fc09ce8 | |||
6149f72d98 | |||
63f7147458 | |||
c7819c98f2 | |||
088c92113b | |||
3e767915dd | |||
6a777089ff | |||
60511c1cc4 | |||
d2f7a1a5d8 | |||
8fbc113a3e | |||
c7bd873452 | |||
7fed47983d | |||
fa94206df2 | |||
88718d391d | |||
c70fc5a97b | |||
7fd2a75581 | |||
8b3d5fd9b1 | |||
a9090d18b9 | |||
2d0426b323 | |||
4158876854 | |||
5c03d02c0e | |||
e1b6f2937b | |||
391ec2ae9e | |||
ea6b102678 | |||
5857d14824 | |||
ae28a6110b | |||
75ae2d8ae7 |
29 changed files with 3081 additions and 4610 deletions
|
@ -2,4 +2,4 @@
|
|||
indent_size = 4
|
||||
indent_style = space
|
||||
charset = utf-8
|
||||
end_of_line = lf
|
||||
end_of_line = lf
|
7
.gitignore
vendored
7
.gitignore
vendored
|
@ -1,3 +1,5 @@
|
|||
*.lsmv
|
||||
!pool/*.lsmv
|
||||
*.log
|
||||
catchem/
|
||||
state/
|
||||
|
@ -6,4 +8,9 @@ crashsave*
|
|||
*.pool*
|
||||
*.lsvs
|
||||
config.lua
|
||||
*.srm
|
||||
*.sfc
|
||||
*.bst
|
||||
namedpipe*
|
||||
build/
|
||||
.VSCodeCounter/
|
||||
|
|
7
LICENSE.namedpipe.md
Normal file
7
LICENSE.namedpipe.md
Normal file
|
@ -0,0 +1,7 @@
|
|||
Copyright © 2014 Peter S. May
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
3525
LibDeflate.lua
3525
LibDeflate.lua
File diff suppressed because it is too large
Load diff
59
README.md
59
README.md
|
@ -2,25 +2,45 @@
|
|||
|
||||
An AI based on SethBling's MarI/O to play Donkey Kong Country 2 with lsnes.
|
||||
|
||||
See [YouTube](https://www.youtube.com/watch?v=Q69_wmEkp-k) for an example run.
|
||||
See [YouTube](https://www.youtube.com/watch?v=-\_UyUbObLeE) for an example run.
|
||||
|
||||
## Requirements
|
||||
|
||||
* lsnes with **Lua 5.2** (do not try to build with 5.3, it does not work!)
|
||||
* socat for Linux, or a fairly recent version of Windows that has PowerShell
|
||||
* A Donkey Kong Country 2 1.1 US ROM (matching hash b79c2bb86f6fc76e1fc61c62fc16d51c664c381e58bc2933be643bbc4d8b610c)
|
||||
|
||||
### Windows
|
||||
|
||||
You will want to install the rrtest-1613424691 version of lsnes. Older versions were crashing for me. The easiest way to do this is to use Chocolatey:
|
||||
|
||||
```powershell
|
||||
# Make sure you use an Administrator shell!
|
||||
|
||||
# Skip this command if you have Chocolatey already.
|
||||
Set-ExecutionPolicy Bypass -Scope Process -Force; [System.Net.ServicePointManager]::SecurityProtocol = [System.Net.ServicePointManager]::SecurityProtocol -bor 3072; iex ((New-Object System.Net.WebClient).DownloadString('https://chocolatey.org/install.ps1'))
|
||||
|
||||
# Install lsnes
|
||||
choco install --version 2.0.24-rrtest-1613424691 lsnes
|
||||
```
|
||||
|
||||
## Instructions
|
||||
|
||||
1. Start lsnes
|
||||
2. Go to `Configure -> Settings -> Advanced` and change `LUA -> Maximum memory use` to `1024MB`
|
||||
3. Load the DKC2 ROM: `File -> Load -> ROM...`
|
||||
4. Load the `neat-donk.lua` script: `Tools -> Run Lua script...`
|
||||
5. You may also want to turn off sound since it may get annoying. `Configure -> Sounds enabled`
|
||||
6. Look at config.lua for some settings you can change. Not all have been tested, but you should be able to change the number on the `_M.Filename =` line to get a different state file from the `_M.State` list. Also note the `Threads =` line. Change this to 1 to prevent multiple instances of lsnes from getting launched at once. If you use more than 1 thread, you may also want to launch `lsnes` using xpra to manage the windows, with the `xpra-run.sh` script.
|
||||
3. Load the `neat-donk.lua` script: `Tools -> Run Lua script...`
|
||||
|
||||
If you want a better idea of what's going on with the tile and sprite calculations you may want to load `donkutil.lua`. It will mark the tiles with their offsets on the screen, give a crosshair with tile measurements (every 32 pixels), and list information about the sprites (you can use the 1 and 2 keys above the letter keys to page through them). Sprites labeled in green are considered "good", red is "bad", normal color is neutral. Solid red means that it's the active sprite in the info viewer.
|
||||
## Config
|
||||
|
||||
<img src="https://github.com/empathicqubit/neat-donk/blob/master/doc/donkutil.png?raw=true" />
|
||||
Look at config.lua for some settings you can change. Not all have been tested.
|
||||
|
||||
* `_M.ROM`: The ROM path, `rom.sfc` by default.
|
||||
* `_M.Filename`: Change the number to a different one from the `_M.State` list
|
||||
to load a different file.
|
||||
* `_M.NeatConfig.Threads`: Change this to 1 to prevent multiple instances of
|
||||
lsnes from getting launched at once, or increase it to run more instances.
|
||||
If you use more than 1 thread, you may also want to launch `lsnes` using xpra
|
||||
to manage the windows, with the [xpra-run.sh](xpra-run.sh) script.
|
||||
|
||||
## Keys
|
||||
1: Stop/start
|
||||
|
@ -33,9 +53,29 @@ If you want a better idea of what's going on with the tile and sprite calculatio
|
|||
|
||||
9: Restart
|
||||
|
||||
## Other Tools
|
||||
|
||||
### Status Overlay
|
||||
|
||||
The status overlay is located at [tools/status-overlay.lua](tools/status-overlay.lua).
|
||||
It will help you see the tile and sprite calculations by marking the tiles with
|
||||
their offsets on the screen, giving a crosshair with tile measurements every
|
||||
32 pixels, and listing information about the sprites. You can use the 1 and 2
|
||||
keys above the letter keys to page through them. Sprites labeled in green are
|
||||
considered "good", red is "bad", normal color is neutral. Solid red means that
|
||||
it's the active sprite in the info viewer.
|
||||
|
||||
<img src="https://github.com/empathicqubit/neat-donk/blob/master/doc/donkutil.png?raw=true" />
|
||||
|
||||
### BSNES Launcher
|
||||
|
||||
Located at [tools/bsnes-launcher.lua](tools/bsnes-launcher.lua), this script
|
||||
gives you an easy way to launch bsnes-plus with breakpoints preset. Run it in
|
||||
lsnes and it will display a message to the Lua console and stderr on how to use it.
|
||||
|
||||
## Notes
|
||||
* Only tested on Pirate Panic
|
||||
* The pool files are gzipped Serpent data
|
||||
* The pool files are PKZIP files with one file, data.serpent, containing Serpent-formatted data
|
||||
|
||||
## Credits
|
||||
|
||||
|
@ -44,9 +84,12 @@ If you want a better idea of what's going on with the tile and sprite calculatio
|
|||
* [Basic tilemap info from p4plus2/DKC2-disassembly](https://github.com/p4plus2/DKC2-disassembly)
|
||||
* [Serpent](https://github.com/pkulchenko/serpent)
|
||||
* [LibDeflate](https://github.com/SafeteeWoW/LibDeflate)
|
||||
* [Billiam's Promise library](https://github.com/Billiam/promise.lua)
|
||||
* [https://github.com/psmay/windows-named-pipe-utils](https://github.com/psmay/windows-named-pipe-utils)
|
||||
|
||||
## TODO
|
||||
|
||||
- [x] Incur penalty for non-hazardous enemy collisions to encourage neutralizing Klobber
|
||||
- [ ] Award for picking up items
|
||||
- [ ] Make enemies neutral when held? (Klobber, Click-Clack, etc.)
|
||||
- [ ] Multiple nets to handle different contexts s/a clicking map items
|
||||
|
|
24
config.lua
24
config.lua
|
@ -17,12 +17,18 @@ _M.ROM = _M.ScriptDir .. "/rom.sfc"
|
|||
Rearrange for other savestates. (will be redone soon)
|
||||
--]]
|
||||
_M.State = {
|
||||
"PiratePanic.lsmv",
|
||||
"PiratePanicDitch.lsmv",
|
||||
"PiratePanicKremcoin.lsmv",
|
||||
-- W1.1 Pirate Panic
|
||||
"PiratePanic.lsmv", -- [1]
|
||||
"PiratePanicDitch.lsmv", -- [2]
|
||||
"PiratePanicKremcoin.lsmv", --[3]
|
||||
|
||||
-- W1.2 Mainbrace Mayhem
|
||||
"MainbraceMayhem.lsmv", -- [4]
|
||||
"MainbraceMayhemBonus.lsmv", -- [5]
|
||||
"MainbraceMayhemTopOfRope.lsmv", -- [6]
|
||||
}
|
||||
|
||||
_M.Filename = _M.PoolDir .. _M.State[1]
|
||||
_M.Filename = _M.PoolDir .. _M.State[4]
|
||||
|
||||
--[[
|
||||
Start game with specific powerup.
|
||||
|
@ -35,9 +41,13 @@ _M.Filename = _M.PoolDir .. _M.State[1]
|
|||
_M.StartPowerup = 0
|
||||
|
||||
_M.NeatConfig = {
|
||||
Threads = 8,
|
||||
DisableSound = true,
|
||||
Threads = 7,
|
||||
ShowInterface = false,
|
||||
AutoSave = true,
|
||||
--Filename = "DP1.state",
|
||||
SaveFile = _M.Filename .. ".pool",
|
||||
SaveFile = _M.PoolDir .. "bigbrain.pool",
|
||||
|
||||
Filename = _M.Filename,
|
||||
Population = 300,
|
||||
DeltaDisjoint = 2.0,
|
||||
|
@ -53,7 +63,7 @@ BiasMutationChance = 0.40,
|
|||
StepSize = 0.1,
|
||||
DisableMutationChance = 0.4,
|
||||
EnableMutationChance = 0.2,
|
||||
TimeoutConstant = 20,
|
||||
TimeoutConstant = 30,
|
||||
MaxNodes = 1000000,
|
||||
}
|
||||
|
||||
|
|
507
game.lua
507
game.lua
|
@ -1,10 +1,15 @@
|
|||
--Notes here
|
||||
local memory, bit, memory2, input, callback, movie, utime = memory, bit, memory2, input, callback, movie, utime
|
||||
|
||||
local base = string.gsub(@@LUA_SCRIPT_FILENAME@@, "(.*[/\\])(.*)", "%1")
|
||||
|
||||
local Promise = nil
|
||||
|
||||
local util = nil
|
||||
local mathFunctions = dofile(base.."/mathFunctions.lua")
|
||||
local config = dofile(base.."/config.lua")
|
||||
local spritelist = dofile(base.."/spritelist.lua")
|
||||
local util = dofile(base.."/util.lua")
|
||||
local mem = dofile(base.."/mem.lua")
|
||||
local _M = {
|
||||
leader = 0,
|
||||
tilePtr = 0,
|
||||
|
@ -22,42 +27,41 @@ local _M = {
|
|||
spritelist.InitSpriteList()
|
||||
spritelist.InitExtSpriteList()
|
||||
|
||||
local KREMCOINS = 0x7e08cc
|
||||
local TILE_SIZE = 32
|
||||
local ENEMY_SIZE = 64
|
||||
local TILE_COLLISION_MATH_POINTER = 0x7e17b2
|
||||
local SPRITE_BASE = 0x7e0de2
|
||||
local SPRITE_SIZE = 94
|
||||
local SPRITE_DYING = 0x1000
|
||||
local VERTICAL_POINTER = 0xc414
|
||||
local TILEDATA_POINTER = 0x7e0098
|
||||
local HAVE_BOTH = 0x7e08c2
|
||||
local CAMERA_X = 0x7e17ba
|
||||
local CAMERA_Y = 0x7e17c0
|
||||
local LEAD_CHAR = 0x7e08a4
|
||||
local PARTY_X = 0x7e0a2a
|
||||
local PARTY_Y = 0x7e0a2c
|
||||
local SOLID_LESS_THAN = 0x7e00a0
|
||||
local KONG_LETTERS = 0x7e0902
|
||||
local MATH_LIVES = 0x7e08be
|
||||
local DISPLAY_LIVES = 0x7e0c0
|
||||
local MAIN_AREA_NUMBER = 0x7e08a8
|
||||
local CURRENT_AREA_NUMBER = 0x7e08c8
|
||||
|
||||
function _M.getPositions()
|
||||
_M.leader = memory.readword(LEAD_CHAR)
|
||||
_M.tilePtr = memory.readhword(TILEDATA_POINTER)
|
||||
_M.vertical = memory.readword(TILE_COLLISION_MATH_POINTER) == VERTICAL_POINTER
|
||||
_M.partyX = memory.readword(PARTY_X)
|
||||
_M.partyY = memory.readword(PARTY_Y)
|
||||
_M.leader = memory.readword(mem.addr.leadChar)
|
||||
_M.tilePtr = memory.readhword(mem.addr.tiledataPointer)
|
||||
_M.vertical = memory.readword(mem.addr.tileCollisionMathPointer) == mem.addr.verticalPointer
|
||||
_M.partyX = memory.readword(mem.addr.partyX)
|
||||
_M.partyY = memory.readword(mem.addr.partyY)
|
||||
|
||||
_M.cameraX = memory.readword(CAMERA_X)
|
||||
_M.cameraY = memory.readword(CAMERA_Y)
|
||||
_M.cameraX = memory.readword(mem.addr.cameraX)
|
||||
_M.cameraY = memory.readword(mem.addr.cameraY)
|
||||
|
||||
_M.screenX = (_M.partyX-256-_M.cameraX)*2
|
||||
_M.screenY = (_M.partyY-256-_M.cameraY)*2
|
||||
end
|
||||
|
||||
function _M.setPartyPosition(x, y)
|
||||
memory.writeword(mem.addr.partyX, x)
|
||||
memory.writeword(mem.addr.partyY, y)
|
||||
_M.setSpritePosition(0, x, y)
|
||||
_M.setSpritePosition(1, x, y)
|
||||
end
|
||||
|
||||
function _M.setCameraPosition(x, y)
|
||||
memory.writeword(mem.addr.cameraX, x)
|
||||
memory.writeword(mem.addr.cameraY, y)
|
||||
memory.writeword(mem.addr.cameraX2, x)
|
||||
memory.writeword(mem.addr.cameraY2, y)
|
||||
end
|
||||
|
||||
function _M.setSpritePosition(index, x, y)
|
||||
local offsets = mem.offset.sprite
|
||||
local spriteBase = mem.addr.spriteBase + index * mem.size.sprite
|
||||
memory.writeword(spriteBase + offsets.x, x)
|
||||
memory.writeword(spriteBase + offsets.y, y)
|
||||
end
|
||||
|
||||
function _M.getBananas()
|
||||
local bananas = memory.readword(0x7e08bc)
|
||||
return bananas
|
||||
|
@ -69,15 +73,202 @@ function _M.getCoins()
|
|||
end
|
||||
|
||||
function _M.getKremCoins()
|
||||
local krem = memory.readword(KREMCOINS)
|
||||
local krem = memory.readword(mem.addr.kremcoins)
|
||||
return krem
|
||||
end
|
||||
|
||||
function _M.getGoalHit()
|
||||
local sprites = _M.getSprites()
|
||||
function _M.getAreaWidth()
|
||||
return memory.readword(mem.addr.areaWidth) + 256
|
||||
end
|
||||
|
||||
function _M.getAreaHeight()
|
||||
return memory.readword(mem.addr.areaHeight)
|
||||
end
|
||||
|
||||
function _M.getAreaLength()
|
||||
return memory.readword(mem.addr.areaLength)
|
||||
end
|
||||
|
||||
local onFrameAdvancedQueue = {}
|
||||
function _M.advanceFrame()
|
||||
local promise = Promise.new()
|
||||
table.insert(onFrameAdvancedQueue, promise)
|
||||
return promise
|
||||
end
|
||||
|
||||
local function processFrameAdvanced()
|
||||
for i=#onFrameAdvancedQueue,1,-1 do
|
||||
table.remove(onFrameAdvancedQueue, i):resolve()
|
||||
end
|
||||
end
|
||||
|
||||
local onSetRewindQueue = {}
|
||||
function _M.setRewindPoint()
|
||||
local promise = Promise.new()
|
||||
table.insert(onSetRewindQueue, promise)
|
||||
movie.unsafe_rewind()
|
||||
return promise
|
||||
end
|
||||
|
||||
local function processSetRewind(state)
|
||||
for i=#onSetRewindQueue,1,-1 do
|
||||
table.remove(onSetRewindQueue, i):resolve(state)
|
||||
end
|
||||
end
|
||||
|
||||
local onRewindQueue = {}
|
||||
function _M.rewind(rew)
|
||||
local promise = Promise.new()
|
||||
movie.unsafe_rewind(rew)
|
||||
table.insert(onRewindQueue, promise)
|
||||
return promise
|
||||
end
|
||||
|
||||
local function processRewind()
|
||||
for i=#onRewindQueue,1,-1 do
|
||||
table.remove(onRewindQueue, i):resolve()
|
||||
end
|
||||
end
|
||||
|
||||
local function findPreferredExitLoop(frame, searchX, searchY, found, uniqueExits)
|
||||
return _M.advanceFrame():next(function()
|
||||
frame = frame + 1
|
||||
if frame % 2 ~=0 then
|
||||
return
|
||||
end
|
||||
|
||||
local areaWidth = _M.getAreaWidth()
|
||||
memory.writebyte(0x7e19ce, 0x16)
|
||||
memory.writebyte(0x7e0e12, 0x99)
|
||||
memory.writebyte(0x7e0e70, 0x99)
|
||||
local sprites = _M.getSprites()
|
||||
for i=1,#sprites,1 do
|
||||
local sprite = sprites[i]
|
||||
local name = spritelist.SpriteNames[sprite.control]
|
||||
if sprite.control == spritelist.GoodSprites.goalTarget or
|
||||
sprite.control == spritelist.GoodSprites.areaExit then
|
||||
found = true
|
||||
uniqueExits[sprite.y * areaWidth + sprite.x] = sprite
|
||||
end
|
||||
end
|
||||
_M.setPartyPosition(searchX, searchY)
|
||||
_M.setCameraPosition(searchX, searchY)
|
||||
searchX = searchX + 0x100
|
||||
|
||||
if searchX > areaWidth then
|
||||
searchX = 0
|
||||
searchY = searchY + 0xe0
|
||||
if searchY > _M.getAreaHeight() then
|
||||
table.sort(uniqueExits, function(a, b)
|
||||
return a.control < b.control
|
||||
end)
|
||||
|
||||
-- Return upper right corner if we can't find anything
|
||||
if found then
|
||||
for id,sprite in pairs(uniqueExits) do
|
||||
return { x = sprite.x, y = sprite.y }
|
||||
end
|
||||
else
|
||||
return { x = areaWidth, y = 0}
|
||||
end
|
||||
end
|
||||
end
|
||||
end):next(function(ret)
|
||||
if ret == nil then
|
||||
return findPreferredExitLoop(frame, searchX, searchY, found, uniqueExits)
|
||||
else
|
||||
return ret
|
||||
end
|
||||
end)
|
||||
end
|
||||
|
||||
function _M.findPreferredExit()
|
||||
local point = nil
|
||||
local result = nil
|
||||
return _M.setRewindPoint():next(function(p)
|
||||
point = p
|
||||
return findPreferredExitLoop(0, 0, 0, false, {})
|
||||
end):next(function(r)
|
||||
result = r
|
||||
return _M.rewind(point)
|
||||
end):next(function()
|
||||
return result
|
||||
end)
|
||||
end
|
||||
|
||||
--- Starting from a specified point in the area, weave back and forth until we reach
|
||||
--- fall to the bottom. Return all points where we hit the floor on the way down.
|
||||
---@param startX integer x coordinate of the starting point
|
||||
---@param startY integer y coordinate of the starting point
|
||||
---@return table table A list of all the points that we collided with a platform
|
||||
function _M.getWaypoints(startX, startY)
|
||||
local areaWidth = _M.getAreaWidth()
|
||||
local areaHeight = _M.getAreaHeight()
|
||||
local increment = mem.size.tile
|
||||
local direction = increment
|
||||
local terminus = areaWidth
|
||||
-- If we're on the right half, move left
|
||||
-- If we're on the left half, move right
|
||||
if startX > areaWidth / 2 then
|
||||
direction = -direction
|
||||
terminus = 0x100
|
||||
end
|
||||
|
||||
local collisions = {}
|
||||
local currentY = startY
|
||||
local currentX = startX
|
||||
local switches = 0
|
||||
while currentY < areaHeight - increment do
|
||||
switches = 0
|
||||
-- Drop down until we collide with the floor
|
||||
while currentY < areaHeight - increment and _M.getAbsoluteTile(currentX, currentY) ~= 1 do
|
||||
currentY = currentY + increment
|
||||
end
|
||||
-- Track the collision
|
||||
table.insert(collisions, {
|
||||
x = currentX,
|
||||
y = currentY,
|
||||
})
|
||||
-- Break if we've hit the bottom
|
||||
if currentY > areaHeight - increment then
|
||||
break
|
||||
end
|
||||
-- Move in the direction until we reach a gap or the edge of the area
|
||||
while currentY < areaHeight - increment do
|
||||
currentX = currentX + direction
|
||||
-- Switch directions if we're out of bounds
|
||||
if direction < 0 and currentX < terminus + increment or direction > 0 and currentX > terminus - increment then
|
||||
switches = switches + 1
|
||||
if switches > 2 then
|
||||
currentY = currentY + increment
|
||||
break
|
||||
end
|
||||
if terminus == 0x100 then
|
||||
terminus = areaWidth
|
||||
direction = increment
|
||||
else
|
||||
terminus = 0x100
|
||||
direction = -increment
|
||||
end
|
||||
elseif _M.getAbsoluteTile(currentX, currentY) ~= 1 then
|
||||
-- Check to make sure there isn't a floor immediately underneath.
|
||||
-- If there is we're probably on an incline.
|
||||
if _M.getAbsoluteTile(currentX, currentY + increment) == 1 then
|
||||
currentY = currentY + increment
|
||||
else
|
||||
break
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
return collisions
|
||||
end
|
||||
|
||||
function _M.getGoalHit(sprites)
|
||||
for i=1,#sprites,1 do
|
||||
local sprite = sprites[i]
|
||||
if sprite.control ~= 0x0164 then
|
||||
if sprite.control ~= spritelist.GoodSprites.goalBarrel then
|
||||
goto continue
|
||||
end
|
||||
-- Check if the goal barrel is moving up
|
||||
|
@ -91,7 +282,7 @@ function _M.getGoalHit()
|
|||
end
|
||||
|
||||
function _M.getKong()
|
||||
local kong = memory.readword(KONG_LETTERS)
|
||||
local kong = memory.readword(mem.addr.kongLetters)
|
||||
return bit.popcount(kong)
|
||||
end
|
||||
|
||||
|
@ -107,7 +298,7 @@ end
|
|||
|
||||
function _M.getBoth()
|
||||
-- FIXME consider invincibility barrels
|
||||
local both = memory.readword(HAVE_BOTH)
|
||||
local both = memory.readword(mem.addr.haveBoth)
|
||||
return bit.band(both, 0x4000)
|
||||
end
|
||||
|
||||
|
@ -132,13 +323,12 @@ function _M.writePowerup(powerup)
|
|||
-- memory.writebyte(0x0019, powerup)
|
||||
end
|
||||
|
||||
|
||||
function _M.getHit(alreadyHit)
|
||||
return not alreadyHit and memory.readword(MATH_LIVES) < memory.readword(DISPLAY_LIVES)
|
||||
return not alreadyHit and memory.readword(mem.addr.mathLives) < memory.readword(mem.addr.displayLives)
|
||||
end
|
||||
|
||||
function _M.getHitTimer(lastBoth)
|
||||
return (memory.readsbyte(DISPLAY_LIVES) - memory.readsbyte(MATH_LIVES))
|
||||
return (memory.readsbyte(mem.addr.displayLives) - memory.readsbyte(mem.addr.mathLives))
|
||||
+ lastBoth - _M.getBoth()
|
||||
end
|
||||
|
||||
|
@ -180,10 +370,6 @@ function _M.tileIsSolid(x, y, tileVal, tileOffset)
|
|||
return false
|
||||
end
|
||||
|
||||
if questionable_tiles then
|
||||
return true
|
||||
end
|
||||
|
||||
local a2 = bit.band(x, 0x1f)
|
||||
|
||||
if bit.band(tileVal, 0x4000) ~= 0 then
|
||||
|
@ -192,7 +378,7 @@ function _M.tileIsSolid(x, y, tileVal, tileOffset)
|
|||
|
||||
tileVal = bit.band(tileVal, 0x3fff)
|
||||
|
||||
local solidLessThan = memory.readword(SOLID_LESS_THAN)
|
||||
local solidLessThan = memory.readword(mem.addr.solidLessThan)
|
||||
|
||||
if tileVal >= solidLessThan then
|
||||
return false
|
||||
|
@ -227,9 +413,9 @@ function _M.tileIsSolid(x, y, tileVal, tileOffset)
|
|||
return true
|
||||
end
|
||||
|
||||
function _M.getTile(dx, dy)
|
||||
local tileX = math.floor((_M.partyX + dx * TILE_SIZE) / TILE_SIZE) * TILE_SIZE
|
||||
local tileY = math.floor((_M.partyY + dy * TILE_SIZE) / TILE_SIZE) * TILE_SIZE
|
||||
function _M.getAbsoluteTile(x, y)
|
||||
local tileX = math.floor(x / mem.size.tile) * mem.size.tile
|
||||
local tileY = math.floor(y / mem.size.tile) * mem.size.tile
|
||||
|
||||
local offset = _M.tileOffsetCalculation(tileX, tileY, _M.vertical)
|
||||
|
||||
|
@ -242,8 +428,12 @@ function _M.getTile(dx, dy)
|
|||
return 1
|
||||
end
|
||||
|
||||
function _M.getTile(dx, dy)
|
||||
return _M.getAbsoluteTile(_M.partyX + dx * mem.size.tile, _M.partyY + dy * mem.size.tile)
|
||||
end
|
||||
|
||||
function _M.getCurrentArea()
|
||||
return memory.readword(CURRENT_AREA_NUMBER)
|
||||
return memory.readword(mem.addr.currentAreaNumber)
|
||||
end
|
||||
|
||||
function _M.getJumpHeight()
|
||||
|
@ -254,28 +444,49 @@ function _M.getJumpHeight()
|
|||
return sprite.jumpHeight
|
||||
end
|
||||
|
||||
function _M.getSprite(idx)
|
||||
local base_addr = idx * SPRITE_SIZE + SPRITE_BASE
|
||||
function _M.diedFromHit()
|
||||
local sprite = _M.getSprite(_M.leader)
|
||||
if sprite == nil then
|
||||
return 0
|
||||
end
|
||||
|
||||
local control = memory.readword(base_addr)
|
||||
return sprite.motion == 0x05
|
||||
end
|
||||
|
||||
function _M.fell()
|
||||
local sprite = _M.getSprite(_M.leader)
|
||||
if sprite == nil then
|
||||
return 0
|
||||
end
|
||||
|
||||
return sprite.motion == 0x3b
|
||||
end
|
||||
|
||||
function _M.getSprite(idx)
|
||||
local baseAddr = idx * mem.size.sprite + mem.addr.spriteBase
|
||||
local spriteData = memory.readregion(baseAddr, mem.size.sprite)
|
||||
|
||||
local offsets = mem.offset.sprite
|
||||
local control = util.regionToWord(spriteData, offsets.control)
|
||||
|
||||
if control == 0 then
|
||||
return nil
|
||||
end
|
||||
|
||||
local x = memory.readword(base_addr + 0x06)
|
||||
local y = memory.readword(base_addr + 0x0a)
|
||||
local x = util.regionToWord(spriteData, offsets.x)
|
||||
local y = util.regionToWord(spriteData, offsets.y)
|
||||
local sprite = {
|
||||
control = control,
|
||||
screenX = x - 256 - _M.cameraX - 256,
|
||||
screenY = y - 256 - _M.cameraY - 256,
|
||||
jumpHeight = memory.readword(base_addr + 0x0e),
|
||||
jumpHeight = util.regionToWord(spriteData, offsets.jumpHeight),
|
||||
-- style bits
|
||||
-- 0x4000 0: Right facing 1: Flipped
|
||||
-- 0x1000 0: Alive 1: Dying
|
||||
style = memory.readword(base_addr + 0x12),
|
||||
velocityX = memory.readsword(base_addr + 0x20),
|
||||
velocityY = memory.readsword(base_addr + 0x24),
|
||||
style = util.regionToWord(spriteData, offsets.style),
|
||||
velocityX = util.regionToSWord(spriteData, offsets.velocityX),
|
||||
velocityY = util.regionToSWord(spriteData, offsets.velocityY),
|
||||
motion = util.regionToWord(spriteData, offsets.motion),
|
||||
x = x,
|
||||
y = y,
|
||||
good = spritelist.Sprites[control]
|
||||
|
@ -305,9 +516,8 @@ end
|
|||
|
||||
-- Currently only for single bananas since they don't
|
||||
-- count as regular computed sprites
|
||||
function _M.getExtendedSprites()
|
||||
function _M.getExtendedSprites(sprites)
|
||||
local oam = memory2.OAM:readregion(0x00, 0x220)
|
||||
local sprites = _M.getSprites()
|
||||
local extended = {}
|
||||
|
||||
for idx=0,0x200/4-1,1 do
|
||||
|
@ -328,15 +538,15 @@ function _M.getExtendedSprites()
|
|||
end
|
||||
|
||||
-- Hide the interface icons
|
||||
if screenSprite.x < 0 or screenSprite.y < TILE_SIZE then
|
||||
if screenSprite.x < 0 or screenSprite.y < mem.size.tile then
|
||||
goto continue
|
||||
end
|
||||
|
||||
-- Hide sprites near computed sprites
|
||||
for s=1,#sprites,1 do
|
||||
local sprite = sprites[s]
|
||||
if screenSprite.x > sprite.screenX - ENEMY_SIZE and screenSprite.x < sprite.screenX + ENEMY_SIZE / 2 and
|
||||
screenSprite.y > sprite.screenY - ENEMY_SIZE and screenSprite.y < sprite.screenY then
|
||||
if screenSprite.x > sprite.screenX - mem.size.enemy and screenSprite.x < sprite.screenX + mem.size.enemy / 2 and
|
||||
screenSprite.y > sprite.screenY - mem.size.enemy and screenSprite.y < sprite.screenY then
|
||||
goto continue
|
||||
end
|
||||
::nextsprite::
|
||||
|
@ -350,61 +560,90 @@ function _M.getExtendedSprites()
|
|||
end
|
||||
|
||||
function _M.getInputs()
|
||||
_M.getPositions()
|
||||
|
||||
local sprites = _M.getSprites()
|
||||
local extended = _M.getExtendedSprites()
|
||||
|
||||
local inputs = {}
|
||||
local inputDeltaDistance = {}
|
||||
|
||||
_M.getPositions()
|
||||
|
||||
local sprites = _M.getSprites()
|
||||
local extended = _M.getExtendedSprites(sprites)
|
||||
|
||||
local inputs = {}
|
||||
local inputDeltaDistance = {}
|
||||
|
||||
for dy = -config.BoxRadius, config.BoxRadius, 1 do
|
||||
for dx = -config.BoxRadius, config.BoxRadius, 1 do
|
||||
inputs[#inputs+1] = 0
|
||||
inputDeltaDistance[#inputDeltaDistance+1] = 1
|
||||
|
||||
tile = _M.getTile(dx, dy)
|
||||
if tile == 1 then
|
||||
if _M.getTile(dx, dy-1) == 1 then
|
||||
inputs[#inputs+1] = 0
|
||||
inputDeltaDistance[#inputDeltaDistance+1] = 1
|
||||
|
||||
local tile = _M.getTile(dx, dy)
|
||||
if tile == 1 then
|
||||
if inputs[#inputs-config.BoxRadius*2-1] == -1 then
|
||||
inputs[#inputs] = -1
|
||||
else
|
||||
inputs[#inputs] = 1
|
||||
end
|
||||
elseif tile == 0 and _M.getTile(dx + 1, dy) == 1 and _M.getTile(dx + 1, dy - 1) == 1 then
|
||||
inputs[#inputs] = -1
|
||||
end
|
||||
|
||||
for i = 1,#sprites do
|
||||
local sprite = sprites[i]
|
||||
local distx = math.abs(sprite.x - (_M.partyX+dx*TILE_SIZE))
|
||||
local disty = math.abs(sprite.y - (_M.partyY+dy*TILE_SIZE))
|
||||
local dist = math.sqrt((distx * distx) + (disty * disty))
|
||||
if dist <= TILE_SIZE * 1.25 then
|
||||
inputs[#inputs] = sprite.good
|
||||
|
||||
if dist > TILE_SIZE then
|
||||
inputDeltaDistance[#inputDeltaDistance] = mathFunctions.squashDistance(dist)
|
||||
end
|
||||
end
|
||||
::continue::
|
||||
end
|
||||
local neighbors = 0
|
||||
for ddy=-1,1,1 do
|
||||
for ddx=-1,1,1 do
|
||||
if (ddy == 0 and ddx == 0) or (ddx == 0 and ddy == 1) then
|
||||
goto continue
|
||||
end
|
||||
|
||||
for i = 1,#extended do
|
||||
local distx = math.abs(extended[i]["x"]+_M.cameraX - (_M.partyX+dx*TILE_SIZE))
|
||||
local disty = math.abs(extended[i]["y"]+_M.cameraY - (_M.partyY+dy*TILE_SIZE))
|
||||
if distx < TILE_SIZE / 2 and disty < TILE_SIZE / 2 then
|
||||
|
||||
inputs[#inputs] = extended[i]["good"]
|
||||
local dist = math.sqrt((distx * distx) + (disty * disty))
|
||||
if dist > TILE_SIZE / 2 then
|
||||
inputDeltaDistance[#inputDeltaDistance] = mathFunctions.squashDistance(dist)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
return inputs, inputDeltaDistance
|
||||
if _M.getTile(dx+ddx, dy+ddy) == 0 then
|
||||
neighbors = neighbors + 1
|
||||
end
|
||||
|
||||
::continue::
|
||||
end
|
||||
end
|
||||
|
||||
if neighbors >= 3 then
|
||||
inputs[#inputs] = 1
|
||||
else
|
||||
inputs[#inputs] = -1
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
for i = 1,#sprites do
|
||||
local sprite = sprites[i]
|
||||
if sprite.good == 0 then
|
||||
goto continue
|
||||
end
|
||||
|
||||
local distx = math.abs(sprite.x - (_M.partyX+dx*mem.size.tile))
|
||||
local disty = math.abs(sprite.y - (_M.partyY+dy*mem.size.tile))
|
||||
local dist = math.sqrt((distx * distx) + (disty * disty))
|
||||
if dist <= mem.size.tile * 1.25 then
|
||||
inputs[#inputs] = sprite.good
|
||||
|
||||
if dist > mem.size.tile then
|
||||
inputDeltaDistance[#inputDeltaDistance] = mathFunctions.squashDistance(dist)
|
||||
end
|
||||
end
|
||||
::continue::
|
||||
end
|
||||
|
||||
for i = 1,#extended do
|
||||
local distx = math.abs(extended[i]["x"]+_M.cameraX - (_M.partyX+dx*mem.size.tile))
|
||||
local disty = math.abs(extended[i]["y"]+_M.cameraY - (_M.partyY+dy*mem.size.tile))
|
||||
if distx < mem.size.tile / 2 and disty < mem.size.tile / 2 then
|
||||
|
||||
inputs[#inputs] = extended[i]["good"]
|
||||
local dist = math.sqrt((distx * distx) + (disty * disty))
|
||||
if dist > mem.size.tile / 2 then
|
||||
inputDeltaDistance[#inputDeltaDistance] = mathFunctions.squashDistance(dist)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
return inputs, inputDeltaDistance
|
||||
end
|
||||
|
||||
function _M.getClimbing()
|
||||
local sprite = _M.getSprite(_M.leader)
|
||||
if sprite == nil then
|
||||
return false
|
||||
end
|
||||
return sprite.motion >= 0x35 and sprite.motion <= 0x39
|
||||
end
|
||||
|
||||
function _M.clearJoypad()
|
||||
|
@ -432,7 +671,7 @@ function _M.onEmptyHit(handler)
|
|||
end
|
||||
|
||||
local function processEmptyHit(addr, val)
|
||||
local idx = math.floor((bit.band(addr, 0xffff) - bit.band(SPRITE_BASE, 0xffff)) / SPRITE_SIZE)
|
||||
local idx = math.floor((bit.band(addr, 0xffff) - bit.band(mem.addr.spriteBase, 0xffff)) / mem.size.sprite)
|
||||
local pow = _M.getSprite(idx)
|
||||
if pow == nil or
|
||||
pow.control ~= 0x0238 then
|
||||
|
@ -442,7 +681,7 @@ local function processEmptyHit(addr, val)
|
|||
local sprites = _M.getSprites()
|
||||
for i=1,#sprites,1 do
|
||||
local sprite = sprites[i]
|
||||
if bit.band(sprite.style, SPRITE_DYING) ~= 0 and
|
||||
if bit.band(sprite.style, mem.flag.sprite.dying) ~= 0 and
|
||||
sprite.good == -1 then
|
||||
return
|
||||
end
|
||||
|
@ -463,6 +702,18 @@ local function processMapLoad()
|
|||
for i=#mapLoadedQueue,1,-1 do
|
||||
table.remove(mapLoadedQueue, i)()
|
||||
end
|
||||
areaLoadedQueue = {} -- We clear this because it doesn't make any sense after the map screen loads
|
||||
end
|
||||
|
||||
function _M.bonusScreenDisplayed(inputs)
|
||||
local count = 0
|
||||
for i=1,#inputs,1 do
|
||||
if inputs[i] ~= 0 then
|
||||
count = count + 1
|
||||
end
|
||||
end
|
||||
|
||||
return count < 10
|
||||
end
|
||||
|
||||
local handlers = {}
|
||||
|
@ -476,7 +727,16 @@ local function registerHandler(space, regname, addr, callback)
|
|||
})
|
||||
end
|
||||
|
||||
local inputHandler = nil
|
||||
local setRewindHandler = nil
|
||||
local rewindHandler = nil
|
||||
function _M.unregisterHandlers()
|
||||
callback.unregister('input', inputHandler)
|
||||
callback.unregister('set_rewind', setRewindHandler)
|
||||
callback.unregister('post_rewind', rewindHandler)
|
||||
inputHandler = nil
|
||||
setRewindHandler = nil
|
||||
rewindHandler = nil
|
||||
for i=#handlers,1,-1 do
|
||||
local handler = table.remove(handlers, i)
|
||||
handler.unregisterFn(handler.space, handler.addr, handler.fn)
|
||||
|
@ -484,11 +744,24 @@ function _M.unregisterHandlers()
|
|||
end
|
||||
|
||||
function _M.registerHandlers()
|
||||
if inputHandler ~= nil then
|
||||
error("Only call register handlers once")
|
||||
end
|
||||
|
||||
inputHandler = callback.register('input', processFrameAdvanced)
|
||||
setRewindHandler = callback.register('set_rewind', processSetRewind)
|
||||
rewindHandler = callback.register('post_rewind', processRewind)
|
||||
registerHandler(memory2.BUS, 'registerwrite', 0xb517b2, processAreaLoad)
|
||||
registerHandler(memory2.WRAM, 'registerread', 0x06b1, processMapLoad)
|
||||
registerHandler(memory2.WRAM, 'registerwrite', 0x069b, processMapLoad)
|
||||
for i=2,22,1 do
|
||||
registerHandler(memory2.WRAM, 'registerwrite', bit.band(SPRITE_BASE + SPRITE_SIZE * i, 0xffff), processEmptyHit)
|
||||
registerHandler(memory2.WRAM, 'registerwrite', bit.band(mem.addr.spriteBase + mem.size.sprite * i, 0xffff), processEmptyHit)
|
||||
end
|
||||
end
|
||||
|
||||
return _M
|
||||
return function(promise)
|
||||
Promise = promise
|
||||
if util == nil then
|
||||
util = dofile(base.."/util.lua")(Promise)
|
||||
end
|
||||
return _M
|
||||
end
|
458
i3.json
Normal file
458
i3.json
Normal file
|
@ -0,0 +1,458 @@
|
|||
[
|
||||
{
|
||||
"border": "normal",
|
||||
"floating": "auto_off",
|
||||
"layout": "splith",
|
||||
"percent": 1,
|
||||
"type": "con",
|
||||
"nodes": [
|
||||
{
|
||||
"border": "normal",
|
||||
"floating": "auto_off",
|
||||
"layout": "splitv",
|
||||
"percent": 0.25,
|
||||
"type": "con",
|
||||
"nodes": [
|
||||
{
|
||||
"border": "normal",
|
||||
"floating": "auto_off",
|
||||
"layout": "tabbed",
|
||||
"percent": 0.5,
|
||||
"type": "con",
|
||||
"nodes": [
|
||||
{
|
||||
"border": "normal",
|
||||
"current_border_width": 3,
|
||||
"floating": "auto_off",
|
||||
"geometry": {
|
||||
"height": 1030,
|
||||
"width": 106,
|
||||
"x": 0,
|
||||
"y": 0
|
||||
},
|
||||
"name": " - Terminal",
|
||||
"percent": 0.333333333333333,
|
||||
"swallows": [
|
||||
{
|
||||
"title": "[Tt]erminal"
|
||||
}
|
||||
],
|
||||
"type": "con"
|
||||
},
|
||||
{
|
||||
"border": "normal",
|
||||
"current_border_width": 3,
|
||||
"floating": "auto_off",
|
||||
"geometry": {
|
||||
"height": 1030,
|
||||
"width": 114,
|
||||
"x": 0,
|
||||
"y": 0
|
||||
},
|
||||
"name": "lsnes: Messages",
|
||||
"percent": 0.333333333333333,
|
||||
"swallows": [
|
||||
{
|
||||
"class": "^Lsnes$",
|
||||
"title": "Messages"
|
||||
}
|
||||
],
|
||||
"type": "con"
|
||||
},
|
||||
{
|
||||
"border": "normal",
|
||||
"current_border_width": 3,
|
||||
"floating": "auto_off",
|
||||
"geometry": {
|
||||
"height": 1030,
|
||||
"width": 122,
|
||||
"x": 0,
|
||||
"y": 0
|
||||
},
|
||||
"name": "lsnes rr2-β24 [null core]",
|
||||
"percent": 0.333333333333333,
|
||||
"swallows": [
|
||||
{
|
||||
"class": "^Lsnes$",
|
||||
"title": "null "
|
||||
}
|
||||
],
|
||||
"type": "con"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"border": "normal",
|
||||
"floating": "auto_off",
|
||||
"layout": "tabbed",
|
||||
"percent": 0.5,
|
||||
"type": "con",
|
||||
"nodes": [
|
||||
{
|
||||
"border": "normal",
|
||||
"current_border_width": 3,
|
||||
"floating": "auto_off",
|
||||
"geometry": {
|
||||
"height": 1030,
|
||||
"width": 1914,
|
||||
"x": 0,
|
||||
"y": 0
|
||||
},
|
||||
"name": "lsnes rr2-β24 [bsnes v085 (Compatibility core)]",
|
||||
"percent": 0.5,
|
||||
"swallows": [
|
||||
{
|
||||
"class": "^Lsnes$",
|
||||
"title": "Compatibility"
|
||||
}
|
||||
],
|
||||
"type": "con"
|
||||
},
|
||||
{
|
||||
"border": "normal",
|
||||
"current_border_width": 3,
|
||||
"floating": "auto_off",
|
||||
"geometry": {
|
||||
"height": 1030,
|
||||
"width": 634,
|
||||
"x": 0,
|
||||
"y": 0
|
||||
},
|
||||
"name": "lsnes: Messages",
|
||||
"percent": 0.5,
|
||||
"swallows": [
|
||||
{
|
||||
"class": "^Lsnes$",
|
||||
"title": "Messages"
|
||||
}
|
||||
],
|
||||
"type": "con"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"border": "normal",
|
||||
"floating": "auto_off",
|
||||
"layout": "splitv",
|
||||
"percent": 0.25,
|
||||
"type": "con",
|
||||
"nodes": [
|
||||
{
|
||||
"border": "normal",
|
||||
"floating": "auto_off",
|
||||
"layout": "tabbed",
|
||||
"percent": 0.5,
|
||||
"type": "con",
|
||||
"nodes": [
|
||||
{
|
||||
"border": "normal",
|
||||
"current_border_width": 3,
|
||||
"floating": "auto_off",
|
||||
"geometry": {
|
||||
"height": 1030,
|
||||
"width": 954,
|
||||
"x": 0,
|
||||
"y": 0
|
||||
},
|
||||
"name": "lsnes rr2-β24 [bsnes v085 (Compatibility core)]",
|
||||
"percent": 0.5,
|
||||
"swallows": [
|
||||
{
|
||||
"class": "^Lsnes$",
|
||||
"title": "Compatibility"
|
||||
}
|
||||
],
|
||||
"type": "con"
|
||||
},
|
||||
{
|
||||
"border": "normal",
|
||||
"current_border_width": 3,
|
||||
"floating": "auto_off",
|
||||
"geometry": {
|
||||
"height": 1030,
|
||||
"width": 474,
|
||||
"x": 0,
|
||||
"y": 0
|
||||
},
|
||||
"name": "lsnes: Messages",
|
||||
"percent": 0.5,
|
||||
"swallows": [
|
||||
{
|
||||
"class": "^Lsnes$",
|
||||
"title": "Messages"
|
||||
}
|
||||
],
|
||||
"type": "con"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"border": "normal",
|
||||
"floating": "auto_off",
|
||||
"layout": "tabbed",
|
||||
"percent": 0.5,
|
||||
"type": "con",
|
||||
"nodes": [
|
||||
{
|
||||
"border": "normal",
|
||||
"current_border_width": 3,
|
||||
"floating": "auto_off",
|
||||
"geometry": {
|
||||
"height": 1030,
|
||||
"width": 378,
|
||||
"x": 0,
|
||||
"y": 0
|
||||
},
|
||||
"name": "lsnes rr2-β24 [bsnes v085 (Compatibility core)]",
|
||||
"percent": 0.5,
|
||||
"swallows": [
|
||||
{
|
||||
"class": "^Lsnes$",
|
||||
"title": "Compatibility"
|
||||
}
|
||||
],
|
||||
"type": "con"
|
||||
},
|
||||
{
|
||||
"border": "normal",
|
||||
"current_border_width": 3,
|
||||
"floating": "auto_off",
|
||||
"geometry": {
|
||||
"height": 1030,
|
||||
"width": 234,
|
||||
"x": 0,
|
||||
"y": 0
|
||||
},
|
||||
"name": "lsnes: Messages",
|
||||
"percent": 0.5,
|
||||
"swallows": [
|
||||
{
|
||||
"class": "^Lsnes$",
|
||||
"title": "Messages"
|
||||
}
|
||||
],
|
||||
"type": "con"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"border": "normal",
|
||||
"floating": "auto_off",
|
||||
"layout": "splitv",
|
||||
"percent": 0.25,
|
||||
"type": "con",
|
||||
"nodes": [
|
||||
{
|
||||
"border": "normal",
|
||||
"floating": "auto_off",
|
||||
"layout": "tabbed",
|
||||
"percent": 0.5,
|
||||
"type": "con",
|
||||
"nodes": [
|
||||
{
|
||||
"border": "normal",
|
||||
"current_border_width": 3,
|
||||
"floating": "auto_off",
|
||||
"geometry": {
|
||||
"height": 1030,
|
||||
"width": 314,
|
||||
"x": 0,
|
||||
"y": 0
|
||||
},
|
||||
"name": "lsnes rr2-β24 [bsnes v085 (Compatibility core)]",
|
||||
"percent": 0.5,
|
||||
"swallows": [
|
||||
{
|
||||
"class": "^Lsnes$",
|
||||
"title": "Compatibility"
|
||||
}
|
||||
],
|
||||
"type": "con"
|
||||
},
|
||||
{
|
||||
"border": "normal",
|
||||
"current_border_width": 3,
|
||||
"floating": "auto_off",
|
||||
"geometry": {
|
||||
"height": 1030,
|
||||
"width": 208,
|
||||
"x": 0,
|
||||
"y": 0
|
||||
},
|
||||
"name": "lsnes: Messages",
|
||||
"percent": 0.5,
|
||||
"swallows": [
|
||||
{
|
||||
"class": "^Lsnes$",
|
||||
"title": "Messages"
|
||||
}
|
||||
],
|
||||
"type": "con"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"border": "normal",
|
||||
"floating": "auto_off",
|
||||
"layout": "tabbed",
|
||||
"percent": 0.5,
|
||||
"type": "con",
|
||||
"nodes": [
|
||||
{
|
||||
"border": "normal",
|
||||
"current_border_width": 3,
|
||||
"floating": "auto_off",
|
||||
"geometry": {
|
||||
"height": 1030,
|
||||
"width": 269,
|
||||
"x": 0,
|
||||
"y": 0
|
||||
},
|
||||
"name": "lsnes rr2-β24 [bsnes v085 (Compatibility core)]",
|
||||
"percent": 0.5,
|
||||
"swallows": [
|
||||
{
|
||||
"class": "^Lsnes$",
|
||||
"title": "Compatibility"
|
||||
}
|
||||
],
|
||||
"type": "con"
|
||||
},
|
||||
{
|
||||
"border": "normal",
|
||||
"current_border_width": 3,
|
||||
"floating": "auto_off",
|
||||
"geometry": {
|
||||
"height": 1030,
|
||||
"width": 154,
|
||||
"x": 0,
|
||||
"y": 0
|
||||
},
|
||||
"name": "lsnes: Messages",
|
||||
"percent": 0.5,
|
||||
"swallows": [
|
||||
{
|
||||
"class": "^Lsnes$",
|
||||
"title": "Messages"
|
||||
}
|
||||
],
|
||||
"type": "con"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"border": "normal",
|
||||
"floating": "auto_off",
|
||||
"layout": "splitv",
|
||||
"percent": 0.25,
|
||||
"type": "con",
|
||||
"nodes": [
|
||||
{
|
||||
"border": "normal",
|
||||
"floating": "auto_off",
|
||||
"layout": "tabbed",
|
||||
"percent": 0.5,
|
||||
"type": "con",
|
||||
"nodes": [
|
||||
{
|
||||
"border": "normal",
|
||||
"current_border_width": 3,
|
||||
"floating": "auto_off",
|
||||
"geometry": {
|
||||
"height": 1030,
|
||||
"width": 186,
|
||||
"x": 0,
|
||||
"y": 0
|
||||
},
|
||||
"name": "lsnes rr2-β24 [bsnes v085 (Compatibility core)]",
|
||||
"percent": 0.5,
|
||||
"swallows": [
|
||||
{
|
||||
"class": "^Lsnes$",
|
||||
"title": "Compatibility"
|
||||
}
|
||||
],
|
||||
"type": "con"
|
||||
},
|
||||
{
|
||||
"border": "normal",
|
||||
"current_border_width": 3,
|
||||
"floating": "auto_off",
|
||||
"geometry": {
|
||||
"height": 1030,
|
||||
"width": 132,
|
||||
"x": 0,
|
||||
"y": 0
|
||||
},
|
||||
"name": "lsnes: Messages",
|
||||
"percent": 0.5,
|
||||
"swallows": [
|
||||
{
|
||||
"class": "^Lsnes$",
|
||||
"title": "Messages"
|
||||
}
|
||||
],
|
||||
"type": "con"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"border": "normal",
|
||||
"floating": "auto_off",
|
||||
"layout": "tabbed",
|
||||
"percent": 0.5,
|
||||
"type": "con",
|
||||
"nodes": [
|
||||
{
|
||||
"border": "normal",
|
||||
"current_border_width": 3,
|
||||
"floating": "auto_off",
|
||||
"geometry": {
|
||||
"height": 1030,
|
||||
"width": 168,
|
||||
"x": 0,
|
||||
"y": 0
|
||||
},
|
||||
"name": "lsnes rr2-β24 [bsnes v085 (Compatibility core)]",
|
||||
"percent": 0.5,
|
||||
"swallows": [
|
||||
{
|
||||
"class": "^Lsnes$",
|
||||
"title": "Compatibility"
|
||||
}
|
||||
],
|
||||
"type": "con"
|
||||
},
|
||||
{
|
||||
"border": "normal",
|
||||
"current_border_width": 3,
|
||||
"floating": "auto_off",
|
||||
"geometry": {
|
||||
"height": 1030,
|
||||
"width": 141,
|
||||
"x": 0,
|
||||
"y": 0
|
||||
},
|
||||
"name": "lsnes: Messages",
|
||||
"percent": 0.5,
|
||||
"swallows": [
|
||||
{
|
||||
"class": "^Lsnes$",
|
||||
"title": "Messages"
|
||||
}
|
||||
],
|
||||
"type": "con"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
279
inflate-bit32.lua
Normal file
279
inflate-bit32.lua
Normal file
|
@ -0,0 +1,279 @@
|
|||
-- zzlib-bit32 - zlib decompression in Lua - version using bit/bit32 libraries
|
||||
|
||||
-- Copyright (c) 2016-2020 Francois Galea <fgalea at free.fr>
|
||||
-- This program is free software. It comes without any warranty, to
|
||||
-- the extent permitted by applicable law. You can redistribute it
|
||||
-- and/or modify it under the terms of the Do What The Fuck You Want
|
||||
-- To Public License, Version 2, as published by Sam Hocevar. See
|
||||
-- the COPYING file or http://www.wtfpl.net/ for more details.
|
||||
|
||||
|
||||
local inflate = {}
|
||||
|
||||
local bit = bit32 or bit
|
||||
|
||||
inflate.band = bit.band
|
||||
inflate.rshift = bit.rshift
|
||||
|
||||
function inflate.bitstream_init(file)
|
||||
local bs = {
|
||||
file = file, -- the open file handle
|
||||
buf = nil, -- character buffer
|
||||
len = nil, -- length of character buffer
|
||||
pos = 1, -- position in char buffer
|
||||
b = 0, -- bit buffer
|
||||
n = 0, -- number of bits in buffer
|
||||
}
|
||||
-- get rid of n first bits
|
||||
function bs:flushb(n)
|
||||
self.n = self.n - n
|
||||
self.b = bit.rshift(self.b,n)
|
||||
end
|
||||
-- peek a number of n bits from stream
|
||||
function bs:peekb(n)
|
||||
while self.n < n do
|
||||
if self.pos > self.len then
|
||||
self.buf = self.file:read(4096)
|
||||
self.len = self.buf:len()
|
||||
self.pos = 1
|
||||
end
|
||||
self.b = self.b + bit.lshift(self.buf:byte(self.pos),self.n)
|
||||
self.pos = self.pos + 1
|
||||
self.n = self.n + 8
|
||||
end
|
||||
return bit.band(self.b,bit.lshift(1,n)-1)
|
||||
end
|
||||
-- get a number of n bits from stream
|
||||
function bs:getb(n)
|
||||
local ret = bs:peekb(n)
|
||||
self.n = self.n - n
|
||||
self.b = bit.rshift(self.b,n)
|
||||
return ret
|
||||
end
|
||||
-- get next variable-size of maximum size=n element from stream, according to Huffman table
|
||||
function bs:getv(hufftable,n)
|
||||
local e = hufftable[bs:peekb(n)]
|
||||
local len = bit.band(e,15)
|
||||
local ret = bit.rshift(e,4)
|
||||
self.n = self.n - len
|
||||
self.b = bit.rshift(self.b,len)
|
||||
return ret
|
||||
end
|
||||
function bs:close()
|
||||
if self.file then
|
||||
self.file:close()
|
||||
end
|
||||
end
|
||||
if type(file) == "string" then
|
||||
bs.file = nil
|
||||
bs.buf = file
|
||||
else
|
||||
bs.buf = file:read(4096)
|
||||
end
|
||||
bs.len = bs.buf:len()
|
||||
return bs
|
||||
end
|
||||
|
||||
local function hufftable_create(depths)
|
||||
local nvalues = #depths
|
||||
local nbits = 1
|
||||
local bl_count = {}
|
||||
local next_code = {}
|
||||
for i=1,nvalues do
|
||||
local d = depths[i]
|
||||
if d > nbits then
|
||||
nbits = d
|
||||
end
|
||||
bl_count[d] = (bl_count[d] or 0) + 1
|
||||
end
|
||||
local table = {}
|
||||
local code = 0
|
||||
bl_count[0] = 0
|
||||
for i=1,nbits do
|
||||
code = (code + (bl_count[i-1] or 0)) * 2
|
||||
next_code[i] = code
|
||||
end
|
||||
for i=1,nvalues do
|
||||
local len = depths[i] or 0
|
||||
if len > 0 then
|
||||
local e = (i-1)*16 + len
|
||||
local code = next_code[len]
|
||||
local rcode = 0
|
||||
for j=1,len do
|
||||
rcode = rcode + bit.lshift(bit.band(1,bit.rshift(code,j-1)),len-j)
|
||||
end
|
||||
for j=0,2^nbits-1,2^len do
|
||||
table[j+rcode] = e
|
||||
end
|
||||
next_code[len] = next_code[len] + 1
|
||||
end
|
||||
end
|
||||
return table,nbits
|
||||
end
|
||||
|
||||
local function block_loop(out,bs,nlit,ndist,littable,disttable)
|
||||
local lit
|
||||
repeat
|
||||
lit = bs:getv(littable,nlit)
|
||||
if lit < 256 then
|
||||
table.insert(out,lit)
|
||||
elseif lit > 256 then
|
||||
local nbits = 0
|
||||
local size = 3
|
||||
local dist = 1
|
||||
if lit < 265 then
|
||||
size = size + lit - 257
|
||||
elseif lit < 285 then
|
||||
nbits = bit.rshift(lit-261,2)
|
||||
size = size + bit.lshift(bit.band(lit-261,3)+4,nbits)
|
||||
else
|
||||
size = 258
|
||||
end
|
||||
if nbits > 0 then
|
||||
size = size + bs:getb(nbits)
|
||||
end
|
||||
local v = bs:getv(disttable,ndist)
|
||||
if v < 4 then
|
||||
dist = dist + v
|
||||
else
|
||||
nbits = bit.rshift(v-2,1)
|
||||
dist = dist + bit.lshift(bit.band(v,1)+2,nbits)
|
||||
dist = dist + bs:getb(nbits)
|
||||
end
|
||||
local p = #out-dist+1
|
||||
while size > 0 do
|
||||
table.insert(out,out[p])
|
||||
p = p + 1
|
||||
size = size - 1
|
||||
end
|
||||
end
|
||||
until lit == 256
|
||||
end
|
||||
|
||||
local function block_dynamic(out,bs)
|
||||
local order = { 17, 18, 19, 1, 9, 8, 10, 7, 11, 6, 12, 5, 13, 4, 14, 3, 15, 2, 16 }
|
||||
local hlit = 257 + bs:getb(5)
|
||||
local hdist = 1 + bs:getb(5)
|
||||
local hclen = 4 + bs:getb(4)
|
||||
local depths = {}
|
||||
for i=1,hclen do
|
||||
local v = bs:getb(3)
|
||||
depths[order[i]] = v
|
||||
end
|
||||
for i=hclen+1,19 do
|
||||
depths[order[i]] = 0
|
||||
end
|
||||
local lengthtable,nlen = hufftable_create(depths)
|
||||
local i=1
|
||||
while i<=hlit+hdist do
|
||||
local v = bs:getv(lengthtable,nlen)
|
||||
if v < 16 then
|
||||
depths[i] = v
|
||||
i = i + 1
|
||||
elseif v < 19 then
|
||||
local nbt = {2,3,7}
|
||||
local nb = nbt[v-15]
|
||||
local c = 0
|
||||
local n = 3 + bs:getb(nb)
|
||||
if v == 16 then
|
||||
c = depths[i-1]
|
||||
elseif v == 18 then
|
||||
n = n + 8
|
||||
end
|
||||
for j=1,n do
|
||||
depths[i] = c
|
||||
i = i + 1
|
||||
end
|
||||
else
|
||||
error("wrong entry in depth table for literal/length alphabet: "..v);
|
||||
end
|
||||
end
|
||||
local litdepths = {} for i=1,hlit do table.insert(litdepths,depths[i]) end
|
||||
local littable,nlit = hufftable_create(litdepths)
|
||||
local distdepths = {} for i=hlit+1,#depths do table.insert(distdepths,depths[i]) end
|
||||
local disttable,ndist = hufftable_create(distdepths)
|
||||
block_loop(out,bs,nlit,ndist,littable,disttable)
|
||||
end
|
||||
|
||||
local function block_static(out,bs)
|
||||
local cnt = { 144, 112, 24, 8 }
|
||||
local dpt = { 8, 9, 7, 8 }
|
||||
local depths = {}
|
||||
for i=1,4 do
|
||||
local d = dpt[i]
|
||||
for j=1,cnt[i] do
|
||||
table.insert(depths,d)
|
||||
end
|
||||
end
|
||||
local littable,nlit = hufftable_create(depths)
|
||||
depths = {}
|
||||
for i=1,32 do
|
||||
depths[i] = 5
|
||||
end
|
||||
local disttable,ndist = hufftable_create(depths)
|
||||
block_loop(out,bs,nlit,ndist,littable,disttable)
|
||||
end
|
||||
|
||||
local function block_uncompressed(out,bs)
|
||||
bs:flushb(bit.band(bs.n,7))
|
||||
local len = bs:getb(16)
|
||||
if bs.n > 0 then
|
||||
error("Unexpected.. should be zero remaining bits in buffer.")
|
||||
end
|
||||
local nlen = bs:getb(16)
|
||||
if bit.bxor(len,nlen) ~= 65535 then
|
||||
error("LEN and NLEN don't match")
|
||||
end
|
||||
for i=bs.pos,bs.pos+len-1 do
|
||||
table.insert(out,bs.buf:byte(i,i))
|
||||
end
|
||||
bs.pos = bs.pos + len
|
||||
end
|
||||
|
||||
function inflate.main(bs)
|
||||
local last,type
|
||||
local output = {}
|
||||
repeat
|
||||
local block
|
||||
last = bs:getb(1)
|
||||
type = bs:getb(2)
|
||||
if type == 0 then
|
||||
block_uncompressed(output,bs)
|
||||
elseif type == 1 then
|
||||
block_static(output,bs)
|
||||
elseif type == 2 then
|
||||
block_dynamic(output,bs)
|
||||
else
|
||||
error("unsupported block type")
|
||||
end
|
||||
until last == 1
|
||||
bs:flushb(bit.band(bs.n,7))
|
||||
return output
|
||||
end
|
||||
|
||||
local crc32_table
|
||||
function inflate.crc32(s,crc)
|
||||
if not crc32_table then
|
||||
crc32_table = {}
|
||||
for i=0,255 do
|
||||
local r=i
|
||||
for j=1,8 do
|
||||
r = bit.bxor(bit.rshift(r,1),bit.band(0xedb88320,bit.bnot(bit.band(r,1)-1)))
|
||||
end
|
||||
crc32_table[i] = r
|
||||
end
|
||||
end
|
||||
crc = bit.bnot(crc or 0)
|
||||
for i=1,#s do
|
||||
local c = s:byte(i)
|
||||
crc = bit.bxor(crc32_table[bit.bxor(c,bit.band(crc,0xff))],bit.rshift(crc,8))
|
||||
end
|
||||
crc = bit.bnot(crc)
|
||||
if crc<0 then
|
||||
-- in Lua < 5.2, sign extension was performed
|
||||
crc = crc + 4294967296
|
||||
end
|
||||
return crc
|
||||
end
|
||||
|
||||
return inflate
|
56
mem.lua
Normal file
56
mem.lua
Normal file
|
@ -0,0 +1,56 @@
|
|||
local _M = {
|
||||
addr = {
|
||||
kremcoins = 0x7e08cc,
|
||||
tileCollisionMathPointer = 0x7e17b2,
|
||||
spriteBase = 0x7e0de2,
|
||||
verticalPointer = 0xc414,
|
||||
tiledataPointer = 0x7e0098,
|
||||
haveBoth = 0x7e08c2,
|
||||
---Height in game units for vertical levels, width for horizontal
|
||||
areaLength = 0x7e17b4,
|
||||
---This is always the traditional width no matter the level type
|
||||
areaWidth = 0x7e0afc,
|
||||
---This is always the traditional height no matter the level type
|
||||
areaHeight = 0x7e0afe,
|
||||
cameraX = 0x7e17ba,
|
||||
cameraY = 0x7e17c0,
|
||||
cameraX2 = 0x7e0ad7,
|
||||
cameraY2 = 0x7e0adb,
|
||||
leadChar = 0x7e08a4,
|
||||
partyX = 0x7e0a2a,
|
||||
partyY = 0x7e0a2c,
|
||||
solidLessThan = 0x7e00a0,
|
||||
kongLetters = 0x7e0902,
|
||||
mathLives = 0x7e08be,
|
||||
displayLives = 0x7e0c0,
|
||||
mainAreaNumber = 0x7e08a8,
|
||||
currentAreaNumber = 0x7e08c8,
|
||||
},
|
||||
|
||||
flag = {
|
||||
sprite = {
|
||||
dying = 0x1000,
|
||||
}
|
||||
},
|
||||
|
||||
size = {
|
||||
tile = 32,
|
||||
enemy = 64,
|
||||
sprite = 94,
|
||||
},
|
||||
|
||||
offset = {
|
||||
sprite = {
|
||||
control = 0x00,
|
||||
x = 0x06,
|
||||
y = 0x0a,
|
||||
jumpHeight = 0x0e,
|
||||
style = 0x12,
|
||||
velocityX = 0x20,
|
||||
velocityY = 0x24,
|
||||
motion = 0x2e,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return _M
|
|
@ -1,15 +1,33 @@
|
|||
--Update to Seth-Bling's MarI/O app
|
||||
local gui = gui
|
||||
|
||||
local base = string.gsub(@@LUA_SCRIPT_FILENAME@@, "(.*[/\\])(.*)", "%1")
|
||||
|
||||
local pool = dofile(base.."/pool.lua")
|
||||
local util = dofile(base.."/util.lua")
|
||||
local util = dofile(base.."/util.lua")()
|
||||
|
||||
local statusLine = nil
|
||||
local statusColor = 0x0000ff00
|
||||
|
||||
pool.onMessage(function(msg, color)
|
||||
print(msg)
|
||||
local stderrColor = util.nearestColor(color, {
|
||||
-- Green
|
||||
['92'] = { r = 0 , g = 255, b = 0 },
|
||||
-- Red
|
||||
['91'] = { r = 255, g = 0 , b = 0 },
|
||||
-- Yellow
|
||||
['93'] = { r = 255, g = 255, b = 0 },
|
||||
-- Blue
|
||||
['94'] = { r = 0 , g = 0 , b = 255},
|
||||
-- Magenta
|
||||
['95'] = { r = 255, g = 0 , b = 255},
|
||||
-- Cyan
|
||||
['96'] = { r = 0 , g = 255, b = 255},
|
||||
-- White
|
||||
['97'] = { r = 255, g = 255, b = 255},
|
||||
})
|
||||
io.stderr:write('\x1b['..stderrColor..'m'..msg..'\x1b[0m\n')
|
||||
statusLine = msg
|
||||
statusColor = color
|
||||
end)
|
||||
|
@ -25,8 +43,22 @@ pool.onRenderForm(function(form)
|
|||
form:draw(-500, 0)
|
||||
|
||||
if statusLine ~= nil then
|
||||
gui.rectangle(-500, guiHeight - 20, 0, 20, 1, 0x00000000, statusColor)
|
||||
gui.rectangle(-500, guiHeight - 20, guiWidth, 20, 1, 0x00000000, statusColor)
|
||||
gui.text(-500, guiHeight - 20, statusLine, 0x00000000)
|
||||
end
|
||||
end)
|
||||
pool.run()
|
||||
|
||||
local DONK_LOAD_POOL = os.getenv('DONK_LOAD_POOL')
|
||||
if DONK_LOAD_POOL ~= nil and DONK_LOAD_POOL ~= "" then
|
||||
pool.requestLoad(DONK_LOAD_POOL)
|
||||
end
|
||||
|
||||
pool.run():next(function()
|
||||
print("The pool finished running!!!")
|
||||
end):catch(function(error)
|
||||
if type(error) == "table" then
|
||||
error = "\n"..table.concat(error, "\n")
|
||||
end
|
||||
io.stderr:write(string.format("There was a problem running the pool: %s", error))
|
||||
print(string.format("There was a problem running the pool: %s", error))
|
||||
end)
|
||||
|
|
11
pipe-test.lua
Normal file
11
pipe-test.lua
Normal file
|
@ -0,0 +1,11 @@
|
|||
local pipey = io.open("\\\\.\\pipe\\asoeuth", "r")
|
||||
|
||||
print('reader')
|
||||
|
||||
function on_timer()
|
||||
print('read')
|
||||
print(pipey:read("*l"))
|
||||
set_timer_timeout(100000)
|
||||
end
|
||||
|
||||
set_timer_timeout(100000)
|
285
pool.lua
285
pool.lua
|
@ -1,16 +1,34 @@
|
|||
local callback, set_timer_timeout, zip = callback, set_timer_timeout, zip
|
||||
|
||||
local base = string.gsub(@@LUA_SCRIPT_FILENAME@@, "(.*[/\\])(.*)", "%1")
|
||||
|
||||
local Promise = dofile(base.."/promise.lua")
|
||||
|
||||
local config = dofile(base.."/config.lua")
|
||||
local util = dofile(base.."/util.lua")(Promise)
|
||||
local serpent = dofile(base.."/serpent.lua")
|
||||
local zzlib = dofile(base.."/zzlib.lua")
|
||||
|
||||
local hasThreads = config.NeatConfig.Threads > 1
|
||||
|
||||
-- Only the parent should manage ticks!
|
||||
callback.register('timer', function()
|
||||
Promise.update()
|
||||
set_timer_timeout(1)
|
||||
end)
|
||||
set_timer_timeout(1)
|
||||
|
||||
local warn = '========== The ROM file to use comes from config.lua.'
|
||||
io.stderr:write(warn)
|
||||
print(warn)
|
||||
|
||||
local Runner = nil
|
||||
if config.NeatConfig.Threads > 1 then
|
||||
if hasThreads then
|
||||
Runner = dofile(base.."/runner-wrapper.lua")
|
||||
else
|
||||
Runner = dofile(base.."/runner.lua")
|
||||
end
|
||||
|
||||
local serpent = dofile(base.."/serpent.lua")
|
||||
local libDeflate = dofile(base.."/LibDeflate.lua")
|
||||
|
||||
local Inputs = config.InputSize+1
|
||||
local Outputs = #config.ButtonNames
|
||||
|
||||
|
@ -374,15 +392,15 @@ local function addToSpecies(child)
|
|||
end
|
||||
end
|
||||
|
||||
local function initializePool(after)
|
||||
pool = newPool()
|
||||
local function initializePool()
|
||||
return util.promiseWrap(function()
|
||||
pool = newPool()
|
||||
|
||||
for i=1,config.NeatConfig.Population do
|
||||
basic = basicGenome()
|
||||
addToSpecies(basic)
|
||||
end
|
||||
|
||||
after()
|
||||
for i=1,config.NeatConfig.Population do
|
||||
local basic = basicGenome()
|
||||
addToSpecies(basic)
|
||||
end
|
||||
end)
|
||||
end
|
||||
|
||||
local function bytes(x)
|
||||
|
@ -393,45 +411,51 @@ local function bytes(x)
|
|||
return string.char(b1,b2,b3,b4)
|
||||
end
|
||||
|
||||
--- Saves the pool to a gzipped Serpent file
|
||||
---@param filename string Filename to write
|
||||
---@return Promise Promise A promise that resolves when the file is saved.
|
||||
local function writeFile(filename)
|
||||
local file = io.open(filename, "w")
|
||||
local dump = serpent.dump(pool)
|
||||
local zlib = libDeflate:CompressDeflate(dump)
|
||||
file:write("\x1f\x8b\x08\x00\x00\x00\x00\x00\x00\x00")
|
||||
file:write(zlib)
|
||||
file:write(string.char(0,0,0,0))
|
||||
file:write(bytes(#dump % (2^32)))
|
||||
file:close()
|
||||
return
|
||||
return util.promiseWrap(function ()
|
||||
local file = zip.writer.new(filename)
|
||||
file:create_file('data.serpent')
|
||||
local dump = serpent.dump(pool)
|
||||
file:write(dump)
|
||||
file:close_file()
|
||||
file:commit()
|
||||
end)
|
||||
end
|
||||
|
||||
-- FIXME Save/load mechanism has to be rethought with items running in parallel
|
||||
local function loadFile(filename, after)
|
||||
message("Loading pool from " .. filename, 0x00999900)
|
||||
local file = io.open(filename, "r")
|
||||
if file == nil then
|
||||
message("File could not be loaded", 0x00990000)
|
||||
return
|
||||
end
|
||||
local contents = file:read("*all")
|
||||
local ok, obj = serpent.load(libDeflate:DecompressDeflate(contents:sub(11, #contents - 8)))
|
||||
if not ok then
|
||||
message("Error parsing pool file", 0x00990000)
|
||||
return
|
||||
end
|
||||
-- FIXME This isn't technically asynchronous. Probably can't be though.
|
||||
local function loadFile(filename)
|
||||
return util.promiseWrap(function()
|
||||
message("Loading pool from " .. filename, 0x00999900)
|
||||
local file = io.open(filename, "r")
|
||||
if file == nil then
|
||||
message("File could not be loaded", 0x00990000)
|
||||
return
|
||||
end
|
||||
local contents = file:read("*all")
|
||||
file:close()
|
||||
local decomp = zzlib.unzip(contents, 'data.serpent')
|
||||
local ok, obj = serpent.load(decomp)
|
||||
if not ok then
|
||||
message("Error parsing pool file", 0x00990000)
|
||||
return
|
||||
end
|
||||
|
||||
pool = obj
|
||||
pool = obj
|
||||
end)
|
||||
end
|
||||
|
||||
local function savePool()
|
||||
local filename = _M.saveLoadFile
|
||||
writeFile(filename)
|
||||
message(string.format("Saved \"%s\"!", filename:sub(#filename - 50)), 0x00009900)
|
||||
return writeFile(filename):next(function()
|
||||
message(string.format("Saved \"%s\"!", filename:sub(#filename - 50)), 0x00009900)
|
||||
end)
|
||||
end
|
||||
|
||||
local function loadPool(after)
|
||||
loadFile(_M.saveLoadFile, after)
|
||||
after()
|
||||
local function loadPool()
|
||||
return loadFile(_M.saveLoadFile)
|
||||
end
|
||||
|
||||
local function processRenderForm(form)
|
||||
|
@ -459,7 +483,7 @@ end
|
|||
local function crossover(g1, g2)
|
||||
-- Make sure g1 is the higher fitness genome
|
||||
if g2.fitness > g1.fitness then
|
||||
tempg = g1
|
||||
local tempg = g1
|
||||
g1 = g2
|
||||
g2 = tempg
|
||||
end
|
||||
|
@ -550,11 +574,11 @@ end
|
|||
local function breedChild(species)
|
||||
local child = {}
|
||||
if math.random() < config.NeatConfig.CrossoverChance then
|
||||
g1 = species.genomes[math.random(1, #species.genomes)]
|
||||
g2 = species.genomes[math.random(1, #species.genomes)]
|
||||
local g1 = species.genomes[math.random(1, #species.genomes)]
|
||||
local g2 = species.genomes[math.random(1, #species.genomes)]
|
||||
child = crossover(g1, g2)
|
||||
else
|
||||
g = species.genomes[math.random(1, #species.genomes)]
|
||||
local g = species.genomes[math.random(1, #species.genomes)]
|
||||
child = copyGenome(g)
|
||||
end
|
||||
|
||||
|
@ -593,7 +617,7 @@ local function removeWeakSpecies()
|
|||
local sum = totalAverageFitness()
|
||||
for s = 1,#pool.species do
|
||||
local species = pool.species[s]
|
||||
breed = math.floor(species.averageFitness / sum * config.NeatConfig.Population)
|
||||
local breed = math.floor(species.averageFitness / sum * config.NeatConfig.Population)
|
||||
if breed >= 1 then
|
||||
table.insert(survived, species)
|
||||
end
|
||||
|
@ -616,7 +640,7 @@ local function newGeneration()
|
|||
local children = {}
|
||||
for s = 1,#pool.species do
|
||||
local species = pool.species[s]
|
||||
breed = math.floor(species.averageFitness / sum * config.NeatConfig.Population) - 1
|
||||
local breed = math.floor(species.averageFitness / sum * config.NeatConfig.Population) - 1
|
||||
for i=1,breed do
|
||||
table.insert(children, breedChild(species))
|
||||
end
|
||||
|
@ -637,10 +661,18 @@ local function newGeneration()
|
|||
|
||||
pool.generation = pool.generation + 1
|
||||
|
||||
writeFile(_M.saveLoadFile .. ".gen" .. pool.generation .. ".pool")
|
||||
return writeFile(_M.saveLoadFile .. ".gen" .. pool.generation .. ".pool"):next(function()
|
||||
if config.NeatConfig.AutoSave then
|
||||
return writeFile(_M.saveLoadFile)
|
||||
end
|
||||
end)
|
||||
end
|
||||
|
||||
local runner = Runner()
|
||||
local function reset()
|
||||
return _M.run(true)
|
||||
end
|
||||
|
||||
local runner = Runner(Promise)
|
||||
runner.onMessage(function(msg, color)
|
||||
message(msg, color)
|
||||
end)
|
||||
|
@ -650,78 +682,87 @@ end)
|
|||
runner.onLoad(function(filename)
|
||||
_M.requestLoad(filename)
|
||||
end)
|
||||
runner.onReset(function(filename)
|
||||
_M.requestReset()
|
||||
end)
|
||||
runner.onRenderForm(function(form)
|
||||
processRenderForm(form)
|
||||
end)
|
||||
|
||||
local loadRequested = false
|
||||
local playTop = nil
|
||||
local topRequested = false
|
||||
|
||||
local loadRequested = config.NeatConfig.AutoSave
|
||||
local saveRequested = false
|
||||
local function mainLoop(currentSpecies)
|
||||
if loadRequested then
|
||||
loadRequested = false
|
||||
loadPool(mainLoop)
|
||||
return
|
||||
end
|
||||
local resetRequested = false
|
||||
local function mainLoop(currentSpecies, topGenome)
|
||||
if currentSpecies == nil then
|
||||
currentSpecies = 1
|
||||
end
|
||||
|
||||
if saveRequested then
|
||||
saveRequested = false
|
||||
savePool()
|
||||
end
|
||||
local slice = pool.species[currentSpecies]
|
||||
return util.promiseWrap(function()
|
||||
if loadRequested then
|
||||
loadRequested = false
|
||||
currentSpecies = nil
|
||||
-- FIXME
|
||||
return loadPool()
|
||||
end
|
||||
|
||||
if topRequested then
|
||||
topRequested = false
|
||||
playTop()
|
||||
return
|
||||
end
|
||||
if saveRequested then
|
||||
saveRequested = false
|
||||
return savePool()
|
||||
end
|
||||
|
||||
if not config.Running then
|
||||
-- FIXME Tick?
|
||||
end
|
||||
if resetRequested then
|
||||
resetRequested = false
|
||||
return reset()
|
||||
end
|
||||
|
||||
if currentSpecies == nil then
|
||||
currentSpecies = 1
|
||||
end
|
||||
if topRequested then
|
||||
topRequested = false
|
||||
return playTop()
|
||||
end
|
||||
|
||||
local slice = pool.species[currentSpecies]
|
||||
if config.NeatConfig.Threads > 1 then
|
||||
slice = {}
|
||||
for i=currentSpecies, currentSpecies + config.NeatConfig.Threads - 1, 1 do
|
||||
if pool.species[i] == nil then
|
||||
break
|
||||
end
|
||||
if not config.Running then
|
||||
-- FIXME Tick?
|
||||
end
|
||||
|
||||
table.insert(slice, pool.species[i])
|
||||
end
|
||||
end
|
||||
local finished = 0
|
||||
runner.run(
|
||||
slice,
|
||||
pool.generation,
|
||||
function()
|
||||
-- Genome callback
|
||||
end,
|
||||
function()
|
||||
if config.NeatConfig.Threads > 1 then
|
||||
finished = finished + 1
|
||||
if finished ~= #slice then
|
||||
return
|
||||
end
|
||||
currentSpecies = currentSpecies + #slice
|
||||
else
|
||||
currentSpecies = currentSpecies + 1
|
||||
end
|
||||
if hasThreads then
|
||||
slice = pool.species
|
||||
end
|
||||
|
||||
if currentSpecies > #pool.species then
|
||||
newGeneration()
|
||||
currentSpecies = 1
|
||||
end
|
||||
mainLoop(currentSpecies)
|
||||
end
|
||||
)
|
||||
return runner.run(
|
||||
slice,
|
||||
pool.generation,
|
||||
function()
|
||||
-- Genome callback
|
||||
-- FIXME Should we do something here??? What was your plan, past me?
|
||||
end
|
||||
):next(function(maxFitness)
|
||||
if maxFitness > pool.maxFitness then
|
||||
pool.maxFitness = maxFitness
|
||||
end
|
||||
|
||||
if hasThreads then
|
||||
currentSpecies = currentSpecies + #slice
|
||||
else
|
||||
currentSpecies = currentSpecies + 1
|
||||
end
|
||||
|
||||
if currentSpecies > #pool.species then
|
||||
newGeneration()
|
||||
currentSpecies = 1
|
||||
end
|
||||
end)
|
||||
end):next(function ()
|
||||
if topGenome == nil then
|
||||
return mainLoop(currentSpecies)
|
||||
end
|
||||
end)
|
||||
end
|
||||
|
||||
local topRequested = false
|
||||
local function playTop()
|
||||
playTop = function()
|
||||
local maxfitness = 0
|
||||
local maxs, maxg
|
||||
for s,species in pairs(pool.species) do
|
||||
|
@ -735,7 +776,7 @@ local function playTop()
|
|||
end
|
||||
|
||||
-- FIXME genome
|
||||
mainLoop(maxs)
|
||||
return mainLoop(maxs, maxg)
|
||||
end
|
||||
|
||||
function _M.requestLoad(filename)
|
||||
|
@ -748,6 +789,10 @@ function _M.requestSave(filename)
|
|||
saveRequested = true
|
||||
end
|
||||
|
||||
function _M.requestReset()
|
||||
resetRequested = true
|
||||
end
|
||||
|
||||
function _M.onMessage(handler)
|
||||
table.insert(_M.onMessageHandler, handler)
|
||||
end
|
||||
|
@ -761,15 +806,23 @@ function _M.requestTop()
|
|||
end
|
||||
|
||||
function _M.run(reset)
|
||||
local promise = nil
|
||||
if pool == nil or reset == true then
|
||||
initializePool(function()
|
||||
writeFile(config.PoolDir.."temp.pool")
|
||||
mainLoop()
|
||||
end)
|
||||
else
|
||||
writeFile(config.PoolDir.."temp.pool")
|
||||
mainLoop()
|
||||
promise = initializePool()
|
||||
else
|
||||
promise = Promise.new()
|
||||
promise:resolve()
|
||||
end
|
||||
|
||||
return promise:next(function()
|
||||
return writeFile(config.PoolDir.."temp.pool")
|
||||
end):next(function ()
|
||||
if not hasThreads then
|
||||
return util.loadAndStart(config.ROM)
|
||||
end
|
||||
end):next(function()
|
||||
return mainLoop()
|
||||
end)
|
||||
end
|
||||
|
||||
return _M
|
||||
return _M
|
BIN
pool/MainbraceMayhem.lsmv
Normal file
BIN
pool/MainbraceMayhem.lsmv
Normal file
Binary file not shown.
BIN
pool/MainbraceMayhemBonus.lsmv
Normal file
BIN
pool/MainbraceMayhemBonus.lsmv
Normal file
Binary file not shown.
BIN
pool/MainbraceMayhemTopOfRope.lsmv
Normal file
BIN
pool/MainbraceMayhemTopOfRope.lsmv
Normal file
Binary file not shown.
Binary file not shown.
|
@ -78,7 +78,6 @@ transition = function(promise, state, value)
|
|||
if promise.state == state
|
||||
or promise.state ~= State.PENDING
|
||||
or ( state ~= State.FULFILLED and state ~= State.REJECTED )
|
||||
or value == nil
|
||||
then
|
||||
return
|
||||
end
|
||||
|
|
|
@ -1,17 +1,18 @@
|
|||
local gui, utime, callback, set_timer_timeout = gui, utime, callback, set_timer_timeout
|
||||
|
||||
local base = string.gsub(@@LUA_SCRIPT_FILENAME@@, "(.*[/\\])(.*)", "%1")
|
||||
|
||||
local Promise = dofile(base.."/promise.lua")
|
||||
-- Only the parent should manage ticks!
|
||||
callback.register('timer', function()
|
||||
Promise.update()
|
||||
set_timer_timeout(1)
|
||||
end)
|
||||
set_timer_timeout(1)
|
||||
|
||||
local Runner = dofile(base.."/runner.lua")
|
||||
local serpent = dofile(base.."/serpent.lua")
|
||||
local util = dofile(base.."/util.lua")
|
||||
|
||||
local inputFilePath = os.getenv("RUNNER_INPUT_FILE")
|
||||
local outputFilePath = os.getenv("RUNNER_OUTPUT_FILE")
|
||||
|
||||
local first = false
|
||||
|
||||
local outContents = {}
|
||||
local util = dofile(base.."/util.lua")(Promise)
|
||||
|
||||
local statusLine = nil
|
||||
local statusColor = 0x0000ff00
|
||||
|
@ -20,26 +21,49 @@ local species = nil
|
|||
local speciesId = -1
|
||||
local generationIndex = nil
|
||||
|
||||
local runner = Runner()
|
||||
local inputPipeName = os.getenv("RUNNER_INPUT_PIPE")
|
||||
local outputPipeName = os.getenv("RUNNER_OUTPUT_PIPE")
|
||||
|
||||
print('Opening input pipe '..inputPipeName)
|
||||
local inputPipe = util.openReadPipe(inputPipeName)
|
||||
if inputPipe == nil then
|
||||
error('Error opening input file')
|
||||
end
|
||||
print('Opened input pipe '..inputPipeName)
|
||||
|
||||
print('Opening output file '..outputPipeName)
|
||||
local outputPipe = util.openReadPipeWriter(outputPipeName)
|
||||
print('Opened output file '..outputPipeName)
|
||||
|
||||
local function writeResponse(object)
|
||||
outputPipe:write(serpent.dump(object).."\n")
|
||||
outputPipe:flush()
|
||||
end
|
||||
|
||||
local function unblockLoop()
|
||||
return util.delay(1000000):next(function()
|
||||
outputPipe:write(".\n")
|
||||
outputPipe:flush()
|
||||
return unblockLoop()
|
||||
end)
|
||||
end
|
||||
|
||||
local runner = Runner(Promise)
|
||||
runner.onMessage(function(msg, color)
|
||||
statusLine = msg
|
||||
statusColor = color
|
||||
print(msg)
|
||||
table.insert(
|
||||
outContents,
|
||||
serpent.dump({
|
||||
type = 'onMessage',
|
||||
speciesId = speciesId,
|
||||
msg = msg,
|
||||
color = color,
|
||||
})
|
||||
)
|
||||
|
||||
writeResponse({
|
||||
type = 'onMessage',
|
||||
speciesId = speciesId,
|
||||
msg = msg,
|
||||
color = color,
|
||||
})
|
||||
end)
|
||||
|
||||
local guiHeight = 0
|
||||
local guiWidth = 0
|
||||
runner.onRenderForm(function(form)
|
||||
guiWidth, guiHeight = gui.resolution()
|
||||
local guiWidth, guiHeight = gui.resolution()
|
||||
gui.left_gap(0)
|
||||
gui.top_gap(0)
|
||||
gui.bottom_gap(0)
|
||||
|
@ -47,7 +71,7 @@ runner.onRenderForm(function(form)
|
|||
form:draw(0, 0)
|
||||
|
||||
if statusLine ~= nil then
|
||||
gui.rectangle(0, guiHeight - 20, 0, 20, 1, 0x00000000, statusColor)
|
||||
gui.rectangle(0, guiHeight - 20, guiWidth, 20, 1, 0x00000000, statusColor)
|
||||
gui.text(0, guiHeight - 20, statusLine, 0x00000000)
|
||||
end
|
||||
|
||||
|
@ -55,110 +79,88 @@ runner.onRenderForm(function(form)
|
|||
end)
|
||||
|
||||
runner.onSave(function(filename)
|
||||
table.insert(
|
||||
outContents,
|
||||
serpent.dump({
|
||||
type = 'onSave',
|
||||
filename = filename,
|
||||
speciesId = speciesId,
|
||||
})
|
||||
)
|
||||
writeResponse({
|
||||
type = 'onSave',
|
||||
filename = filename,
|
||||
speciesId = speciesId,
|
||||
})
|
||||
end)
|
||||
|
||||
runner.onLoad(function(filename)
|
||||
table.insert(
|
||||
outContents,
|
||||
serpent.dump({
|
||||
type = 'onLoad',
|
||||
filename = filename,
|
||||
speciesId = speciesId,
|
||||
})
|
||||
)
|
||||
writeResponse({
|
||||
type = 'onLoad',
|
||||
filename = filename,
|
||||
speciesId = speciesId,
|
||||
})
|
||||
end)
|
||||
|
||||
local waiter = util.waitForChange(inputFilePath)
|
||||
runner.onReset(function()
|
||||
writeResponse({
|
||||
type = 'onReset',
|
||||
speciesId = speciesId,
|
||||
})
|
||||
end)
|
||||
|
||||
local function waitLoop()
|
||||
if not first then
|
||||
local sec, usec = utime()
|
||||
local ts = sec * 1000000 + usec
|
||||
local function waitLoop(inputLine)
|
||||
return util.promiseWrap(function()
|
||||
local ok, inputData = serpent.load(inputLine)
|
||||
|
||||
local outFile = io.open(outputFilePath, "w")
|
||||
outFile:write(serpent.dump({ type = 'onInit', ts = ts }))
|
||||
outFile:close()
|
||||
|
||||
print(string.format('Wrote init to output at %d', ts))
|
||||
|
||||
first = true
|
||||
end
|
||||
|
||||
print('Waiting for input from master process')
|
||||
|
||||
waiter:read("*a")
|
||||
util.closeCmd(waiter)
|
||||
|
||||
print('Received input from master process')
|
||||
|
||||
local inputData = nil
|
||||
local ok = false
|
||||
while not ok or inputData == nil or speciesId == inputData[1].id do
|
||||
local inputFile = io.open(inputFilePath, 'r')
|
||||
ok, inputData = serpent.load(inputFile:read('*a'))
|
||||
inputFile:close()
|
||||
|
||||
if not ok then
|
||||
print("Deserialization error")
|
||||
if not ok or inputData == nil then
|
||||
io.stderr:write("Deserialization error\n")
|
||||
io.stderr:write(inputLine.."\n")
|
||||
return
|
||||
end
|
||||
end
|
||||
|
||||
species = inputData[1]
|
||||
print('Received input from master process')
|
||||
|
||||
speciesId = species.id
|
||||
species = inputData[1]
|
||||
|
||||
generationIndex = inputData[2]
|
||||
speciesId = species.id
|
||||
|
||||
outContents = {}
|
||||
generationIndex = inputData[2]
|
||||
|
||||
print('Running')
|
||||
print('Running')
|
||||
|
||||
runner.run(
|
||||
species,
|
||||
generationIndex,
|
||||
function(genome, index)
|
||||
table.insert(
|
||||
outContents,
|
||||
serpent.dump({
|
||||
return runner.run(
|
||||
species,
|
||||
generationIndex,
|
||||
function(genome, index)
|
||||
writeResponse({
|
||||
type = 'onGenome',
|
||||
genome = genome,
|
||||
genomeIndex = index,
|
||||
speciesId = speciesId,
|
||||
})
|
||||
)
|
||||
end,
|
||||
function()
|
||||
table.insert(
|
||||
outContents,
|
||||
serpent.dump({
|
||||
type = 'onFinish',
|
||||
speciesId = speciesId,
|
||||
})
|
||||
)
|
||||
|
||||
-- Truncate the input file to reduce the amount of time
|
||||
-- wasted if we reopen it too early
|
||||
local inputFile = io.open(inputFilePath, "w")
|
||||
inputFile:close()
|
||||
|
||||
waiter = util.waitForChange(inputFilePath)
|
||||
|
||||
-- Write the result
|
||||
local outFile = io.open(outputFilePath, "w")
|
||||
outFile:write(table.concat(outContents, "\n"))
|
||||
outFile:close()
|
||||
|
||||
waitLoop()
|
||||
end
|
||||
)
|
||||
end
|
||||
):next(function(maxFitness)
|
||||
writeResponse({
|
||||
type = 'onFinish',
|
||||
maxFitness = maxFitness,
|
||||
speciesId = speciesId,
|
||||
})
|
||||
end)
|
||||
end):next(function()
|
||||
return inputPipe:read("*l")
|
||||
end):next(waitLoop)
|
||||
end
|
||||
|
||||
waitLoop()
|
||||
local sec, usec = utime()
|
||||
local ts = sec * 1000000 + usec
|
||||
|
||||
local waiter = util.promiseWrap(function()
|
||||
return inputPipe:read("*l")
|
||||
end)
|
||||
|
||||
writeResponse({ type = 'onInit', ts = ts })
|
||||
|
||||
print(string.format('Wrote init to output at %d', ts))
|
||||
|
||||
waiter:next(function(inputLine)
|
||||
return waitLoop(inputLine)
|
||||
end):catch(function(error)
|
||||
if type(error) == "table" then
|
||||
error = "\n"..table.concat(error, "\n")
|
||||
end
|
||||
print('Runner process error: '..error)
|
||||
io.stderr:write('Runner process error: '..error..'\n')
|
||||
end)
|
|
@ -1,28 +1,20 @@
|
|||
local random = random
|
||||
|
||||
local base = string.gsub(@@LUA_SCRIPT_FILENAME@@, "(.*[/\\])(.*)", "%1")
|
||||
|
||||
local util = dofile(base.."/util.lua")
|
||||
local Promise = nil
|
||||
|
||||
local util = nil
|
||||
local config = dofile(base.."/config.lua")
|
||||
local serpent = dofile(base.."/serpent.lua")
|
||||
local temps = {
|
||||
os.getenv("TMPDIR"),
|
||||
os.getenv("TEMP"),
|
||||
os.getenv("TEMPDIR"),
|
||||
os.getenv("TMP"),
|
||||
}
|
||||
|
||||
local tempDir = "/tmp"
|
||||
for i=1,#temps,1 do
|
||||
local temp = temps[i]
|
||||
if temp ~= nil and temp ~= "" then
|
||||
tempDir = temps[i]
|
||||
break
|
||||
end
|
||||
end
|
||||
|
||||
local tmpFileName = tempDir.."/donk_runner_"..
|
||||
local pipePrefix = "donk_runner_"..
|
||||
string.hex(math.floor(random.integer(0, 0xffffffff)))..
|
||||
string.hex(math.floor(random.integer(0, 0xffffffff)))
|
||||
|
||||
local inputPrefix = pipePrefix..'_input_'
|
||||
local outputPrefix = pipePrefix..'_output_'
|
||||
|
||||
local function message(_M, msg, color)
|
||||
if color == nil then
|
||||
color = 0x00009900
|
||||
|
@ -53,18 +45,96 @@ local function onLoad(_M, handler)
|
|||
table.insert(_M.onLoadHandler, handler)
|
||||
end
|
||||
|
||||
local function reset(_M)
|
||||
for i=#_M.onResetHandler,1,-1 do
|
||||
_M.onResetHandler[i]()
|
||||
end
|
||||
end
|
||||
|
||||
local function onReset(_M, handler)
|
||||
table.insert(_M.onResetHandler, handler)
|
||||
end
|
||||
|
||||
local function onMessage(_M, handler)
|
||||
table.insert(_M.onMessageHandler, handler)
|
||||
end
|
||||
|
||||
return function()
|
||||
--- Launches the child processes
|
||||
---@param _M table The instance
|
||||
---@param count integer Number of processes needed
|
||||
---@return Promise Promise A promise that resolves when all the processes are ready
|
||||
local function launchChildren(_M, count)
|
||||
local promises = {}
|
||||
for i=#_M.poppets+1,count,1 do
|
||||
local newOne = {
|
||||
process = nil,
|
||||
output = util.openReadPipe(outputPrefix..i),
|
||||
input = nil,
|
||||
}
|
||||
|
||||
local outputPipeName = outputPrefix..i
|
||||
local inputPipeName = inputPrefix..i
|
||||
|
||||
local settingsDir = nil
|
||||
if util.isWin then
|
||||
settingsDir = util.getTempDir().."/donk_runner_settings_"..i
|
||||
util.mkdir(settingsDir)
|
||||
end
|
||||
|
||||
local envs = {
|
||||
RUNNER_INPUT_PIPE = inputPipeName,
|
||||
RUNNER_OUTPUT_PIPE = outputPipeName,
|
||||
APPDATA = settingsDir,
|
||||
}
|
||||
|
||||
local cmd = '"'.._M.hostProcess..'" "--rom='..config.ROM..'" --unpause "--lua='..base..'/runner-process.lua"'
|
||||
newOne.process = util.popenCmd(cmd, nil, envs)
|
||||
|
||||
-- Wait for init
|
||||
local promise = util.promiseWrap(function()
|
||||
newOne.output:read("*l")
|
||||
while newOne.input == nil do
|
||||
newOne.input = util.openReadPipeWriter(inputPipeName)
|
||||
end
|
||||
end)
|
||||
table.insert(promises, promise)
|
||||
table.insert(_M.poppets, newOne)
|
||||
end
|
||||
|
||||
return Promise.all(table.unpack(promises))
|
||||
end
|
||||
|
||||
return function(promise)
|
||||
-- FIXME Should this be a global???
|
||||
Promise = promise
|
||||
if util == nil then
|
||||
util = dofile(base.."/util.lua")(Promise)
|
||||
end
|
||||
-- FIXME Maybe don't do this in the "constructor"?
|
||||
if util.isWin then
|
||||
util.downloadFile("https://github.com/psmay/windows-named-pipe-utils/releases/download/v0.1.1/build.zip", base.."/namedpipe.zip")
|
||||
util.unzip(base.."/namedpipe.zip", base)
|
||||
os.rename(base.."/build", "namedpipe")
|
||||
end
|
||||
|
||||
local _M = {
|
||||
onMessageHandler = {},
|
||||
onResetHandler = {},
|
||||
onSaveHandler = {},
|
||||
onLoadHandler = {},
|
||||
poppets = {},
|
||||
hostProcess = "lsnes",
|
||||
}
|
||||
|
||||
if util.isWin then
|
||||
_M.hostProcess = util.scrapeCmd('*l', 'powershell "(Get-WmiObject Win32_Process -Filter ProcessId=$((Get-WmiObject Win32_Process -Filter ProcessId=$((Get-WmiObject Win32_Process -Filter ProcessId=$PID).ParentProcessId)).ParentProcessId)").ExecutablePath')
|
||||
if _M.hostProcess == nil or _M.hostProcess == "" then
|
||||
_M.hostProcess = "lsnes-bsnes.exe"
|
||||
end
|
||||
else
|
||||
-- FIXME Linux
|
||||
end
|
||||
|
||||
_M.onRenderForm = function(handler)
|
||||
end
|
||||
|
||||
|
@ -84,116 +154,110 @@ return function()
|
|||
onLoad(_M, handler)
|
||||
end
|
||||
|
||||
_M.run = function(species, generationIdx, genomeCallback, finishCallback)
|
||||
local hostProcess = "lsnes"
|
||||
if util.isWin then
|
||||
hostProcess = util.scrapeCmd('*l', 'powershell "(Get-WmiObject Win32_Process -Filter ProcessId=$((Get-WmiObject Win32_Process -Filter ProcessId=$((Get-WmiObject Win32_Process -Filter ProcessId=$PID).ParentProcessId)).ParentProcessId)").ExecutablePath')
|
||||
if hostProcess == nil or hostProcess == "" then
|
||||
hostProcess = "lsnes-bsnes.exe"
|
||||
end
|
||||
else
|
||||
-- FIXME Linux
|
||||
end
|
||||
_M.onReset = function(handler)
|
||||
onReset(_M, handler)
|
||||
end
|
||||
|
||||
local inputPrefix = tmpFileName..'_input_'
|
||||
local outputPrefix = tmpFileName..'_output_'
|
||||
|
||||
-- Create the input files and output files
|
||||
for i=1,#species,1 do
|
||||
local inputFileName = inputPrefix..i
|
||||
local inputFile = io.open(inputFileName, 'a')
|
||||
inputFile:close()
|
||||
_M.run = function(species, generationIdx, genomeCallback)
|
||||
local promise = Promise.new()
|
||||
promise:resolve()
|
||||
return promise:next(function()
|
||||
return launchChildren(_M, config.NeatConfig.Threads)
|
||||
end):next(function()
|
||||
message(_M, 'Setting up child processes')
|
||||
|
||||
local outputFileName = outputPrefix..i
|
||||
local outputFile = io.open(outputFileName, 'a')
|
||||
outputFile:close()
|
||||
end
|
||||
|
||||
while #_M.poppets < #species do
|
||||
local i = #_M.poppets+1
|
||||
local outputFileName = outputPrefix..i
|
||||
local inputFileName = inputPrefix..i
|
||||
|
||||
message(_M, hostProcess)
|
||||
|
||||
local envs = {
|
||||
RUNNER_INPUT_FILE = inputFileName,
|
||||
RUNNER_OUTPUT_FILE = outputFileName,
|
||||
}
|
||||
|
||||
local waiter = util.waitForChange(outputFileName)
|
||||
|
||||
local cmd = '"'..hostProcess..'" "--rom='..config.ROM..'" --unpause "--lua='..base..'/runner-process.lua"'
|
||||
local poppet = util.popenCmd(cmd, nil, envs)
|
||||
table.insert(_M.poppets, poppet)
|
||||
|
||||
waiter:read("*a")
|
||||
util.closeCmd(waiter)
|
||||
end
|
||||
|
||||
local waiters = {}
|
||||
for i=1,#species,1 do
|
||||
table.insert(waiters, outputPrefix..i)
|
||||
end
|
||||
|
||||
local waiter = util.waitForChange(waiters, nil, tmpFileName.."output_*")
|
||||
|
||||
message(_M, 'Setting up child processes')
|
||||
|
||||
for i=1,#species,1 do
|
||||
|
||||
local inputFileName = tmpFileName.."_input_"..i
|
||||
local inputFile = io.open(inputFileName, 'w')
|
||||
inputFile:write(serpent.dump({species[i], generationIdx}))
|
||||
inputFile:close()
|
||||
end
|
||||
|
||||
message(_M, 'Waiting for child processes to finish')
|
||||
|
||||
waiter:read("*a")
|
||||
util.closeCmd(waiter)
|
||||
|
||||
message(_M, 'Child processes finished')
|
||||
|
||||
for i=1,#species,1 do
|
||||
message(_M, "Processing output "..i)
|
||||
local outputFileName = tmpFileName..'_output_'..i
|
||||
local outputFile = io.open(outputFileName, "r")
|
||||
local line = ""
|
||||
repeat
|
||||
local ok, obj = serpent.load(line)
|
||||
if not ok then
|
||||
goto continue
|
||||
end
|
||||
|
||||
if obj == nil then
|
||||
goto continue
|
||||
end
|
||||
|
||||
if obj.type == 'onMessage' then
|
||||
message(_M, obj.msg, obj.color)
|
||||
elseif obj.type == 'onLoad' then
|
||||
load(_M, obj.filename)
|
||||
elseif obj.type == 'onSave' then
|
||||
save(_M, obj.filename)
|
||||
elseif obj.type == 'onGenome' then
|
||||
for i=1,#species,1 do
|
||||
local s = species[i]
|
||||
if s.id == obj.speciesId then
|
||||
s.genomes[obj.genomeIndex] = obj.genome
|
||||
break
|
||||
end
|
||||
local maxFitness = nil
|
||||
local function readLoop(outputPipe)
|
||||
return util.promiseWrap(function()
|
||||
return outputPipe:read("*l")
|
||||
end):next(function(line)
|
||||
if line == nil or line == "" then
|
||||
util.closeCmd(outputPipe)
|
||||
end
|
||||
genomeCallback(obj.genome, obj.index)
|
||||
elseif obj.type == 'onFinish' then
|
||||
finishCallback()
|
||||
end
|
||||
|
||||
::continue::
|
||||
line = outputFile:read()
|
||||
until(line == "" or line == nil)
|
||||
outputFile:close()
|
||||
end
|
||||
local ok, obj = serpent.load(line)
|
||||
if not ok then
|
||||
return false
|
||||
end
|
||||
|
||||
if obj == nil then
|
||||
return false
|
||||
end
|
||||
|
||||
if obj.type == 'onMessage' then
|
||||
message(_M, obj.msg, obj.color)
|
||||
elseif obj.type == 'onLoad' then
|
||||
load(_M, obj.filename)
|
||||
elseif obj.type == 'onSave' then
|
||||
save(_M, obj.filename)
|
||||
elseif obj.type == 'onReset' then
|
||||
reset(_M)
|
||||
elseif obj.type == 'onGenome' then
|
||||
for i=1,#species,1 do
|
||||
local s = species[i]
|
||||
if s.id == obj.speciesId then
|
||||
message(_M, string.format('Write Species %d Genome %d', obj.speciesId, obj.genomeIndex))
|
||||
s.genomes[obj.genomeIndex] = obj.genome
|
||||
break
|
||||
end
|
||||
end
|
||||
genomeCallback(obj.genome, obj.index)
|
||||
elseif obj.type == 'onFinish' then
|
||||
if maxFitness == nil or obj.maxFitness > maxFitness then
|
||||
maxFitness = obj.maxFitness
|
||||
end
|
||||
return true
|
||||
end
|
||||
|
||||
end):next(function(finished)
|
||||
if finished then
|
||||
return maxFitness
|
||||
end
|
||||
|
||||
return readLoop(outputPipe)
|
||||
end)
|
||||
end
|
||||
|
||||
local waiters = {}
|
||||
for t=1,config.NeatConfig.Threads,1 do
|
||||
waiters[t] = Promise.new()
|
||||
waiters[t]:resolve()
|
||||
end
|
||||
|
||||
local currentSpecies = 1
|
||||
while currentSpecies < #species do
|
||||
for t=1,config.NeatConfig.Threads,1 do
|
||||
local s = species[currentSpecies]
|
||||
if s == nil then
|
||||
break
|
||||
end
|
||||
|
||||
local inputPipe = _M.poppets[t].input
|
||||
local outputPipe = _M.poppets[t].output
|
||||
waiters[t] = waiters[t]:next(function()
|
||||
inputPipe:write(serpent.dump({s, generationIdx}).."\n")
|
||||
inputPipe:flush()
|
||||
|
||||
return readLoop(outputPipe)
|
||||
end)
|
||||
currentSpecies = currentSpecies + 1
|
||||
end
|
||||
end
|
||||
|
||||
message(_M, 'Waiting for child processes to finish')
|
||||
|
||||
return Promise.all(table.unpack(waiters))
|
||||
end):next(function(maxFitnesses)
|
||||
message(_M, 'Child processes finished')
|
||||
local maxestFitness = maxFitnesses[1]
|
||||
for i=1,#maxFitnesses,1 do
|
||||
local maxFitness = maxFitnesses[i]
|
||||
if maxFitness > maxestFitness then
|
||||
maxestFitness = maxFitness
|
||||
end
|
||||
end
|
||||
return maxestFitness
|
||||
end)
|
||||
end
|
||||
|
||||
return _M
|
||||
|
|
755
runner.lua
755
runner.lua
|
@ -1,8 +1,13 @@
|
|||
local mem = require "mem"
|
||||
local gui, input, movie, settings, exec, callback, set_timer_timeout, memory, bsnes = gui, input, movie, settings, exec, callback, set_timer_timeout, memory, bsnes
|
||||
|
||||
local base = string.gsub(@@LUA_SCRIPT_FILENAME@@, "(.*[/\\])(.*)", "%1")
|
||||
local Promise = nil
|
||||
|
||||
local config = dofile(base.."/config.lua")
|
||||
local game = dofile(base.."/game.lua")
|
||||
local game = nil
|
||||
local mathFunctions = dofile(base.."/mathFunctions.lua")
|
||||
local util = dofile(base.."/util.lua")()
|
||||
|
||||
local Inputs = config.InputSize+1
|
||||
local Outputs = #config.ButtonNames
|
||||
|
@ -81,6 +86,18 @@ local function displayGenome(genome)
|
|||
if gene.enabled then
|
||||
local c1 = cells[gene.into]
|
||||
local c2 = cells[gene.out]
|
||||
if c1 == nil then
|
||||
c1 = {
|
||||
x = 0,
|
||||
y = 0,
|
||||
}
|
||||
end
|
||||
if c2 == nil then
|
||||
c2 = {
|
||||
x = 0,
|
||||
y = 0,
|
||||
}
|
||||
end
|
||||
if gene.into > Inputs and gene.into <= config.NeatConfig.MaxNodes then
|
||||
c1.x = 0.75*c1.x + 0.25*c2.x
|
||||
if c1.x >= c2.x then
|
||||
|
@ -147,6 +164,18 @@ local function displayGenome(genome)
|
|||
if true then
|
||||
local c1 = cells[gene.into]
|
||||
local c2 = cells[gene.out]
|
||||
if(c1 == nil) then
|
||||
c1 = {
|
||||
x = 0,
|
||||
y = 0,
|
||||
}
|
||||
end
|
||||
if(c2 == nil) then
|
||||
c2 = {
|
||||
x = 0,
|
||||
y = 0,
|
||||
}
|
||||
end
|
||||
local alpha = 0x20000000
|
||||
if c1.value == 0 then
|
||||
alpha = 0xA0000000
|
||||
|
@ -187,19 +216,9 @@ local function displayGenome(genome)
|
|||
gui.renderctx.setnull()
|
||||
end
|
||||
|
||||
local function advanceFrame(_M, after)
|
||||
table.insert(_M.onFrameAdvancedHandler, after)
|
||||
end
|
||||
|
||||
local function processFrameAdvanced(_M)
|
||||
for i=#_M.onFrameAdvancedHandler,1,-1 do
|
||||
table.remove(_M.onFrameAdvancedHandler, i)()
|
||||
end
|
||||
end
|
||||
|
||||
local buttons = nil
|
||||
local buttonCtx = gui.renderctx.new(500, 70)
|
||||
function displayButtons()
|
||||
local function displayButtons(_M)
|
||||
buttonCtx:set()
|
||||
buttonCtx:clear()
|
||||
|
||||
|
@ -213,7 +232,7 @@ function displayButtons()
|
|||
end
|
||||
gui.text(5, 2, "[1] "..startStop)
|
||||
|
||||
gui.text(130, 2, "[4] Play Top")
|
||||
--gui.text(130, 2, "[4] Play Top")
|
||||
|
||||
gui.text(240, 2, "[6] Save")
|
||||
|
||||
|
@ -223,7 +242,7 @@ function displayButtons()
|
|||
|
||||
local insert = ""
|
||||
local confirm = "[Tab] Type in filename"
|
||||
if inputmode then
|
||||
if _M.inputmode then
|
||||
insert = "_"
|
||||
confirm = "[Tab] Confirm filename"
|
||||
end
|
||||
|
@ -236,14 +255,26 @@ function displayButtons()
|
|||
gui.renderctx.setnull()
|
||||
end
|
||||
|
||||
local function getDistanceTraversed(areaInfos)
|
||||
local distanceTraversed = 0
|
||||
for _,areaInfo in pairs(areaInfos) do
|
||||
for i=1,#areaInfo.waypoints,1 do
|
||||
local waypoint = areaInfo.waypoints[i]
|
||||
distanceTraversed = distanceTraversed + (waypoint.startDistance - waypoint.shortest)
|
||||
end
|
||||
end
|
||||
return distanceTraversed
|
||||
end
|
||||
|
||||
local formCtx = nil
|
||||
local form = nil
|
||||
function displayForm(_M)
|
||||
if #_M.onRenderFormHandler == 0 then
|
||||
local function displayForm(_M)
|
||||
if config.NeatConfig.ShowInterface == false or #_M.onRenderFormHandler == 0 then
|
||||
return
|
||||
end
|
||||
|
||||
if form ~= nil and _M.drawFrame % 10 ~= 0 then
|
||||
gui.renderctx.setnull()
|
||||
for i=#_M.onRenderFormHandler,1,-1 do
|
||||
_M.onRenderFormHandler[i](form)
|
||||
end
|
||||
|
@ -254,30 +285,33 @@ function displayForm(_M)
|
|||
formCtx:clear()
|
||||
gui.rectangle(0, 0, 500, guiHeight, 1, 0x00ffffff, 0xbb000000)
|
||||
--gui.circle(game.screenX-84, game.screenY-84, 192 / 2, 1, 0x50000000)
|
||||
|
||||
local rightmost = _M.rightmost[_M.currentArea]
|
||||
if rightmost == nil then
|
||||
rightmost = 0
|
||||
|
||||
local distanceTraversed = getDistanceTraversed(_M.areaInfo)
|
||||
local goalX = 0
|
||||
local goalY = 0
|
||||
local areaInfo = _M.areaInfo[_M.currentArea]
|
||||
if areaInfo ~= nil then
|
||||
goalX = areaInfo.preferredExit.x
|
||||
goalY = areaInfo.preferredExit.y
|
||||
end
|
||||
|
||||
gui.text(5, 30, "Timeout: " .. _M.timeout)
|
||||
gui.text(5, 5, "Generation: " .. _M.currentGenerationIndex)
|
||||
gui.text(130, 5, "Species: " .. _M.currentSpecies.id)
|
||||
gui.text(230, 5, "Genome: " .. _M.currentGenomeIndex)
|
||||
gui.text(130, 30, "Max: " .. math.floor(_M.maxFitness))
|
||||
--gui.text(330, 5, "Measured: " .. math.floor(measured/total*100) .. "%")
|
||||
gui.text(5, 65, "Bananas: " .. (game.getBananas() - _M.startBananas))
|
||||
gui.text(5, 80, "KONG: " .. (game.getKong() - _M.startKong))
|
||||
gui.text(5, 95, "Krem: " .. (game.getKremCoins() - _M.startKrem))
|
||||
gui.text(130, 65, "Coins: " .. (game.getCoins() - _M.startCoins))
|
||||
gui.text(130, 80, "Lives: " .. game.getLives())
|
||||
gui.text(130, 95, "Bumps: " .. _M.bumps)
|
||||
gui.text(230, 65, "Damage: " .. _M.partyHitCounter)
|
||||
gui.text(230, 80, "PowerUp: " .. _M.powerUpCounter)
|
||||
gui.text(320, 65, string.format("Current Area: %04x", _M.currentArea))
|
||||
gui.text(320, 80, "Rightmost: "..rightmost)
|
||||
gui.text(5, 5, string.format([[
|
||||
Generation: %4d Species: %4d Genome: %4d
|
||||
|
||||
displayButtons()
|
||||
Timeout: %4d Max: %6d
|
||||
|
||||
Bananas: %4d Coins: %3d Damage: %3d Current area: %04x
|
||||
KONG: %7d Lives: %3d Powerup: %2d Traveled: %8d
|
||||
Krem: %7d Bumps: %3d Goal Offset: %8d, %7d
|
||||
]],
|
||||
_M.currentGenerationIndex, _M.currentSpecies.id, _M.currentGenomeIndex,
|
||||
_M.timeout, math.floor(_M.maxFitness),
|
||||
_M.totalBananas, game.getCoins() - _M.startCoins, _M.partyHitCounter, _M.currentArea,
|
||||
game.getKong() - _M.startKong, game.getLives(), _M.powerUpCounter, distanceTraversed,
|
||||
game.getKremCoins() - _M.startKrem, _M.bumps, goalX - game.partyX, goalY - game.partyY
|
||||
))
|
||||
|
||||
displayButtons(_M)
|
||||
formCtx:set()
|
||||
buttons:draw(5, 130)
|
||||
|
||||
|
@ -302,14 +336,13 @@ local function painting(_M)
|
|||
displayForm(_M)
|
||||
end
|
||||
|
||||
local function evaluateNetwork(network, inputs, inputDeltas)
|
||||
local function evaluateNetwork(_M, network, inputs, inputDeltas)
|
||||
table.insert(inputs, 1)
|
||||
table.insert(inputDeltas,99)
|
||||
if #inputs ~= Inputs then
|
||||
message(_M, "Incorrect number of neural network inputs.", 0x00990000)
|
||||
return {}
|
||||
end
|
||||
|
||||
|
||||
for i=1,Inputs do
|
||||
network.neurons[i].value = inputs[i] * inputDeltas[i]
|
||||
|
@ -347,23 +380,8 @@ local function evaluateNetwork(network, inputs, inputDeltas)
|
|||
return outputs
|
||||
end
|
||||
|
||||
local function evaluateCurrent(_M)
|
||||
local genome = _M.currentSpecies.genomes[_M.currentGenomeIndex]
|
||||
|
||||
local inputDeltas = {}
|
||||
inputs, inputDeltas = game.getInputs()
|
||||
|
||||
controller = evaluateNetwork(genome.network, inputs, inputDeltas)
|
||||
|
||||
if controller[6] and controller[7] then
|
||||
controller[6] = false
|
||||
controller[7] = false
|
||||
end
|
||||
if controller[4] and controller[5] then
|
||||
controller[4] = false
|
||||
controller[5] = false
|
||||
end
|
||||
|
||||
local controller = {}
|
||||
local function updateController()
|
||||
for b=0,#config.ButtonNames - 1,1 do
|
||||
if controller[b] then
|
||||
input.set(0, b, 1)
|
||||
|
@ -373,222 +391,30 @@ local function evaluateCurrent(_M)
|
|||
end
|
||||
end
|
||||
|
||||
local frame = 0
|
||||
local lastFrame = 0
|
||||
|
||||
local function evaluateCurrent(_M, inputs, inputDeltas)
|
||||
local genome = _M.currentSpecies.genomes[_M.currentGenomeIndex]
|
||||
|
||||
controller = evaluateNetwork(_M, genome.network, inputs, inputDeltas)
|
||||
|
||||
if controller[6] and controller[7] then
|
||||
controller[6] = false
|
||||
controller[7] = false
|
||||
end
|
||||
if controller[4] and controller[5] then
|
||||
controller[4] = false
|
||||
controller[5] = false
|
||||
end
|
||||
end
|
||||
|
||||
local function fitnessAlreadyMeasured(_M)
|
||||
local genome = _M.currentSpecies.genomes[_M.currentGenomeIndex]
|
||||
|
||||
return genome.fitness ~= 0
|
||||
end
|
||||
|
||||
local rew = movie.to_rewind(config.NeatConfig.Filename)
|
||||
|
||||
local function initializeRun(_M, after)
|
||||
message(_M, string.format("Total Genomes: %d", #_M.currentSpecies.genomes))
|
||||
|
||||
settings.set_speed("turbo")
|
||||
-- XXX Does this actually work or only affects new VM loads?
|
||||
settings.set('lua-maxmem', 1024)
|
||||
exec('enable-sound off')
|
||||
gui.subframe_update(false)
|
||||
table.insert(_M.runInitialized, after)
|
||||
movie.unsafe_rewind(rew)
|
||||
end
|
||||
|
||||
local function mainLoop(_M, genome)
|
||||
advanceFrame(_M, function()
|
||||
if genome ~= nil then
|
||||
_M.currentFrame = _M.currentFrame + 1
|
||||
end
|
||||
|
||||
genome = _M.currentSpecies.genomes[_M.currentGenomeIndex]
|
||||
|
||||
if _M.drawFrame % 10 == 0 then
|
||||
if not pcall(function()
|
||||
displayGenome(genome)
|
||||
end) then
|
||||
message(_M, "Could not render genome graph", 0x00990000)
|
||||
end
|
||||
end
|
||||
|
||||
if _M.currentFrame%5 == 0 then
|
||||
evaluateCurrent(_M)
|
||||
end
|
||||
|
||||
for b=0,#config.ButtonNames - 1,1 do
|
||||
if controller[b] then
|
||||
input.set(0, b, 1)
|
||||
else
|
||||
input.set(0, b, 0)
|
||||
end
|
||||
end
|
||||
|
||||
game.getPositions()
|
||||
local timeoutConst = 0
|
||||
if game.vertical then
|
||||
timeoutConst = config.NeatConfig.TimeoutConstant * 10
|
||||
else
|
||||
timeoutConst = config.NeatConfig.TimeoutConstant
|
||||
end
|
||||
|
||||
-- Don't punish being launched by barrels
|
||||
-- FIXME Will this skew mine cart levels?
|
||||
if game.getVelocityY() < -2104 then
|
||||
message(_M, "BARREL! ".._M.drawFrame, 0x00ffff00)
|
||||
if _M.timeout < timeoutConst + 60 * 12 then
|
||||
_M.timeout = _M.timeout + 60 * 12
|
||||
end
|
||||
end
|
||||
|
||||
local nextArea = game.getCurrentArea()
|
||||
if nextArea ~= _M.lastArea then
|
||||
_M.lastArea = nextArea
|
||||
game.onceAreaLoaded(function()
|
||||
message(_M, "Loady")
|
||||
_M.timeout = _M.timeout + 60 * 5
|
||||
_M.currentArea = nextArea
|
||||
_M.lastArea = _M.currentArea
|
||||
if _M.rightmost[_M.currentArea] == nil then
|
||||
_M.rightmost[_M.currentArea] = 0
|
||||
_M.upmost[_M.currentArea] = 0
|
||||
end
|
||||
end)
|
||||
end
|
||||
|
||||
if not game.vertical then
|
||||
if game.partyX > _M.rightmost[_M.currentArea] then
|
||||
_M.rightmost[_M.currentArea] = game.partyX
|
||||
if _M.timeout < timeoutConst then
|
||||
_M.timeout = timeoutConst
|
||||
end
|
||||
end
|
||||
else
|
||||
if game.partyY > _M.upmost[_M.currentArea] then
|
||||
_M.upmost[_M.currentArea] = game.partyY
|
||||
if _M.timeout < timeoutConst then
|
||||
_M.timeout = timeoutConst
|
||||
end
|
||||
end
|
||||
end
|
||||
-- FIXME Measure distance to target / area exit
|
||||
-- We might not always be horizontal
|
||||
|
||||
local hitTimer = game.getHitTimer(_M.lastBoth)
|
||||
|
||||
if hitTimer > 0 then
|
||||
_M.partyHitCounter = _M.partyHitCounter + 1
|
||||
--message(_M, "party took damage, hit counter: " .. _M.partyHitCounter)
|
||||
end
|
||||
|
||||
local powerUp = game.getBoth()
|
||||
_M.lastBoth = powerUp
|
||||
if powerUp > 0 then
|
||||
if powerUp ~= _M.powerUpBefore then
|
||||
_M.powerUpCounter = _M.powerUpCounter + 1
|
||||
_M.powerUpBefore = powerUp
|
||||
end
|
||||
end
|
||||
|
||||
local krem = game.getKremCoins() - _M.startKrem
|
||||
if krem > _M.lastKrem then
|
||||
message(_M, string.format("Kremcoin grabbed: %d", _M.timeout), 0x00009900)
|
||||
_M.lastKrem = krem
|
||||
_M.timeout = _M.timeout + 60 * 10
|
||||
end
|
||||
|
||||
_M.timeout = _M.timeout - 1
|
||||
|
||||
-- Continue if we haven't timed out
|
||||
local timeoutBonus = _M.currentFrame / 4
|
||||
if _M.timeout + timeoutBonus > 0 then
|
||||
mainLoop(_M, genome)
|
||||
return
|
||||
end
|
||||
|
||||
-- Timeout calculations beyond this point
|
||||
-- Manipulating the timeout value won't have
|
||||
-- any effect
|
||||
local bananas = game.getBananas() - _M.startBananas
|
||||
local coins = game.getCoins() - _M.startCoins
|
||||
local kong = game.getKong()
|
||||
|
||||
message(_M, string.format("Bananas: %d, coins: %d, Krem: %d, KONG: %d", bananas, coins, krem, kong))
|
||||
|
||||
local bananaCoinsFitness = (krem * 100) + (kong * 60) + (bananas * 50) + (coins * 0.2)
|
||||
if (bananas + coins) > 0 then
|
||||
message(_M, "Bananas, Coins, KONG added " .. bananaCoinsFitness .. " fitness")
|
||||
end
|
||||
|
||||
local hitPenalty = _M.partyHitCounter * 100
|
||||
local bumpPenalty = _M.bumps * 100
|
||||
local powerUpBonus = _M.powerUpCounter * 100
|
||||
|
||||
local most = 0
|
||||
if not game.vertical then
|
||||
for k,v in pairs(_M.rightmost) do
|
||||
most = most + v
|
||||
end
|
||||
most = most - _M.currentFrame / 2
|
||||
else
|
||||
for k,v in pairs(_M.upmost) do
|
||||
most = most + v
|
||||
end
|
||||
most = most - _M.currentFrame / 2
|
||||
end
|
||||
|
||||
local fitness = bananaCoinsFitness - bumpPenalty - hitPenalty + powerUpBonus + most + game.getJumpHeight() / 100
|
||||
|
||||
local lives = game.getLives()
|
||||
|
||||
if _M.startLives < lives then
|
||||
local extraLiveBonus = (lives - _M.startLives)*1000
|
||||
fitness = fitness + extraLiveBonus
|
||||
message(_M, "Extra live bonus added " .. extraLiveBonus)
|
||||
end
|
||||
|
||||
if game.getGoalHit() then
|
||||
fitness = fitness + 1000
|
||||
message(_M, string.format("LEVEL WON! Fitness: %d", fitness), 0x0000ff00)
|
||||
end
|
||||
if fitness == 0 then
|
||||
fitness = -1
|
||||
end
|
||||
genome.fitness = fitness
|
||||
|
||||
if fitness > _M.maxFitness then
|
||||
_M.maxFitness = fitness
|
||||
end
|
||||
|
||||
if _M.genomeCallback ~= nil then
|
||||
_M.genomeCallback(genome, _M.currentGenomeIndex)
|
||||
end
|
||||
|
||||
message(_M, string.format("Gen %d species %d genome %d fitness: %d", _M.currentGenerationIndex, _M.currentSpecies.id, _M.currentGenomeIndex, math.floor(fitness)))
|
||||
_M.currentGenomeIndex = 1
|
||||
while fitnessAlreadyMeasured(_M) do
|
||||
_M.currentGenomeIndex = _M.currentGenomeIndex + 1
|
||||
if _M.currentGenomeIndex > #_M.currentSpecies.genomes then
|
||||
game.unregisterHandlers()
|
||||
for i=#_M.dereg,1,-1 do
|
||||
local d = table.remove(_M.dereg, i)
|
||||
callback.unregister(d[1], d[2])
|
||||
end
|
||||
|
||||
input.keyhook("1", false)
|
||||
input.keyhook("4", false)
|
||||
input.keyhook("6", false)
|
||||
input.keyhook("8", false)
|
||||
input.keyhook("9", false)
|
||||
input.keyhook("tab", false)
|
||||
|
||||
_M.finishCallback()
|
||||
return
|
||||
end
|
||||
end
|
||||
initializeRun(_M, function()
|
||||
mainLoop(_M, genome)
|
||||
end)
|
||||
end)
|
||||
end
|
||||
|
||||
local function newNeuron()
|
||||
local neuron = {}
|
||||
neuron.incoming = {}
|
||||
|
@ -635,45 +461,290 @@ local function generateNetwork(genome)
|
|||
genome.network = network
|
||||
end
|
||||
|
||||
local function elapsed(_M)
|
||||
if config.StartPowerup ~= nil then
|
||||
game.writePowerup(config.StartPowerup)
|
||||
end
|
||||
_M.currentFrame = 0
|
||||
_M.timeout = config.NeatConfig.TimeoutConstant
|
||||
-- Kill the run if we go back to the map screen
|
||||
game.onceMapLoaded(function()
|
||||
_M.timeout = -100000
|
||||
local beginRewindState = nil
|
||||
local function rewind()
|
||||
return game.rewind(beginRewindState):next(function()
|
||||
frame = 0
|
||||
lastFrame = 0
|
||||
end)
|
||||
_M.bumps = 0
|
||||
-- Penalize player for collisions that do not result in enemy deaths
|
||||
game.onEmptyHit(function()
|
||||
_M.bumps = _M.bumps + 1
|
||||
end)
|
||||
game.clearJoypad()
|
||||
_M.startKong = game.getKong()
|
||||
_M.startBananas = game.getBananas()
|
||||
_M.startKrem = game.getKremCoins()
|
||||
_M.lastKrem = _M.startKrem
|
||||
_M.startCoins = game.getCoins()
|
||||
_M.startLives = game.getLives()
|
||||
_M.partyHitCounter = 0
|
||||
_M.powerUpCounter = 0
|
||||
_M.powerUpBefore = game.getBoth()
|
||||
_M.currentArea = game.getCurrentArea()
|
||||
_M.lastArea = _M.currentArea
|
||||
_M.rightmost = { [_M.currentArea] = 0 }
|
||||
_M.upmost = { [_M.currentArea] = 0 }
|
||||
local genome = _M.currentSpecies.genomes[_M.currentGenomeIndex]
|
||||
generateNetwork(genome)
|
||||
evaluateCurrent(_M)
|
||||
for i=#_M.runInitialized,1,-1 do
|
||||
table.remove(_M.runInitialized, i)()
|
||||
end
|
||||
end
|
||||
|
||||
local function rewound()
|
||||
set_timer_timeout(1)
|
||||
local function initializeRun(_M)
|
||||
settings.set_speed("turbo")
|
||||
-- XXX Does this actually work or only affects new VM loads?
|
||||
settings.set('lua-maxmem', 1024)
|
||||
local enableSound = 'on'
|
||||
if config.NeatConfig.DisableSound then
|
||||
enableSound = 'off'
|
||||
end
|
||||
exec('enable-sound '..enableSound)
|
||||
gui.subframe_update(false)
|
||||
|
||||
return rewind():next(function()
|
||||
bsnes.enablelayer(0, 0, true)
|
||||
bsnes.enablelayer(0, 1, false)
|
||||
bsnes.enablelayer(1, 0, false)
|
||||
bsnes.enablelayer(1, 1, false)
|
||||
bsnes.enablelayer(2, 0, false)
|
||||
bsnes.enablelayer(2, 1, false)
|
||||
bsnes.enablelayer(3, 0, false)
|
||||
bsnes.enablelayer(3, 1, false)
|
||||
bsnes.enablelayer(4, 0, true)
|
||||
bsnes.enablelayer(4, 1, true)
|
||||
bsnes.enablelayer(4, 2, true)
|
||||
bsnes.enablelayer(4, 3, true)
|
||||
|
||||
if config.StartPowerup ~= nil then
|
||||
game.writePowerup(config.StartPowerup)
|
||||
end
|
||||
_M.currentFrame = 0
|
||||
_M.timeout = config.NeatConfig.TimeoutConstant
|
||||
-- Kill the run if we go back to the map screen
|
||||
game.onceMapLoaded(function()
|
||||
_M.timeout = -100000
|
||||
end)
|
||||
_M.bumps = 0
|
||||
-- Penalize player for collisions that do not result in enemy deaths
|
||||
game.onEmptyHit(function()
|
||||
_M.bumps = _M.bumps + 1
|
||||
end)
|
||||
game.clearJoypad()
|
||||
_M.startKong = game.getKong()
|
||||
_M.totalBananas = 0
|
||||
_M.lastBananas = game.getBananas()
|
||||
_M.startKrem = game.getKremCoins()
|
||||
_M.lastKrem = _M.startKrem
|
||||
_M.startCoins = game.getCoins()
|
||||
_M.startLives = game.getLives()
|
||||
_M.partyHitCounter = 0
|
||||
_M.powerUpCounter = 0
|
||||
_M.powerUpBefore = game.getBoth()
|
||||
_M.currentArea = game.getCurrentArea()
|
||||
_M.lastArea = _M.currentArea
|
||||
|
||||
for _,areaInfo in pairs(_M.areaInfo) do
|
||||
for i=1,#areaInfo.waypoints,1 do
|
||||
local waypoint = areaInfo.waypoints[i]
|
||||
waypoint.shortest = waypoint.startDistance
|
||||
end
|
||||
end
|
||||
|
||||
local genome = _M.currentSpecies.genomes[_M.currentGenomeIndex]
|
||||
generateNetwork(genome)
|
||||
|
||||
local inputs, inputDeltas = game.getInputs()
|
||||
evaluateCurrent(_M, inputs, inputDeltas)
|
||||
end)
|
||||
end
|
||||
|
||||
local function mainLoop(_M, genome)
|
||||
return game.advanceFrame():next(function()
|
||||
local nextArea = game.getCurrentArea()
|
||||
if nextArea ~= _M.lastArea then
|
||||
_M.lastArea = nextArea
|
||||
game.onceAreaLoaded(function()
|
||||
message(_M, 'Loaded area '..nextArea)
|
||||
_M.timeout = _M.timeout + 60 * 5
|
||||
_M.currentArea = nextArea
|
||||
_M.lastArea = _M.currentArea
|
||||
end)
|
||||
elseif _M.currentArea == _M.lastArea and _M.areaInfo[_M.currentArea] == nil then
|
||||
message(_M, 'Searching for the main exit in this area')
|
||||
return game.findPreferredExit():next(function(preferredExit)
|
||||
local areaInfo = {
|
||||
preferredExit = preferredExit,
|
||||
waypoints = game.getWaypoints(preferredExit.x, preferredExit.y),
|
||||
}
|
||||
table.insert(areaInfo.waypoints, 1, preferredExit)
|
||||
|
||||
for i=#areaInfo.waypoints,1,-1 do
|
||||
local waypoint = areaInfo.waypoints[i]
|
||||
if waypoint.y > game.partyY + mem.size.tile * 7 then
|
||||
message(_M, string.format('Skipped waypoint %d,%d', waypoint.x, waypoint.y), 0x00ffff00)
|
||||
table.remove(areaInfo.waypoints, i)
|
||||
goto continue
|
||||
end
|
||||
local startDistance = math.floor(math.sqrt((waypoint.y - game.partyY) ^ 2 + (waypoint.x - game.partyX) ^ 2))
|
||||
waypoint.startDistance = startDistance
|
||||
waypoint.shortest = startDistance
|
||||
::continue::
|
||||
end
|
||||
|
||||
message(_M, string.format('Found %d waypoints', #areaInfo.waypoints))
|
||||
|
||||
_M.areaInfo[_M.currentArea] = areaInfo
|
||||
end)
|
||||
end
|
||||
end):next(function()
|
||||
if lastFrame + 1 ~= frame then
|
||||
message(_M, string.format("We missed %d frames", frame - lastFrame), 0x00ff0000)
|
||||
end
|
||||
lastFrame = frame
|
||||
|
||||
if genome ~= nil then
|
||||
_M.currentFrame = _M.currentFrame + 1
|
||||
end
|
||||
|
||||
genome = _M.currentSpecies.genomes[_M.currentGenomeIndex]
|
||||
|
||||
if _M.drawFrame % 10 == 0 then
|
||||
displayGenome(genome)
|
||||
end
|
||||
|
||||
game.getPositions()
|
||||
local timeoutConst = config.NeatConfig.TimeoutConstant
|
||||
|
||||
local fell = game.fell()
|
||||
if (fell or game.diedFromHit()) and _M.timeout > 0 then
|
||||
_M.timeout = 0
|
||||
end
|
||||
|
||||
if _M.currentFrame % 5 == 0 then
|
||||
local sprites = game.getSprites()
|
||||
local inputs, inputDeltas = game.getInputs(sprites)
|
||||
if game.bonusScreenDisplayed(inputs) and _M.timeout > -1000 and _M.timeout < timeoutConst then
|
||||
_M.timeout = timeoutConst
|
||||
end
|
||||
|
||||
evaluateCurrent(_M, inputs, inputDeltas)
|
||||
end
|
||||
|
||||
local areaInfo = _M.areaInfo[_M.currentArea]
|
||||
if areaInfo ~= nil and game.partyY ~= 0 and game.partyX ~= 0 then
|
||||
for i=1,#areaInfo.waypoints,1 do
|
||||
local waypoint = areaInfo.waypoints[i]
|
||||
local dist = math.floor(math.sqrt((waypoint.y - game.partyY) ^ 2 + (waypoint.x - game.partyX) ^ 2))
|
||||
if dist < waypoint.shortest then
|
||||
waypoint.shortest = dist
|
||||
if _M.timeout < timeoutConst then
|
||||
_M.timeout = timeoutConst
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
local hitTimer = game.getHitTimer(_M.lastBoth)
|
||||
|
||||
if hitTimer > 0 then
|
||||
_M.partyHitCounter = _M.partyHitCounter + 1
|
||||
--message(_M, "party took damage, hit counter: " .. _M.partyHitCounter)
|
||||
end
|
||||
|
||||
local powerUp = game.getBoth()
|
||||
_M.lastBoth = powerUp
|
||||
if powerUp > 0 then
|
||||
if powerUp ~= _M.powerUpBefore then
|
||||
_M.powerUpCounter = _M.powerUpCounter + 1
|
||||
_M.powerUpBefore = powerUp
|
||||
end
|
||||
end
|
||||
|
||||
local krem = game.getKremCoins() - _M.startKrem
|
||||
if krem > _M.lastKrem then
|
||||
message(_M, string.format("Kremcoin grabbed: %d", _M.timeout), 0x00009900)
|
||||
_M.lastKrem = krem
|
||||
_M.timeout = _M.timeout + 60 * 10
|
||||
end
|
||||
|
||||
local currentBananas = game.getBananas()
|
||||
local moreBananas = currentBananas - _M.lastBananas
|
||||
if moreBananas > 0 then
|
||||
_M.totalBananas = _M.totalBananas + moreBananas
|
||||
end
|
||||
_M.lastBananas = currentBananas
|
||||
|
||||
_M.timeout = _M.timeout - 1
|
||||
|
||||
if lastFrame ~= frame then
|
||||
message(_M, string.format("We missed %d frames", frame - lastFrame), 0x00990000)
|
||||
end
|
||||
|
||||
-- Continue if we haven't timed out
|
||||
local timeoutBonus = _M.currentFrame / 4
|
||||
if _M.timeout + timeoutBonus > 0 then
|
||||
return mainLoop(_M, genome)
|
||||
end
|
||||
|
||||
-- Timeout calculations beyond this point
|
||||
-- Manipulating the timeout value won't have
|
||||
-- any effect
|
||||
local coins = game.getCoins() - _M.startCoins
|
||||
local kong = game.getKong()
|
||||
|
||||
message(_M, string.format("Bananas: %d, coins: %d, Krem: %d, KONG: %d", _M.totalBananas, coins, krem, kong))
|
||||
|
||||
local bananaCoinsFitness = (krem * 100) + (kong * 60) + (_M.totalBananas * 50) + (coins * 0.2)
|
||||
if (_M.totalBananas + coins) > 0 then
|
||||
message(_M, "Bananas, Coins, KONG added " .. bananaCoinsFitness .. " fitness")
|
||||
end
|
||||
|
||||
local hitPenalty = _M.partyHitCounter * 100
|
||||
local bumpPenalty = _M.bumps * 100
|
||||
local powerUpBonus = _M.powerUpCounter * 100
|
||||
|
||||
local distanceTraversed = getDistanceTraversed(_M.areaInfo) - _M.currentFrame / 2
|
||||
|
||||
local fitness = bananaCoinsFitness - bumpPenalty - hitPenalty + powerUpBonus + distanceTraversed
|
||||
|
||||
if fell then
|
||||
fitness = fitness / 10
|
||||
message(_M, "Fall penalty 1/10")
|
||||
end
|
||||
|
||||
local lives = game.getLives()
|
||||
|
||||
if _M.startLives < lives then
|
||||
local extraLiveBonus = (lives - _M.startLives)*1000
|
||||
fitness = fitness + extraLiveBonus
|
||||
message(_M, "Extra live bonus added " .. extraLiveBonus)
|
||||
end
|
||||
|
||||
local sprites = game.getSprites()
|
||||
-- FIXME We should test this before we time out
|
||||
if game.getGoalHit(sprites) then
|
||||
fitness = fitness + 1000
|
||||
message(_M, string.format("LEVEL WON! Fitness: %d", fitness), 0x0000ff00)
|
||||
end
|
||||
|
||||
if fitness == 0 then
|
||||
fitness = -1
|
||||
end
|
||||
genome.fitness = fitness
|
||||
|
||||
if _M.maxFitness == nil or fitness > _M.maxFitness then
|
||||
_M.maxFitness = fitness
|
||||
end
|
||||
|
||||
if _M.genomeCallback ~= nil then
|
||||
_M.genomeCallback(genome, _M.currentGenomeIndex)
|
||||
end
|
||||
|
||||
message(_M, string.format("Gen %d species %d genome %d fitness: %d", _M.currentGenerationIndex, _M.currentSpecies.id, _M.currentGenomeIndex, math.floor(fitness)))
|
||||
_M.currentGenomeIndex = 1
|
||||
while fitnessAlreadyMeasured(_M) do
|
||||
_M.currentGenomeIndex = _M.currentGenomeIndex + 1
|
||||
if _M.currentGenomeIndex > #_M.currentSpecies.genomes then
|
||||
game.unregisterHandlers()
|
||||
for i=#_M.dereg,1,-1 do
|
||||
local d = table.remove(_M.dereg, i)
|
||||
callback.unregister(d[1], d[2])
|
||||
end
|
||||
|
||||
input.keyhook("1", false)
|
||||
input.keyhook("4", false)
|
||||
input.keyhook("6", false)
|
||||
input.keyhook("8", false)
|
||||
input.keyhook("9", false)
|
||||
input.keyhook("tab", false)
|
||||
|
||||
return _M.maxFitness
|
||||
end
|
||||
end
|
||||
|
||||
return initializeRun(_M):next(function()
|
||||
return mainLoop(_M, genome)
|
||||
end)
|
||||
end)
|
||||
end
|
||||
|
||||
local function register(_M, name, func)
|
||||
|
@ -705,17 +776,30 @@ local function onLoad(_M, handler)
|
|||
table.insert(_M.onLoadHandler, handler)
|
||||
end
|
||||
|
||||
local function reset(_M)
|
||||
for i=#_M.onResetHandler,1,-1 do
|
||||
_M.onResetHandler[i]()
|
||||
end
|
||||
|
||||
message(_M, "Will be reset once all currently active threads finish", 0x00990000)
|
||||
end
|
||||
|
||||
local function onReset(_M, handler)
|
||||
table.insert(_M.onResetHandler, handler)
|
||||
end
|
||||
|
||||
local function keyhook (_M, key, state)
|
||||
if state.value == 1 then
|
||||
if key == "tab" then
|
||||
_M.inputmode = not _M.inputmode
|
||||
_M.helddown = key
|
||||
elseif inputmode then
|
||||
elseif _M.inputmode then
|
||||
return
|
||||
elseif key == "1" then
|
||||
_M.helddown = key
|
||||
config.Running = not config.Running
|
||||
elseif key == "4" then
|
||||
-- FIXME Should be handled similarly to other events
|
||||
_M.helddown = key
|
||||
pool.requestTop()
|
||||
elseif key == "6" then
|
||||
|
@ -726,21 +810,21 @@ local function keyhook (_M, key, state)
|
|||
load(_M)
|
||||
elseif key == "9" then
|
||||
_M.helddown = key
|
||||
pool.run(true)
|
||||
reset(_M)
|
||||
end
|
||||
elseif state.value == 0 then
|
||||
helddown = nil
|
||||
_M.helddown = nil
|
||||
end
|
||||
end
|
||||
|
||||
local function saveLoadInput(_M)
|
||||
local inputs = input.raw()
|
||||
if not inputmode then
|
||||
if not _M.inputmode then
|
||||
-- FIXME
|
||||
_M.saveLoadFile = config.NeatConfig.SaveFile
|
||||
return
|
||||
end
|
||||
if helddown == nil then
|
||||
if _M.helddown == nil then
|
||||
local mapping = {
|
||||
backslash = "\\",
|
||||
colon = ":",
|
||||
|
@ -778,7 +862,7 @@ local function saveLoadInput(_M)
|
|||
end
|
||||
if k == "back" then
|
||||
config.NeatConfig.SaveFile = config.NeatConfig.SaveFile:sub(1, #config.NeatConfig.SaveFile-1)
|
||||
helddown = k
|
||||
_M.helddown = k
|
||||
goto continue
|
||||
end
|
||||
local m = k
|
||||
|
@ -789,38 +873,35 @@ local function saveLoadInput(_M)
|
|||
goto continue
|
||||
end
|
||||
config.NeatConfig.SaveFile = config.NeatConfig.SaveFile..m
|
||||
helddown = k
|
||||
_M.helddown = k
|
||||
::continue::
|
||||
end
|
||||
elseif helddown ~= nil and inputs[helddown]["value"] ~= 1 then
|
||||
helddown = nil
|
||||
elseif _M.helddown ~= nil and inputs[_M.helddown]["value"] ~= 1 then
|
||||
_M.helddown = nil
|
||||
end
|
||||
end
|
||||
|
||||
local function run(_M, species, generationIdx, genomeCallback, finishCallback)
|
||||
local function run(_M, species, generationIdx, genomeCallback)
|
||||
if beginRewindState == nil then
|
||||
beginRewindState = movie.to_rewind(config.NeatConfig.Filename)
|
||||
end
|
||||
game.registerHandlers()
|
||||
|
||||
_M.currentGenerationIndex = generationIdx
|
||||
_M.currentSpecies = species
|
||||
_M.currentGenomeIndex = 1
|
||||
_M.genomeCallback = genomeCallback
|
||||
_M.finishCallback = finishCallback
|
||||
register(_M, 'paint', function()
|
||||
painting(_M)
|
||||
end)
|
||||
register(_M, 'input', function()
|
||||
processFrameAdvanced(_M)
|
||||
end)
|
||||
register(_M, 'input', function()
|
||||
frame = frame + 1
|
||||
updateController()
|
||||
saveLoadInput(_M)
|
||||
end)
|
||||
register(_M, 'keyhook', function(key, state)
|
||||
keyhook(_M, key, state)
|
||||
end)
|
||||
register(_M, 'post_rewind', rewound)
|
||||
register(_M, 'timer', function()
|
||||
elapsed(_M)
|
||||
end)
|
||||
|
||||
input.keyhook("1", true)
|
||||
input.keyhook("4", true)
|
||||
|
@ -829,8 +910,8 @@ local function run(_M, species, generationIdx, genomeCallback, finishCallback)
|
|||
input.keyhook("9", true)
|
||||
input.keyhook("tab", true)
|
||||
|
||||
initializeRun(_M, function()
|
||||
mainLoop(_M)
|
||||
return initializeRun(_M):next(function()
|
||||
return mainLoop(_M)
|
||||
end)
|
||||
end
|
||||
|
||||
|
@ -842,16 +923,19 @@ local function onRenderForm(_M, handler)
|
|||
table.insert(_M.onRenderFormHandler, handler)
|
||||
end
|
||||
|
||||
return function()
|
||||
return function(promise)
|
||||
Promise = promise
|
||||
if game == nil then
|
||||
game = dofile(base.."/game.lua")(Promise)
|
||||
end
|
||||
local _M = {
|
||||
currentGenerationIndex = 1,
|
||||
currentSpecies = nil,
|
||||
finishCallback = nil,
|
||||
genomeCallback = nil,
|
||||
currentGenomeIndex = 1,
|
||||
currentFrame = 0,
|
||||
drawFrame = 0,
|
||||
maxFitness = 0,
|
||||
maxFitness = nil,
|
||||
|
||||
dereg = {},
|
||||
inputmode = false,
|
||||
|
@ -861,7 +945,8 @@ return function()
|
|||
timeout = 0,
|
||||
bumps = 0,
|
||||
startKong = 0,
|
||||
startBananas = 0,
|
||||
lastBananas = 0,
|
||||
totalBananas = 0,
|
||||
startKrem = 0,
|
||||
lastKrem = 0,
|
||||
startCoins = 0,
|
||||
|
@ -871,18 +956,14 @@ return function()
|
|||
powerUpBefore = 0,
|
||||
currentArea = 0,
|
||||
lastArea = 0,
|
||||
rightmost = {},
|
||||
upmost = {},
|
||||
areaInfo = {},
|
||||
lastBoth = 0,
|
||||
|
||||
runInitialized = {},
|
||||
|
||||
onMessageHandler = {},
|
||||
onSaveHandler = {},
|
||||
onLoadHandler = {},
|
||||
onResetHandler = {},
|
||||
onRenderFormHandler = {},
|
||||
onFrameAdvancedHandler = {},
|
||||
|
||||
}
|
||||
|
||||
_M.onRenderForm = function(handler)
|
||||
|
@ -901,9 +982,13 @@ return function()
|
|||
onLoad(_M, handler)
|
||||
end
|
||||
|
||||
_M.run = function(species, generationIdx, genomeCallback, finishCallback)
|
||||
run(_M, species, generationIdx, genomeCallback, finishCallback)
|
||||
_M.onReset = function(handler)
|
||||
onReset(_M, handler)
|
||||
end
|
||||
|
||||
_M.run = function(species, generationIdx, genomeCallback)
|
||||
return run(_M, species, generationIdx, genomeCallback)
|
||||
end
|
||||
|
||||
return _M
|
||||
end
|
||||
end
|
189
spritelist.lua
189
spritelist.lua
|
@ -4,100 +4,115 @@ _M.Sprites = {}
|
|||
|
||||
-- Make sure this list is sorted before initialization.
|
||||
_M.NeutralSprites = {
|
||||
0x0020, -- Krow egg fragments
|
||||
0x0060, -- Barrel fragments
|
||||
0x0064, -- Barrel fragments
|
||||
krowEggFragments = 0x0020,
|
||||
barrelFragments = 0x0060,
|
||||
barrelFragments2 = 0x0064,
|
||||
|
||||
-- Our heroes
|
||||
0x00e4, -- Diddy
|
||||
0x00e8, -- Dixie
|
||||
0x0100, -- Stars
|
||||
diddy = 0x00e4,
|
||||
dixie = 0x00e8,
|
||||
stars = 0x0100,
|
||||
|
||||
-- Items that require too much interaction
|
||||
0x01a4, -- Barrel
|
||||
0x01b0, -- Cannonball (immobile)
|
||||
0x01c0, -- Chest
|
||||
0x01bc, -- Small crate
|
||||
0x011c, -- Barrel
|
||||
0x013c, -- Cannon
|
||||
0x014c, -- Hook
|
||||
0x01b8, -- TNT
|
||||
barrel = 0x01a4, -- Barrel
|
||||
cannonball = 0x01b0,
|
||||
chest = 0x01c0,
|
||||
smallCrate = 0x01bc,
|
||||
barrel2 = 0x011c,
|
||||
cannon = 0x013c,
|
||||
hook = 0x014c,
|
||||
tnt = 0x01b8,
|
||||
|
||||
-- Inert
|
||||
0x0168, -- Goal pole
|
||||
0x016c, -- Goal roulette
|
||||
0x0160, -- Goal base
|
||||
0x0164, -- Goal barrel
|
||||
goalPole = 0x0168,
|
||||
goalroulette = 0x016c,
|
||||
goalBase = 0x0160,
|
||||
goalBarrel = 0x0164,
|
||||
|
||||
0x0238, -- Pow
|
||||
0x023c, -- Exploding crate
|
||||
0x0258, -- No Animals Sign
|
||||
pow = 0x0238,
|
||||
explodingCrate = 0x023c,
|
||||
noAnimalsSign = 0x0258,
|
||||
}
|
||||
|
||||
-- Make sure this list is sorted before initialization.
|
||||
_M.GoodSprites = {
|
||||
-- Destinations
|
||||
0x0094, -- Area exit
|
||||
0x00b0, -- Goal target
|
||||
areaExit = 0x0094,
|
||||
goalTarget = 0x00b0,
|
||||
|
||||
0x0120, -- Bonus barrel
|
||||
0x0128, -- Hot air balloon
|
||||
0x0140, -- Launch barrel
|
||||
0x0148, -- Animal crate
|
||||
0x0150, -- Invincibility barrel
|
||||
0x0154, -- Midpoint
|
||||
0x015c, -- Banana Coin/Kremkoin/DK Coin
|
||||
0x0170, -- Banana bunch
|
||||
0x0174, -- KONG letters
|
||||
0x0178, -- xUP balloon
|
||||
bonusBarrel = 0x0120,
|
||||
hotAirBalloon = 0x0128,
|
||||
launchBarrel = 0x0140,
|
||||
animalCrate = 0x0148,
|
||||
invincibilityBarrel = 0x0150,
|
||||
midpoint = 0x0154,
|
||||
allCoins = 0x015c, -- Banana Coin/Kremkoin/DK Coin
|
||||
bananaBunch = 0x0170,
|
||||
kongLetter = 0x0174,
|
||||
upBalloon = 0x0178, -- xUP balloon
|
||||
|
||||
-- Animals
|
||||
0x0190, -- Squitter
|
||||
0x0194, -- Rattly
|
||||
0x0198, -- Squawks
|
||||
0x019c, -- Rambi
|
||||
0x0304, -- Clapper
|
||||
squitter = 0x0190,
|
||||
rattly = 0x0194,
|
||||
squawks = 0x0198,
|
||||
rambi = 0x019c,
|
||||
clapper = 0x0304,
|
||||
|
||||
0x01a8, -- DK Barrel label
|
||||
dkBarrelLabel = 0x01a8,
|
||||
|
||||
0x01b4, -- Krow's eggs
|
||||
krowEgg = 0x01b4,
|
||||
|
||||
0x0220, -- Flitter (used as unavoidable platforms in some levels)
|
||||
0x02d4, -- Krochead (red and green)
|
||||
flitter = 0x0220,
|
||||
krocheadAllColors = 0x02d4,
|
||||
}
|
||||
|
||||
-- Currently not used.
|
||||
_M.BadSprites = {
|
||||
-- Baddies
|
||||
0x006c, -- Kannon
|
||||
0x01ac, -- Klobber (yellow and green)
|
||||
0x01d0, -- Kannon's fodder (Ball/barrel)
|
||||
0x01d8, -- Krusha
|
||||
0x01dc, -- Click-Clack
|
||||
0x01e4, -- Neek
|
||||
0x01ec, -- Klomp
|
||||
0x01e8, -- Klobber (awake)
|
||||
0x01f0, -- Klampon
|
||||
0x01f8, -- Flotsam
|
||||
0x0200, -- Klinger
|
||||
0x0208, -- Puftup
|
||||
0x0218, -- Zinger (red and yellow)
|
||||
0x0214, -- Mini-Necky
|
||||
0x020c, -- Lockjaw
|
||||
0x021c, -- Kaboing
|
||||
0x0224, -- Krow (Boss)
|
||||
0x025c, -- Krook (very large)
|
||||
kannon = 0x006c,
|
||||
klobberAllColors = 0x01ac,
|
||||
kannonFodder = 0x01d0,
|
||||
krusha = 0x01d8,
|
||||
clickClack = 0x01dc,
|
||||
neek = 0x01e4,
|
||||
klomp = 0x01ec,
|
||||
klobberAwake = 0x01e8,
|
||||
klampon = 0x01f0,
|
||||
flotsam = 0x01f8,
|
||||
klinger = 0x0200,
|
||||
klingerSkidCloud = 0x0014,
|
||||
puftup = 0x0208,
|
||||
zingerAllColors = 0x0218,
|
||||
miniNecky = 0x0214,
|
||||
lockjaw = 0x020c,
|
||||
kaboing = 0x021c,
|
||||
krow = 0x0224, -- Boss
|
||||
krook = 0x025c,
|
||||
}
|
||||
|
||||
_M.SpriteNames = {}
|
||||
|
||||
function _M.InitSpriteNames()
|
||||
for v,k in pairs(_M.GoodSprites) do
|
||||
_M.SpriteNames[k] = v
|
||||
end
|
||||
for v,k in pairs(_M.BadSprites) do
|
||||
_M.SpriteNames[k] = v
|
||||
end
|
||||
for v,k in pairs(_M.NeutralSprites) do
|
||||
_M.SpriteNames[k] = v
|
||||
end
|
||||
end
|
||||
|
||||
function _M.InitSpriteList()
|
||||
for i=1,#_M.GoodSprites,1 do
|
||||
_M.Sprites[_M.GoodSprites[i]] = 1
|
||||
for k,v in pairs(_M.GoodSprites) do
|
||||
_M.Sprites[v] = 1
|
||||
end
|
||||
for i=1,#_M.BadSprites,1 do
|
||||
_M.Sprites[_M.BadSprites[i]] = -1
|
||||
for k,v in pairs(_M.BadSprites) do
|
||||
_M.Sprites[v] = -1
|
||||
end
|
||||
for i=1,#_M.NeutralSprites,1 do
|
||||
_M.Sprites[_M.NeutralSprites[i]] = 0
|
||||
for k,v in pairs(_M.NeutralSprites) do
|
||||
_M.Sprites[v] = 0
|
||||
end
|
||||
end
|
||||
|
||||
|
@ -108,21 +123,21 @@ _M.ExtNeutralSprites = {
|
|||
}
|
||||
|
||||
_M.ExtGoodSprites = {
|
||||
0xe0, -- banana
|
||||
0xe1, -- banana
|
||||
0xe2, -- banana
|
||||
0xe3, -- banana
|
||||
0xe4, -- banana
|
||||
0xe5, -- banana
|
||||
0xe6, -- banana
|
||||
0xe7, -- banana
|
||||
0xe8, -- banana
|
||||
0xe9, -- banana
|
||||
0xea, -- banana
|
||||
0xeb, -- banana
|
||||
0xec, -- banana
|
||||
0xed, -- banana
|
||||
0xee, -- banana
|
||||
banana01 = 0xe0, -- banana
|
||||
banana02 = 0xe1, -- banana
|
||||
banana03 = 0xe2, -- banana
|
||||
banana04 = 0xe3, -- banana
|
||||
banana05 = 0xe4, -- banana
|
||||
banana06 = 0xe5, -- banana
|
||||
banana07 = 0xe6, -- banana
|
||||
banana08 = 0xe7, -- banana
|
||||
banana09 = 0xe8, -- banana
|
||||
banana10 = 0xe9, -- banana
|
||||
banana11 = 0xea, -- banana
|
||||
banana12 = 0xeb, -- banana
|
||||
banana13 = 0xec, -- banana
|
||||
banana14 = 0xed, -- banana
|
||||
banana15 = 0xee, -- banana
|
||||
}
|
||||
|
||||
-- Currently not used.
|
||||
|
@ -130,14 +145,14 @@ _M.ExtBadSprites = {
|
|||
}
|
||||
|
||||
function _M.InitExtSpriteList()
|
||||
for i=1,#_M.ExtGoodSprites,1 do
|
||||
_M.extSprites[_M.ExtGoodSprites[i]] = 1
|
||||
for k,v in pairs(_M.ExtGoodSprites) do
|
||||
_M.extSprites[v] = 1
|
||||
end
|
||||
for i=1,#_M.ExtBadSprites,1 do
|
||||
_M.extSprites[_M.ExtBadSprites[i]] = -1
|
||||
for k,v in pairs(_M.ExtBadSprites) do
|
||||
_M.extSprites[v] = -1
|
||||
end
|
||||
for i=1,#_M.ExtNeutralSprites,1 do
|
||||
_M.extSprites[_M.ExtNeutralSprites[i]] = 0
|
||||
for k,v in pairs(_M.ExtNeutralSprites) do
|
||||
_M.extSprites[v] = 0
|
||||
end
|
||||
end
|
||||
|
||||
|
|
|
@ -1,9 +1,19 @@
|
|||
local errtest = io.popen("farts", 'r')
|
||||
errtest:read("*a")
|
||||
print(errtest:close())
|
||||
errtest = io.popen("dir", 'r')
|
||||
errtest:read("*a")
|
||||
local ok, exit, code = errtest:close()
|
||||
print(ok)
|
||||
print(exit)
|
||||
print(code)
|
||||
local memory, movie, utime, callback, set_timer_timeout, input, gui, exec, settings = memory, movie, utime, callback, set_timer_timeout, input, gui, exec, settings
|
||||
|
||||
local base = string.gsub(@@LUA_SCRIPT_FILENAME@@, "(.*[/\\])(.*)", "%1")
|
||||
local Promise = dofile(base.."/promise.lua")
|
||||
callback.register('timer', function()
|
||||
Promise.update()
|
||||
set_timer_timeout(1)
|
||||
end)
|
||||
set_timer_timeout(1)
|
||||
local game = dofile(base.."/game.lua")(Promise)
|
||||
local util = dofile(base.."/util.lua")(Promise)
|
||||
local serpent = dofile(base.."/serpent.lua")
|
||||
|
||||
game.registerHandlers()
|
||||
game.getPositions()
|
||||
|
||||
game.findPreferredExit():next(function(preferredExit)
|
||||
game.getWaypoints(preferredExit.x, preferredExit.y)
|
||||
end)
|
151
tools/bsnes-launcher.lua
Normal file
151
tools/bsnes-launcher.lua
Normal file
|
@ -0,0 +1,151 @@
|
|||
local base = string.gsub(@@LUA_SCRIPT_FILENAME@@, "(.*[/\\])(.*)", "%1").."/.."
|
||||
|
||||
local util = dofile(base.."/util.lua")()
|
||||
local config = dofile(base.."/config.lua")
|
||||
local mem = dofile(base.."/mem.lua")
|
||||
|
||||
local function text(text)
|
||||
io.stderr:write(text)
|
||||
io.stderr:write('\n')
|
||||
print(text)
|
||||
end
|
||||
|
||||
local function help()
|
||||
text([[
|
||||
Syntax: BSNES_LAUNCHER_ARGS='<arguments>' lsnes --lua=]]..base..[[/bsnes-launcher.lua
|
||||
|
||||
--sprite-startindex=<number> Which sprite to start at. Can be a value from 0-22
|
||||
|
||||
Sprite breakpoint arguments:
|
||||
These will create breakpoints for all sprite slots with properties matching the
|
||||
given pattern.
|
||||
]])
|
||||
for propName,_ in pairs(mem.offset.sprite) do
|
||||
text('--sprite-'..propName..'<Breakpoint format>')
|
||||
end
|
||||
|
||||
text([[
|
||||
|
||||
Breakpoint format: --<switchname>[=<value>][:<rwx>]
|
||||
rwx = read / write / execute flags
|
||||
|
||||
For example, --sprite-x:r would match any reads of any sprite X position
|
||||
--sprite-x>10:w would match any writes of any sprite X position greater
|
||||
than 0x10 (16). Omitting the rwx will create the breakpoints with the values
|
||||
specified but they will not trigger until you enable them manually.
|
||||
|
||||
Example:
|
||||
Matching values of the goalpost sprites in Pirate Panic:
|
||||
|
||||
LSNES_HIDE_MESSAGES=1 \
|
||||
LSNES_HIDE_STATUSPANEL=1 \
|
||||
BSNES_LAUNCHER_ARGS='--sprite-startindex=2 --sprite-control=160:w --sprite-control=164:w --sprite-control=16c:w --sprite-control=168:w --sprite-x=1c3c:w --sprite-x=1c48:w --sprite-x=1c5d:w --sprite-y=ff63:w --sprite-y=ff8f:w' \
|
||||
lsnes --lua=tools/bsnes-launcher.lua
|
||||
|
||||
Note that ff is included in some values since bsnes-plus can only match one
|
||||
byte at a time, so sometimes has false positives on those bytes since the true
|
||||
value is not unique enough.
|
||||
]])
|
||||
end
|
||||
|
||||
local bps = {}
|
||||
|
||||
--- Add breakpoint switch
|
||||
---@param switchName string The name of the switch, without dashes
|
||||
---@param source string address space
|
||||
---@param startAddress integer The start address
|
||||
---@param arg string The argument to test
|
||||
---@param valWidth integer byte size of value - defaults to 2
|
||||
---@param addrWidth integer byte size of address - defaults to 3
|
||||
local function bpSwitch(switchName, source, startAddress, arg, valWidth, addrWidth)
|
||||
if addrWidth == nil then
|
||||
addrWidth = 3
|
||||
end
|
||||
if valWidth == nil then
|
||||
valWidth = 2
|
||||
end
|
||||
if arg:sub(1, #switchName+2) == '--'..switchName then
|
||||
local op, valHex, rwx = arg:sub(#switchName+3):match('^([><=]*)([0-9a-fA-F]*):?([rwxRWX]*)$')
|
||||
local valPad = ''
|
||||
if valHex ~= '' then
|
||||
local val = tonumber(valHex, 16)
|
||||
valPad = string.format('%0'..(valWidth*2)..'x', val)
|
||||
end
|
||||
local fmt = '%0'..(addrWidth*2)..'x%s%s:%s:%s'
|
||||
if valWidth == 2 then
|
||||
-- SNES is LE!
|
||||
local valLeast = valPad:sub(3, 4)
|
||||
local valMost = valPad:sub(1, 2)
|
||||
local opLeast = ''
|
||||
local opMost = ''
|
||||
|
||||
-- FIXME This is surely wrong but better than nothing?
|
||||
if op == '>' then
|
||||
opLeast = '>'
|
||||
opMost = '>='
|
||||
elseif op == '=' then
|
||||
opLeast = '='
|
||||
opMost = '='
|
||||
elseif op == '<' then
|
||||
opLeast = '<'
|
||||
opMost = '<='
|
||||
elseif op == '<=' then
|
||||
opLeast = '<='
|
||||
opMost = '<='
|
||||
elseif op == '>=' then
|
||||
opLeast = '>='
|
||||
opMost = '>='
|
||||
else
|
||||
valLeast = ''
|
||||
valMost = ''
|
||||
end
|
||||
table.insert(bps, string.format(fmt, startAddress, opLeast, valLeast, rwx, source))
|
||||
table.insert(bps, string.format(fmt, startAddress + 1, opMost, valMost, rwx, source))
|
||||
elseif valWidth == 1 then
|
||||
table.insert(bps, string.format(fmt, startAddress, op, valPad, rwx, source))
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
local count = 0
|
||||
local startIndex = 0
|
||||
for arg in os.getenv('BSNES_LAUNCHER_ARGS'):gmatch('[^ ]+') do
|
||||
if arg:sub(1,20) == '--sprite-startindex=' then
|
||||
startIndex = tonumber(arg:sub(21))
|
||||
end
|
||||
count = count + 1
|
||||
for propName,offset in pairs(mem.offset.sprite) do
|
||||
for i=startIndex,22,1 do
|
||||
local startAddress = mem.addr.spriteBase + mem.size.sprite * i + offset
|
||||
bpSwitch('sprite-'..propName, 'cpu', startAddress, arg)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
local cmdFmt = 'bsnes %s--show-debugger "%s"'
|
||||
|
||||
local bpArgs = ''
|
||||
local withoutBps = string.format(cmdFmt, bpArgs, config.ROM)
|
||||
if #bps > 0 then
|
||||
bpArgs = '-b "'..table.concat(bps, '" -b "')..'" '
|
||||
end
|
||||
local withBps = string.format(cmdFmt, bpArgs, config.ROM)
|
||||
|
||||
if count == 0 then
|
||||
text('====================')
|
||||
help()
|
||||
end
|
||||
|
||||
text('====================')
|
||||
|
||||
text('Note that you will need to turn off breakpoint saving for this app to work correctly.')
|
||||
text('')
|
||||
text(withBps)
|
||||
text('====================')
|
||||
|
||||
while true do
|
||||
text('Starting without breakpoints first. Save your state where you want with F2, then quit to load with breakpoints enabled')
|
||||
util.doCmd(withoutBps)
|
||||
text('Starting with breakpoints enabled. Load your state with F4')
|
||||
util.doCmd(withBps)
|
||||
end
|
|
@ -1,52 +1,53 @@
|
|||
local base = string.gsub(@@LUA_SCRIPT_FILENAME@@, "(.*[/\\])(.*)", "%1")
|
||||
local base = string.gsub(@@LUA_SCRIPT_FILENAME@@, "(.*[/\\])(.*)", "%1").."/.."
|
||||
|
||||
local util = dofile(base.."/util.lua")
|
||||
local set_timer_timeout, memory, memory2, gui, input, bit, callback = set_timer_timeout, memory, memory2, gui, input, bit, callback
|
||||
|
||||
local warn = '========== The ROM must be running before running this script'
|
||||
io.stderr:write(warn)
|
||||
print(warn)
|
||||
|
||||
local Promise = dofile(base.."/promise.lua")
|
||||
callback.register('timer', function()
|
||||
Promise.update()
|
||||
set_timer_timeout(1)
|
||||
end)
|
||||
set_timer_timeout(1)
|
||||
local util = dofile(base.."/util.lua")(Promise)
|
||||
local mem = dofile(base.."/mem.lua")
|
||||
local spritelist = dofile(base.."/spritelist.lua")
|
||||
local game = dofile(base.."/game.lua")
|
||||
local config = dofile(base.."/config.lua")
|
||||
local game = dofile(base.."/game.lua")(Promise)
|
||||
|
||||
spritelist.InitSpriteList()
|
||||
spritelist.InitExtSpriteList()
|
||||
|
||||
FG_COLOR = 0x00ffffff
|
||||
BG_COLOR = 0x99000000
|
||||
ENEMY_SIZE = 64
|
||||
TILEDATA_POINTER = 0x7e0098
|
||||
TILE_SIZE = 32
|
||||
TILE_RADIUS = 5
|
||||
SPRITE_BASE = 0x7e0de2
|
||||
SOLID_LESS_THAN = 0x7e00a0
|
||||
DIDDY_X_VELOCITY = 0x7e0e02
|
||||
DIDDY_Y_VELOCITY = 0x7e0e06
|
||||
DIXIE_X_VELOCITY = 0x7e0e60
|
||||
DIXIE_Y_VELOCITY = 0x7e0e64
|
||||
STAGE_NUMBER = 0x7e08a8
|
||||
STAGE_NUMBER_MOVEMENT = 0x7e08c8
|
||||
CAMERA_X = 0x7e17ba
|
||||
CAMERA_Y = 0x7e17c0
|
||||
CAMERA_MODE = 0x7e054f
|
||||
TILE_COLLISION_MATH_POINTER = 0x7e17b2
|
||||
VERTICAL_POINTER = 0xc414
|
||||
PARTY_X = 0x7e0a2a
|
||||
PARTY_Y = 0x7e0a2c
|
||||
game.registerHandlers()
|
||||
|
||||
count = 0
|
||||
detailsidx = -1
|
||||
jumping = false
|
||||
helddown = false
|
||||
floatmode = false
|
||||
rulers = true
|
||||
pokemon = false
|
||||
pokecount = 0
|
||||
showhelp = false
|
||||
locked = false
|
||||
lockdata = nil
|
||||
incsprite = 0
|
||||
questionable_tiles = false
|
||||
local CAMERA_MODE = 0x7e054f
|
||||
local DIDDY_X_VELOCITY = 0x7e0e02
|
||||
local DIDDY_Y_VELOCITY = 0x7e0e06
|
||||
local DIXIE_X_VELOCITY = 0x7e0e60
|
||||
local DIXIE_Y_VELOCITY = 0x7e0e64
|
||||
local FG_COLOR = 0x00ffffff
|
||||
local BG_COLOR = 0x99000000
|
||||
local TILE_RADIUS = 5
|
||||
|
||||
font = gui.font.load(base.."font.font")
|
||||
local frame = 0
|
||||
local detailsidx = -1
|
||||
local jumping = false
|
||||
local helddown = false
|
||||
local floatmode = false
|
||||
local rulers = true
|
||||
local pokemon = false
|
||||
local pokecount = 0
|
||||
local showhelp = false
|
||||
local locked = false
|
||||
local lockdata = nil
|
||||
local incsprite = 0
|
||||
local questionable_tiles = false
|
||||
|
||||
function text(x, y, msg, fg, bg)
|
||||
local font = gui.font.load(base.."/font.font")
|
||||
|
||||
local function text(x, y, msg, fg, bg)
|
||||
if fg == nil then
|
||||
fg = FG_COLOR
|
||||
end
|
||||
|
@ -136,48 +137,48 @@ function on_input (subframe)
|
|||
end
|
||||
end
|
||||
|
||||
function get_sprite(base_addr)
|
||||
local cameraX = memory.readword(CAMERA_X) - 256
|
||||
local cameraY = memory.readword(CAMERA_Y) - 256
|
||||
local x = memory.readword(base_addr + 0x06)
|
||||
local y = memory.readword(base_addr + 0x0a)
|
||||
local function get_sprite(baseAddr)
|
||||
local spriteData = memory.readregion(baseAddr, mem.size.sprite)
|
||||
local offsets = mem.offset.sprite
|
||||
local x = util.regionToWord(spriteData, offsets.x)
|
||||
local y = util.regionToWord(spriteData, offsets.y)
|
||||
return {
|
||||
base_addr = string.format("%04x", base_addr),
|
||||
screenX = x - 256 - cameraX,
|
||||
screenY = y - 256 - cameraY - TILE_SIZE / 3,
|
||||
control = memory.readword(base_addr),
|
||||
draworder = memory.readword(base_addr + 0x02),
|
||||
base_addr = string.format("%04x", baseAddr),
|
||||
screenX = x - game.cameraX,
|
||||
screenY = y - game.cameraY - mem.size.tile / 3,
|
||||
control = util.regionToWord(spriteData, offsets.control),
|
||||
draworder = util.regionToWord(spriteData, 0x02),
|
||||
x = x,
|
||||
y = y,
|
||||
jumpheight = memory.readword(base_addr + 0x0e),
|
||||
style = memory.readword(base_addr + 0x12),
|
||||
currentframe = memory.readword(base_addr + 0x18),
|
||||
nextframe = memory.readword(base_addr + 0x1a),
|
||||
state = memory.readword(base_addr + 0x1e),
|
||||
velox = memory.readsword(base_addr + 0x20),
|
||||
veloy = memory.readsword(base_addr + 0x24),
|
||||
velomaxx = memory.readsword(base_addr + 0x26),
|
||||
velomaxy = memory.readsword(base_addr + 0x2a),
|
||||
motion = memory.readword(base_addr + 0x2e),
|
||||
attr = memory.readword(base_addr + 0x30),
|
||||
animnum = memory.readword(base_addr + 0x36),
|
||||
remainingframe = memory.readword(base_addr + 0x38),
|
||||
animcontrol = memory.readword(base_addr + 0x3a),
|
||||
animreadpos = memory.readword(base_addr + 0x3c),
|
||||
animcontrol2 = memory.readword(base_addr + 0x3e),
|
||||
animformat = memory.readword(base_addr + 0x40),
|
||||
damage1 = memory.readword(base_addr + 0x44),
|
||||
damage2 = memory.readword(base_addr + 0x46),
|
||||
damage3 = memory.readword(base_addr + 0x48),
|
||||
damage4 = memory.readword(base_addr + 0x4a),
|
||||
damage5 = memory.readword(base_addr + 0x4c),
|
||||
damage6 = memory.readword(base_addr + 0x4e),
|
||||
spriteparam = memory.readword(base_addr + 0x58),
|
||||
jumpHeight = util.regionToWord(spriteData, offsets.jumpHeight),
|
||||
style = util.regionToWord(spriteData, offsets.style),
|
||||
currentframe = util.regionToWord(spriteData, 0x18),
|
||||
nextframe = util.regionToWord(spriteData, 0x1a),
|
||||
state = util.regionToWord(spriteData, 0x1e),
|
||||
velocityX = util.regionToSWord(spriteData, offsets.velocityX),
|
||||
velocityY = util.regionToSWord(spriteData, offsets.velocityY),
|
||||
velomaxx = util.regionToSWord(spriteData, 0x26),
|
||||
velomaxy = util.regionToSWord(spriteData, 0x2a),
|
||||
motion = util.regionToWord(spriteData, offsets.motion),
|
||||
attr = util.regionToWord(spriteData, 0x30),
|
||||
animnum = util.regionToWord(spriteData, 0x36),
|
||||
remainingframe = util.regionToWord(spriteData, 0x38),
|
||||
animcontrol = util.regionToWord(spriteData, 0x3a),
|
||||
animreadpos = util.regionToWord(spriteData, 0x3c),
|
||||
animcontrol2 = util.regionToWord(spriteData, 0x3e),
|
||||
animformat = util.regionToWord(spriteData, 0x40),
|
||||
damage1 = util.regionToWord(spriteData, 0x44),
|
||||
damage2 = util.regionToWord(spriteData, 0x46),
|
||||
damage3 = util.regionToWord(spriteData, 0x48),
|
||||
damage4 = util.regionToWord(spriteData, 0x4a),
|
||||
damage5 = util.regionToWord(spriteData, 0x4c),
|
||||
damage6 = util.regionToWord(spriteData, 0x4e),
|
||||
spriteparam = util.regionToWord(spriteData, 0x58),
|
||||
}
|
||||
end
|
||||
|
||||
function sprite_details(idx)
|
||||
local base_addr = idx * 94 + SPRITE_BASE
|
||||
local function sprite_details(idx)
|
||||
local base_addr = idx * mem.size.sprite + mem.addr.spriteBase
|
||||
|
||||
local sprite = get_sprite(base_addr)
|
||||
|
||||
|
@ -197,20 +198,22 @@ function sprite_details(idx)
|
|||
end
|
||||
|
||||
if locked and lockdata == nil then
|
||||
lockdata = memory.readregion(base_addr, 94)
|
||||
lockdata = memory.readregion(base_addr, mem.size.sprite)
|
||||
end
|
||||
|
||||
if lockdata ~= nil and locked then
|
||||
memory.writeregion(base_addr, 94, lockdata)
|
||||
memory.writeregion(base_addr, mem.size.sprite, lockdata)
|
||||
end
|
||||
|
||||
text(0, 0, "Sprite "..idx..(locked and " (Locked)" or "")..":\n\n"..util.table_to_string(sprite))
|
||||
end
|
||||
|
||||
function on_paint (not_synth)
|
||||
count = count + 1
|
||||
|
||||
local guiWidth, guiHeight = gui.resolution()
|
||||
local waypoints = {}
|
||||
local overlayCtx = nil
|
||||
local overlay = nil
|
||||
local function renderOverlay(guiWidth, guiHeight)
|
||||
overlayCtx:set()
|
||||
overlayCtx:clear()
|
||||
|
||||
if showhelp then
|
||||
text(0, 0, [[
|
||||
|
@ -230,9 +233,13 @@ Sprite Details:
|
|||
[8] Enable / Disable stage tile rulers
|
||||
[9] Enable / Disable hidden tiles
|
||||
]])
|
||||
overlay = overlayCtx:render()
|
||||
gui.renderctx.setnull()
|
||||
return
|
||||
end
|
||||
|
||||
game.getPositions()
|
||||
|
||||
local toggles = ""
|
||||
|
||||
if pokemon then
|
||||
|
@ -256,34 +263,28 @@ Sprite Details:
|
|||
"Up"
|
||||
}
|
||||
|
||||
local cameraX = memory.readword(CAMERA_X) - 256
|
||||
local cameraY = memory.readword(CAMERA_Y) - 256
|
||||
local cameraDir = memory.readbyte(CAMERA_MODE)
|
||||
|
||||
local direction = directions[cameraDir+1]
|
||||
|
||||
local vertical = memory.readword(TILE_COLLISION_MATH_POINTER) == VERTICAL_POINTER
|
||||
|
||||
local partyX = memory.readword(PARTY_X)
|
||||
local partyY = memory.readword(PARTY_Y)
|
||||
local partyTileOffset = game.tileOffsetCalculation(partyX, partyY, vertical)
|
||||
local partyTileOffset = game.tileOffsetCalculation(game.partyX, game.partyY, game.vertical)
|
||||
|
||||
local stats = string.format([[
|
||||
%s camera %d,%d
|
||||
Vertical: %s
|
||||
Tile offset: %04x
|
||||
Stage number: %04x
|
||||
Stage (movement): %04x
|
||||
Main area: %04x
|
||||
Current area: %04x
|
||||
%s
|
||||
]], direction, cameraX, cameraY, vertical, partyTileOffset, memory.readword(STAGE_NUMBER), memory.readword(STAGE_NUMBER_MOVEMENT), util.table_to_string(game.getInputs()):gsub("[\\{\\},\n\"]", ""):gsub("-1", "X"):gsub("0", "."):gsub("1", "O"):gsub("(.............)", "%1\n"))
|
||||
]], direction, game.cameraX, game.cameraY, game.vertical, partyTileOffset, memory.readword(mem.addr.mainAreaNumber), memory.readword(mem.addr.currentAreaNumber), util.table_to_string(game.getInputs()):gsub("[\\{\\},\n\"]", ""):gsub("-1", "X"):gsub("0", "."):gsub("1", "O"):gsub("(.............)", "%1\n"))
|
||||
|
||||
text(guiWidth - 125, guiHeight - 200, stats)
|
||||
|
||||
text((partyX - 256 - cameraX) * 2, (partyY - 256 - cameraY) * 2 + 20, "Party")
|
||||
text((game.partyX - game.cameraX) * 2, (game.partyY - game.cameraY) * 2 + 20, "Party")
|
||||
|
||||
local sprites = {}
|
||||
for idx = 0,22,1 do
|
||||
local base_addr = idx * 94 + SPRITE_BASE
|
||||
local base_addr = idx * mem.size.sprite + mem.addr.spriteBase
|
||||
|
||||
local sprite = get_sprite(base_addr)
|
||||
|
||||
|
@ -305,38 +306,63 @@ Stage (movement): %04x
|
|||
text(sprite.screenX * 2, sprite.screenY * 2, string.format("%04x, %04x, %04x", sprite.control, sprite.animnum, sprite.attr), FG_COLOR, sprcolor)
|
||||
|
||||
local filename = os.getenv("HOME").."/neat-donk/catchem/"..sprite.animnum..","..sprite.attr..".png"
|
||||
if pokemon and spriteScreenX > (guiWidth / 4) and spriteScreenX < (guiWidth / 4) * 3 and spriteScreenY > (guiHeight / 3) and spriteScreenY < guiHeight and not util.file_exists(filename) then
|
||||
if pokemon and sprite.screenX > (guiWidth / 4) and sprite.screenY < (guiWidth / 4) * 3 and sprite.screenY > (guiHeight / 3) and sprite.screenY < guiHeight and not util.file_exists(filename) then
|
||||
gui.screenshot(filename)
|
||||
pokecount = pokecount + 1
|
||||
end
|
||||
::continue::
|
||||
end
|
||||
|
||||
if rulers and cameraX >= 0 then
|
||||
for i=1,#waypoints,1 do
|
||||
local screenX = (waypoints[i].x - game.cameraX) * 2
|
||||
local screenY = (waypoints[i].y - game.cameraY) * 2
|
||||
|
||||
if screenX > guiWidth - mem.size.tile * 2 then
|
||||
screenX = guiWidth - mem.size.tile * 2
|
||||
end
|
||||
|
||||
if screenY > guiHeight then
|
||||
screenY = guiHeight - 20
|
||||
end
|
||||
|
||||
if screenX < 0 then
|
||||
screenX = 0
|
||||
end
|
||||
|
||||
if screenY < 0 then
|
||||
screenY = 0
|
||||
end
|
||||
|
||||
text(screenX, screenY, "WAYPOINT "..i)
|
||||
end
|
||||
|
||||
text(guiWidth / 2, guiHeight - 20, "WAYPOINTS: "..#waypoints)
|
||||
|
||||
if rulers and game.cameraX >= 0 then
|
||||
local halfWidth = math.floor(guiWidth / 2)
|
||||
local halfHeight = math.floor(guiHeight / 2)
|
||||
|
||||
local cameraTileX = math.floor(cameraX / TILE_SIZE)
|
||||
local cameraTileX = math.floor(game.cameraX / mem.size.tile)
|
||||
gui.line(0, halfHeight, guiWidth, halfHeight, BG_COLOR)
|
||||
for i = cameraTileX, cameraTileX + guiWidth / TILE_SIZE / 2,1 do
|
||||
text((i * TILE_SIZE - cameraX) * 2, halfHeight, tostring(i), FG_COLOR, BG_COLOR)
|
||||
for i = cameraTileX, cameraTileX + guiWidth / mem.size.tile / 2,1 do
|
||||
text((i * mem.size.tile - game.cameraX) * 2, halfHeight, tostring(i), FG_COLOR, BG_COLOR)
|
||||
end
|
||||
|
||||
local cameraTileY = math.floor(cameraY / TILE_SIZE)
|
||||
local cameraTileY = math.floor(game.cameraY / mem.size.tile)
|
||||
gui.line(halfWidth, 0, halfWidth, guiHeight, BG_COLOR)
|
||||
for i = cameraTileY, cameraTileY + guiHeight / TILE_SIZE / 2,1 do
|
||||
text(halfWidth, (i * TILE_SIZE - cameraY) * 2, tostring(i), FG_COLOR, BG_COLOR)
|
||||
for i = cameraTileY, cameraTileY + guiHeight / mem.size.tile / 2,1 do
|
||||
text(halfWidth, (i * mem.size.tile - game.cameraY) * 2, tostring(i), FG_COLOR, BG_COLOR)
|
||||
end
|
||||
end
|
||||
|
||||
local tilePtr = memory.readhword(TILEDATA_POINTER)
|
||||
local tilePtr = memory.readhword(mem.addr.tiledataPointer)
|
||||
|
||||
for x = -TILE_RADIUS, TILE_RADIUS, 1 do
|
||||
for y = -TILE_RADIUS, TILE_RADIUS, 1 do
|
||||
local tileX = math.floor((partyX + x * TILE_SIZE) / TILE_SIZE) * TILE_SIZE
|
||||
local tileY = math.floor((partyY + y * TILE_SIZE) / TILE_SIZE) * TILE_SIZE
|
||||
local tileX = math.floor((game.partyX + x * mem.size.tile) / mem.size.tile) * mem.size.tile
|
||||
local tileY = math.floor((game.partyY + y * mem.size.tile) / mem.size.tile) * mem.size.tile
|
||||
|
||||
local offset = game.tileOffsetCalculation(tileX, tileY, vertical)
|
||||
local offset = game.tileOffsetCalculation(tileX, tileY, game.vertical)
|
||||
|
||||
local tile = memory.readword(tilePtr + offset)
|
||||
|
||||
|
@ -344,8 +370,8 @@ Stage (movement): %04x
|
|||
goto continue
|
||||
end
|
||||
|
||||
local screenX = (tileX - 256 - cameraX) * 2
|
||||
local screenY = (tileY - 256 - cameraY) * 2
|
||||
local screenX = (tileX - 256 - game.cameraX) * 2
|
||||
local screenY = (tileY - 256 - game.cameraY) * 2
|
||||
if screenX < 0 or screenX > guiWidth or
|
||||
screenY < 0 or screenY > guiHeight then
|
||||
--goto continue
|
||||
|
@ -357,7 +383,7 @@ Stage (movement): %04x
|
|||
end
|
||||
end
|
||||
|
||||
if cameraX >= 0 then
|
||||
if game.cameraX >= 0 then
|
||||
local oam = memory2.OAM:readregion(0x00, 0x220)
|
||||
|
||||
for idx=0,0x200/4-1,1 do
|
||||
|
@ -369,7 +395,7 @@ Stage (movement): %04x
|
|||
flags = oam[idx * 4 + 4],
|
||||
}
|
||||
|
||||
if screenSprite.x < 0 or screenSprite.y > guiHeight / 2 or screenSprite.y < TILE_SIZE then
|
||||
if screenSprite.x < 0 or screenSprite.y > guiHeight / 2 or screenSprite.y < mem.size.tile then
|
||||
goto continue
|
||||
end
|
||||
|
||||
|
@ -378,8 +404,8 @@ Stage (movement): %04x
|
|||
if sprite.control == 0 then
|
||||
goto nextsprite
|
||||
end
|
||||
if screenSprite.x > sprite.screenX - ENEMY_SIZE and screenSprite.x < sprite.screenX + ENEMY_SIZE / 2 and
|
||||
screenSprite.y > sprite.screenY - ENEMY_SIZE and screenSprite.y < sprite.screenY then
|
||||
if screenSprite.x > sprite.screenX - mem.size.enemy and screenSprite.x < sprite.screenX + mem.size.enemy / 2 and
|
||||
screenSprite.y > sprite.screenY - mem.size.enemy and screenSprite.y < sprite.screenY then
|
||||
goto continue
|
||||
end
|
||||
::nextsprite::
|
||||
|
@ -400,10 +426,26 @@ Stage (movement): %04x
|
|||
end
|
||||
|
||||
text(guiWidth - 125, 20, "Help [Hold 0]")
|
||||
|
||||
overlay = overlayCtx:render()
|
||||
gui.renderctx.setnull()
|
||||
end
|
||||
|
||||
function on_timer()
|
||||
set_timer_timeout(100 * 1000)
|
||||
function on_paint (not_synth)
|
||||
frame = frame + 1
|
||||
|
||||
local guiWidth, guiHeight = gui.resolution()
|
||||
if overlayCtx == nil then
|
||||
overlayCtx = gui.renderctx.new(guiWidth, guiHeight)
|
||||
end
|
||||
|
||||
if frame % 3 == 0 then
|
||||
renderOverlay(guiWidth, guiHeight)
|
||||
end
|
||||
|
||||
if overlay ~= nil then
|
||||
overlay:draw(0, 0)
|
||||
end
|
||||
end
|
||||
|
||||
input.keyhook("1", true)
|
||||
|
@ -417,4 +459,16 @@ input.keyhook("8", true)
|
|||
input.keyhook("9", true)
|
||||
input.keyhook("0", true)
|
||||
|
||||
set_timer_timeout(100 * 1000)
|
||||
game.findPreferredExit():next(function(preferredExit)
|
||||
return game.getWaypoints(preferredExit.x, preferredExit.y)
|
||||
end):next(function(w)
|
||||
waypoints = w
|
||||
end)
|
||||
|
||||
-- fe0a58 crate: near bunch and klomp on barrels
|
||||
-- fe0a58: Crate X position
|
||||
-- fe0a60: Crate Y position
|
||||
|
||||
-- fe0a70 bunch: near crate and klomp on barrels
|
||||
-- fe0a70: X position
|
||||
-- fe0a72: Y position
|
286
util.lua
286
util.lua
|
@ -1,7 +1,150 @@
|
|||
local utime, bit, callback, exec = utime, bit, callback, exec
|
||||
|
||||
local base = string.gsub(@@LUA_SCRIPT_FILENAME@@, "(.*[/\\])(.*)", "%1")
|
||||
|
||||
local Promise = nil
|
||||
|
||||
local _M = {}
|
||||
|
||||
_M.isWin = package.config:sub(1, 1) == '\\'
|
||||
|
||||
--- Converts a function into a promise.
|
||||
--- Useful for decoupling code from the original event it was fired in.
|
||||
---@param next function The function to resolve on the next tick
|
||||
---@return Promise Promise A promise that returns the value of the next function
|
||||
function _M.promiseWrap(next, value)
|
||||
local promise = Promise.new()
|
||||
promise:resolve(value)
|
||||
return promise:next(next)
|
||||
end
|
||||
|
||||
local times = {}
|
||||
|
||||
--- Track how long a function takes
|
||||
---@param toCall function Function that executes synchronously
|
||||
---@param name string What to print in the finish text
|
||||
---@return any any The original value
|
||||
function _M.time(toCall, name)
|
||||
if name == nil then
|
||||
name = 'function'
|
||||
end
|
||||
|
||||
local sec, usec = utime()
|
||||
local startTime = sec * 1000000 + usec
|
||||
local finishTime = 0
|
||||
local ret = toCall()
|
||||
sec, usec = utime()
|
||||
finishTime = sec * 1000000 + usec
|
||||
local t = times[name]
|
||||
if t == nil then
|
||||
t = {}
|
||||
times[name] = t
|
||||
end
|
||||
|
||||
if #t > 50 then
|
||||
table.remove(t, 1)
|
||||
end
|
||||
|
||||
t[#t+1] = finishTime - startTime
|
||||
local sum = 0
|
||||
for i=1,#t,1 do
|
||||
sum = sum + t[i]
|
||||
end
|
||||
|
||||
print(name..' is averaging '..math.floor(sum / #t))
|
||||
|
||||
return ret
|
||||
end
|
||||
|
||||
--- Wait for a specified amount of time. Note that this is dependent on the
|
||||
--- timer timeout getting set elsewhere in the code, probably in the Promise
|
||||
--- handler setup
|
||||
---@param delayUsec number Number of microseconds to wait
|
||||
---@return Promise Promise A promise that resolves when the time has elapsed
|
||||
function _M.delay(delayUsec)
|
||||
return Promise.new(function(res, rej)
|
||||
local sec, usec = utime()
|
||||
local start = sec * 1000000 + usec
|
||||
local finish = start
|
||||
local unTimer = nil
|
||||
local function onTimer()
|
||||
sec, usec = utime()
|
||||
finish = sec * 1000000 + usec
|
||||
if finish - start >= delayUsec then
|
||||
callback.unregister('timer', unTimer)
|
||||
res()
|
||||
end
|
||||
end
|
||||
unTimer = callback.register('timer', onTimer)
|
||||
end)
|
||||
end
|
||||
|
||||
function _M.saveState(filename)
|
||||
return Promise.new(function(res, rej)
|
||||
local unSave = nil
|
||||
local unErrSave = nil
|
||||
local function errSave(f)
|
||||
if f == filename then
|
||||
callback.unregister('post_save', unSave)
|
||||
callback.unregister('err_save', unErrSave)
|
||||
rej()
|
||||
end
|
||||
end
|
||||
local function postSave(f)
|
||||
if f == filename then
|
||||
callback.unregister('post_save', unSave)
|
||||
callback.unregister('err_save', unErrSave)
|
||||
res()
|
||||
end
|
||||
end
|
||||
unSave = callback.register('post_save', postSave)
|
||||
unErrSave = callback.register('err_save', errSave)
|
||||
exec('save-state '..filename)
|
||||
end)
|
||||
end
|
||||
|
||||
function _M.loadAndStart(romFile)
|
||||
return Promise.new(function(res, rej)
|
||||
local unPaint = nil
|
||||
local paint = 0
|
||||
local function onPaint()
|
||||
paint = paint + 1
|
||||
|
||||
_M.promiseWrap(function()
|
||||
if paint == 1 then
|
||||
exec('pause-emulator')
|
||||
elseif paint > 1 then
|
||||
callback.unregister('paint', unPaint)
|
||||
res()
|
||||
end
|
||||
end)
|
||||
end
|
||||
unPaint = callback.register('paint', onPaint)
|
||||
|
||||
exec('load-rom '..romFile)
|
||||
end)
|
||||
end
|
||||
|
||||
function _M.getTempDir()
|
||||
local temps = {
|
||||
os.getenv("TMPDIR"),
|
||||
os.getenv("TEMP"),
|
||||
os.getenv("TEMPDIR"),
|
||||
os.getenv("TMP"),
|
||||
}
|
||||
|
||||
local tempDir = "/tmp"
|
||||
for i=1,#temps,1 do
|
||||
local temp = temps[i]
|
||||
if temp ~= nil and temp ~= "" then
|
||||
tempDir = temps[i]
|
||||
break
|
||||
end
|
||||
end
|
||||
|
||||
return tempDir
|
||||
end
|
||||
|
||||
--- Echo a command, run it, and return the file handle
|
||||
--- @param cmd string The command to execute
|
||||
--- @param workdir string The working directory
|
||||
|
@ -42,6 +185,54 @@ function _M.doCmd(...)
|
|||
return _M.scrapeCmd('*a', ...)
|
||||
end
|
||||
|
||||
function _M.openReadPipe(name)
|
||||
if _M.isWin then
|
||||
local cmd = 'cd /d "'..base..'" && "'..base..'/namedpipe/createAndReadPipe.exe" "'..name..'"'
|
||||
print(cmd)
|
||||
return io.popen(cmd, 'r')
|
||||
else
|
||||
return io.popen("socat 'UNIX-LISTEN:".._M.getTempDir().."/"..name.."' -", 'r')
|
||||
end
|
||||
end
|
||||
|
||||
function _M.openReadPipeWriter(name)
|
||||
local writer = nil
|
||||
while writer == nil do
|
||||
if _M.isWin then
|
||||
writer = io.open('\\\\.\\pipe\\'..name, 'w')
|
||||
else
|
||||
writer = io.popen("socat 'UNIX-CONNECT:".._M.getTempDir().."/"..name.."' -", 'w')
|
||||
end
|
||||
end
|
||||
|
||||
return writer
|
||||
end
|
||||
|
||||
--- Download a url
|
||||
--- @param url string URI of resource to download
|
||||
--- @param dest string File to save resource to
|
||||
function _M.downloadFile (url, dest)
|
||||
return _M.doCmd('curl -sL "'..url..'" > "'..dest..'" || wget -qO- "'..url..'" > "'..dest..'"')
|
||||
end
|
||||
|
||||
--- Unzip a ZIP file with unzip or tar
|
||||
--- @param zipfile string The ZIP file path
|
||||
--- @param dest string Where to unzip the ZIP file. Beware ZIP bombs.
|
||||
function _M.unzip (zipfile, dest)
|
||||
return _M.doCmd('tar -xvf "'..zipfile..'" 2>&1 || unzip -n "'..zipfile..'" -d "'..dest..
|
||||
'" 2>&1', dest)
|
||||
end
|
||||
|
||||
--- Create a directory
|
||||
--- @return string dir The directory to create
|
||||
function _M.mkdir(dir)
|
||||
if _M.isWin then
|
||||
return _M.doCmd('if not exist "'..dir..'" mkdir "'..dir..'"')
|
||||
else
|
||||
return _M.doCmd("mkdir '"..dir.."'")
|
||||
end
|
||||
end
|
||||
|
||||
--- Run a command and get the output
|
||||
--- @param formats table|string|number List or single io.read() specifier
|
||||
--- @return table table List of results based on read specifiers
|
||||
|
@ -68,52 +259,7 @@ function _M.closeCmd(handle)
|
|||
return
|
||||
end
|
||||
if code ~= 0 then
|
||||
error("The last command failed")
|
||||
end
|
||||
end
|
||||
|
||||
function _M.waitForChange(filenames, count, wild)
|
||||
if type(filenames) == 'string' then
|
||||
if wild == nil then
|
||||
wild = filenames
|
||||
end
|
||||
|
||||
filenames = {filenames}
|
||||
end
|
||||
|
||||
if count == nil then
|
||||
count = #filenames
|
||||
end
|
||||
|
||||
if _M.isWin then
|
||||
local sec, usec = utime()
|
||||
print(string.format('Starting watching file at %d', sec * 1000000 + usec))
|
||||
|
||||
return _M.popenCmd([[powershell "$filename = ']]..wild..
|
||||
[[' ; $targetCount = ]]..count..[[ ; $count = 0 ; Register-ObjectEvent (New-Object IO.FileSystemWatcher (Split-Path $filename), (Split-Path -Leaf $filename) -Property @{ IncludeSubdirectories = $false ; NotifyFilter = [IO.NotifyFilters]'FileName, LastWrite'}) -EventName Changed -SourceIdentifier RunnerDataChanged -Action { $count += 1 ; if ( $count -ge $targetCount ) { [Environment]::Exit(0) } } ; while($true) { Start-Sleep -Milliseconds 1 }"]])
|
||||
else
|
||||
local watchCmd = ''
|
||||
if count == 1 then
|
||||
watchCmd = [[which inotifywait >/dev/null && { inotifywait -q -e close_write ']]..filenames[1]..[[' || exit 0 ; }]]
|
||||
else
|
||||
watchCmd = [[bash <<'EOF'
|
||||
COUNT=]]..count..[[
|
||||
FILENAMES=(']]..table.concat(filenames, "' '")..[[')
|
||||
declare -A SEEN
|
||||
((I = 0))
|
||||
set -m
|
||||
which inotifywait >/dev/null
|
||||
( inotifywait -q -m -e close_write "${FILENAMES[@]}" | while read LINE ; do
|
||||
SEEN["$LINE"]=1
|
||||
TOTAL=${#SEEN[@]}
|
||||
if ((TOTAL == COUNT)) ; then
|
||||
kill -s TERM 0
|
||||
fi
|
||||
done ) &
|
||||
wait
|
||||
EOF]]
|
||||
end
|
||||
return _M.popenCmd(watchCmd)
|
||||
error(string.format("The last command failed: %s %d", state, code))
|
||||
end
|
||||
end
|
||||
|
||||
|
@ -160,4 +306,50 @@ function _M.file_exists(name)
|
|||
if f~=nil then io.close(f) return true else return false end
|
||||
end
|
||||
|
||||
return _M
|
||||
function _M.nearestColor(needle, colors)
|
||||
local opacity = bit.band(needle, 0xff000000)
|
||||
local needle = {
|
||||
r = bit.lrshift(bit.band(needle, 0x00ff0000), 4),
|
||||
g = bit.lrshift(bit.band(needle, 0x0000ff00), 2),
|
||||
b = bit.band(needle, 0x000000ff),
|
||||
}
|
||||
local minDistanceSq = 0x7fffffff
|
||||
local value = nil
|
||||
for name,color in pairs(colors) do
|
||||
local distanceSq = (
|
||||
math.pow(needle.r - color.r, 2) +
|
||||
math.pow(needle.g - color.g, 2) +
|
||||
math.pow(needle.b - color.b, 2)
|
||||
)
|
||||
if distanceSq < minDistanceSq then
|
||||
minDistanceSq = distanceSq
|
||||
value = name
|
||||
end
|
||||
if value == nil then
|
||||
value = name
|
||||
end
|
||||
end
|
||||
|
||||
return value
|
||||
end
|
||||
|
||||
function _M.regionToWord(region, offset)
|
||||
return bit.compose(region[offset], region[offset + 1])
|
||||
end
|
||||
|
||||
function _M.regionToSWord(region, offset)
|
||||
local unsigned = _M.regionToWord(region, offset)
|
||||
local base = bit.band(unsigned, 0x7fff)
|
||||
local sign = bit.lrshift(unsigned, 15)
|
||||
-- There is a more mathematical way to do this but whatever
|
||||
if sign == 1 then
|
||||
return base-0x8000
|
||||
else
|
||||
return base
|
||||
end
|
||||
end
|
||||
|
||||
return function(promise)
|
||||
Promise = promise
|
||||
return _M
|
||||
end
|
14
xpra-run.sh
14
xpra-run.sh
|
@ -3,10 +3,12 @@ SCRIPT_DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" &> /dev/null && pwd )"
|
|||
#ARGS=("--lua=$SCRIPT_DIR/neat-donk.lua")
|
||||
ARGS=()
|
||||
PORT=5309
|
||||
(
|
||||
while ! nc -z localhost $PORT ; do
|
||||
sleep 0.1
|
||||
done
|
||||
xdg-open http://127.0.0.1:$PORT
|
||||
) &
|
||||
xpra start --bind-tcp=127.0.0.1:$PORT --html=on \
|
||||
--start-child="lsnes ${ARGS[*]}" \
|
||||
--exit-with-child=yes --start-new-commands=no
|
||||
while ! nc -z localhost $PORT ; do
|
||||
sleep 0.1
|
||||
done
|
||||
xdg-open http://127.0.0.1:$PORT
|
||||
--start-child="/etc/alternatives/x-terminal-emulator -e 'lsnes ${ARGS[*]}'" \
|
||||
--exit-with-child=yes --start-new-commands=no --daemon=no
|
||||
|
|
2
xpra.css
2
xpra.css
|
@ -1,4 +1,4 @@
|
|||
.wmclass-Lsnes {
|
||||
#screen .wmclass-Lsnes, #screen .window[class*="term"] {
|
||||
display: block;
|
||||
position: relative;
|
||||
top: 0 !important;
|
||||
|
|
193
zzlib.lua
Normal file
193
zzlib.lua
Normal file
|
@ -0,0 +1,193 @@
|
|||
-- zzlib - zlib decompression in Lua - Implementation-independent code
|
||||
|
||||
-- Copyright (c) 2016-2020 Francois Galea <fgalea at free.fr>
|
||||
-- This program is free software. It comes without any warranty, to
|
||||
-- the extent permitted by applicable law. You can redistribute it
|
||||
-- and/or modify it under the terms of the Do What The Fuck You Want
|
||||
-- To Public License, Version 2, as published by Sam Hocevar. See
|
||||
-- the COPYING file or http://www.wtfpl.net/ for more details.
|
||||
|
||||
local base = string.gsub(@@LUA_SCRIPT_FILENAME@@, "(.*[/\\])(.*)", "%1")
|
||||
|
||||
local unpack = table.unpack or unpack
|
||||
local infl = dofile(base.."/inflate-bit32.lua")
|
||||
|
||||
local zzlib = {}
|
||||
|
||||
local function arraytostr(array)
|
||||
local tmp = {}
|
||||
local size = #array
|
||||
local pos = 1
|
||||
local imax = 1
|
||||
while size > 0 do
|
||||
local bsize = size>=2048 and 2048 or size
|
||||
local s = string.char(unpack(array,pos,pos+bsize-1))
|
||||
pos = pos + bsize
|
||||
size = size - bsize
|
||||
local i = 1
|
||||
while tmp[i] do
|
||||
s = tmp[i]..s
|
||||
tmp[i] = nil
|
||||
i = i + 1
|
||||
end
|
||||
if i > imax then
|
||||
imax = i
|
||||
end
|
||||
tmp[i] = s
|
||||
end
|
||||
local str = ""
|
||||
for i=1,imax do
|
||||
if tmp[i] then
|
||||
str = tmp[i]..str
|
||||
end
|
||||
end
|
||||
return str
|
||||
end
|
||||
|
||||
local function inflate_gzip(bs)
|
||||
local id1,id2,cm,flg = bs.buf:byte(1,4)
|
||||
if id1 ~= 31 or id2 ~= 139 then
|
||||
error("invalid gzip header")
|
||||
end
|
||||
if cm ~= 8 then
|
||||
error("only deflate format is supported")
|
||||
end
|
||||
bs.pos=11
|
||||
if infl.band(flg,4) ~= 0 then
|
||||
local xl1,xl2 = bs.buf.byte(bs.pos,bs.pos+1)
|
||||
local xlen = xl2*256+xl1
|
||||
bs.pos = bs.pos+xlen+2
|
||||
end
|
||||
if infl.band(flg,8) ~= 0 then
|
||||
local pos = bs.buf:find("\0",bs.pos)
|
||||
bs.pos = pos+1
|
||||
end
|
||||
if infl.band(flg,16) ~= 0 then
|
||||
local pos = bs.buf:find("\0",bs.pos)
|
||||
bs.pos = pos+1
|
||||
end
|
||||
if infl.band(flg,2) ~= 0 then
|
||||
-- TODO: check header CRC16
|
||||
bs.pos = bs.pos+2
|
||||
end
|
||||
local result = arraytostr(infl.main(bs))
|
||||
local crc = bs:getb(8)+256*(bs:getb(8)+256*(bs:getb(8)+256*bs:getb(8)))
|
||||
bs:close()
|
||||
if crc ~= infl.crc32(result) then
|
||||
error("checksum verification failed")
|
||||
end
|
||||
return result
|
||||
end
|
||||
|
||||
-- compute Adler-32 checksum
|
||||
local function adler32(s)
|
||||
local s1 = 1
|
||||
local s2 = 0
|
||||
for i=1,#s do
|
||||
local c = s:byte(i)
|
||||
s1 = (s1+c)%65521
|
||||
s2 = (s2+s1)%65521
|
||||
end
|
||||
return s2*65536+s1
|
||||
end
|
||||
|
||||
local function inflate_zlib(bs)
|
||||
local cmf = bs.buf:byte(1)
|
||||
local flg = bs.buf:byte(2)
|
||||
if (cmf*256+flg)%31 ~= 0 then
|
||||
error("zlib header check bits are incorrect")
|
||||
end
|
||||
if infl.band(cmf,15) ~= 8 then
|
||||
error("only deflate format is supported")
|
||||
end
|
||||
if infl.rshift(cmf,4) ~= 7 then
|
||||
error("unsupported window size")
|
||||
end
|
||||
if infl.band(flg,32) ~= 0 then
|
||||
error("preset dictionary not implemented")
|
||||
end
|
||||
bs.pos=3
|
||||
local result = arraytostr(infl.main(bs))
|
||||
local adler = ((bs:getb(8)*256+bs:getb(8))*256+bs:getb(8))*256+bs:getb(8)
|
||||
bs:close()
|
||||
if adler ~= adler32(result) then
|
||||
error("checksum verification failed")
|
||||
end
|
||||
return result
|
||||
end
|
||||
|
||||
function zzlib.gunzipf(filename)
|
||||
local file,err = io.open(filename,"rb")
|
||||
if not file then
|
||||
return nil,err
|
||||
end
|
||||
return inflate_gzip(infl.bitstream_init(file))
|
||||
end
|
||||
|
||||
function zzlib.gunzip(str)
|
||||
return inflate_gzip(infl.bitstream_init(str))
|
||||
end
|
||||
|
||||
function zzlib.inflate(str)
|
||||
return inflate_zlib(infl.bitstream_init(str))
|
||||
end
|
||||
|
||||
local function int2le(str,pos)
|
||||
local a,b = str:byte(pos,pos+1)
|
||||
return b*256+a
|
||||
end
|
||||
|
||||
local function int4le(str,pos)
|
||||
local a,b,c,d = str:byte(pos,pos+3)
|
||||
return ((d*256+c)*256+b)*256+a
|
||||
end
|
||||
|
||||
function zzlib.unzip(buf,filename)
|
||||
local p = #buf-21
|
||||
local quit = false
|
||||
if int4le(buf,p) ~= 0x06054b50 then
|
||||
-- not sure there is a reliable way to locate the end of central directory record
|
||||
-- if it has a variable sized comment field
|
||||
error(".ZIP file comments not supported")
|
||||
end
|
||||
local cdoffset = int4le(buf,p+16)
|
||||
local nfiles = int2le(buf,p+10)
|
||||
p = cdoffset+1
|
||||
for i=1,nfiles do
|
||||
if int4le(buf,p) ~= 0x02014b50 then
|
||||
error("invalid central directory header signature")
|
||||
end
|
||||
local flag = int2le(buf,p+8)
|
||||
local method = int2le(buf,p+10)
|
||||
local crc = int4le(buf,p+16)
|
||||
local namelen = int2le(buf,p+28)
|
||||
local name = buf:sub(p+46,p+45+namelen)
|
||||
if name == filename then
|
||||
local headoffset = int4le(buf,p+42)
|
||||
p = 1+headoffset
|
||||
if int4le(buf,p) ~= 0x04034b50 then
|
||||
error("invalid local header signature")
|
||||
end
|
||||
local csize = int4le(buf,p+18)
|
||||
local extlen = int2le(buf,p+28)
|
||||
p = p+30+namelen+extlen
|
||||
if method == 0 then
|
||||
-- no compression
|
||||
result = buf:sub(p,p+csize-1)
|
||||
else
|
||||
-- DEFLATE compression
|
||||
local bs = infl.bitstream_init(buf)
|
||||
bs.pos = p
|
||||
result = arraytostr(infl.main(bs))
|
||||
end
|
||||
if crc ~= infl.crc32(result) then
|
||||
error("checksum verification failed")
|
||||
end
|
||||
return result
|
||||
end
|
||||
p = p+46+namelen+int2le(buf,p+30)+int2le(buf,p+32)
|
||||
end
|
||||
error("file '"..filename.."' not found in ZIP archive")
|
||||
end
|
||||
|
||||
return zzlib
|
Loading…
Add table
Reference in a new issue