Compare commits

..

41 commits

Author SHA1 Message Date
65bdce0140 Add zzlib 2021-05-13 20:24:57 -04:00
707bfb0059 Fix potential problem with input and output pipes 2021-05-13 20:10:56 -04:00
ed5e64c6b7 Handle threading in the wrapper 2021-05-13 05:55:09 -04:00
097f1cbfb4 Add instruction about ROM path 2021-05-13 04:26:01 -04:00
aae1d30eb9 Remove instructions to load the ROM. It should load automatically 2021-05-13 04:23:52 -04:00
ce0639fd40 Use builtin ZIP writing functionality for performance reasons. 2021-05-13 04:21:47 -04:00
3756979b6a Increase performance by disabling some layers 2021-05-13 03:02:49 -04:00
f4229bfc48 Add option to auto load and save the pool file 2021-05-12 22:24:59 -04:00
0646e57826 Update link 2021-05-12 20:03:23 -04:00
7ef36d98f2 random logic remove 2021-05-09 00:29:01 -04:00
8233847f46 Pass sprites around 2021-05-08 23:44:18 -04:00
22b41082a7 Properly bubble up the max fitness because some calculations use it. 2021-05-08 09:31:11 -04:00
6e718eb3b0 Don't use separate timeout for vertical levels since more movements are
counted towards resetting the timer.
2021-05-08 07:01:47 -04:00
30ada92db7 Add config option to turn off UI to try to speed things up. 2021-05-08 06:41:51 -04:00
d224ba7805 Extra argument 2021-05-08 00:42:50 -04:00
0d9afa4ced Paranoid distrust of variable scoping 2021-05-08 00:07:52 -04:00
bd5b9cc4cb Log model writes. 2021-05-07 23:43:15 -04:00
2e53f92179 Fix possible issue with iteration variable changing 2021-05-07 23:35:10 -04:00
32778cf9fd Added a bunch of code to add waypoints to the goal. Not sure at what
point this becomes cheating.
2021-05-07 21:54:25 -04:00
eb53739618 Fix bitness of velocity. Added target scan function. Currently broken. 2021-05-07 03:35:13 -04:00
7fcb93c83c Automatically start the ROM in single thread mode. 2021-05-06 23:58:21 -04:00
55d0bff81c Don't reset timeout if already dropped 2021-05-06 05:05:25 -04:00
cf4842e124 Detect falling before bonus screen reset 2021-05-06 05:00:41 -04:00
d6cf4c1505 Consolidate UI text 2021-05-06 04:29:13 -04:00
603dcf502c Goal offset indicator 2021-05-06 02:13:35 -04:00
3f872c169c Severe punishment for falling 2021-05-06 01:40:17 -04:00
75c3ef6f4b Update layout 2021-05-06 00:48:57 -04:00
ca2496f7d5 Allow repeat species again since there's no chance of accidentally
rereading the file.
2021-05-05 20:01:16 -04:00
5a12540259 Added i3 layout for whoever may want it 2021-05-05 18:46:34 -04:00
5ed93c28da Added save state 2021-05-05 18:25:42 -04:00
0c42d8452c Don't change the timeout if we're already timed out 2021-05-05 18:22:16 -04:00
1c1307a0e2 Don't kill the run on the bonus intro screen 2021-05-05 18:18:59 -04:00
0a59ab8b21 Fix reset functionality 2021-05-05 08:38:43 -04:00
fda82e0cd7 More advanced logic to calculate good tiles which allows some ropes. 2021-05-05 05:33:35 -04:00
1370bfd9a1 Environment variable to load pool file 2021-05-05 03:34:04 -04:00
80ab0405ec Check text on error 2021-05-05 00:41:09 -04:00
1cd3618b6f Add climbing. Fix sprite list regression 2021-05-04 23:29:31 -04:00
123d7508d1 Fix pipes in Linux with socat 2021-05-04 20:30:52 -04:00
a5b089520b Clean up some old garbage 2021-05-04 08:36:50 -04:00
6c1c4fa0eb Named pipes in Windows. It is glorious. 2021-05-04 08:20:35 -04:00
542682e126 Added Mainbrace Mayhem state 2021-05-04 01:46:41 -04:00
25 changed files with 1880 additions and 4305 deletions

3
.gitignore vendored
View file

@ -11,5 +11,6 @@ config.lua
*.srm
*.sfc
*.bst
watchexec*
namedpipe*
build/
.VSCodeCounter/

7
LICENSE.namedpipe.md Normal file
View 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.

View file

@ -1,201 +0,0 @@
Apache License
Version 2.0, January 2004
http://www.apache.org/licenses/
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
1. Definitions.
"License" shall mean the terms and conditions for use, reproduction,
and distribution as defined by Sections 1 through 9 of this document.
"Licensor" shall mean the copyright owner or entity authorized by
the copyright owner that is granting the License.
"Legal Entity" shall mean the union of the acting entity and all
other entities that control, are controlled by, or are under common
control with that entity. For the purposes of this definition,
"control" means (i) the power, direct or indirect, to cause the
direction or management of such entity, whether by contract or
otherwise, or (ii) ownership of fifty percent (50%) or more of the
outstanding shares, or (iii) beneficial ownership of such entity.
"You" (or "Your") shall mean an individual or Legal Entity
exercising permissions granted by this License.
"Source" form shall mean the preferred form for making modifications,
including but not limited to software source code, documentation
source, and configuration files.
"Object" form shall mean any form resulting from mechanical
transformation or translation of a Source form, including but
not limited to compiled object code, generated documentation,
and conversions to other media types.
"Work" shall mean the work of authorship, whether in Source or
Object form, made available under the License, as indicated by a
copyright notice that is included in or attached to the work
(an example is provided in the Appendix below).
"Derivative Works" shall mean any work, whether in Source or Object
form, that is based on (or derived from) the Work and for which the
editorial revisions, annotations, elaborations, or other modifications
represent, as a whole, an original work of authorship. For the purposes
of this License, Derivative Works shall not include works that remain
separable from, or merely link (or bind by name) to the interfaces of,
the Work and Derivative Works thereof.
"Contribution" shall mean any work of authorship, including
the original version of the Work and any modifications or additions
to that Work or Derivative Works thereof, that is intentionally
submitted to Licensor for inclusion in the Work by the copyright owner
or by an individual or Legal Entity authorized to submit on behalf of
the copyright owner. For the purposes of this definition, "submitted"
means any form of electronic, verbal, or written communication sent
to the Licensor or its representatives, including but not limited to
communication on electronic mailing lists, source code control systems,
and issue tracking systems that are managed by, or on behalf of, the
Licensor for the purpose of discussing and improving the Work, but
excluding communication that is conspicuously marked or otherwise
designated in writing by the copyright owner as "Not a Contribution."
"Contributor" shall mean Licensor and any individual or Legal Entity
on behalf of whom a Contribution has been received by Licensor and
subsequently incorporated within the Work.
2. Grant of Copyright License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
copyright license to reproduce, prepare Derivative Works of,
publicly display, publicly perform, sublicense, and distribute the
Work and such Derivative Works in Source or Object form.
3. Grant of Patent License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
(except as stated in this section) patent license to make, have made,
use, offer to sell, sell, import, and otherwise transfer the Work,
where such license applies only to those patent claims licensable
by such Contributor that are necessarily infringed by their
Contribution(s) alone or by combination of their Contribution(s)
with the Work to which such Contribution(s) was submitted. If You
institute patent litigation against any entity (including a
cross-claim or counterclaim in a lawsuit) alleging that the Work
or a Contribution incorporated within the Work constitutes direct
or contributory patent infringement, then any patent licenses
granted to You under this License for that Work shall terminate
as of the date such litigation is filed.
4. Redistribution. You may reproduce and distribute copies of the
Work or Derivative Works thereof in any medium, with or without
modifications, and in Source or Object form, provided that You
meet the following conditions:
(a) You must give any other recipients of the Work or
Derivative Works a copy of this License; and
(b) You must cause any modified files to carry prominent notices
stating that You changed the files; and
(c) You must retain, in the Source form of any Derivative Works
that You distribute, all copyright, patent, trademark, and
attribution notices from the Source form of the Work,
excluding those notices that do not pertain to any part of
the Derivative Works; and
(d) If the Work includes a "NOTICE" text file as part of its
distribution, then any Derivative Works that You distribute must
include a readable copy of the attribution notices contained
within such NOTICE file, excluding those notices that do not
pertain to any part of the Derivative Works, in at least one
of the following places: within a NOTICE text file distributed
as part of the Derivative Works; within the Source form or
documentation, if provided along with the Derivative Works; or,
within a display generated by the Derivative Works, if and
wherever such third-party notices normally appear. The contents
of the NOTICE file are for informational purposes only and
do not modify the License. You may add Your own attribution
notices within Derivative Works that You distribute, alongside
or as an addendum to the NOTICE text from the Work, provided
that such additional attribution notices cannot be construed
as modifying the License.
You may add Your own copyright statement to Your modifications and
may provide additional or different license terms and conditions
for use, reproduction, or distribution of Your modifications, or
for any such Derivative Works as a whole, provided Your use,
reproduction, and distribution of the Work otherwise complies with
the conditions stated in this License.
5. Submission of Contributions. Unless You explicitly state otherwise,
any Contribution intentionally submitted for inclusion in the Work
by You to the Licensor shall be under the terms and conditions of
this License, without any additional terms or conditions.
Notwithstanding the above, nothing herein shall supersede or modify
the terms of any separate license agreement you may have executed
with Licensor regarding such Contributions.
6. Trademarks. This License does not grant permission to use the trade
names, trademarks, service marks, or product names of the Licensor,
except as required for reasonable and customary use in describing the
origin of the Work and reproducing the content of the NOTICE file.
7. Disclaimer of Warranty. Unless required by applicable law or
agreed to in writing, Licensor provides the Work (and each
Contributor provides its Contributions) on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
implied, including, without limitation, any warranties or conditions
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
PARTICULAR PURPOSE. You are solely responsible for determining the
appropriateness of using or redistributing the Work and assume any
risks associated with Your exercise of permissions under this License.
8. Limitation of Liability. In no event and under no legal theory,
whether in tort (including negligence), contract, or otherwise,
unless required by applicable law (such as deliberate and grossly
negligent acts) or agreed to in writing, shall any Contributor be
liable to You for damages, including any direct, indirect, special,
incidental, or consequential damages of any character arising as a
result of this License or out of the use or inability to use the
Work (including but not limited to damages for loss of goodwill,
work stoppage, computer failure or malfunction, or any and all
other commercial damages or losses), even if such Contributor
has been advised of the possibility of such damages.
9. Accepting Warranty or Additional Liability. While redistributing
the Work or Derivative Works thereof, You may choose to offer,
and charge a fee for, acceptance of support, warranty, indemnity,
or other liability obligations and/or rights consistent with this
License. However, in accepting such obligations, You may act only
on Your own behalf and on Your sole responsibility, not on behalf
of any other Contributor, and only if You agree to indemnify,
defend, and hold each Contributor harmless for any liability
incurred by, or claims asserted against, such Contributor by reason
of your accepting any such warranty or additional liability.
END OF TERMS AND CONDITIONS
APPENDIX: How to apply the Apache License to your work.
To apply the Apache License to your work, attach the following
boilerplate notice, with the fields enclosed by brackets "{}"
replaced with your own identifying information. (Don't include
the brackets!) The text should be enclosed in the appropriate
comment syntax for the file format. We also recommend that a
file or class name and description of purpose be included on the
same "printed page" as the copyright notice for easier
identification within third-party archives.
Copyright {yyyy} {name of copyright owner}
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.

File diff suppressed because it is too large Load diff

View file

@ -2,22 +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!)
* inotifywait for Linux, or a fairly recent version of Windows that has PowerShell
* 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. Currently Windows does not support multiple threads.
3. Load the `neat-donk.lua` script: `Tools -> Run Lua script...`
## Config
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
@ -34,17 +57,25 @@ See [YouTube](https://www.youtube.com/watch?v=Q69_wmEkp-k) for an example run.
### 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.
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.
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
@ -53,8 +84,8 @@ Located at [tools/bsnes-launcher.lua](tools/bsnes-launcher.lua), this script giv
* [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)
* [watchexec](https://github.com/watchexec/watchexec/blob/main/LICENSE)
* [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

View file

@ -24,6 +24,8 @@ _M.State = {
-- W1.2 Mainbrace Mayhem
"MainbraceMayhem.lsmv", -- [4]
"MainbraceMayhemBonus.lsmv", -- [5]
"MainbraceMayhemTopOfRope.lsmv", -- [6]
}
_M.Filename = _M.PoolDir .. _M.State[4]
@ -41,8 +43,10 @@ _M.StartPowerup = 0
_M.NeatConfig = {
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,
@ -59,7 +63,7 @@ BiasMutationChance = 0.40,
StepSize = 0.1,
DisableMutationChance = 0.4,
EnableMutationChance = 0.2,
TimeoutConstant = 20,
TimeoutConstant = 30,
MaxNodes = 1000000,
}

255
game.lua
View file

@ -1,5 +1,5 @@
--Notes here
local memory, bit, memory2, input, callback, movie = memory, bit, memory2, input, callback, movie
local memory, bit, memory2, input, callback, movie, utime = memory, bit, memory2, input, callback, movie, utime
local base = string.gsub(@@LUA_SCRIPT_FILENAME@@, "(.*[/\\])(.*)", "%1")
@ -126,8 +126,7 @@ end
local function processRewind()
for i=#onRewindQueue,1,-1 do
local promise = table.remove(onRewindQueue, i)
promise:resolve()
table.remove(onRewindQueue, i):resolve()
end
end
@ -197,11 +196,79 @@ function _M.findPreferredExit()
end)
end
function _M.getGoalHit()
local sprites = _M.getSprites()
--- 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
@ -346,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 * mem.size.tile) / mem.size.tile) * mem.size.tile
local tileY = math.floor((_M.partyY + dy * mem.size.tile) / mem.size.tile) * mem.size.tile
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)
@ -361,6 +428,10 @@ 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(mem.addr.currentAreaNumber)
end
@ -373,6 +444,24 @@ function _M.getJumpHeight()
return sprite.jumpHeight
end
function _M.diedFromHit()
local sprite = _M.getSprite(_M.leader)
if sprite == nil then
return 0
end
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)
@ -395,8 +484,9 @@ function _M.getSprite(idx)
-- 0x4000 0: Right facing 1: Flipped
-- 0x1000 0: Alive 1: Dying
style = util.regionToWord(spriteData, offsets.style),
velocityX = util.regionToWord(spriteData, offsets.velocityX),
velocityY = util.regionToWord(spriteData, offsets.velocityY),
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]
@ -426,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
@ -471,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
local 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*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
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*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
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()
@ -587,6 +705,17 @@ local function processMapLoad()
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 = {}
local function registerHandler(space, regname, addr, callback)
table.insert(handlers, {

458
i3.json Normal file
View 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
View 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

View file

@ -48,6 +48,7 @@ local _M = {
style = 0x12,
velocityX = 0x20,
velocityY = 0x24,
motion = 0x2e,
}
}
}

View file

@ -48,6 +48,11 @@ pool.onRenderForm(function(form)
end
end)
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)

11
pipe-test.lua Normal file
View 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)

101
pool.lua
View file

@ -1,4 +1,4 @@
local callback, set_timer_timeout = callback, set_timer_timeout
local callback, set_timer_timeout, zip = callback, set_timer_timeout, zip
local base = string.gsub(@@LUA_SCRIPT_FILENAME@@, "(.*[/\\])(.*)", "%1")
@ -7,11 +7,9 @@ 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 libDeflate = dofile(base.."/LibDeflate.lua")
local zzlib = dofile(base.."/zzlib.lua")
local hasThreads =
not util.isWin and
config.NeatConfig.Threads > 1
local hasThreads = config.NeatConfig.Threads > 1
-- Only the parent should manage ticks!
callback.register('timer', function()
@ -20,18 +18,14 @@ callback.register('timer', function()
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 hasThreads then
local warn = '========== When using threads, the ROM file to use comes from config.lua. Also, you do not need to start any ROM in the parent process.'
io.stderr:write(warn)
print(warn)
Runner = dofile(base.."/runner-wrapper.lua")
else
local warn = '========== The ROM must already be running when you only have one thread.'
io.stderr:write(warn)
print(warn)
Runner = dofile(base.."/runner.lua")
end
@ -399,9 +393,7 @@ local function addToSpecies(child)
end
local function initializePool()
local promise = Promise.new()
promise:resolve()
return promise:next(function()
return util.promiseWrap(function()
pool = newPool()
for i=1,config.NeatConfig.Population do
@ -419,26 +411,23 @@ 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 promise = Promise.new()
promise:resolve()
return promise:next(function ()
local file = io.open(filename, "w")
return util.promiseWrap(function ()
local file = zip.writer.new(filename)
file:create_file('data.serpent')
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()
file:write(dump)
file:close_file()
file:commit()
end)
end
-- FIXME This isn't technically asynchronous. Probably can't be though.
local function loadFile(filename)
local promise = Promise.new()
promise:resolve()
return promise:next(function()
return util.promiseWrap(function()
message("Loading pool from " .. filename, 0x00999900)
local file = io.open(filename, "r")
if file == nil then
@ -446,7 +435,8 @@ local function loadFile(filename)
return
end
local contents = file:read("*all")
local decomp, _ = libDeflate:DecompressDeflate(contents:sub(11, #contents - 8))
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)
@ -671,7 +661,15 @@ 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 function reset()
return _M.run(true)
end
local runner = Runner(Promise)
@ -684,6 +682,9 @@ end)
runner.onLoad(function(filename)
_M.requestLoad(filename)
end)
runner.onReset(function(filename)
_M.requestReset()
end)
runner.onRenderForm(function(form)
processRenderForm(form)
end)
@ -691,17 +692,16 @@ end)
local playTop = nil
local topRequested = false
local loadRequested = false
local loadRequested = config.NeatConfig.AutoSave
local saveRequested = false
local resetRequested = false
local function mainLoop(currentSpecies, topGenome)
if currentSpecies == nil then
currentSpecies = 1
end
local slice = pool.species[currentSpecies]
local promise = Promise.new()
promise:resolve()
return promise:next(function()
return util.promiseWrap(function()
if loadRequested then
loadRequested = false
currentSpecies = nil
@ -714,6 +714,11 @@ local function mainLoop(currentSpecies, topGenome)
return savePool()
end
if resetRequested then
resetRequested = false
return reset()
end
if topRequested then
topRequested = false
return playTop()
@ -724,16 +729,8 @@ local function mainLoop(currentSpecies, topGenome)
end
if hasThreads then
slice = {}
for i=currentSpecies, currentSpecies + config.NeatConfig.Threads - 1, 1 do
if pool.species[i] == nil then
break
end
table.insert(slice, pool.species[i])
end
slice = pool.species
end
local finished = 0
return runner.run(
slice,
@ -742,7 +739,11 @@ local function mainLoop(currentSpecies, topGenome)
-- Genome callback
-- FIXME Should we do something here??? What was your plan, past me?
end
):next(function()
):next(function(maxFitness)
if maxFitness > pool.maxFitness then
pool.maxFitness = maxFitness
end
if hasThreads then
currentSpecies = currentSpecies + #slice
else
@ -788,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
@ -812,8 +817,12 @@ function _M.run(reset)
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

Binary file not shown.

Binary file not shown.

Binary file not shown.

View file

@ -10,16 +10,10 @@ callback.register('timer', function()
end)
set_timer_timeout(1)
local Runner = dofile(base.."/runner.lua")
local serpent = dofile(base.."/serpent.lua")
local util = dofile(base.."/util.lua")(Promise)
local inputFilePath = os.getenv("RUNNER_INPUT_FILE")
local outputFilePath = os.getenv("RUNNER_OUTPUT_FILE")
local outContents = {}
local statusLine = nil
local statusColor = 0x0000ff00
@ -27,20 +21,45 @@ local species = nil
local speciesId = -1
local generationIndex = nil
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)
runner.onRenderForm(function(form)
@ -60,115 +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 function waitLoop()
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()
runner.onReset(function()
writeResponse({
type = 'onReset',
speciesId = speciesId,
})
end)
if not ok then
print("Deserialization error")
local function waitLoop(inputLine)
return util.promiseWrap(function()
local ok, inputData = serpent.load(inputLine)
if not ok or inputData == nil then
io.stderr:write("Deserialization error\n")
io.stderr:write(inputLine.."\n")
return
end
end
print('Received input from master process')
print('Received input from master process')
species = inputData[1]
species = inputData[1]
speciesId = species.id
speciesId = species.id
generationIndex = inputData[2]
generationIndex = inputData[2]
outContents = {}
print('Running')
print('Running')
return 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
):next(function()
table.insert(
outContents,
serpent.dump({
end
):next(function(maxFitness)
writeResponse({
type = 'onFinish',
maxFitness = maxFitness,
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()
local waiter = nil
if util.isWin then
waiter = Promise.new()
waiter:resolve()
else
waiter = util.waitForFiles(inputFilePath)[1]
end
-- Write the result
local outFile = io.open(outputFilePath, "w")
outFile:write(table.concat(outContents, "\n"))
outFile:close()
return waiter
end)
end):next(function()
return inputPipe:read("*l")
end):next(waitLoop)
end
local waiter = nil
if util.isWin then
waiter = Promise.new()
waiter:resolve()
else
waiter = util.waitForFiles(inputFilePath)[1]
end
local sec, usec = utime()
local ts = sec * 1000000 + usec
local outFile = io.open(outputFilePath, "w")
outFile:write(serpent.dump({ type = 'onInit', ts = ts }))
outFile:close()
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(waitLoop):catch(function(error)
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)
end)

View file

@ -7,28 +7,13 @@ 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 = tmpFileName..'_input_'
local outputPrefix = tmpFileName..'_output_'
local inputPrefix = pipePrefix..'_input_'
local outputPrefix = pipePrefix..'_output_'
local function message(_M, msg, color)
if color == nil then
@ -60,6 +45,16 @@ 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
@ -69,34 +64,44 @@ end
---@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 children = {}
while #_M.poppets < count do
local i = #_M.poppets+1
local outputFileName = outputPrefix..i
local inputFileName = inputPrefix..i
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 = tempDir.."/donk_runner_settings_"..i
settingsDir = util.getTempDir().."/donk_runner_settings_"..i
util.mkdir(settingsDir)
end
local envs = {
RUNNER_INPUT_FILE = inputFileName,
RUNNER_OUTPUT_FILE = outputFileName,
RUNNER_INPUT_PIPE = inputPipeName,
RUNNER_OUTPUT_PIPE = outputPipeName,
APPDATA = settingsDir,
}
local child = util.waitForFiles(outputFileName)[1]
local cmd = '"'.._M.hostProcess..'" "--rom='..config.ROM..'" --unpause "--lua='..base..'/runner-process.lua"'
local poppet = util.popenCmd(cmd, nil, envs)
table.insert(_M.poppets, poppet)
newOne.process = util.popenCmd(cmd, nil, envs)
table.insert(children, child)
-- 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(children))
return Promise.all(table.unpack(promises))
end
return function(promise)
@ -107,13 +112,14 @@ return function(promise)
end
-- FIXME Maybe don't do this in the "constructor"?
if util.isWin then
util.downloadFile('https://github.com/watchexec/watchexec/releases/download/1.13.1/watchexec-1.13.1-x86_64-pc-windows-gnu.zip', base..'/watchexec.zip')
util.unzip(base..'/watchexec.zip', base)
os.rename(base..'watchexec-1.13.1-x86_64-pc-windows-gnu', base..'/watchexec')
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 = {},
@ -148,86 +154,109 @@ return function(promise)
onLoad(_M, handler)
end
_M.run = function(speciesSlice, generationIdx, genomeCallback)
_M.onReset = function(handler)
onReset(_M, handler)
end
_M.run = function(species, generationIdx, genomeCallback)
local promise = Promise.new()
promise:resolve()
return promise:next(function()
-- Create the input files and output files
for i=1,#speciesSlice,1 do
local inputFileName = inputPrefix..i
local inputFile = io.open(inputFileName, 'a')
inputFile:close()
local outputFileName = outputPrefix..i
local outputFile = io.open(outputFileName, 'a')
outputFile:close()
end
return launchChildren(_M, #speciesSlice)
return launchChildren(_M, config.NeatConfig.Threads)
end):next(function()
local outputFileNames = {}
for i=1,#speciesSlice,1 do
table.insert(outputFileNames, outputPrefix..i)
end
local waiters = util.waitForFiles(outputFileNames)
message(_M, 'Setting up child processes')
for i=1,#speciesSlice,1 do
local inputFileName = tmpFileName.."_input_"..i
local inputFile = io.open(inputFileName, 'w')
inputFile:write(serpent.dump({speciesSlice[i], generationIdx}))
inputFile:close()
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
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')
for i=1,#waiters,1 do
waiters[i] = waiters[i]:next(function(outputFileName)
message(_M, "Processing 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,#speciesSlice,1 do
local s = speciesSlice[i]
if s.id == obj.speciesId then
s.genomes[obj.genomeIndex] = obj.genome
break
end
end
genomeCallback(obj.genome, obj.index)
elseif obj.type == 'onFinish' then
message(_M, "Finished processing output "..i)
return
end
::continue::
line = outputFile:read()
until(line == "" or line == nil)
error("Child process "..i.." never finished")
end)
end
return Promise.all(table.unpack(waiters))
end):next(function()
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

View file

@ -1,7 +1,7 @@
local gui, input, movie, settings, exec, callback, set_timer_timeout = gui, input, movie, settings, exec, callback, set_timer_timeout
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")
@ -255,10 +255,21 @@ local function displayButtons(_M)
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
local function displayForm(_M)
if #_M.onRenderFormHandler == 0 then
if config.NeatConfig.ShowInterface == false or #_M.onRenderFormHandler == 0 then
return
end
@ -275,28 +286,30 @@ local function displayForm(_M)
gui.rectangle(0, 0, 500, guiHeight, 1, 0x00ffffff, 0xbb000000)
--gui.circle(game.screenX-84, game.screenY-84, 192 / 2, 1, 0x50000000)
local distanceTraversed = getDistanceTraversed(_M.areaInfo)
local goalX = 0
local goalY = 0
local areaInfo = _M.areaInfo[_M.currentArea]
local distanceTraversed = 0
if areaInfo ~= nil then
distanceTraversed = areaInfo.startDistance - areaInfo.shortest
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: " .. _M.totalBananas)
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, string.format("Traveled: %d", distanceTraversed))
gui.text(5, 5, string.format([[
Generation: %4d Species: %4d Genome: %4d
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()
@ -330,7 +343,6 @@ local function evaluateNetwork(_M, network, inputs, inputDeltas)
message(_M, "Incorrect number of neural network inputs.", 0x00990000)
return {}
end
for i=1,Inputs do
network.neurons[i].value = inputs[i] * inputDeltas[i]
@ -382,12 +394,9 @@ end
local frame = 0
local lastFrame = 0
local function evaluateCurrent(_M)
local function evaluateCurrent(_M, inputs, inputDeltas)
local genome = _M.currentSpecies.genomes[_M.currentGenomeIndex]
local inputDeltas = {}
local inputs, inputDeltas = game.getInputs()
controller = evaluateNetwork(_M, genome.network, inputs, inputDeltas)
if controller[6] and controller[7] then
@ -452,9 +461,9 @@ local function generateNetwork(genome)
genome.network = network
end
local rew = movie.to_rewind(config.NeatConfig.Filename)
local beginRewindState = nil
local function rewind()
return game.rewind(rew):next(function()
return game.rewind(beginRewindState):next(function()
frame = 0
lastFrame = 0
end)
@ -471,7 +480,20 @@ local function initializeRun(_M)
exec('enable-sound '..enableSound)
gui.subframe_update(false)
return rewind():next(function(preferredExit)
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
@ -500,22 +522,19 @@ local function initializeRun(_M)
_M.currentArea = game.getCurrentArea()
_M.lastArea = _M.currentArea
for areaId,areaInfo in pairs(_M.areaInfo) do
areaInfo.shortest = areaInfo.startDistance
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)
evaluateCurrent(_M)
end)
end
local function getDistanceTraversed(areaInfo)
local distanceTraversed = 0
for areaId,areaInfo in pairs(areaInfo) do
distanceTraversed = areaInfo.startDistance - areaInfo.shortest
end
return distanceTraversed
local inputs, inputDeltas = game.getInputs()
evaluateCurrent(_M, inputs, inputDeltas)
end)
end
local function mainLoop(_M, genome)
@ -524,6 +543,7 @@ local function mainLoop(_M, genome)
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
@ -531,12 +551,28 @@ local function mainLoop(_M, genome)
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 startDistance = math.floor(math.sqrt((preferredExit.y - game.partyY) ^ 2 + (preferredExit.x - game.partyX) ^ 2))
_M.areaInfo[_M.currentArea] = {
startDistance = startDistance,
local areaInfo = {
preferredExit = preferredExit,
shortest = startDistance,
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()
@ -555,34 +591,34 @@ local function mainLoop(_M, genome)
displayGenome(genome)
end
if _M.currentFrame%5 == 0 then
evaluateCurrent(_M)
end
game.getPositions()
local timeoutConst = 0
if game.vertical then
timeoutConst = config.NeatConfig.TimeoutConstant * 10
else
timeoutConst = config.NeatConfig.TimeoutConstant
local timeoutConst = config.NeatConfig.TimeoutConstant
local fell = game.fell()
if (fell or game.diedFromHit()) and _M.timeout > 0 then
_M.timeout = 0
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
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
local exitDist = math.floor(math.sqrt((areaInfo.preferredExit.y - game.partyY) ^ 2 + (areaInfo.preferredExit.x - game.partyX) ^ 2))
if exitDist < areaInfo.shortest then
areaInfo.shortest = exitDist
if _M.timeout < timeoutConst then
_M.timeout = timeoutConst
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
@ -648,7 +684,12 @@ local function mainLoop(_M, genome)
local distanceTraversed = getDistanceTraversed(_M.areaInfo) - _M.currentFrame / 2
local fitness = bananaCoinsFitness - bumpPenalty - hitPenalty + powerUpBonus + distanceTraversed + game.getJumpHeight() / 100
local fitness = bananaCoinsFitness - bumpPenalty - hitPenalty + powerUpBonus + distanceTraversed
if fell then
fitness = fitness / 10
message(_M, "Fall penalty 1/10")
end
local lives = game.getLives()
@ -658,16 +699,19 @@ local function mainLoop(_M, genome)
message(_M, "Extra live bonus added " .. extraLiveBonus)
end
if game.getGoalHit() then
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 fitness > _M.maxFitness then
if _M.maxFitness == nil or fitness > _M.maxFitness then
_M.maxFitness = fitness
end
@ -693,7 +737,7 @@ local function mainLoop(_M, genome)
input.keyhook("9", false)
input.keyhook("tab", false)
return
return _M.maxFitness
end
end
@ -732,6 +776,18 @@ 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
@ -753,9 +809,8 @@ local function keyhook (_M, key, state)
_M.helddown = key
load(_M)
elseif key == "9" then
-- FIXME Event inversion
_M.helddown = key
pool.run(true)
reset(_M)
end
elseif state.value == 0 then
_M.helddown = nil
@ -827,6 +882,9 @@ local function saveLoadInput(_M)
end
local function run(_M, species, generationIdx, genomeCallback)
if beginRewindState == nil then
beginRewindState = movie.to_rewind(config.NeatConfig.Filename)
end
game.registerHandlers()
_M.currentGenerationIndex = generationIdx
@ -877,7 +935,7 @@ return function(promise)
currentGenomeIndex = 1,
currentFrame = 0,
drawFrame = 0,
maxFitness = 0,
maxFitness = nil,
dereg = {},
inputmode = false,
@ -904,6 +962,7 @@ return function(promise)
onMessageHandler = {},
onSaveHandler = {},
onLoadHandler = {},
onResetHandler = {},
onRenderFormHandler = {},
}
@ -923,9 +982,13 @@ return function(promise)
onLoad(_M, handler)
end
_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

View file

@ -80,6 +80,7 @@ _M.BadSprites = {
klampon = 0x01f0,
flotsam = 0x01f8,
klinger = 0x0200,
klingerSkidCloud = 0x0014,
puftup = 0x0208,
zingerAllColors = 0x0218,
miniNecky = 0x0214,
@ -105,13 +106,13 @@ end
function _M.InitSpriteList()
for k,v in pairs(_M.GoodSprites) do
_M.extSprites[v] = 1
_M.Sprites[v] = 1
end
for k,v in pairs(_M.BadSprites) do
_M.extSprites[v] = -1
_M.Sprites[v] = -1
end
for k,v in pairs(_M.NeutralSprites) do
_M.extSprites[v] = 0
_M.Sprites[v] = 0
end
end

View file

@ -1,4 +1,4 @@
local memory, movie, utime, callback, set_timer_timeout = memory, movie, utime, callback, set_timer_timeout
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")
@ -9,10 +9,11 @@ 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(exit)
io.stderr:write(util.table_to_string(exit))
io.stderr:write('\n')
game.findPreferredExit():next(function(preferredExit)
game.getWaypoints(preferredExit.x, preferredExit.y)
end)

View file

@ -1,20 +1,27 @@
local base = string.gsub(@@LUA_SCRIPT_FILENAME@@, "(.*[/\\])(.*)", "%1").."/.."
local set_timer_timeout, memory, memory2, gui, input, bit = set_timer_timeout, memory, memory2, gui, input, bit
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 util = dofile(base.."/util.lua")()
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()
game.registerHandlers()
local CAMERA_MODE = 0x7e054f
local DIDDY_X_VELOCITY = 0x7e0e02
local DIDDY_Y_VELOCITY = 0x7e0e06
@ -130,44 +137,43 @@ function on_input (subframe)
end
end
local function get_sprite(base_addr)
local function get_sprite(baseAddr)
local spriteData = memory.readregion(baseAddr, mem.size.sprite)
local offsets = mem.offset.sprite
local cameraX = memory.readword(mem.addr.cameraX) - 256
local cameraY = memory.readword(mem.addr.cameraY) - 256
local x = memory.readword(base_addr + offsets.x)
local y = memory.readword(base_addr + offsets.y)
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 - mem.size.tile / 3,
control = memory.readword(base_addr + offsets.control),
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 + offsets.jumpHeight),
style = memory.readword(base_addr + offsets.style),
currentframe = memory.readword(base_addr + 0x18),
nextframe = memory.readword(base_addr + 0x1a),
state = memory.readword(base_addr + 0x1e),
velocityX = memory.readsword(base_addr + offsets.velocityX),
velocityY = memory.readsword(base_addr + offsets.velocityY),
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
@ -202,6 +208,7 @@ local function sprite_details(idx)
text(0, 0, "Sprite "..idx..(locked and " (Locked)" or "")..":\n\n"..util.table_to_string(sprite))
end
local waypoints = {}
local overlayCtx = nil
local overlay = nil
local function renderOverlay(guiWidth, guiHeight)
@ -231,6 +238,8 @@ Sprite Details:
return
end
game.getPositions()
local toggles = ""
if pokemon then
@ -254,17 +263,11 @@ Sprite Details:
"Up"
}
local cameraX = memory.readword(mem.addr.cameraX) - 256
local cameraY = memory.readword(mem.addr.cameraY) - 256
local cameraDir = memory.readbyte(CAMERA_MODE)
local direction = directions[cameraDir+1]
local vertical = memory.readword(mem.addr.tileCollisionMathPointer) == mem.addr.verticalPointer
local partyX = memory.readword(mem.addr.partyX)
local partyY = memory.readword(mem.addr.partyY)
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
@ -273,11 +276,11 @@ Tile offset: %04x
Main area: %04x
Current area: %04x
%s
]], direction, cameraX, cameraY, 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"))
]], 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
@ -310,20 +313,45 @@ Current area: %04x
::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 / mem.size.tile)
local cameraTileX = math.floor(game.cameraX / mem.size.tile)
gui.line(0, halfHeight, guiWidth, halfHeight, BG_COLOR)
for i = cameraTileX, cameraTileX + guiWidth / mem.size.tile / 2,1 do
text((i * mem.size.tile - cameraX) * 2, halfHeight, tostring(i), FG_COLOR, BG_COLOR)
text((i * mem.size.tile - game.cameraX) * 2, halfHeight, tostring(i), FG_COLOR, BG_COLOR)
end
local cameraTileY = math.floor(cameraY / mem.size.tile)
local cameraTileY = math.floor(game.cameraY / mem.size.tile)
gui.line(halfWidth, 0, halfWidth, guiHeight, BG_COLOR)
for i = cameraTileY, cameraTileY + guiHeight / mem.size.tile / 2,1 do
text(halfWidth, (i * mem.size.tile - cameraY) * 2, tostring(i), FG_COLOR, BG_COLOR)
text(halfWidth, (i * mem.size.tile - game.cameraY) * 2, tostring(i), FG_COLOR, BG_COLOR)
end
end
@ -331,10 +359,10 @@ Current area: %04x
for x = -TILE_RADIUS, TILE_RADIUS, 1 do
for y = -TILE_RADIUS, TILE_RADIUS, 1 do
local tileX = math.floor((partyX + x * mem.size.tile) / mem.size.tile) * mem.size.tile
local tileY = math.floor((partyY + y * mem.size.tile) / mem.size.tile) * mem.size.tile
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)
@ -342,8 +370,8 @@ Current area: %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
@ -355,7 +383,7 @@ Current area: %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
@ -420,10 +448,6 @@ function on_paint (not_synth)
end
end
function on_timer()
set_timer_timeout(100 * 1000)
end
input.keyhook("1", true)
input.keyhook("2", true)
input.keyhook("3", true)
@ -435,13 +459,11 @@ input.keyhook("8", true)
input.keyhook("9", true)
input.keyhook("0", true)
set_timer_timeout(100 * 1000)
for i=0,22,1 do
memory2.BUS:registerwrite(mem.addr.spriteBase + mem.size.sprite * i + mem.offset.sprite.x, function(addr, val)
print(memory.getregister('pc'))
end)
end
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

266
util.lua
View file

@ -1,4 +1,4 @@
local utime, bit = utime, bit
local utime, bit, callback, exec = utime, bit, callback, exec
local base = string.gsub(@@LUA_SCRIPT_FILENAME@@, "(.*[/\\])(.*)", "%1")
@ -8,6 +8,143 @@ 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
@ -48,6 +185,29 @@ 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
@ -59,9 +219,8 @@ end
--- @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('unzip "'..zipfile..'" -d "'..dest..
'" 2>&1 || tar -C "'..dest..'" -xvf "'..zipfile..
'" 2>&1', nil)
return _M.doCmd('tar -xvf "'..zipfile..'" 2>&1 || unzip -n "'..zipfile..'" -d "'..dest..
'" 2>&1', dest)
end
--- Create a directory
@ -100,92 +259,7 @@ function _M.closeCmd(handle)
return
end
if code ~= 0 then
error("The last command failed")
end
end
function _M.waitForFiles(filenames)
if type(filenames) == 'string' then
filenames = {filenames}
end
local poppet = nil
if _M.isWin then
local sec, usec = utime()
print(string.format('Starting watching file at %d', sec * 1000000 + usec))
local cmd = '"'..base..'/watchexec/watchexec.exe" "-w" "'..table.concat(filenames, '" "-w" "')..'" "echo" "%WATCHEXEC_WRITTEN_PATH%"'
poppet = _M.popenCmd(cmd, base)
poppet:read("*l")
local waiters = {}
for i=1,#filenames,1 do
local waiter = Promise.new()
table.insert(waiters, waiter)
end
-- To defer the check of the files
local promise = Promise.new()
promise:resolve()
promise:next(function()
local i = 1
while i <= filenames do
local line = poppet:read("*l")
for chr in line:gmatch(";") do
i = i + 1
end
i = i + 1
end
-- FIXME synchronous
for i=1,#filenames,1 do
waiters[i]:resolve(filenames[i])
end
end):catch(function(reason)
for i=1,#filenames,1 do
waiters[i]:reject(reason)
end
end)
return waiters
else
local watchCmd = [[bash ]]..base..[[/watch.sh ']]..table.concat(filenames, [[' ']])..[[']]
poppet = _M.popenCmd(watchCmd)
local waiters = {}
for i=1,#filenames,1 do
local waiter = Promise.new()
table.insert(waiters, waiter)
end
local finished = 0
local function waitLoop()
local promise = Promise.new()
promise:resolve()
return promise:next(function()
local line = poppet:read("*l")
finished = finished + 1
local filename = line:gsub('%s+[^%s]+$', "")
for i=1,#filenames,1 do
if filename == filenames[i] then
waiters[i]:resolve(filenames[i])
break
end
end
if finished ~= #filenames then
return waitLoop()
end
end)
end
waitLoop():catch(function(reason)
for i=1,#waiters,1 do
waiters:reject(reason)
end
end)
return waiters
error(string.format("The last command failed: %s %d", state, code))
end
end
@ -263,6 +337,18 @@ 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

View file

@ -1,21 +0,0 @@
#! /bin/bash
FILENAMES=("$@")
declare -A SEEN
((I = 0))
set -m
which inotifywait >/dev/null
function checker {
inotifywait -q -m -e close_write "${FILENAMES[@]}" | while read LINE ; do
if ! [ ${SEEN["$LINE"]+y} ] ; then
SEEN["$LINE"]=1
echo "$LINE"
fi ;
TOTAL=${#SEEN[@]}
COUNT=$#
if ((TOTAL == COUNT)) ; then
kill -s TERM 0
fi
done
}
checker &
wait

193
zzlib.lua Normal file
View 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