diff --git a/.gitignore b/.gitignore index b576be257a..e5eb3acc54 100644 --- a/.gitignore +++ b/.gitignore @@ -7,4 +7,6 @@ result chain*.json *-node-*-deku_state_*.tar.gz tezos_js_bridge.bundle.js -flextesa_chain/data +flextesa_chain +target +logs_* diff --git a/Cargo.lock b/Cargo.lock new file mode 100644 index 0000000000..79a54b10c0 --- /dev/null +++ b/Cargo.lock @@ -0,0 +1,682 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 3 + +[[package]] +name = "adler" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f26201604c87b1e01bd3d98f8d5d9a8fcbb815e8cedb41ffccbeb4bf593a35fe" + +[[package]] +name = "aho-corasick" +version = "0.7.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e37cfd5e7657ada45f742d6e99ca5788580b5c529dc78faf11ece6dc702656f" +dependencies = [ + "memchr", +] + +[[package]] +name = "alsa-sys" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b0edcbbf9ef68f15ae1b620f722180b82a98b6f0628d30baa6b8d2a5abc87d58" +dependencies = [ + "libc", + "pkg-config", +] + +[[package]] +name = "argparse" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f8ebf5827e4ac4fd5946560e6a99776ea73b596d80898f357007317a7141e47" + +[[package]] +name = "autocfg" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cdb031dd78e28731d87d56cc8ffef4a8f36ca26c38fe2de700543e627f8a464a" + +[[package]] +name = "bindgen" +version = "0.56.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2da379dbebc0b76ef63ca68d8fc6e71c0f13e59432e0987e508c1820e6ab5239" +dependencies = [ + "bitflags", + "cexpr", + "clang-sys", + "lazy_static 1.4.0", + "lazycell", + "peeking_take_while", + "proc-macro2", + "quote", + "regex", + "rustc-hash", + "shlex", +] + +[[package]] +name = "bitflags" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cf1de2fe8c75bc145a2f577add951f8134889b4795d47466a54a5c846d691693" + +[[package]] +name = "blip_buf" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "45c136cf50e8d0c47fe415008a6be8b50a9a56a6741fffbe2e580e007eda417d" +dependencies = [ + "blip_buf-sys", + "libc", +] + +[[package]] +name = "blip_buf-sys" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3b84654178cd24117082f5cb10dbc0580425fc06aa02ae209171fe070cd4522e" +dependencies = [ + "cc", + "libc", +] + +[[package]] +name = "bumpalo" +version = "3.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9c59e7af012c713f529e7a3ee57ce9b31ddd858d4b512923602f74608b009631" + +[[package]] +name = "cc" +version = "1.0.68" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4a72c244c1ff497a746a7e1fb3d14bd08420ecda70c8f25c7112f2781652d787" + +[[package]] +name = "cexpr" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f4aedb84272dbe89af497cf81375129abda4fc0a9e7c5d317498c15cc30c0d27" +dependencies = [ + "nom", +] + +[[package]] +name = "cfg-if" +version = "0.1.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4785bdd1c96b2a846b2bd7cc02e86b6b3dbf14e7e53446c4f54c92a361040822" + +[[package]] +name = "cfg-if" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" + +[[package]] +name = "clang-sys" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "853eda514c284c2287f4bf20ae614f8781f40a81d32ecda6e91449304dfe077c" +dependencies = [ + "glob", + "libc", + "libloading", +] + +[[package]] +name = "cmake" +version = "0.1.45" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eb6210b637171dfba4cda12e579ac6dc73f5165ad56133e5d72ef3131f320855" +dependencies = [ + "cc", +] + +[[package]] +name = "core-foundation-sys" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "716c271e8613ace48344f723b60b900a93150271e5be206212d052bbc0883efa" +dependencies = [ + "libc", +] + +[[package]] +name = "coreaudio-rs" +version = "0.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f229761965dad3e9b11081668a6ea00f1def7aa46062321b5ec245b834f6e491" +dependencies = [ + "bitflags", + "coreaudio-sys", +] + +[[package]] +name = "coreaudio-sys" +version = "0.2.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2b7e3347be6a09b46aba228d6608386739fb70beff4f61e07422da87b0bb31fa" +dependencies = [ + "bindgen", +] + +[[package]] +name = "cpal" +version = "0.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d58ae1ed6536b1b233f5e3aeb6997a046ddb4d05e3f61701b58a92eb254a829e" +dependencies = [ + "alsa-sys", + "core-foundation-sys", + "coreaudio-rs", + "lazy_static 1.4.0", + "libc", + "stdweb", + "winapi 0.3.9", +] + +[[package]] +name = "crc32fast" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "81156fece84ab6a9f2afdb109ce3ae577e42b1228441eded99bd77f627953b1a" +dependencies = [ + "cfg-if 1.0.0", +] + +[[package]] +name = "encoding_rs" +version = "0.8.28" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "80df024fbc5ac80f87dfef0d9f5209a252f2a497f7f42944cff24d8253cac065" +dependencies = [ + "cfg-if 1.0.0", +] + +[[package]] +name = "filetime" +version = "0.2.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1d34cfa13a63ae058bfa601fe9e313bbdb3746427c1459185464ce0fcf62e1e8" +dependencies = [ + "cfg-if 1.0.0", + "libc", + "redox_syscall", + "winapi 0.3.9", +] + +[[package]] +name = "flate2" +version = "1.0.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cd3aec53de10fe96d7d8c565eb17f2c687bb5518a2ec453b5b1252964526abe0" +dependencies = [ + "cfg-if 1.0.0", + "crc32fast", + "libc", + "miniz_oxide", +] + +[[package]] +name = "gameboy" +version = "0.1.0" +dependencies = [ + "argparse", + "blip_buf", + "cpal", + "minifb", + "rog", +] + +[[package]] +name = "gdi32-sys" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e3eb92c1107527888f86b6ebb0b7f82794777dbf172a932998660a0a2e26c11" +dependencies = [ + "winapi 0.2.8", + "winapi-build", +] + +[[package]] +name = "glob" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b919933a397b79c37e33b77bb2aa3dc8eb6e165ad809e58ff75bc7db2e34574" + +[[package]] +name = "js-sys" +version = "0.3.51" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "83bdfbace3a0e81a4253f73b49e960b053e396a11012cbd49b9b74d6a2b67062" +dependencies = [ + "wasm-bindgen", +] + +[[package]] +name = "kernel32-sys" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7507624b29483431c0ba2d82aece8ca6cdba9382bff4ddd0f7490560c056098d" +dependencies = [ + "winapi 0.2.8", + "winapi-build", +] + +[[package]] +name = "lazy_static" +version = "0.2.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "76f033c7ad61445c5b347c7382dd1237847eb1bce590fe50365dcb33d546be73" + +[[package]] +name = "lazy_static" +version = "1.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646" + +[[package]] +name = "lazycell" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "830d08ce1d1d941e6b30645f1a0eb5643013d835ce3779a5fc208261dbe10f55" + +[[package]] +name = "libc" +version = "0.2.97" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "12b8adadd720df158f4d70dfe7ccc6adb0472d7c55ca83445f6a5ab3e36f8fb6" + +[[package]] +name = "libloading" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6f84d96438c15fcd6c3f244c8fce01d1e2b9c6b5623e9c711dc9286d8fc92d6a" +dependencies = [ + "cfg-if 1.0.0", + "winapi 0.3.9", +] + +[[package]] +name = "log" +version = "0.4.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "51b9bbe6c47d51fc3e1a9b945965946b4c44142ab8792c50835a980d362c2710" +dependencies = [ + "cfg-if 1.0.0", +] + +[[package]] +name = "memchr" +version = "2.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b16bd47d9e329435e309c58469fe0791c2d0d1ba96ec0954152a5ae2b04387dc" + +[[package]] +name = "minifb" +version = "0.11.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85c2cedede43aad485232acf318a3e191ee5a3c2250ca8a3556b849a48e8b901" +dependencies = [ + "cc", + "gdi32-sys", + "kernel32-sys", + "orbclient", + "time", + "user32-sys", + "winapi 0.2.8", + "x11-dl", +] + +[[package]] +name = "miniz_oxide" +version = "0.4.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a92518e98c078586bc6c934028adcca4c92a53d6a958196de835170a01d84e4b" +dependencies = [ + "adler", + "autocfg", +] + +[[package]] +name = "nom" +version = "5.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ffb4262d26ed83a1c0a33a38fe2bb15797329c85770da05e6b828ddb782627af" +dependencies = [ + "memchr", + "version_check", +] + +[[package]] +name = "orbclient" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c976c5018e7f1db4359616d8b31ef8ae7d9649b11803c0b38fff67fd2999fc8" +dependencies = [ + "libc", + "raw-window-handle", + "redox_syscall", + "sdl2", + "sdl2-sys", + "wasm-bindgen", + "web-sys", +] + +[[package]] +name = "peeking_take_while" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "19b17cddbe7ec3f8bc800887bab5e717348c95ea2ca0b1bf0837fb964dc67099" + +[[package]] +name = "pkg-config" +version = "0.3.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3831453b3449ceb48b6d9c7ad7c96d5ea673e9b470a1dc578c2ce6521230884c" + +[[package]] +name = "proc-macro2" +version = "1.0.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f0d8caf72986c1a598726adc988bb5984792ef84f5ee5aa50209145ee8077038" +dependencies = [ + "unicode-xid", +] + +[[package]] +name = "quote" +version = "1.0.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c3d0b9745dc2debf507c8422de05d7226cc1f0644216dfdfead988f9b1ab32a7" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "raw-window-handle" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0a441a7a6c80ad6473bd4b74ec1c9a4c951794285bf941c2126f607c72e48211" +dependencies = [ + "libc", +] + +[[package]] +name = "redox_syscall" +version = "0.2.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5ab49abadf3f9e1c4bc499e8845e152ad87d2ad2d30371841171169e9d75feee" +dependencies = [ + "bitflags", +] + +[[package]] +name = "regex" +version = "1.5.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d07a8629359eb56f1e2fb1652bb04212c072a87ba68546a04065d525673ac461" +dependencies = [ + "aho-corasick", + "memchr", + "regex-syntax", +] + +[[package]] +name = "regex-syntax" +version = "0.6.25" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f497285884f3fcff424ffc933e56d7cbca511def0c9831a7f9b5f6153e3cc89b" + +[[package]] +name = "rog" +version = "0.1.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a1317f3f2e394a9fcffa0e65d690f4c0311779a2b87888a78b669207f89e75d6" + +[[package]] +name = "rustc-hash" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "08d43f7aa6b08d49f382cde6a7982047c3426db949b1424bc4b7ec9ae12c6ce2" + +[[package]] +name = "sdl2" +version = "0.34.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "deecbc3fa9460acff5a1e563e05cb5f31bba0aa0c214bb49a43db8159176d54b" +dependencies = [ + "bitflags", + "lazy_static 1.4.0", + "libc", + "raw-window-handle", + "sdl2-sys", +] + +[[package]] +name = "sdl2-sys" +version = "0.34.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "41a29aa21f175b5a41a6e26da572d5e5d1ee5660d35f9f9d0913e8a802098f74" +dependencies = [ + "cfg-if 0.1.10", + "cmake", + "flate2", + "libc", + "tar", + "unidiff", + "version-compare", +] + +[[package]] +name = "shlex" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7fdf1b9db47230893d76faad238fd6097fd6d6a9245cd7a4d90dbd639536bbd2" + +[[package]] +name = "stdweb" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ef5430c8e36b713e13b48a9f709cc21e046723fe44ce34587b73a830203b533e" + +[[package]] +name = "syn" +version = "1.0.73" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f71489ff30030d2ae598524f61326b902466f72a0fb1a8564c001cc63425bcc7" +dependencies = [ + "proc-macro2", + "quote", + "unicode-xid", +] + +[[package]] +name = "tar" +version = "0.4.35" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7d779dc6aeff029314570f666ec83f19df7280bb36ef338442cfa8c604021b80" +dependencies = [ + "filetime", + "libc", + "xattr", +] + +[[package]] +name = "time" +version = "0.1.44" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6db9e6914ab8b1ae1c260a4ae7a49b6c5611b40328a735b21862567685e73255" +dependencies = [ + "libc", + "wasi", + "winapi 0.3.9", +] + +[[package]] +name = "unicode-xid" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ccb82d61f80a663efe1f787a51b16b5a51e3314d6ac365b08639f52387b33f3" + +[[package]] +name = "unidiff" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d8a62719acf1933bfdbeb73a657ecd9ecece70b405125267dd549e2e2edc232c" +dependencies = [ + "encoding_rs", + "lazy_static 1.4.0", + "regex", +] + +[[package]] +name = "user32-sys" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6b719983b952c04198829b51653c06af36f0e44c967fcc1a2bb397ceafbf80a" +dependencies = [ + "winapi 0.2.8", + "winapi-build", +] + +[[package]] +name = "version-compare" +version = "0.0.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d63556a25bae6ea31b52e640d7c41d1ab27faba4ccb600013837a3d0b3994ca1" + +[[package]] +name = "version_check" +version = "0.9.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5fecdca9a5291cc2b8dcf7dc02453fee791a280f3743cb0905f8822ae463b3fe" + +[[package]] +name = "wasi" +version = "0.10.0+wasi-snapshot-preview1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1a143597ca7c7793eff794def352d41792a93c481eb1042423ff7ff72ba2c31f" + +[[package]] +name = "wasm-bindgen" +version = "0.2.74" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d54ee1d4ed486f78874278e63e4069fc1ab9f6a18ca492076ffb90c5eb2997fd" +dependencies = [ + "cfg-if 1.0.0", + "wasm-bindgen-macro", +] + +[[package]] +name = "wasm-bindgen-backend" +version = "0.2.74" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3b33f6a0694ccfea53d94db8b2ed1c3a8a4c86dd936b13b9f0a15ec4a451b900" +dependencies = [ + "bumpalo", + "lazy_static 1.4.0", + "log", + "proc-macro2", + "quote", + "syn", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-macro" +version = "0.2.74" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "088169ca61430fe1e58b8096c24975251700e7b1f6fd91cc9d59b04fb9b18bd4" +dependencies = [ + "quote", + "wasm-bindgen-macro-support", +] + +[[package]] +name = "wasm-bindgen-macro-support" +version = "0.2.74" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "be2241542ff3d9f241f5e2cb6dd09b37efe786df8851c54957683a49f0987a97" +dependencies = [ + "proc-macro2", + "quote", + "syn", + "wasm-bindgen-backend", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-shared" +version = "0.2.74" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d7cff876b8f18eed75a66cf49b65e7f967cb354a7aa16003fb55dbfd25b44b4f" + +[[package]] +name = "web-sys" +version = "0.3.51" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e828417b379f3df7111d3a2a9e5753706cae29c41f7c4029ee9fd77f3e09e582" +dependencies = [ + "js-sys", + "wasm-bindgen", +] + +[[package]] +name = "winapi" +version = "0.2.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "167dc9d6949a9b857f3451275e911c3f44255842c1f7a76f33c55103a909087a" + +[[package]] +name = "winapi" +version = "0.3.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419" +dependencies = [ + "winapi-i686-pc-windows-gnu", + "winapi-x86_64-pc-windows-gnu", +] + +[[package]] +name = "winapi-build" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2d315eee3b34aca4797b2da6b13ed88266e6d612562a0c46390af8299fc699bc" + +[[package]] +name = "winapi-i686-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" + +[[package]] +name = "winapi-x86_64-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" + +[[package]] +name = "x11-dl" +version = "2.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "326c500cdc166fd7c70dd8c8a829cd5c0ce7be5a5d98c25817de2b9bdc67faf8" +dependencies = [ + "lazy_static 0.2.11", + "libc", + "pkg-config", +] + +[[package]] +name = "xattr" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "244c3741f4240ef46274860397c7c74e50eb23624996930e484c16679633a54c" +dependencies = [ + "libc", +] diff --git a/Cargo.toml b/Cargo.toml new file mode 100644 index 0000000000..7fcc316298 --- /dev/null +++ b/Cargo.toml @@ -0,0 +1,17 @@ +[package] +name = "gameboy" +version = "0.1.0" +authors = ["mohanson "] +edition = "2018" + +[features] +default = ["gui", "audio"] +gui = ["minifb", "cpal"] +audio = ["cpal"] + +[dependencies] +argparse = "0.2" +blip_buf = "0.1" +cpal = { version = "0.8", optional = true } +minifb = { version = "0.11", optional = true } +rog = "=0.1.8" diff --git a/deku-c/client/src/deku-c/contract.ts b/deku-c/client/src/deku-c/contract.ts deleted file mode 100644 index 7e4e7de686..0000000000 --- a/deku-c/client/src/deku-c/contract.ts +++ /dev/null @@ -1,225 +0,0 @@ -import { assert } from "console"; -import { DekuCClient } from "."; -import { DekuPClient } from "../deku-p/index"; -import * as LigoRpc from "./ligoRpc"; -import { JSONType } from "./utils"; - -const parseContractState = (json: JSONType): JSONType => { - if (json === null) return null; - if (!Array.isArray(json)) return null; - const type = json[0] as string; - switch (type) { - case "Int": - const value = json[1] as string; - return Number.parseInt(value); - case "String": { - const value = json[1] as string; - return value; - } - case "Map": { - const mapValues = json[1]; - if (mapValues === null) return null; - if (!Array.isArray(mapValues)) return null; - return mapValues.reduce((acc: { [key: string]: JSONType }, entry) => { - if (!Array.isArray(entry)) return acc; - const key = parseContractState(entry[0]) as string; // It should always be a string - const value = parseContractState(entry[1]); - return { [key]: value, ...acc }; - }, {}); - } - case "Pair": { - const value = json[1] as Array; - const first = value[0] as JSONType; - const second = value[1] as JSONType; - return [parseContractState(first), parseContractState(second)]; - } - case "List": { - const first = json[1] as Array; - return first.map((json) => parseContractState(json)); - } - case "Set": { - const first = json[1] as Array; - return first.map((json) => parseContractState(json)); - } - case "Union": { - const first = json[1] as Array; - const type = first[0] as string; - const value = first[1] as JSONType; - switch (type) { - case "Right": - return { right: parseContractState(value) }; - case "Left": - return { left: parseContractState(value) }; - default: { - return null; // TODO: remove this default case which is not possible - } - } - } - case "Option": { - const first = json[1] as Array; - const type = first[0] as string; - const value = first[1] as JSONType; - switch (type) { - case "None": - return { none: true }; - case "Some": - return { none: false, some: parseContractState(value) }; - default: - return null; // TODO: remove this default case which is not possible - } - } - case "Unit": { - return null; - } - default: - console.error(`type ${type} is not yet implemented`); - return null; - } -}; - -const NO_LIGO_RPC = - "You must initialize the DekuCClient with a ligo rpc URL to use this method."; - -export class Contract { - private deku: DekuCClient; - private address: string; - private fetchInterval: NodeJS.Timer | null; - private code?: { source: string; kind: LigoRpc.SupportedLang }; - - constructor( - params: { - deku: DekuCClient; - contractAddress: string; - } & ({ source: string; kind: LigoRpc.SupportedLang } | {}) - ) { - this.deku = params.deku; - this.address = params.contractAddress; - this.fetchInterval = null; - this.code = - "source" in params - ? { source: params.source, kind: params.kind } - : undefined; - } - - /** - * Invoke a deku-c smart contrat with a tunac-provided expression - * @param parameter the parameter of the contract as provided by tunac - * @returns the hash of the operation - */ - async invokeRaw(parameter: any): Promise { - const operation = { - address: this.address, - argument: parameter, - }; - const hash = await this.deku.submitVmOperation(operation, []); - return hash; - } - - async invokeMichelson(expression: string): Promise { - if (this.deku.ligoRpc) { - const { operation, tickets } = await LigoRpc.invoke(this.deku.ligoRpc, { - kind: "michelson", - expression, - address: this.address, - }); - const hash = await this.deku.submitVmOperation(operation, tickets); - return hash; - } else { - throw new Error(NO_LIGO_RPC); - } - } - - /** - * Compiles a Ligo argument and invokes a deku-c smart contract - * @param parameter the parameter of the contract, in Ligo // FIXME lang - * @returns the hash of the operation - */ - async invokeLigo(expression: string): Promise { - switch (true) { - case !this.deku.ligoRpc: - throw new Error(NO_LIGO_RPC); - case !this.code: - throw new Error( - "You must initialize the Contract class with Ligo source code to use the Ligo functionality" - ); - case this.code!.kind == "michelson": - throw new Error( - "Can't use Ligo functionality when the provided source code is Michelson. Did you mean to use `invoke` instead of `invokeLigo`?" - ); - default: - const { operation, tickets } = await LigoRpc.invoke( - this.deku.ligoRpc!, - { - source: this.code!.source, - kind: this.code!.kind, - expression, - address: this.address, - } - ); - const hash = await this.deku.submitVmOperation(operation, tickets); - return hash; - } - } - - /** - * Returns the data of the contract as a wasm-vm object - * @returns an object - */ - async getRawInfos(): Promise<{ [key: string]: JSONType } | null> { - const response: { [key: string]: JSONType } = - (await this.deku.getVmState()) as { [key: string]: JSONType }; - if (response === null) return null; - const state = response[this.address]; - if (state === null) return null; - return state as { [key: string]: JSONType }; - } - - /** - * Returns the state of the contract as a wasm-vm state object - * @returns an object representing the state of the contract - */ - async getRawState(): Promise { - const json = await this.getRawInfos(); - if (json === null) return null; - return json["state"]; - } - - /** - * Returns the state of the contract - * Parses it to a readable javascript object - * @returns javascript object - */ - async getState(): Promise { - const state = await this.getRawState(); - if (state === null) return null; - return parseContractState(state); - } - - /** - * Returns the entrypoints of the contract - * @returns javascript object - */ - async getEntrypoints(): Promise { - const json = await this.getRawInfos(); - if (json === null) return null; - return json["entrypoints"]; - } - - async onNewState(callback: (state: JSONType) => void): Promise { - // pull strategy - let previous: JSONType = null; - if (this.fetchInterval) clearInterval(this.fetchInterval); - this.fetchInterval = setInterval(() => { - this.getState() - .then((state) => { - const previousState = JSON.stringify(previous); - const nextState = JSON.stringify(state); - if (nextState === previousState) return null; - callback(state); - previous = state; - return null; - }) - .catch(console.error); - }, 2000); - } -} diff --git a/deku-c/client/src/deku-c/default-parameters.ts b/deku-c/client/src/deku-c/default-parameters.ts deleted file mode 100644 index 29a09fdc81..0000000000 --- a/deku-c/client/src/deku-c/default-parameters.ts +++ /dev/null @@ -1,2 +0,0 @@ -export const DEKU_API_URL = "https://deku-canonical-vm0.deku-v1.marigold.dev/"; -export const LIGO_DEKU_RPC_URL = "https://ligo-deku-rpc.marigold.dev"; diff --git a/deku-c/client/src/deku-c/index.ts b/deku-c/client/src/deku-c/index.ts deleted file mode 100644 index 628e41d9a9..0000000000 --- a/deku-c/client/src/deku-c/index.ts +++ /dev/null @@ -1,96 +0,0 @@ -import { DekuPClient } from "../deku-p"; -import { Contract } from "./contract"; -import { DekuSigner } from "../deku-p/utils/signers"; -import { operationHashToContractAddress, isDefined } from "./utils"; -import { DEKU_API_URL, LIGO_DEKU_RPC_URL } from "./default-parameters"; -import * as LigoRpc from "./ligoRpc"; - -export type Settings = { - dekuRpc?: string; - ligoRpc?: string; - dekuSigner?: DekuSigner; -}; - -export class DekuCClient extends DekuPClient { - readonly ligoRpc: string; - - constructor(settings: Settings) { - super({ ...settings, dekuRpc: settings.dekuRpc ?? DEKU_API_URL }); - this.ligoRpc = settings.ligoRpc ?? LIGO_DEKU_RPC_URL; - } - - assertHasSigner(): DekuSigner { - if (!isDefined(this._dekuSigner)) { - throw new Error("Tezos wallet required"); - } - return this._dekuSigner; - } - - /** - * Originate a contract on deku-c from a Ligo source code - * @param <{kind, code, storage}> the kind can be "jsligo" or "mligo", the code in the associated kind and its intialStorage - * @returns the address of the contract - */ - async originateLigo({ - kind, - source, - initialStorage, - }: { - kind: LigoRpc.LigoSyntax; - source: string; - initialStorage: string; - }): Promise<{ operation: string; address: string }> { - this.assertHasSigner(); - - const { operation, tickets } = await LigoRpc.originate(this.ligoRpc, { - kind, - source, - initialStorage, - target: "wasm", - }); - const hash = await this.submitVmOperation(operation, tickets); - const address = await operationHashToContractAddress(this.dekuRpc, hash); - - return { operation: hash, address }; - } - - /** - * Originate a contract on deku-c from a Michelson source code - * @param <{code, storage}> the code in Michelson and its intialStorage - * @returns the address of the contract - */ - async originateTz({ - source, - initialStorage, - }: { - source: string; - initialStorage: string; - }): Promise<{ operation: string; address: string }> { - this.assertHasSigner(); - - const { operation, tickets } = await LigoRpc.originate(this.ligoRpc, { - kind: "michelson", - source, - initialStorage, - target: "wasm", - }); - const hash = await this.submitVmOperation(operation, tickets); - const address = await operationHashToContractAddress(this.dekuRpc, hash); - - return { operation: hash, address }; - } - - /** - * Returns the contract associated to the given address - * @param contractAddress address of the contract / the hash of the origination operation - * @returns the contract associated to the given contract address - */ - contract( - contractAddress: string, - code?: { source: string; kind: LigoRpc.SupportedLang } - ): Contract { - return new Contract({ deku: this, contractAddress, ...code }); - } -} - -export { Contract }; diff --git a/deku-c/client/src/deku-c/ligoRpc.ts b/deku-c/client/src/deku-c/ligoRpc.ts deleted file mode 100644 index 06dda70567..0000000000 --- a/deku-c/client/src/deku-c/ligoRpc.ts +++ /dev/null @@ -1,101 +0,0 @@ -import { JSONType, urlJoin } from "./utils"; - -export type LigoSyntax = "jsligo" | "mligo" | "ligo"; -export type SupportedLang = LigoSyntax | "michelson"; -export type CompilationTarget = "wasm" | "michelson"; - -export function isValidLang(s: string): s is SupportedLang { - const languages = new Set(["jsligo", "mligo", "ligo", "michelson"]); - return languages.has(s); -} - -export function isLigo(s: string): s is LigoSyntax { - const languages = new Set(["jsligo", "mligo", "ligo"]); - return languages.has(s); -} - -type OriginateParams = - | { - kind: LigoSyntax; - source: string; - initialStorage: string; - target: "wasm" | "michelson"; - } - | { - kind: "michelson"; - source: string; - initialStorage: string; - target: "wasm"; - }; - -export const originate = async ( - ligoRpc: string, - { kind, source, initialStorage, target = "wasm" }: OriginateParams -): Promise<{ - operation: JSONType; - tickets: []; // TODO: eventually we need to support tickets here -}> => { - const params = { - lang: kind, - source, - storage: initialStorage, - }; - const options = { - method: "POST", - type: "appliction/json", - body: JSON.stringify(params), - }; - const compilationQueryString = - target === "michelson" ? "?target=michelson" : ""; - const url = urlJoin( - ligoRpc, - "/api/v1/compile-contract" + compilationQueryString - ); - const result = await fetch(url, options); - if (result.status > 200) { - // TODO: extract a better error message from the result here? - throw new Error(result.statusText); - } else { - return result.json(); - } -}; - -type invokeParams = - | { - source: string; - kind: "jsligo" | "mligo" | "ligo"; - expression: string; - address: string; - target?: "wasm" | "michelson"; - } - | { - kind: "michelson"; - expression: string; - address: string; - target?: "wasm"; - }; - -export const invoke = async (ligoRpc: string, params: invokeParams) => { - const dekuOptions = { - method: "POST", - body: JSON.stringify({ - source: "source" in params ? params.source : "", - lang: params.kind, - expression: params.expression, - address: params.address, - }), - }; - const compilationQueryString = - // empty query parameters default to wasm - params.target === "michelson" ? "?target=michelson" : ""; - const dekuRes = await fetch( - ligoRpc + "/api/v1/compile-invocation" + compilationQueryString, - dekuOptions - ); - if (dekuRes.status > 200) { - const errorText = await dekuRes.text(); - throw new Error(errorText); - } else { - return dekuRes.json(); - } -}; diff --git a/deku-c/client/src/deku-c/utils.ts b/deku-c/client/src/deku-c/utils.ts deleted file mode 100644 index 9b1e4f906f..0000000000 --- a/deku-c/client/src/deku-c/utils.ts +++ /dev/null @@ -1,113 +0,0 @@ -export type JSONType = - | string - | number - | boolean - | { [x: string]: JSONType } - | Array - | null; - -export function urlJoin(...args: string[]) { - // Adapted from https://github.com/jfromaniello/url-join - // MIT License - - // Copyright (c) 2015 José F. Romaniello - - // 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. - function normalize(strArray: string[]) { - const resultArray = []; - if (strArray.length === 0) { - return ""; - } - - if (typeof strArray[0] !== "string") { - throw new TypeError("Url must be a string. Received " + strArray[0]); - } - - // If the first part is a plain protocol, we combine it with the next part. - if (strArray[0].match(/^[^/:]+:\/*$/) && strArray.length > 1) { - strArray[0] = strArray.shift() + strArray[0]; - } - - // There must be two or three slashes in the file protocol, two slashes in anything else. - if (strArray[0].match(/^file:\/\/\//)) { - strArray[0] = strArray[0].replace(/^([^/:]+):\/*/, "$1:///"); - } else { - strArray[0] = strArray[0].replace(/^([^/:]+):\/*/, "$1://"); - } - - for (let i = 0; i < strArray.length; i++) { - let component = strArray[i]; - - if (typeof component !== "string") { - throw new TypeError("Url must be a string. Received " + component); - } - - if (component === "") { - continue; - } - - if (i > 0) { - // Removing the starting slashes for each component but the first. - component = component.replace(/^[\/]+/, ""); - } - if (i < strArray.length - 1) { - // Removing the ending slashes for each component but the last. - component = component.replace(/[\/]+$/, ""); - } else { - // For the last component we will combine multiple slashes to a single one. - component = component.replace(/[\/]+$/, "/"); - } - - resultArray.push(component); - } - - let str = resultArray.join("/"); - // Each input component is now separated by a single slash except the possible first plain protocol part. - - // remove trailing slash before parameters or hash - str = str.replace(/\/(\?|&|#[^!])/g, "$1"); - - // replace ? in parameters with & - const parts = str.split("?"); - str = parts.shift() + (parts.length > 0 ? "?" : "") + parts.join("&"); - - return str; - } - - const parts = Array.from(Array.isArray(args[0]) ? args[0] : args); - return normalize(parts); -} - -export const operationHashToContractAddress = async ( - dekuRpc: string, - hash: string -): Promise => { - const body = { hash }; - const response = await fetch( - urlJoin(dekuRpc, "/api/v1/helpers/compute-contract-hash"), - { method: "POST", body: JSON.stringify(body) } - ); - const json = await response.json(); - if (response.ok) return json.address; - throw json; -}; - -export function isDefined(val: T | undefined | null): val is T { - return val !== undefined && val !== null; -} diff --git a/deku-c/client/src/deku-p/core/operation-encoding.ts b/deku-c/client/src/deku-p/core/operation-encoding.ts index 3153f23d7a..be18c0a6d9 100644 --- a/deku-c/client/src/deku-p/core/operation-encoding.ts +++ b/deku-c/client/src/deku-p/core/operation-encoding.ts @@ -1,4 +1,3 @@ -import { JSONType } from "../utils/json"; import { OperationHash as OperationHashType } from "./operation-hash"; import { hashOperation } from "../utils/hash"; import { Nonce } from "./nonce"; @@ -17,6 +16,20 @@ export const TicketId = (ticketer: string, data: string) => export type Level = Nominal; export const Level = (level: number) => level as Level; +export type JoypadKey = + | "Down" + | "Up" + | "Left" + | "Right" + | "Start" + | "Select" + | "B" + | "A"; + +export type GovernanceMode = "Anarchy" | "Democracy"; + +export type Vote = JoypadKey | GovernanceMode; + export type Operation_json = | { type: "ticket_transfer"; @@ -26,13 +39,26 @@ export type Operation_json = amount: string; } | { - type: "vm_transaction"; + type: "attest_twitch_handle"; + sender: KeyHash; + twitch_handle: string; + } + | { + type: "attest_deku_address"; + sender: KeyHash; + deku_address: KeyHash; + twitch_handle: string; + } + | { + type: "vote"; sender: KeyHash; - operation: { - // TODO: better type for WASM operations. We can do this with JSON Schema. - operation: JSONType; - tickets: [TicketId, Amount][]; - }; + vote: Vote; + } + | { + type: "delegated_vote"; + sender: KeyHash; + twitch_handle: string; + vote: Vote; } | { type: "withdraw"; @@ -82,22 +108,90 @@ export const createTicketTransfer = async ( return { bytes, hash, nonce, level, operation }; }; -export const createVmOperation = async ( +export const createAttestTwitchHandle = async ( + encodeOperation: encodeOperation, + level: Level, + nonce: Nonce, + sender: KeyHash, + twitch_handle: string +): Promise => { + const operation: Operation_json = { + type: "attest_twitch_handle", + sender, + twitch_handle, + }; + const bytes = await encodeOperation(nonce, level, operation); + const hash = hashOperation(bytes); + return { + bytes, + hash, + nonce, + level, + operation, + }; +}; +export const createAttestDekuAddress = async ( + encodeOperation: encodeOperation, + level: Level, + nonce: Nonce, + sender: KeyHash, + twitch_handle: string, + deku_address: KeyHash +): Promise => { + const operation: Operation_json = { + type: "attest_deku_address", + sender, + twitch_handle, + deku_address, + }; + const bytes = await encodeOperation(nonce, level, operation); + const hash = hashOperation(bytes); + return { + bytes, + hash, + nonce, + level, + operation, + }; +}; + +export const createVote = async ( + encodeOperation: encodeOperation, + level: Level, + nonce: Nonce, + sender: KeyHash, + vote: Vote +): Promise => { + const operation: Operation_json = { + type: "vote", + sender, + vote, + }; + console.log("Operation json:", operation); + const bytes = await encodeOperation(nonce, level, operation); + const hash = hashOperation(bytes); + return { + bytes, + hash, + nonce, + level, + operation, + }; +}; + +export const createDelegatedVote = async ( encodeOperation: encodeOperation, level: Level, nonce: Nonce, sender: KeyHash, - payload: JSONType, - tickets: { ticket_id: TicketId; amount: number }[] + vote: Vote, + twitch_handle: string ): Promise => { - const tickets_payload = tickets.map( - ({ ticket_id, amount }) => - [ticket_id, amount.toString()] as [TicketId, Amount] - ); const operation: Operation_json = { - type: "vm_transaction", + type: "delegated_vote", sender, - operation: { operation: payload, tickets: tickets_payload }, + vote, + twitch_handle, }; const bytes = await encodeOperation(nonce, level, operation); const hash = hashOperation(bytes); diff --git a/deku-c/client/src/deku-p/core/vote.ts b/deku-c/client/src/deku-p/core/vote.ts new file mode 100644 index 0000000000..352047da03 --- /dev/null +++ b/deku-c/client/src/deku-p/core/vote.ts @@ -0,0 +1 @@ +import JSONValue from "../utils/json"; diff --git a/deku-c/client/src/deku-p/index.ts b/deku-c/client/src/deku-p/index.ts index c480e4bb11..25067966dd 100644 --- a/deku-c/client/src/deku-p/index.ts +++ b/deku-c/client/src/deku-p/index.ts @@ -8,10 +8,14 @@ import { KeyHash, TicketId, Level, + Vote, Operation_json, Initial_operation_hash_encoding, createTicketTransfer, - createVmOperation, + createAttestTwitchHandle, + createAttestDekuAddress, + createVote, + createDelegatedVote, createWithdraw, createNoop, } from "./core/operation-encoding"; @@ -288,25 +292,94 @@ export class DekuPClient { } /** - * Submits an operation to the vm - * @param payload the string (TODO: is it better to have a json instead of a string ?) - * @param options {level, nonce} optional options - * @returns the hash the submitted operation + * Attest this wallet's twitch handle + * @param twitch_handle + * @param options to define a custom level/nonce + * @returns the operation hash + */ + async attestTwitchHandle( + twitch_handle: string, + options?: OptOptions + ): Promise { + const { source, level, nonce } = await this.parseOperationOptions(options); + // Create the vm transaction + const vmOperation = await createAttestTwitchHandle( + this.encodeOperation, + level, + nonce, + source, + twitch_handle + ); + return this.submitOperation(vmOperation); + } + + /** + * Attest the Deku address of a Twitch user + * (Only used by the designated Twitch oracle) + * @param twitch_handle The Twitch handle of the user + * @param deku_address The key hash of the Deku user + * @param options to define a custom level/nonce + * @returns the operation hash + */ + async attestDekuAddress( + twitch_handle: string, + deku_address: string, + options?: OptOptions + ): Promise { + const { source, level, nonce } = await this.parseOperationOptions(options); + // Create the vm transaction + const vmOperation = await createAttestDekuAddress( + this.encodeOperation, + level, + nonce, + source, + twitch_handle, + KeyHash(deku_address) + ); + return this.submitOperation(vmOperation); + } + + /** + * Vote on the next operation to be executed + * @param vote A vote for a governance mode or Gameboy input + * @param options to define a custom level/nonce + * @returns the operation hash + */ + async vote(vote: Vote, options?: OptOptions): Promise { + const { source, level, nonce } = await this.parseOperationOptions(options); + // Create the vm transaction + const vmOperation = await createVote( + this.encodeOperation, + level, + nonce, + source, + vote + ); + return this.submitOperation(vmOperation); + } + + /** + * A vote on another users behalf + * (Only used by the designated Twitch oracle) + * @param vote A vote for a governance mode or Gameboy input + * @param twitch_handle the Twitch handle on behalf of whom the oracle is voting + * @param options to define a custom level/nonce + * @returns the operation hash */ - async submitVmOperation( - payload: JSONType, - tickets: { ticket_id: TicketId; amount: number }[], + async delegatedVote( + vote: Vote, + twitch_handle: string, options?: OptOptions ): Promise { const { source, level, nonce } = await this.parseOperationOptions(options); // Create the vm transaction - const vmOperation = await createVmOperation( + const vmOperation = await createDelegatedVote( this.encodeOperation, level, nonce, source, - payload, - tickets + vote, + twitch_handle ); return this.submitOperation(vmOperation); } diff --git a/deku-c/client/src/index.ts b/deku-c/client/src/index.ts index 347be7f9c8..eb6541b333 100644 --- a/deku-c/client/src/index.ts +++ b/deku-c/client/src/index.ts @@ -4,11 +4,9 @@ export { fromCustomSigner, fromMemorySigner, } from "./deku-p"; -export { DekuCClient, Contract } from "./deku-c"; + export { - SupportedLang, - LigoSyntax, - isValidLang, - isLigo, -} from "./deku-c/ligoRpc"; -export { DEKU_API_URL, LIGO_DEKU_RPC_URL } from "./deku-c/default-parameters"; + Vote, + JoypadKey, + GovernanceMode, +} from "./deku-p/core/operation-encoding"; diff --git a/deku-p/src/core/bin/api/deku_api.ml b/deku-p/src/core/bin/api/deku_api.ml index 661df5f034..f11c44975f 100644 --- a/deku-p/src/core/bin/api/deku_api.ml +++ b/deku-p/src/core/bin/api/deku_api.ml @@ -8,6 +8,7 @@ open Api_state open Deku_protocol open Deku_consensus open Deku_concepts +open Deku_crypto let make_dump_loop ~sw ~env ~data_folder = let resolver_ref = Atomic.make None in @@ -71,10 +72,11 @@ let apply_block ~sw ~state ~block = match receipt with | Ticket_transfer_receipt { operation; _ } | Withdraw_receipt { operation; _ } - | Vm_origination_receipt { operation; _ } - | Vm_transaction_receipt { operation; _ } -> + | Attest_twitch_handle { operation; _ } + | Attest_deku_address { operation; _ } + | Game_vote { operation; _ } + | Delegated_game_vote { operation; _ } -> operation - | Vm_transaction_error { operation; _ } -> operation in Operation_hash.Map.add hash receipt receipts) state.receipts receipts @@ -140,8 +142,8 @@ let start_api ~env ~sw ~port ~state = |> Server.with_body (module Helpers_operation_message) |> Server.with_body (module Helpers_hash_operation) |> Server.with_body (module Post_operation) - |> Server.without_body (module Get_vm_state) - |> Server.without_body (module Get_vm_state_key) + (* |> Server.without_body (module Get_vm_state) + |> Server.without_body (module Get_vm_state_key) *) |> Server.without_body (module Get_stats) |> Server.with_body (module Encode_operation) |> Server.with_body (module Decode_operation) @@ -153,7 +155,10 @@ let start_api ~env ~sw ~port ~state = in let config = Piaf.Server.Config.create port in let server = Piaf.Server.create ~config request_handler in - let _ = Piaf.Server.Command.start ~sw env server in + let _ = + Piaf.Server.Command.start ~bind_to_address:Eio.Net.Ipaddr.V4.any ~sw env + server + in () type params = { @@ -165,6 +170,7 @@ type params = { database_uri : Uri.t; [@env "DEKU_API_DATABASE_URI"] domains : int; [@default 8] [@env "DEKU_API_DOMAINS"] data_folder : string; [@env "DEKU_API_DATA_FOLDER"] + twitch_oracle_address : Key_hash.t; [@env "DEKU_TWITCH_ORACLE_ADDRESS"] } [@@deriving cmdliner] @@ -194,6 +200,7 @@ let main params style_renderer log_level = database_uri; domains; data_folder; + twitch_oracle_address; } = params in @@ -229,8 +236,11 @@ let main params style_renderer log_level = let state = match state with | None -> - let vm_state = Ocaml_wasm_vm.State.empty in - let protocol = Protocol.initial_with_vm_state ~vm_state in + (* FIXME: *) + let twitch_oracle_address = + Deku_ledger.Address.of_key_hash twitch_oracle_address + in + let protocol = Protocol.initial ~twitch_oracle_address () in let current_block = Genesis.block in let receipts = Operation_hash.Map.empty in let (Block { level; _ }) = current_block in diff --git a/deku-p/src/core/bin/api/handlers.ml b/deku-p/src/core/bin/api/handlers.ml index 25c95bbe45..60d98bad09 100644 --- a/deku-p/src/core/bin/api/handlers.ml +++ b/deku-p/src/core/bin/api/handlers.ml @@ -350,49 +350,49 @@ module Post_operation : HANDLERS = struct Ok { hash = operation_hash } end -module Get_vm_state : NO_BODY_HANDLERS = struct - type path = unit - type response = Ocaml_wasm_vm.State.t - - let response_encoding = Ocaml_wasm_vm.State.api_encoding - let meth = `GET - let path = Routes.(version / s "state" / s "unix" /? nil) - let route = Routes.(path @--> ()) - - let handler ~path:_ ~state = - let Api_state.{ protocol; _ } = state in - let (Protocol.Protocol { vm_state; _ }) = protocol in - Ok vm_state -end - -module Get_vm_state_key : NO_BODY_HANDLERS = struct - open Deku_ledger - - type path = Contract_address.t - type response = Ocaml_wasm_vm.State_entry.t option - - let response_encoding = - let open Data_encoding in - conv - (fun state_entry -> state_entry) - (fun state_entry -> state_entry) - (option Ocaml_wasm_vm.State_entry.encoding) - - let meth = `GET - - let path = - Routes.( - version / s "state" / s "unix" / Api_path.Contract_address.parser /? nil) - - let route = Routes.(path @--> fun key -> key) - - let handler ~path:key ~state = - let Api_state.{ protocol; _ } = state in - let (Protocol.Protocol { vm_state; _ }) = protocol in - Ok - (try Some (Ocaml_wasm_vm.State.fetch_contract vm_state key) - with _ -> None) -end +(* module Get_vm_state : NO_BODY_HANDLERS = struct + type path = unit + type response = Ocaml_wasm_vm.State.t + + let response_encoding = Ocaml_wasm_vm.State.api_encoding + let meth = `GET + let path = Routes.(version / s "state" / s "unix" /? nil) + let route = Routes.(path @--> ()) + + let handler ~path:_ ~state = + let Api_state.{ protocol; _ } = state in + let (Protocol.Protocol { vm_state; _ }) = protocol in + Ok vm_state + end *) + +(* module Get_vm_state_key : NO_BODY_HANDLERS = struct + open Deku_ledger + + type path = Contract_address.t + type response = Ocaml_wasm_vm.State_entry.t option + + let response_encoding = + let open Data_encoding in + conv + (fun state_entry -> state_entry) + (fun state_entry -> state_entry) + (option Ocaml_wasm_vm.State_entry.encoding) + + let meth = `GET + + let path = + Routes.( + version / s "state" / s "unix" / Api_path.Contract_address.parser /? nil) + + let route = Routes.(path @--> fun key -> key) + + let handler ~path:key ~state = + let Api_state.{ protocol; _ } = state in + let (Protocol.Protocol { vm_state; _ }) = protocol in + Ok + (try Some (Ocaml_wasm_vm.State.fetch_contract vm_state key) + with _ -> None) + end *) module Get_stats : NO_BODY_HANDLERS = struct type path = unit diff --git a/deku-p/src/core/bin/benchmark/deku_benchmark.ml b/deku-p/src/core/bin/benchmark/deku_benchmark.ml index 894a6bb3ef..0d0ee1b752 100644 --- a/deku-p/src/core/bin/benchmark/deku_benchmark.ml +++ b/deku-p/src/core/bin/benchmark/deku_benchmark.ml @@ -15,7 +15,7 @@ let block ~default_block_size = let withdrawal_handles_hash = BLAKE2b.hash "potato" in let producer = Producer.empty in Producer.produce ~identity ~default_block_size ~above ~withdrawal_handles_hash - producer + ~game_decision:None producer module type BENCH = sig type t diff --git a/deku-p/src/core/bin/node/deku_node.ml b/deku-p/src/core/bin/node/deku_node.ml index a433b86105..ef654e696d 100644 --- a/deku-p/src/core/bin/node/deku_node.ml +++ b/deku-p/src/core/bin/node/deku_node.ml @@ -52,6 +52,7 @@ type params = { [@env "DEKU_TEZOS_CONSENSUS_ADDRESS"] (** The address of the consensus contract on Tezos. *) api_uri : string; [@env "DEKU_API_URI"] [@default "127.0.0.1:5550"] + twitch_oracle_address : Key_hash.t; [@env "DEKU_TWITCH_ORACLE_ADDRESS"] } [@@deriving cmdliner] @@ -86,6 +87,7 @@ let main params style_renderer log_level = tezos_secret; tezos_consensus_address; api_uri; + twitch_oracle_address; } = params in @@ -95,6 +97,9 @@ let main params style_renderer log_level = Parallel.Pool.run ~env ~domains @@ fun () -> Logs.info (fun m -> m "Using %d domains" domains); Logs.info (fun m -> m "Default block size: %d" default_block_size); + let twitch_oracle_address = + Deku_ledger.Address.of_key_hash twitch_oracle_address + in let indexer = let domains = Eio.Stdenv.domain_mgr env in let worker = Parallel.Worker.make ~domains ~sw in @@ -117,10 +122,9 @@ let main params style_renderer log_level = let validator_uris = if port = 4440 then api_uri :: validator_uris else validator_uris in - - (* The VM must be started before the node because this call is blocking - Logs.info (fun m -> - m "Starting IPC with external vm at path %s" named_pipe_path); *) + let net = Eio.Stdenv.net env in + (* Initialize network connection with emulator *) + Deku_gameboy.init net; let identity = Identity.make (Secret.Ed25519 secret) in Logs.info (fun m -> m "Running as validator %s" (Identity.key_hash identity |> Key_hash.to_b58)); @@ -131,9 +135,7 @@ let main params style_renderer log_level = let chain = match chain with | Some chain -> chain - | None -> - let vm_state = Ocaml_wasm_vm.State.empty in - Chain.make ~validators ~vm_state + | None -> Chain.make ~validators ~twitch_oracle_address () in let dump = make_dump_loop ~sw ~env ~folder:data_folder in let node = @@ -143,10 +145,8 @@ let main params style_renderer log_level = let (Chain { consensus; _ }) = chain in let (Block { level; _ }) = Deku_consensus.Consensus.trusted_block consensus in Logs.info (fun m -> m "Chain started at level: %a" Level.pp level); - let tezos = - (tezos_rpc_node, Secret.Ed25519 tezos_secret, tezos_consensus_address) - in - Node.start ~sw ~env ~port ~nodes:validator_uris ~tezos:(Some tezos) node + ignore (tezos_consensus_address, tezos_rpc_node, tezos_secret); + Node.start ~sw ~env ~port ~nodes:validator_uris ~tezos:None node let main () = let open Cmdliner in diff --git a/deku-p/src/core/bin/node/dune b/deku-p/src/core/bin/node/dune index c4e44d5c41..07e09568b5 100644 --- a/deku-p/src/core/bin/node/dune +++ b/deku-p/src/core/bin/node/dune @@ -8,6 +8,7 @@ deku_tezos deku_tezos_interop deku_storage + deku_gameboy deku_external_vm deku_metrics cmdliner diff --git a/deku-p/src/core/bin/node/node.ml b/deku-p/src/core/bin/node/node.ml index 30b9379858..9d81030230 100644 --- a/deku-p/src/core/bin/node/node.ml +++ b/deku-p/src/core/bin/node/node.ml @@ -34,13 +34,12 @@ let send_blocks ~sw ~connection ~above node = | Some indexer -> Eio.Fiber.fork ~sw @@ fun () -> let rec send_while level = - let level = Level.next level in match Block_storage.find_block_and_votes_by_level ~level indexer with | Some network -> let (Network_message { raw_header; raw_content }) = network in Network_manager.send ~connection ~raw_header ~raw_content node.network; - send_while level + send_while (Level.next level) | None -> () in send_while above diff --git a/deku-p/src/core/bin/node/test_node.ml b/deku-p/src/core/bin/node/test_node.ml index 6d611c5b0e..6213129d7d 100644 --- a/deku-p/src/core/bin/node/test_node.ml +++ b/deku-p/src/core/bin/node/test_node.ml @@ -35,7 +35,7 @@ let main () = in let start ~sw ~identity ~port = - let chain = Chain.make ~validators ~vm_state:Ocaml_wasm_vm.State.empty in + let chain = Chain.make ~validators () in let dump _chain = () in let node = Node.make ~identity ~default_block_size:100_000 ~dump ~chain ~indexer:None diff --git a/deku-p/src/core/chain/chain.ml b/deku-p/src/core/chain/chain.ml index 91ccaa79c7..02024220e0 100644 --- a/deku-p/src/core/chain/chain.ml +++ b/deku-p/src/core/chain/chain.ml @@ -30,6 +30,7 @@ type fragment = producer : Producer.t; above : Block.t; withdrawal_handles_hash : BLAKE2b.t; + game_decision : Deku_gameboy.Joypad.t option; } | Fragment_apply of { (* TODO: votes here is weird, only happens to commit after fragment *) @@ -75,10 +76,10 @@ type action = } [@@deriving show] -let make ~validators ~vm_state = +let make ~validators ?twitch_oracle_address () = let gossip = Gossip.initial in let validators = Validators.of_key_hash_list validators in - let protocol = Protocol.initial_with_vm_state ~vm_state in + let protocol = Protocol.initial ?twitch_oracle_address () in let consensus = Consensus.make ~validators in let producer = Producer.empty in Chain { gossip; protocol; consensus; producer } @@ -127,12 +128,14 @@ let apply_consensus_action chain consensus_action = | Consensus_timeout { until } -> (chain, [ Chain_timeout { until } ]) | Consensus_produce { above } -> let (Chain { protocol; producer; _ }) = chain in - let (Protocol { ledger; _ }) = protocol in + let (Protocol { ledger; game; _ }) = protocol in let withdrawal_handles_hash = Deku_ledger.Ledger.withdrawal_handles_root_hash ledger in + let game_decision = Game.get_decision game in let fragment = - Fragment_produce { producer; above; withdrawal_handles_hash } + Fragment_produce + { producer; above; withdrawal_handles_hash; game_decision } in (match minimum_block_latency with | 0. -> () @@ -178,7 +181,7 @@ let apply_consensus_actions chain consensus_actions = (* core *) let incoming_block ~identity ~current ~block chain = - Logs.info (fun m -> m "Incoming block %a" Block.pp block); + Logs.debug (fun m -> m "Incoming block %a" Block.pp block); let (Chain ({ consensus; _ } as chain)) = chain in let consensus, actions = Consensus.incoming_block ~identity ~current ~block consensus @@ -210,8 +213,13 @@ let incoming_operation ~operation chain = match operation with | Operation_ticket_transfer _ -> Logs.info (fun m -> m "Incoming ticket transfer: %s" hash) - | Operation_vm_transaction _ -> - Logs.info (fun m -> m "Incoming vm transaction: %s" hash) + | Operation_attest_twitch_handle _ -> + Logs.info (fun m -> m "Incoming attest twitch handle: %s" hash) + | Operation_attest_deku_address _ -> + Logs.info (fun m -> m "Incoming attest twitch handle: %s" hash) + | Operation_vote _ -> Logs.info (fun m -> m "Incoming vote: %s" hash) + | Operation_delegated_vote _ -> + Logs.info (fun m -> m "Incoming delegated vote: %s" hash) | Operation_withdraw _ -> Logs.info (fun m -> m "Incoming vm withdraw: %s" hash) | Operation_noop _ -> Logs.info (fun m -> m "Incoming noop: %s" hash) @@ -250,6 +258,13 @@ let incoming_request ~connection ~above chain = let apply_gossip_action ~identity ~current ~gossip_action chain = match gossip_action with | Gossip.Gossip_apply_and_broadcast { content; network } -> + let () = + match content with + | Message.Content.Content_operation operation -> + Format.printf "Node: got operation %a\n%!" Operation.Signed.pp + operation + | _ -> () + in let chain, actions = incoming_message ~identity ~current ~content chain in let broadcast = let (Network_message { raw_header; raw_content }) = network in @@ -379,10 +394,11 @@ let compute ~identity ~default_block_size fragment = | Fragment_gossip { fragment } -> let outcome = Gossip.compute fragment in Outcome_gossip { outcome } - | Fragment_produce { producer; above; withdrawal_handles_hash } -> + | Fragment_produce { producer; above; withdrawal_handles_hash; game_decision } + -> let block = Producer.produce ~identity ~default_block_size ~above - ~withdrawal_handles_hash producer + ~withdrawal_handles_hash ~game_decision producer in Outcome_produce { block } | Fragment_apply { protocol; votes; block } -> @@ -457,7 +473,7 @@ let test () = [ key_hash ] in - let chain = make ~validators ~vm_state:Ocaml_wasm_vm.State.empty in + let chain = make ~validators () in let block = let (Block { hash = current_block; level = current_level; _ }) = Genesis.block diff --git a/deku-p/src/core/chain/chain.mli b/deku-p/src/core/chain/chain.mli index 7238ed8334..879482ec78 100644 --- a/deku-p/src/core/chain/chain.mli +++ b/deku-p/src/core/chain/chain.mli @@ -41,7 +41,11 @@ type action = private } [@@deriving show] -val make : validators:Key_hash.t list -> vm_state:Ocaml_wasm_vm.State.t -> chain +val make : + validators:Deku_crypto.Key_hash.key_hash list -> + ?twitch_oracle_address:Deku_ledger.Address.address -> + unit -> + t val incoming : raw_header:string -> raw_content:string -> chain -> chain * fragment option diff --git a/deku-p/src/core/consensus/producer.ml b/deku-p/src/core/consensus/producer.ml index 0d6a69f4b0..d093403c8a 100644 --- a/deku-p/src/core/consensus/producer.ml +++ b/deku-p/src/core/consensus/producer.ml @@ -63,9 +63,10 @@ let clean ~receipts ~tezos_operations producer = (fun operations receipt -> match receipt with | Receipt.Ticket_transfer_receipt { operation = hash } - | Receipt.Vm_origination_receipt { operation = hash; _ } - | Receipt.Vm_transaction_receipt { operation = hash; _ } - | Receipt.Vm_transaction_error { operation = hash; _ } + | Receipt.Attest_twitch_handle { operation = hash; _ } + | Receipt.Attest_deku_address { operation = hash; _ } + | Receipt.Game_vote { operation = hash; _ } + | Receipt.Delegated_game_vote { operation = hash; _ } | Receipt.Withdraw_receipt { operation = hash; _ } -> Operation_hash.Map.remove hash operations) operations receipts @@ -94,8 +95,10 @@ let fill_with_noop ~identity ~level ~default_block_size operations = fill dummy_op_size operations let produce ~identity ~default_block_size ~above ~withdrawal_handles_hash - producer = + ~game_decision producer = let open Block in + Unix.sleep 1; + let _cycles_advanced = Deku_gameboy.send_input (Input game_decision) in let (Producer { operations; tezos_operations }) = producer in let (Block { hash = current_block; level = current_level; _ }) = above in diff --git a/deku-p/src/core/consensus/producer.mli b/deku-p/src/core/consensus/producer.mli index 017930a2f5..49f9ae2739 100644 --- a/deku-p/src/core/consensus/producer.mli +++ b/deku-p/src/core/consensus/producer.mli @@ -23,5 +23,6 @@ val produce : default_block_size:int -> above:Block.block -> withdrawal_handles_hash:Deku_crypto.BLAKE2b.BLAKE2b_256.hash -> + game_decision:Deku_gameboy.Joypad.t option -> t -> Block.block diff --git a/deku-p/src/core/gameboy/deku_gameboy.ml b/deku-p/src/core/gameboy/deku_gameboy.ml new file mode 100644 index 0000000000..4c3354a1e3 --- /dev/null +++ b/deku-p/src/core/gameboy/deku_gameboy.ml @@ -0,0 +1,134 @@ +open Deku_stdlib + +type state = Bytes.t + +let state_encoding = Data_encoding.bytes +let empty = Bytes.empty + +module Joypad = struct + type t = Down | Up | Left | Right | Start | Select | B | A + [@@deriving show, eq] + + let to_string = function + | Down -> "Down" + | Up -> "Up" + | Left -> "Left" + | Right -> "Right" + | Start -> "Start" + | Select -> "Select" + | A -> "A" + | B -> "B" + + let of_string = function + | "Down" -> Down + | "Up" -> Up + | "Left" -> Left + | "Right" -> Right + | "Start" -> Start + | "Select" -> Select + | "B" -> B + | "A" -> A + | x -> + raise @@ Invalid_argument (Format.sprintf "Unknown gameboy input %s" x) + + let cmdliner_converter = + let of_string s = + try `Ok (of_string s) with Invalid_argument err -> `Error err + in + let to_string fmt t = Format.fprintf fmt "%s" (to_string t) in + (of_string, to_string) + + let encoding = + let open Data_encoding in + union ~tag_size:`Uint8 + [ + case ~title:"Down" (Tag 0) + (Data_encoding.constant "Down") + (function Down -> Some () | _ -> None) + (fun () -> Down); + case ~title:"Up" (Tag 1) + (Data_encoding.constant "Up") + (function Up -> Some () | _ -> None) + (fun () -> Up); + case ~title:"Left" (Tag 2) + (Data_encoding.constant "Left") + (function Left -> Some () | _ -> None) + (fun () -> Left); + case ~title:"Right" (Tag 3) + (Data_encoding.constant "Right") + (function Right -> Some () | _ -> None) + (fun () -> Right); + case ~title:"Start" (Tag 4) + (Data_encoding.constant "Start") + (function Start -> Some () | _ -> None) + (fun () -> Start); + case ~title:"Select" (Tag 5) + (Data_encoding.constant "Select") + (function Select -> Some () | _ -> None) + (fun () -> Select); + case ~title:"A" (Tag 6) + (Data_encoding.constant "A") + (function A -> Some () | _ -> None) + (fun () -> A); + case ~title:"B" (Tag 7) + (Data_encoding.constant "B") + (function B -> Some () | _ -> None) + (fun () -> B); + ] + + let%expect_test "Data_encoding round_trip" = + List.iter + (fun key -> + let serialized = Data_encoding.Binary.to_bytes_exn encoding key in + let deserialized = + Data_encoding.Binary.of_bytes_exn encoding serialized + in + Format.printf "%a <-> %a\n" Hex.pp (Hex.of_bytes serialized) pp + deserialized) + [ Down; Up; Left; Right; Start; Select; A; B ]; + [%expect + {| + 00 <-> Deku_gameboy.Joypad.Down + 01 <-> Deku_gameboy.Joypad.Up + 02 <-> Deku_gameboy.Joypad.Left + 03 <-> Deku_gameboy.Joypad.Right + 04 <-> Deku_gameboy.Joypad.Start + 05 <-> Deku_gameboy.Joypad.Select + 06 <-> Deku_gameboy.Joypad.A + 07 <-> Deku_gameboy.Joypad.B |}] +end + +type command = + | Input of Joypad.t option + | Input_and_advance of Joypad.t option * int + +let max_size = 128 * 1024 * 1024 +let net_ref : Eio.Net.t option ref = ref None +let init net = net_ref := Some net + +let send_input command = + match !net_ref with + | None -> failwith "You must initialize the network first" + | Some net -> + Eio.Switch.run @@ fun sw -> + let flow = + Eio.Net.connect ~sw net (`Tcp (Eio.Net.Ipaddr.V4.loopback, 2222)) + in + let command_str = + match command with + | Input (Some joypad) -> + Format.sprintf "Input %s" (Joypad.to_string joypad) + | Input None -> "Input Empty" + | Input_and_advance (None, n) -> + Format.sprintf "Input_and_advance Empty %d" n + | Input_and_advance (Some joypad, n) -> + Format.sprintf "Input_and_advance %s %d" (Joypad.to_string joypad) n + in + (* Logs.app (fun m -> m "Sending emulator command: '%s'" command_str); *) + Eio.Flow.copy_string (command_str ^ "\n") flow; + let response = Eio.Buf_read.of_flow ~max_size flow in + let response = + Eio.Buf_read.lines response |> List.of_seq |> String.concat "\n" + in + (* Logs.app (fun m -> m "Received emulator response: '%s'" response); *) + Z.of_string response |> N.of_z |> Option.get diff --git a/deku-p/src/core/gameboy/deku_gameboy.mli b/deku-p/src/core/gameboy/deku_gameboy.mli new file mode 100644 index 0000000000..43f17594e4 --- /dev/null +++ b/deku-p/src/core/gameboy/deku_gameboy.mli @@ -0,0 +1,27 @@ +open Deku_stdlib + +type state + +val state_encoding : state Data_encoding.t +val empty : state + +module Joypad : sig + type t = Down | Up | Left | Right | Start | Select | B | A + [@@deriving eq, show] + + val to_string : t -> string + val of_string : string -> t + + val cmdliner_converter : + (string -> [> `Error of string | `Ok of t ]) + * (Format.formatter -> t -> unit) + + val encoding : t Data_encoding.t +end + +type command = + | Input of Joypad.t option + | Input_and_advance of Joypad.t option * int + +val init : Eio.Net.t -> unit +val send_input : command -> N.t diff --git a/deku-p/src/core/gameboy/dune b/deku-p/src/core/gameboy/dune new file mode 100644 index 0000000000..c0f0cad2ab --- /dev/null +++ b/deku-p/src/core/gameboy/dune @@ -0,0 +1,25 @@ +(library + (name deku_gameboy) + (modules deku_gameboy) + (libraries + deku_stdlib + deku_external_vm + unix + data-encoding + piaf + logs + logs.fmt) + (inline_tests) + (preprocess + (pps + ppx_let_binding + ppx_deriving.eq + ppx_deriving.show + ppx_deriving.ord + ppx_expect))) + +(executable + (name gameboy_test) + (modules gameboy_test) + (public_name gameboy-test) + (libraries deku_gameboy)) diff --git a/deku-p/src/core/gameboy/gameboy_test.ml b/deku-p/src/core/gameboy/gameboy_test.ml new file mode 100644 index 0000000000..46dca207bc --- /dev/null +++ b/deku-p/src/core/gameboy/gameboy_test.ml @@ -0,0 +1,16 @@ +open Deku_gameboy +open Deku_stdlib + +let main () = + Logs.set_level (Some Logs.Info); + Logs.set_reporter (Logs_fmt.reporter ()); + Eio_main.run @@ fun env -> + let net = Eio.Stdenv.net env in + Deku_gameboy.init net; + while true do + Unix.sleep 1; + let state = Deku_gameboy.send_input (Input (Some Joypad.A)) in + Logs.info (fun m -> m "Current cycle state: %a" N.pp state) + done + +let () = main () diff --git a/deku-p/src/core/gossip/gossip.ml b/deku-p/src/core/gossip/gossip.ml index 94bb63853f..4af6f1eeeb 100644 --- a/deku-p/src/core/gossip/gossip.ml +++ b/deku-p/src/core/gossip/gossip.ml @@ -66,14 +66,20 @@ let broadcast_message ~content = Fragment_broadcast_message { fragment } let incoming_message ~raw_header ~raw_content gossip = + if String.starts_with ~prefix:{|["Content_operation|} raw_content then + Format.printf "Node: Decoding operation message\n%!"; let (Gossip { pending_request; message_pool }) = gossip in let message_pool, fragment = Message_pool.decode ~raw_header ~raw_content message_pool in + let gossip = Gossip { pending_request; message_pool } in let fragment = match fragment with - | Some fragment -> Some (Fragment_broadcast_message { fragment }) + | Some fragment -> + if String.starts_with ~prefix:{|["Content_operation|} raw_content then + Format.printf "successfully decoded: %s\n%!" raw_content; + Some (Fragment_broadcast_message { fragment }) | None -> None in (gossip, fragment) diff --git a/deku-p/src/core/protocol/dune b/deku-p/src/core/protocol/dune index 5fbce8a8c9..e1145bc5ad 100644 --- a/deku-p/src/core/protocol/dune +++ b/deku-p/src/core/protocol/dune @@ -6,7 +6,7 @@ deku_concepts deku_constants tezos-micheline - deku_external_vm + deku_gameboy deku_tezos deku_ledger deku_metrics diff --git a/deku-p/src/core/protocol/game.ml b/deku-p/src/core/protocol/game.ml new file mode 100644 index 0000000000..60f68c4ab0 --- /dev/null +++ b/deku-p/src/core/protocol/game.ml @@ -0,0 +1,381 @@ +open Deku_gameboy +open Deku_ledger +open Deku_crypto + +type governance_mode = Anarchy | Democracy [@@deriving show, eq] + +let governance_mode_encoding = + let open Data_encoding in + union ~tag_size:`Uint8 + [ + case ~title:"Anarchy" (Tag 0) + (Data_encoding.constant "Anarchy") + (function Anarchy -> Some () | _ -> None) + (fun () -> Anarchy); + case ~title:"Democracy" (Tag 1) + (Data_encoding.constant "Democracy") + (function Democracy -> Some () | _ -> None) + (fun () -> Democracy); + ] + +module Vote = struct + type t = Governance of governance_mode | Input of Joypad.t + [@@deriving show, eq] + + let encoding = + let open Data_encoding in + union ~tag_size:`Uint8 + [ + case ~title:"Governance" (Tag 0) + (Data_encoding.dynamic_size governance_mode_encoding) + (function + | Governance governance_mode -> Some governance_mode | _ -> None) + (fun governance_mode -> Governance governance_mode); + case ~title:"Input" (Tag 1) + (Data_encoding.dynamic_size Joypad.encoding) + (function Input input -> Some input | _ -> None) + (fun input -> Input input); + ] + + let%expect_test "Data_encoding round_trip" = + let vote = Input Joypad.A in + let serialized = Data_encoding.Binary.to_bytes_exn encoding vote in + let deserialized = Data_encoding.Binary.of_bytes_exn encoding serialized in + Format.printf "%a <-> %a\n" Hex.pp (Hex.of_bytes serialized) pp deserialized; + [%expect + {| + 010000000106 <-> (Game.Vote.Input Deku_gameboy.Joypad.A) |}] + + module Map = Deku_stdlib.Map.Make (struct + type nonrec t = t + + let compare = compare + let encoding = encoding + end) +end + +module Votes = struct + type t = Vote.t Address.Map.t + + let encoding = Address.Map.encoding Vote.encoding + let empty = Address.Map.empty + let add_vote voter vote t = Address.Map.add voter vote t + + (* TODO: tests for this function *) + let get_majority t = + let votes = + Address.Map.fold + (fun _address button votes -> + Vote.Map.update button + (function + | Some votes_for_button -> Some (votes_for_button + 1) + | None -> Some 1) + votes) + t Vote.Map.empty + in + Vote.Map.fold + (fun button votes majority -> + match majority with + | Some (_majority_button, majority) when votes > majority -> + Some (button, votes) + | Some (majority_button, majority) -> Some (majority_button, majority) + | None when votes > 0 -> Some (button, votes) + | None -> None) + votes None + |> Option.map fst + + module Test = struct + type outcome = Vote.t option [@@deriving show] + + open Joypad + open Vote + + let address_1 = + Key_hash.of_b58 "tz1Nm5D1MDerXaTMdc4kz2HWeVuJ6RHCF4WB" + |> Option.get |> Address.of_key_hash + + let address_2 = + Key_hash.of_b58 "tz1Uc11Ds3xwG4Hw1ragWLf3KLj4v4xu9BY2" + |> Option.get |> Address.of_key_hash + + let address_3 = + Key_hash.of_b58 "tz1hAGxu4tEDEoFQNwPFZf1U4gVrQcmstnnp" + |> Option.get |> Address.of_key_hash + + let%expect_test "voting: no votes" = + let t = empty in + let outcome = get_majority t in + Format.printf "%a" pp_outcome outcome; + [%expect {| None |}] + + let%expect_test "voting: clear majority" = + let t = + add_vote address_1 (Input A) empty + |> add_vote address_2 (Input A) + |> add_vote address_3 (Input B) + in + let outcome = get_majority t in + Format.printf "%a" pp_outcome outcome; + [%expect {| (Some (Game.Vote.Input Deku_gameboy.Joypad.A)) |}] + + let%expect_test "voting: ties are broken" = + let t = + add_vote address_1 (Input A) empty |> add_vote address_2 (Input Start) + in + let outcome = get_majority t in + Format.printf "%a" pp_outcome outcome; + [%expect {| (Some (Game.Vote.Input Deku_gameboy.Joypad.Start)) |}] + + let%expect_test "voting: voting is idempotent" = + let t = + add_vote address_1 (Input Start) empty + |> add_vote address_1 (Input Start) + |> add_vote address_2 (Input A) + |> add_vote address_3 (Input A) + in + let outcome = get_majority t in + Format.printf "%a" pp_outcome outcome; + [%expect {| (Some (Game.Vote.Input Deku_gameboy.Joypad.A)) |}] + end +end + +module Twitch_handle = struct + type t = string [@@deriving eq, show] + + let of_string s = + match String.length s <= 26 with + | true -> Ok s + | false -> Error "twitch handle two long" + + let compare = String.compare + let encoding = Data_encoding.string +end + +module Twitch_handle_map = Deku_stdlib.Map.Make (Twitch_handle) + +type t = { + twitch_oracle_address : Address.t; + governance_mode : governance_mode; + votes : Votes.t; + address_to_twitch_attestations : Twitch_handle.t Address.Map.t; + twitch_to_address_attestations : Address.t Twitch_handle_map.t; + first_vote : Joypad.t option; + prev_round_decision : Joypad.t option; +} + +let encoding = + let open Data_encoding in + conv + (fun { + twitch_oracle_address; + governance_mode; + votes; + address_to_twitch_attestations; + twitch_to_address_attestations; + first_vote; + prev_round_decision; + } -> + ( twitch_oracle_address, + governance_mode, + votes, + address_to_twitch_attestations, + twitch_to_address_attestations, + first_vote, + prev_round_decision )) + (fun ( twitch_oracle_address, + governance_mode, + votes, + address_to_twitch_attestations, + twitch_to_address_attestations, + first_vote, + prev_round_decision ) -> + { + twitch_oracle_address; + governance_mode; + votes; + address_to_twitch_attestations; + twitch_to_address_attestations; + first_vote; + prev_round_decision; + }) + (tup7 + (dynamic_size Address.encoding) + governance_mode_encoding Votes.encoding + (Address.Map.encoding (dynamic_size Twitch_handle.encoding)) + (Twitch_handle_map.encoding (dynamic_size Address.encoding)) + (option Joypad.encoding) (option Joypad.encoding)) + +let empty ?twitch_oracle_address () = + let twitch_oracle_address = + match twitch_oracle_address with + | Some address -> address + | None -> + Key_hash.of_b58 "tz1cTyRNTn3c83gKkrGXKtYWTeVfKaxxt8s5" + |> Option.get |> Address.of_key_hash + in + { + twitch_oracle_address; + governance_mode = Democracy; + votes = Votes.empty; + address_to_twitch_attestations = Address.Map.empty; + twitch_to_address_attestations = Twitch_handle_map.empty; + first_vote = None; + prev_round_decision = None; + } + +let attest_twitch_handle ~sender ~twitch_handle t = + let { + twitch_oracle_address; + governance_mode; + votes; + address_to_twitch_attestations; + twitch_to_address_attestations; + first_vote; + prev_round_decision; + } = + t + in + let address_to_twitch_attestations = + Address.Map.add sender twitch_handle address_to_twitch_attestations + in + { + twitch_oracle_address; + governance_mode; + votes; + address_to_twitch_attestations; + twitch_to_address_attestations; + first_vote; + prev_round_decision; + } + +let attest_deku_address ~sender ~deku_address ~twitch_handle t = + let { + twitch_oracle_address; + governance_mode; + votes; + address_to_twitch_attestations; + twitch_to_address_attestations; + first_vote; + prev_round_decision; + } = + t + in + match Address.equal sender twitch_oracle_address with + | true -> + let twitch_to_address_attestations = + Twitch_handle_map.add twitch_handle deku_address + twitch_to_address_attestations + in + Some + { + twitch_oracle_address; + governance_mode; + votes; + address_to_twitch_attestations; + twitch_to_address_attestations; + first_vote; + prev_round_decision; + } + | _ -> None + +let update_first_vote ~vote t = + let { first_vote; _ } = t in + match (vote, first_vote) with + | Vote.Input input, None -> { t with first_vote = Some input } + | _ -> t + +let vote ~sender ~vote t = + let { + twitch_oracle_address; + governance_mode; + votes; + address_to_twitch_attestations; + twitch_to_address_attestations; + first_vote; + prev_round_decision; + } = + t + in + let votes = Votes.add_vote sender vote votes in + let t = + { + twitch_oracle_address; + governance_mode; + votes; + address_to_twitch_attestations; + twitch_to_address_attestations; + first_vote; + prev_round_decision; + } + in + update_first_vote ~vote t + +let delegated_vote ~sender ~twitch_handle ~vote t = + let { + twitch_oracle_address; + governance_mode; + votes; + address_to_twitch_attestations; + twitch_to_address_attestations; + first_vote; + prev_round_decision; + } = + t + in + match Address.equal sender twitch_oracle_address with + | true -> ( + match + Twitch_handle_map.find_opt twitch_handle twitch_to_address_attestations + with + | Some voter_address -> ( + match + Address.Map.find_opt voter_address address_to_twitch_attestations + with + | Some attested_handle when String.equal twitch_handle attested_handle + -> + let votes = Votes.add_vote voter_address vote votes in + let t = + { + twitch_oracle_address; + governance_mode; + votes; + address_to_twitch_attestations; + twitch_to_address_attestations; + first_vote; + prev_round_decision; + } + in + Some (update_first_vote ~vote t) + | _ -> None) + | None -> None) + | false -> None + +let new_round t = + let { + twitch_oracle_address = _; + governance_mode; + votes; + address_to_twitch_attestations = _; + twitch_to_address_attestations = _; + first_vote; + prev_round_decision = _; + } = + t + in + let majority = Votes.get_majority votes in + let t = { t with prev_round_decision = None } in + let t = + match (governance_mode, majority, first_vote) with + | Anarchy, Some (Governance Democracy), _ -> + { t with governance_mode = Democracy } + | Anarchy, _, Some input -> { t with prev_round_decision = Some input } + | Anarchy, _, _ -> t + | Democracy, Some (Governance Anarchy), _ -> + { t with governance_mode = Anarchy } + | Democracy, Some (Input input), _ -> + { t with prev_round_decision = Some input } + | Democracy, Some (Governance Democracy), _ | Democracy, None, _ -> t + in + { t with votes = Votes.empty; first_vote = None } + +let get_decision t = t.prev_round_decision diff --git a/deku-p/src/core/protocol/game.mli b/deku-p/src/core/protocol/game.mli new file mode 100644 index 0000000000..5613078a2b --- /dev/null +++ b/deku-p/src/core/protocol/game.mli @@ -0,0 +1,44 @@ +open Deku_gameboy + +type governance_mode = Anarchy | Democracy [@@deriving show, eq] + +module Vote : sig + type t = Governance of governance_mode | Input of Deku_gameboy.Joypad.t + [@@deriving show, eq] + + val encoding : t Data_encoding.t +end + +module Twitch_handle : sig + type t = string [@@deriving ord, eq, show] + + val of_string : string -> (t, string) Result.t + val encoding : t Data_encoding.t +end + +type t + +val encoding : t Data_encoding.t +val empty : ?twitch_oracle_address:Deku_ledger.Address.address -> unit -> t + +val attest_twitch_handle : + sender:Deku_ledger.Address.address -> twitch_handle:Twitch_handle.t -> t -> t + +val attest_deku_address : + sender:Deku_ledger.Address.address -> + deku_address:Deku_ledger.Address.address -> + twitch_handle:string -> + t -> + t option + +val vote : sender:Deku_ledger.Address.address -> vote:Vote.t -> t -> t + +val delegated_vote : + sender:Deku_ledger.Address.address -> + twitch_handle:Twitch_handle.t -> + vote:Vote.t -> + t -> + t option + +val new_round : t -> t +val get_decision : t -> Joypad.t option diff --git a/deku-p/src/core/protocol/operation.ml b/deku-p/src/core/protocol/operation.ml index 1b80d429b3..b7b5077c0c 100644 --- a/deku-p/src/core/protocol/operation.ml +++ b/deku-p/src/core/protocol/operation.ml @@ -13,9 +13,20 @@ type operation = ticket_id : Ticket_id.t; amount : Amount.t; } - | Operation_vm_transaction of { + | Operation_attest_twitch_handle of { sender : Address.t; - operation : Ocaml_wasm_vm.Operation_payload.t; + twitch_handle : string; + } + | Operation_attest_deku_address of { + sender : Address.t; + deku_address : Address.t; + twitch_handle : Game.Twitch_handle.t; + } + | Operation_vote of { sender : Address.t; vote : Game.Vote.t } + | Operation_delegated_vote of { + sender : Address.t; + twitch_handle : Game.Twitch_handle.t; + vote : Game.Vote.t; } | Operation_withdraw of { sender : Address.t; @@ -47,19 +58,56 @@ let encoding = | _ -> None) (fun ((), sender, receiver, ticket_id, amount) -> Operation_ticket_transfer { sender; receiver; ticket_id; amount }); - case ~title:"vm_transaction" (Tag 1) + case ~title:"attest_twitch_handle" (Tag 1) + (obj3 + (req "type" (constant "attest_twitch_handle")) + (req "sender" (Data_encoding.dynamic_size Address.encoding)) + (req "twitch_handle" string)) + (fun operation -> + match operation with + | Operation_attest_twitch_handle { sender; twitch_handle } -> + Some ((), sender, twitch_handle) + | _ -> None) + (fun ((), sender, twitch_handle) -> + Operation_attest_twitch_handle { sender; twitch_handle }); + case ~title:"attest_deku_address" (Tag 2) + (obj4 + (req "type" (constant "attest_deku_address")) + (req "sender" (Data_encoding.dynamic_size Address.encoding)) + (req "deku_address" (Data_encoding.dynamic_size Address.encoding)) + (req "twitch_handle" string)) + (fun operation -> + match operation with + | Operation_attest_deku_address + { sender; deku_address; twitch_handle } -> + Some ((), sender, deku_address, twitch_handle) + | _ -> None) + (fun ((), sender, deku_address, twitch_handle) -> + Operation_attest_deku_address { sender; deku_address; twitch_handle }); + case ~title:"vote" (Tag 3) (obj3 - (req "type" (constant "vm_transaction")) + (req "type" (constant "vote")) + (req "sender" (Data_encoding.dynamic_size Address.encoding)) + (req "vote" Game.Vote.encoding)) + (fun operation -> + match operation with + | Operation_vote { sender; vote } -> Some ((), sender, vote) + | _ -> None) + (fun ((), sender, vote) -> Operation_vote { sender; vote }); + case ~title:"delegated_vote" (Tag 4) + (obj4 + (req "type" (constant "delegated_vote")) (req "sender" (Data_encoding.dynamic_size Address.encoding)) - (req "operation" Ocaml_wasm_vm.Operation_payload.encoding)) + (req "twitch_handle" string) + (req "vote" Game.Vote.encoding)) (fun operation -> match operation with - | Operation_vm_transaction { sender; operation } -> - Some ((), sender, operation) + | Operation_delegated_vote { sender; twitch_handle; vote } -> + Some ((), sender, twitch_handle, vote) | _ -> None) - (fun ((), sender, operation) -> - Operation_vm_transaction { sender; operation }); - case ~title:"withdraw" (Tag 2) + (fun ((), sender, twitch_handle, vote) -> + Operation_delegated_vote { sender; twitch_handle; vote }); + case ~title:"withdraw" (Tag 5) (obj5 (req "type" (constant "withdraw")) (req "sender" (Data_encoding.dynamic_size Address.encoding)) @@ -73,7 +121,7 @@ let encoding = | _ -> None) (fun ((), sender, ticket_id, amount, owner) -> Operation_withdraw { sender; owner; ticket_id; amount }); - case ~title:"noop" (Tag 3) + case ~title:"noop" (Tag 6) (obj2 (req "type" (constant "noop")) (req "sender" Address.encoding)) (fun operation -> match operation with @@ -100,18 +148,28 @@ let%expect_test "Operation encoding" = let json = Data_encoding.Json.construct encoding op in Format.printf "%a\n---------\n%!" Data_encoding.Json.pp json in + show_op + (Operation_attest_twitch_handle + { sender = address; twitch_handle = "d4hines" }); + show_op + (Operation_attest_deku_address + { sender = address; deku_address = address; twitch_handle = "d4hines" }); + show_op + (Operation_vote + { sender = address; vote = Game.Vote.Input Deku_gameboy.Joypad.A }); + show_op + (Operation_vote + { sender = address; vote = Game.Vote.Governance Game.Anarchy }); + show_op + (Operation_delegated_vote + { + sender = address; + vote = Game.Vote.Input Deku_gameboy.Joypad.A; + twitch_handle = "d4hines"; + }); show_op @@ Operation_ticket_transfer { sender = address; receiver = address; ticket_id; amount = Amount.zero }; - - let operation = - let open Ocaml_wasm_vm in - let argument = Value.(Union (Left (Union (Right (Int (Z.of_int 5)))))) in - let operation = Operation.Call { address; argument } in - Operation_payload.{ operation; tickets = [ (ticket_id, Amount.zero) ] } - in - (* TODO: this one is a big ugly with nested "operation" keys. We should fix it. *) - show_op @@ Operation_vm_transaction { sender = address; operation }; show_op @@ Operation_withdraw { @@ -123,21 +181,30 @@ let%expect_test "Operation encoding" = show_op @@ Operation_noop { sender = address }; [%expect {| + { "type": "attest_twitch_handle", + "sender": "tz1UAxwRXXDvpZ5sAanbbP8tjKBoa2dxKUHE", + "twitch_handle": "d4hines" } + --------- + { "type": "attest_deku_address", + "sender": "tz1UAxwRXXDvpZ5sAanbbP8tjKBoa2dxKUHE", + "deku_address": "tz1UAxwRXXDvpZ5sAanbbP8tjKBoa2dxKUHE", + "twitch_handle": "d4hines" } + --------- + { "type": "vote", "sender": "tz1UAxwRXXDvpZ5sAanbbP8tjKBoa2dxKUHE", + "vote": "A" } + --------- + { "type": "vote", "sender": "tz1UAxwRXXDvpZ5sAanbbP8tjKBoa2dxKUHE", + "vote": "Anarchy" } + --------- + { "type": "delegated_vote", "sender": "tz1UAxwRXXDvpZ5sAanbbP8tjKBoa2dxKUHE", + "twitch_handle": "d4hines", "vote": "A" } + --------- { "type": "ticket_transfer", "sender": "tz1UAxwRXXDvpZ5sAanbbP8tjKBoa2dxKUHE", "receiver": "tz1UAxwRXXDvpZ5sAanbbP8tjKBoa2dxKUHE", "ticket_id": [ "KT1LiabSxPyVUmVZCqHneCFLJrqQcLHkmX9d", "68656c6c6f" ], "amount": "0" } --------- - { "type": "vm_transaction", "sender": "tz1UAxwRXXDvpZ5sAanbbP8tjKBoa2dxKUHE", - "operation": - { "operation": - { "address": "tz1UAxwRXXDvpZ5sAanbbP8tjKBoa2dxKUHE", - "argument": - [ "Union", [ "Left", [ "Union", [ "Right", [ "Int", "5" ] ] ] ] ] }, - "tickets": - [ [ [ "KT1LiabSxPyVUmVZCqHneCFLJrqQcLHkmX9d", "68656c6c6f" ], "0" ] ] } } - --------- { "type": "withdraw", "sender": "tz1UAxwRXXDvpZ5sAanbbP8tjKBoa2dxKUHE", "ticket_id": [ "KT1LiabSxPyVUmVZCqHneCFLJrqQcLHkmX9d", "68656c6c6f" ], "amount": "0", "owner": "tz1UAxwRXXDvpZ5sAanbbP8tjKBoa2dxKUHE" } @@ -172,6 +239,50 @@ module Initial = struct let binary = "\x80" ^ binary in Operation_hash.hash binary + let%expect_test "Hashing a Input Vote" = + let sender = + let secret = + Secret.Ed25519 + (Ed25519.Secret.of_b58 + "edsk3MVrH9TbnFw7VsbBZX2yMNv4ApszZecLpPqrigx3HNnsDwQGio" + |> Option.get) + in + Identity.make secret |> Identity.key_hash |> Address.of_key_hash + in + let open Deku_gameboy in + let nonce = Nonce.of_n N.one in + let level = Level.zero in + let operation = + Operation_attest_twitch_handle { sender; twitch_handle = "1337gmr" } + in + let operation_hash = hash ~nonce ~level ~operation in + Format.printf "Operation_attest_twitch_handle: %a\n" Operation_hash.pp + operation_hash; + let operation = + Operation_attest_deku_address + { sender; deku_address = sender; twitch_handle = "1337gmr" } + in + let operation_hash = hash ~nonce ~level ~operation in + Format.printf "Operation_attest_deku_address: %a\n" Operation_hash.pp + operation_hash; + let operation = Operation_vote { sender; vote = Input Joypad.A } in + let operation_hash = hash ~nonce ~level ~operation in + Format.printf "Operation_vote: %a\n" Operation_hash.pp operation_hash; + let operation = + Operation_delegated_vote + { sender; vote = Input Joypad.A; twitch_handle = "1337gmr" } + in + let operation_hash = hash ~nonce ~level ~operation in + Format.printf "Operation_delegated_vote: %a\n" Operation_hash.pp + operation_hash; + + [%expect + {| + Operation_attest_twitch_handle: Do3nPqZQPXSVq7JmtWmus5WTBYf8PcLzZqgcr9wrL34Z1absaW2r + Operation_attest_deku_address: Do41eSeJG6d5bAST6TWeESGuivoz8Qm3A6eRaTM5qMUvtHxAiKMN + Operation_vote: Do3bzgbxFUWpkKp65otHmNqGRDXEjUHHQVc9y3CPHkeNgHHcpDWb + Operation_delegated_vote: Do2w6q7qGjP4RjSYhcggYh6CRJwcLCFWJthvQcBWi5jwZNhvHSou |}] + let make ~nonce ~level ~operation = let hash = hash ~nonce ~level ~operation in Initial_operation { hash; nonce; level; operation } @@ -206,70 +317,44 @@ module Initial = struct let noop = make ~nonce ~level:Level.zero ~operation in show_initial noop; let operation = - let address = - Address.of_b58 "tz1UAxwRXXDvpZ5sAanbbP8tjKBoa2dxKUHE" |> Option.get - in - let contract_address = - Deku_tezos.Contract_hash.of_b58 "KT1LiabSxPyVUmVZCqHneCFLJrqQcLHkmX9d" - |> Option.get - in - let ticketer = Ticket_id.Tezos contract_address in - let ticket_id = Ticket_id.make ticketer (Bytes.of_string "hello") in - let open Ocaml_wasm_vm in - let argument = Value.(Union (Left (Union (Right (Int (Z.of_int 5)))))) in - let operation = Operation.Call { address; argument } in - let payload = - Operation_payload.{ operation; tickets = [ (ticket_id, Amount.zero) ] } - in - Operation_vm_transaction { sender = address; operation = payload } + Operation_delegated_vote + { + sender; + twitch_handle = "d4hines"; + vote = Game.Vote.Governance Game.Anarchy; + } in - (* TODO: triple-nested `operation` tags is pretty ugly. We should make it prettier. *) - let vm_operation = make ~nonce ~level:Level.zero ~operation in - show_initial vm_operation; + let vote_operation = make ~nonce ~level:Level.zero ~operation in + show_initial vote_operation; [%expect {| ------- Pretty: Operation.Initial.Initial_operation { - hash = Do2XVsHk8txd6V4YTt6io1nyJMHujD6APbhWWW64DwM3y8D5XxhF; + hash = Do3StPYpof6NyxvDj4GJhv6FXL84qHx9KWn6DJpJSQVC4M13QX5o; nonce = 0; level = 0; operation = Operation.Operation_noop { sender = (Address.Implicit tz1UAxwRXXDvpZ5sAanbbP8tjKBoa2dxKUHE)}} - Hex: 000000010000000001000300005d9ac49706a3566b65f1ad56dd1433e4569a0367 + Hex: 000000010000000001000600005d9ac49706a3566b65f1ad56dd1433e4569a0367 Json: { "nonce": "0", "level": 0, "operation": { "type": "noop", "sender": "tz1UAxwRXXDvpZ5sAanbbP8tjKBoa2dxKUHE" } } ------- Pretty: Operation.Initial.Initial_operation { - hash = Do2PvNgvfLPoj7WJoAMECmtPB5chhKuSbj2cmo9XF31bXRxoxheu; + hash = Do2nsjgRhfUYDJqNELzQSXyhqY11i78j1SSZwQn4CFY4Zw6hoyUQ; nonce = 0; level = 0; operation = - Operation.Operation_vm_transaction { + Operation.Operation_delegated_vote { sender = (Address.Implicit tz1UAxwRXXDvpZ5sAanbbP8tjKBoa2dxKUHE); - operation = - { Operation_payload.operation = - Operation.Call { - address = - (Address.Implicit tz1UAxwRXXDvpZ5sAanbbP8tjKBoa2dxKUHE); - argument = - (Value.V.Union - (Value.V.Left (Value.V.Union (Value.V.Right 5))))}; - tickets = }}} - Hex: 00000001000000000100010000001600005d9ac49706a3566b65f1ad56dd1433e4569a036700000029010000001600005d9ac49706a3566b65f1ad56dd1433e4569a036709000000090f09000000031000050000002300851badd1d782c28269474322b2662d7774545bf50000000568656c6c6f0000000100 + twitch_handle = "d4hines"; + vote = (Game.Vote.Governance Game.Anarchy)}} + Hex: 00000001000000000100040000001600005d9ac49706a3566b65f1ad56dd1433e4569a036700000007643468696e6573000000000100 Json: { "nonce": "0", "level": 0, "operation": - { "type": "vm_transaction", + { "type": "delegated_vote", "sender": "tz1UAxwRXXDvpZ5sAanbbP8tjKBoa2dxKUHE", - "operation": - { "operation": - { "address": "tz1UAxwRXXDvpZ5sAanbbP8tjKBoa2dxKUHE", - "argument": - [ "Union", - [ "Left", [ "Union", [ "Right", [ "Int", "5" ] ] ] ] ] }, - "tickets": - [ [ [ "KT1LiabSxPyVUmVZCqHneCFLJrqQcLHkmX9d", - "68656c6c6f" ], "0" ] ] } } } |}] + "twitch_handle": "d4hines", "vote": "Anarchy" } } |}] let includable_operation_window = Deku_constants.includable_operation_window @@ -320,7 +405,10 @@ module Signed = struct let sender = match operation with | Operation_ticket_transfer { sender; _ } -> sender - | Operation_vm_transaction { sender; _ } -> sender + | Operation_attest_twitch_handle { sender; _ } -> sender + | Operation_attest_deku_address { sender; _ } -> sender + | Operation_vote { sender; _ } -> sender + | Operation_delegated_vote { sender; _ } -> sender | Operation_withdraw { sender; _ } -> sender | Operation_noop { sender } -> sender in @@ -362,9 +450,29 @@ module Signed = struct let initial = Initial.make ~nonce ~level ~operation in make ~identity ~initial - let vm_transaction ~nonce ~level ~content ~identity = + let vote ~nonce ~level ~vote ~identity = + let sender = Address.of_key_hash (Identity.key_hash identity) in + let operation = Operation_vote { sender; vote } in + let initial = Initial.make ~nonce ~level ~operation in + make ~identity ~initial + + let delegated_vote ~nonce ~level ~vote ~twitch_handle ~identity = + let sender = Address.of_key_hash (Identity.key_hash identity) in + let operation = Operation_delegated_vote { sender; twitch_handle; vote } in + let initial = Initial.make ~nonce ~level ~operation in + make ~identity ~initial + + let attest_twitch_handle ~nonce ~level ~twitch_handle ~identity = + let sender = Address.of_key_hash (Identity.key_hash identity) in + let operation = Operation_attest_twitch_handle { sender; twitch_handle } in + let initial = Initial.make ~nonce ~level ~operation in + make ~identity ~initial + + let attest_deku_address ~nonce ~level ~deku_address ~twitch_handle ~identity = let sender = Address.of_key_hash (Identity.key_hash identity) in - let operation = Operation_vm_transaction { sender; operation = content } in + let operation = + Operation_attest_deku_address { sender; deku_address; twitch_handle } + in let initial = Initial.make ~nonce ~level ~operation in make ~identity ~initial end diff --git a/deku-p/src/core/protocol/operation.mli b/deku-p/src/core/protocol/operation.mli index eda6bfcfcd..3dc3aab532 100644 --- a/deku-p/src/core/protocol/operation.mli +++ b/deku-p/src/core/protocol/operation.mli @@ -5,16 +5,27 @@ open Deku_ledger exception Invalid_signature exception Invalid_source -type operation = private +type operation = | Operation_ticket_transfer of { sender : Address.t; receiver : Address.t; ticket_id : Ticket_id.t; amount : Amount.t; } - | Operation_vm_transaction of { + | Operation_attest_twitch_handle of { sender : Address.t; - operation : Ocaml_wasm_vm.Operation_payload.t; + twitch_handle : string; + } + | Operation_attest_deku_address of { + sender : Address.t; + deku_address : Address.t; + twitch_handle : string; + } + | Operation_vote of { sender : Address.t; vote : Game.Vote.t } + | Operation_delegated_vote of { + sender : Address.t; + twitch_handle : Game.Twitch_handle.t; + vote : Game.Vote.t; } | Operation_withdraw of { sender : Address.t; @@ -86,13 +97,36 @@ module Signed : sig amount:Amount.t -> signed_operation - val vm_transaction : + val noop : + identity:Identity.t -> nonce:Nonce.t -> level:Level.t -> signed_operation + + val vote : nonce:Nonce.t -> level:Level.t -> - content:Ocaml_wasm_vm.Operation_payload.t -> + vote:Game.Vote.t -> identity:Identity.t -> signed_operation - val noop : - identity:Identity.t -> nonce:Nonce.t -> level:Level.t -> signed_operation + val delegated_vote : + nonce:Nonce.t -> + level:Level.t -> + vote:Game.Vote.t -> + twitch_handle:string -> + identity:Identity.t -> + signed_operation + + val attest_twitch_handle : + nonce:Nonce.t -> + level:Level.t -> + twitch_handle:string -> + identity:Identity.t -> + signed_operation + + val attest_deku_address : + nonce:Nonce.t -> + level:Level.t -> + deku_address:Address.t -> + twitch_handle:string -> + identity:Identity.t -> + signed_operation end diff --git a/deku-p/src/core/protocol/protocol.ml b/deku-p/src/core/protocol/protocol.ml index 3008487c82..b5111d1395 100644 --- a/deku-p/src/core/protocol/protocol.ml +++ b/deku-p/src/core/protocol/protocol.ml @@ -8,7 +8,8 @@ type protocol = included_operations : Included_operation_set.t; included_tezos_operations : Deku_tezos.Tezos_operation_hash.Set.t; ledger : Ledger.t; - vm_state : Ocaml_wasm_vm.State.t; + vm_state : Deku_gameboy.state; + game : Game.t; } and t = protocol @@ -17,37 +18,49 @@ let encoding = let open Data_encoding in conv (fun (Protocol - { included_operations; included_tezos_operations; ledger; vm_state }) -> - (included_operations, included_tezos_operations, ledger, vm_state)) - (fun (included_operations, included_tezos_operations, ledger, vm_state) -> + { + included_operations; + included_tezos_operations; + ledger; + vm_state; + game; + }) -> + (included_operations, included_tezos_operations, ledger, vm_state, game)) + (fun (included_operations, included_tezos_operations, ledger, vm_state, game) + -> Protocol - { included_operations; included_tezos_operations; ledger; vm_state }) - (tup4 Included_operation_set.encoding + { + included_operations; + included_tezos_operations; + ledger; + vm_state; + game; + }) + (tup5 Included_operation_set.encoding Deku_tezos.Tezos_operation_hash.Set.encoding Ledger.encoding - Ocaml_wasm_vm.State.encoding) + Deku_gameboy.state_encoding Game.encoding) -let initial = +let initial ?twitch_oracle_address () = Protocol { included_operations = Included_operation_set.empty; included_tezos_operations = Deku_tezos.Tezos_operation_hash.Set.empty; ledger = Ledger.initial; - vm_state = Ocaml_wasm_vm.State.empty; + vm_state = Deku_gameboy.empty; + game = Game.empty ?twitch_oracle_address (); } -let initial_with_vm_state ~vm_state = - let (Protocol - { included_operations; included_tezos_operations; ledger; vm_state = _ }) - = - initial - in - Protocol { included_operations; included_tezos_operations; ledger; vm_state } - let apply_operation ~current_level protocol operation : (t * Receipt.t option * exn option) option = let open Operation.Initial in let (Protocol - { included_operations; ledger; included_tezos_operations; vm_state }) = + { + included_operations; + ledger; + included_tezos_operations; + vm_state; + game; + }) = protocol in let (Initial_operation { hash; nonce = _; level; operation = content }) = @@ -63,86 +76,53 @@ let apply_operation ~current_level protocol operation : let included_operations = Included_operation_set.add operation included_operations in - let ledger, receipt, vm_state, error = + let ledger, receipt, game, error = match content with | Operation_ticket_transfer { sender; receiver; ticket_id; amount } -> ( let receipt = Ticket_transfer_receipt { operation = hash } in match Ledger.transfer ~sender ~receiver ~ticket_id ~amount ledger with - | Ok ledger -> (ledger, Some receipt, vm_state, None) - | Error error -> (ledger, Some receipt, vm_state, Some error)) - | Operation_vm_transaction { sender; operation } -> ( - let open Deku_ledger in - let receipt = - match operation.operation with - | View { address; _ } -> - Receipt.Vm_transaction_receipt - { operation = hash; contract_address = address } - | Originate _ -> - (* TODO: wrap this in a function exposed from the VM *) - let contract_address = - Contract_address.of_user_operation_hash - @@ Operation_hash.to_blake2b hash - in - Receipt.Vm_origination_receipt - { operation = hash; contract_address } - | Call { address; _ } -> ( - match Deku_ledger.Address.to_contract_address address with - | Some (contract_address, _) -> - Receipt.Vm_origination_receipt - { operation = hash; contract_address } - | None -> - (* TODO: in the future we should return a Vm_trnasaction_error here *) - failwith - (Format.sprintf "Invalid contract address '%s'" - (Address.to_b58 address))) - in - let result () = - let%ok ledger = - if operation.tickets = [] then Ok ledger - else - Ledger.with_ticket_table ledger (fun ~get_table ~set_table -> - let tickets = operation.tickets in - let result = - Ticket_table.take_tickets ~sender ~ticket_ids:tickets - (get_table ()) - in - match result with - | Ok (_, table) -> Ok (set_table table) - | Error err -> ( - match err with - | `Insufficient_funds -> Error "Insufficient_funds")) - in - let source = sender in - match - Ocaml_wasm_vm.( - Env.execute - (Env.make ~state:vm_state ~ledger ~sender ~source) - ~operation:operation.operation - ~tickets: - (List.map - (fun (k, v) -> (k, Deku_concepts.Amount.to_n v)) - operation.tickets) - ~operation_hash:(Operation_hash.to_blake2b hash) - |> Env.finalize) - with - | Ok (vm_state, ledger) -> - Ok (ledger, Some receipt, vm_state, None) - | Error err -> Error err - in - match result () with - | Ok result -> result - | Error err -> + | Ok ledger -> (ledger, Some receipt, game, None) + | Error error -> (ledger, Some receipt, game, Some error)) + | Operation_attest_twitch_handle { sender; twitch_handle } -> + let game = Game.attest_twitch_handle ~sender ~twitch_handle game in + ( ledger, + Some + (Receipt.Attest_twitch_handle + { operation = hash; deku_address = sender; twitch_handle }), + game, + None ) + | Operation_attest_deku_address { sender; deku_address; twitch_handle } + -> ( + match + Game.attest_deku_address ~sender ~deku_address ~twitch_handle game + with + | Some game -> + ( ledger, + Some + (Receipt.Attest_deku_address + { operation = hash; deku_address; twitch_handle }), + game, + None ) + | None -> (ledger, None, game, None)) + | Operation_vote { sender; vote } -> + let game = Game.vote ~sender ~vote game in + ( ledger, + Some (Receipt.Game_vote { operation = hash; sender; vote }), + game, + None ) + | Operation_delegated_vote { sender; twitch_handle; vote } -> ( + match Game.delegated_vote ~sender ~twitch_handle ~vote game with + | Some game -> ( ledger, - Some receipt, - vm_state, Some - (Failure - (Format.sprintf - "Error while executing external vm transaction : %s" - err)) )) - | Operation_noop { sender = _ } -> (ledger, None, vm_state, None) + (Receipt.Delegated_game_vote + { operation = hash; delegator = twitch_handle; vote }), + game, + None ) + | None -> (ledger, None, game, None)) + | Operation_noop { sender = _ } -> (ledger, None, game, None) | Operation_withdraw { sender; owner; amount; ticket_id } -> ( match Ledger.withdraw ~sender ~destination:owner ~amount ~ticket_id @@ -151,20 +131,32 @@ let apply_operation ~current_level protocol operation : | Ok (ledger, handle) -> ( ledger, Some (Withdraw_receipt { handle; operation = hash }), - vm_state, + game, None ) - | Error error -> (ledger, None, vm_state, Some error)) + | Error error -> (ledger, None, game, Some error)) in Some ( Protocol - { included_operations; included_tezos_operations; ledger; vm_state }, + { + included_operations; + included_tezos_operations; + ledger; + vm_state; + game; + }, receipt, error ) | false -> None let apply_tezos_operation protocol tezos_operation = let (Protocol - { included_operations; included_tezos_operations; ledger; vm_state }) = + { + included_operations; + included_tezos_operations; + ledger; + vm_state; + game; + }) = protocol in let Tezos_operation.{ hash; operations } = tezos_operation in @@ -177,7 +169,13 @@ let apply_tezos_operation protocol tezos_operation = in let protocol = Protocol - { included_operations; included_tezos_operations; ledger; vm_state } + { + included_operations; + included_tezos_operations; + ledger; + vm_state; + game; + } in List.fold_left (fun protocol tezos_operation -> @@ -189,6 +187,7 @@ let apply_tezos_operation protocol tezos_operation = included_operations; included_tezos_operations; vm_state; + game; }) = protocol in @@ -203,6 +202,7 @@ let apply_tezos_operation protocol tezos_operation = included_operations; included_tezos_operations; vm_state; + game; }) protocol operations | false -> protocol @@ -240,13 +240,20 @@ let apply_payload ~current_level ~payload protocol = let clean ~current_level protocol = let (Protocol - { included_operations; included_tezos_operations; ledger; vm_state }) = + { + included_operations; + included_tezos_operations; + ledger; + vm_state; + game; + }) = protocol in let included_operations = Included_operation_set.drop ~current_level included_operations in - Protocol { included_operations; included_tezos_operations; ledger; vm_state } + Protocol + { included_operations; included_tezos_operations; ledger; vm_state; game } let prepare ~parallel ~payload = parallel parse_operation payload @@ -254,6 +261,21 @@ let apply ~current_level ~payload ~tezos_operations protocol = let protocol, receipts, errors = apply_payload ~current_level ~payload protocol in + let (Protocol + { + included_operations; + included_tezos_operations; + ledger; + vm_state; + game; + }) = + protocol + in + let game = Game.new_round game in + let protocol = + Protocol + { included_operations; included_tezos_operations; ledger; vm_state; game } + in let protocol = clean ~current_level protocol in (* TODO: how to clean the set of tezos operations in memory? *) let protocol = apply_tezos_operations tezos_operations protocol in diff --git a/deku-p/src/core/protocol/protocol.mli b/deku-p/src/core/protocol/protocol.mli index 81c3a0f2ad..e7cf502e9d 100644 --- a/deku-p/src/core/protocol/protocol.mli +++ b/deku-p/src/core/protocol/protocol.mli @@ -1,20 +1,19 @@ open Deku_concepts -open Deku_tezos open Deku_ledger type protocol = private | Protocol of { included_operations : Included_operation_set.t; - included_tezos_operations : Tezos_operation_hash.Set.t; + included_tezos_operations : Deku_tezos.Tezos_operation_hash.Set.t; ledger : Ledger.t; - vm_state : Ocaml_wasm_vm.State.t; + vm_state : Deku_gameboy.state; + game : Game.t; } type t = protocol val encoding : protocol Data_encoding.t -val initial : protocol -val initial_with_vm_state : vm_state:Ocaml_wasm_vm.State.t -> protocol +val initial : ?twitch_oracle_address:Address.t -> unit -> protocol val prepare : parallel: diff --git a/deku-p/src/core/protocol/receipt.ml b/deku-p/src/core/protocol/receipt.ml index 4546033fc1..36954c53f3 100644 --- a/deku-p/src/core/protocol/receipt.ml +++ b/deku-p/src/core/protocol/receipt.ml @@ -1,22 +1,34 @@ open Deku_ledger +open Game type receipt = | Ticket_transfer_receipt of { operation : Operation_hash.t } | Withdraw_receipt of { operation : Operation_hash.t; - handle : Ledger.Withdrawal_handle.t; + handle : Ledger.Withdrawal_handle.t; [@opaque] } - | Vm_origination_receipt of { + | Attest_twitch_handle of { operation : Operation_hash.t; - contract_address : Deku_ledger.Contract_address.t; + deku_address : Address.t; + twitch_handle : Game.Twitch_handle.t; } - | Vm_transaction_receipt of { + | Attest_deku_address of { operation : Operation_hash.t; - contract_address : Deku_ledger.Contract_address.t; + deku_address : Address.t; + twitch_handle : Game.Twitch_handle.t; + } + | Game_vote of { + operation : Operation_hash.t; + sender : Address.t; + vote : Game.Vote.t; + } + | Delegated_game_vote of { + operation : Operation_hash.t; + delegator : Game.Twitch_handle.t; + vote : Game.Vote.t; } - | Vm_transaction_error of { operation : Operation_hash.t; message : string } -and t = receipt [@@deriving eq] +and t = receipt [@@deriving eq, show] let encoding = let open Data_encoding in @@ -37,33 +49,50 @@ let encoding = | Withdraw_receipt { operation; handle } -> Some (operation, handle) | _ -> None) (fun (operation, handle) -> Withdraw_receipt { operation; handle }); - case ~title:"vm_origination" (Tag 2) + case ~title:"attest_twitch_handle" (Tag 2) (Data_encoding.dynamic_size - (tup2 Operation_hash.encoding Deku_ledger.Contract_address.encoding)) + (tup3 Operation_hash.encoding + (dynamic_size Address.encoding) + (dynamic_size Twitch_handle.encoding))) (fun receipt -> match receipt with - | Vm_origination_receipt { operation; contract_address } -> - Some (operation, contract_address) + | Attest_twitch_handle { operation; deku_address; twitch_handle } -> + Some (operation, deku_address, twitch_handle) | _ -> None) - (fun (operation, contract_address) -> - Vm_origination_receipt { operation; contract_address }); - case ~title:"vm_transaction" (Tag 3) + (fun (operation, deku_address, twitch_handle) -> + Attest_twitch_handle { operation; deku_address; twitch_handle }); + case ~title:"attest_deku_address" (Tag 3) (Data_encoding.dynamic_size - (tup2 Operation_hash.encoding Deku_ledger.Contract_address.encoding)) + (tup3 Operation_hash.encoding + (dynamic_size Address.encoding) + (dynamic_size Twitch_handle.encoding))) (fun receipt -> match receipt with - | Vm_transaction_receipt { operation; contract_address } -> - Some (operation, contract_address) + | Attest_deku_address { operation; deku_address; twitch_handle } -> + Some (operation, deku_address, twitch_handle) | _ -> None) - (fun (operation, contract_address) -> - Vm_transaction_receipt { operation; contract_address }); - case ~title:"vm_transaction_error" (Tag 4) - (Data_encoding.dynamic_size (tup2 Operation_hash.encoding string)) + (fun (operation, deku_address, twitch_handle) -> + Attest_twitch_handle { operation; deku_address; twitch_handle }); + case ~title:"game_vote" (Tag 4) + (Data_encoding.dynamic_size + (tup3 Operation_hash.encoding + (dynamic_size Address.encoding) + (dynamic_size Game.Vote.encoding))) + (fun receipt -> + match receipt with + | Game_vote { operation; sender; vote } -> + Some (operation, sender, vote) + | _ -> None) + (fun (operation, sender, vote) -> Game_vote { operation; sender; vote }); + case ~title:"delegated_game_vote" (Tag 5) + (Data_encoding.dynamic_size + (tup3 Operation_hash.encoding Twitch_handle.encoding + Game.Vote.encoding)) (fun receipt -> match receipt with - | Vm_transaction_error { operation; message } -> - Some (operation, message) + | Delegated_game_vote { operation; delegator; vote } -> + Some (operation, delegator, vote) | _ -> None) - (fun (operation, message) -> - Vm_transaction_error { operation; message }); + (fun (operation, delegator, vote) -> + Delegated_game_vote { operation; delegator; vote }); ] diff --git a/deku-p/src/core/protocol/receipt.mli b/deku-p/src/core/protocol/receipt.mli index 777ae9e8f5..7967785487 100644 --- a/deku-p/src/core/protocol/receipt.mli +++ b/deku-p/src/core/protocol/receipt.mli @@ -4,18 +4,29 @@ type receipt = | Ticket_transfer_receipt of { operation : Operation_hash.t } | Withdraw_receipt of { operation : Operation_hash.t; - handle : Ledger.Withdrawal_handle.t; + handle : Ledger.Withdrawal_handle.t; [@opaque] } - | Vm_origination_receipt of { + | Attest_twitch_handle of { operation : Operation_hash.t; - contract_address : Deku_ledger.Contract_address.t; + deku_address : Address.t; + twitch_handle : Game.Twitch_handle.t; } - | Vm_transaction_receipt of { + | Attest_deku_address of { operation : Operation_hash.t; - contract_address : Deku_ledger.Contract_address.t; + deku_address : Address.t; + twitch_handle : Game.Twitch_handle.t; + } + | Game_vote of { + operation : Operation_hash.t; + sender : Address.t; + vote : Game.Vote.t; + } + | Delegated_game_vote of { + operation : Operation_hash.t; + delegator : Game.Twitch_handle.t; + vote : Game.Vote.t; } - | Vm_transaction_error of { operation : Operation_hash.t; message : string } -type t = receipt [@@deriving eq] +type t = receipt [@@deriving eq, show] val encoding : receipt Data_encoding.t diff --git a/deku-p/src/core/protocol/test_protocol_game.ml b/deku-p/src/core/protocol/test_protocol_game.ml new file mode 100644 index 0000000000..49caf6a626 --- /dev/null +++ b/deku-p/src/core/protocol/test_protocol_game.ml @@ -0,0 +1,119 @@ +open Deku_stdlib +open Deku_crypto +open Deku_concepts +open Deku_gameboy +open Deku_ledger +open Protocol + +let level_of_int i = Z.of_int i |> N.of_z |> Option.get |> Level.of_n + +let identity secret = + let secret = Ed25519.Secret.of_b58 secret |> Option.get in + let secret = Secret.Ed25519 secret in + Identity.make secret + +let alice = identity "edsk3MVrH9TbnFw7VsbBZX2yMNv4ApszZecLpPqrigx3HNnsDwQGio" +let bob = identity "edsk4At2eerMsJdimc78faX2ryQch6LceH1MzTvnWifJSKpvAx3THD" + +let twitch_oracle = + identity "edsk2ticWQE2Lsk4V8q4fP4RsfKzKXv2hFMTTzoWVUP2yFgJGpFrve" + +let show_game protocol = + let (Protocol { game; _ }) = protocol in + let game_json = + Data_encoding.Json.construct Game.encoding game + |> Data_encoding.Json.to_string + in + Format.printf "%s\n" game_json + +let counter = ref 0 + +let get_nonce () = + counter := !counter + 1; + Z.of_int !counter |> N.of_z |> Option.get |> Nonce.of_n + +let%expect_test "delegated workflow happy path" = + let current_level = level_of_int 0 in + let deku_address = Identity.key_hash alice |> Address.of_key_hash in + let (Signed_operation { initial = op1; _ }) = + Operation.Signed.attest_twitch_handle ~nonce:(get_nonce ()) + ~level:current_level ~twitch_handle:"gamergirl47" ~identity:alice + in + let (Signed_operation { initial = op2; _ }) = + Operation.Signed.attest_deku_address ~nonce:(get_nonce ()) + ~level:current_level ~deku_address ~twitch_handle:"gamergirl47" + ~identity:twitch_oracle + in + let (Signed_operation { initial = op3; _ }) = + Operation.Signed.delegated_vote ~nonce:(get_nonce ()) ~level:current_level + ~vote:(Game.Vote.Input Joypad.A) ~twitch_handle:"gamergirl47" + ~identity:twitch_oracle + in + let twitch_oracle_address = + Identity.key_hash twitch_oracle |> Address.of_key_hash + in + let protocol = Protocol.initial ~twitch_oracle_address () in + let payload = [ op1; op2; op3 ] in + print_endline "After applying operations:"; + let protocol, _, _ = + apply ~current_level ~payload ~tezos_operations:[] protocol + in + let current_level = Level.next current_level in + show_game protocol; + let protocol, _, _ = + apply ~current_level ~payload:[] ~tezos_operations:[] protocol + in + print_endline "After another block:"; + show_game protocol; + [%expect + {| + After applying operations: + [ "tz1bPULi5xw5zSm2KQhaFf3p8QBpwoGaaFYG", "Democracy", [], + [ [ "tz1XxLeiKfdARmtxjTCGRTL15XDcyQeYnKW5", "gamergirl47" ] ], + [ [ "gamergirl47", "tz1XxLeiKfdARmtxjTCGRTL15XDcyQeYnKW5" ] ], null, "A" ] + After another block: + [ "tz1bPULi5xw5zSm2KQhaFf3p8QBpwoGaaFYG", "Democracy", [], + [ [ "tz1XxLeiKfdARmtxjTCGRTL15XDcyQeYnKW5", "gamergirl47" ] ], + [ [ "gamergirl47", "tz1XxLeiKfdARmtxjTCGRTL15XDcyQeYnKW5" ] ], null, null ] |}] + +let%expect_test "path" = + let current_level = level_of_int 0 in + let deku_address = Identity.key_hash alice |> Address.of_key_hash in + let (Signed_operation { initial = op1; _ }) = + Operation.Signed.attest_twitch_handle ~nonce:(get_nonce ()) + ~level:current_level ~twitch_handle:"gamergirl47" ~identity:alice + in + let (Signed_operation { initial = op2; _ }) = + Operation.Signed.attest_deku_address ~nonce:(get_nonce ()) + ~level:current_level ~deku_address ~twitch_handle:"gamergirl47" + ~identity:twitch_oracle + in + let (Signed_operation { initial = op3; _ }) = + Operation.Signed.delegated_vote ~nonce:(get_nonce ()) ~level:current_level + ~vote:(Game.Vote.Input Joypad.A) ~twitch_handle:"gamergirl47" + ~identity:twitch_oracle + in + let twitch_oracle_address = + Identity.key_hash twitch_oracle |> Address.of_key_hash + in + let protocol = Protocol.initial ~twitch_oracle_address () in + let payload = [ op1; op2; op3 ] in + let protocol, _, _ = + apply ~current_level ~payload ~tezos_operations:[] protocol + in + let current_level = Level.next current_level in + show_game protocol; + let protocol, _, _ = + apply ~current_level ~payload:[] ~tezos_operations:[] protocol + in + print_endline "After another block:"; + show_game protocol; + [%expect + {| + [ "tz1bPULi5xw5zSm2KQhaFf3p8QBpwoGaaFYG", "Democracy", [], + [ [ "tz1XxLeiKfdARmtxjTCGRTL15XDcyQeYnKW5", "gamergirl47" ] ], + [ [ "gamergirl47", "tz1XxLeiKfdARmtxjTCGRTL15XDcyQeYnKW5" ] ], null, "A" ] + After another block: + [ "tz1bPULi5xw5zSm2KQhaFf3p8QBpwoGaaFYG", "Democracy", [], + [ [ "tz1XxLeiKfdARmtxjTCGRTL15XDcyQeYnKW5", "gamergirl47" ] ], + [ [ "gamergirl47", "tz1XxLeiKfdARmtxjTCGRTL15XDcyQeYnKW5" ] ], null, null ] |}] diff --git a/deku-p/src/core/protocol/tests/deku_protocol_tests.ml b/deku-p/src/core/protocol/tests/deku_protocol_tests.ml index d8ef958fbf..e5e6f2b6f8 100644 --- a/deku-p/src/core/protocol/tests/deku_protocol_tests.ml +++ b/deku-p/src/core/protocol/tests/deku_protocol_tests.ml @@ -1,4 +1,4 @@ let () = Test_ledger.run (); - Test_operation.run (); - Test_protocol.run () + Test_operation.run () +(* Test_protocol.run () *) diff --git a/deku-p/src/core/protocol/tests/test_protocol.ml b/deku-p/src/core/protocol/tests/test_protocol.ml index 0ef59c58c2..3e34d8af0a 100644 --- a/deku-p/src/core/protocol/tests/test_protocol.ml +++ b/deku-p/src/core/protocol/tests/test_protocol.ml @@ -1,348 +1,348 @@ -open Deku_concepts -open Deku_crypto -open Deku_stdlib -open Deku_protocol -open Deku_constants -open Deku_ledger +(* open Deku_concepts + open Deku_crypto + open Deku_stdlib + open Deku_protocol + open Deku_constants + open Deku_ledger -let alice_secret = - Secret.of_b58 "edsk3EYk77QNx9HM4YDh5rv5nzBL68z2YGtBUGXhkw3rMhB2eCNvHf" - |> Option.get + let alice_secret = + Secret.of_b58 "edsk3EYk77QNx9HM4YDh5rv5nzBL68z2YGtBUGXhkw3rMhB2eCNvHf" + |> Option.get -let alice = Identity.make alice_secret + let alice = Identity.make alice_secret -let bob_secret = - Secret.of_b58 "edsk4Qejwxwj7JD93B45gvhYHVfMzNjkBWRQDaYkdt5JcUWLT4VDkh" - |> Option.get + let bob_secret = + Secret.of_b58 "edsk4Qejwxwj7JD93B45gvhYHVfMzNjkBWRQDaYkdt5JcUWLT4VDkh" + |> Option.get -let bob = Identity.make bob_secret + let bob = Identity.make bob_secret -let ticket_id = - let address = - Deku_tezos.Contract_hash.of_b58 "KT1JQ5JQB4P1c8U8ACxfnodtZ4phDVMSDzgi" - |> Option.get - in - let data = Bytes.of_string "" in - Ticket_id.make (Tezos address) data + let ticket_id = + let address = + Deku_tezos.Contract_hash.of_b58 "KT1JQ5JQB4P1c8U8ACxfnodtZ4phDVMSDzgi" + |> Option.get + in + let data = Bytes.of_string "" in + Ticket_id.make (Tezos address) data -(* helper to create in an easy way an operation that transfer n token from alice to bob *) -let make_operation ?(nonce = 1) ?(level = 0) ?(amount = 0) () = - let level = Level.of_n (N.of_z (Z.of_int level) |> Option.get) in - let nonce = Nonce.of_n (N.of_z (Z.of_int nonce) |> Option.get) in - let amount = Amount.of_n (N.of_z (Z.of_int amount) |> Option.get) in - let operation = - Operation.Signed.ticket_transfer ~identity:alice ~level ~nonce - ~receiver:(Address.of_key_hash (Identity.key_hash bob)) - ~ticket_id ~amount - in - let operation_str = - Data_encoding.Binary.to_string_exn Operation.Signed.encoding operation - in - let (Signed_operation { initial = Initial_operation { hash; _ }; _ }) = - operation - in - (operation, operation_str, hash) + (* helper to create in an easy way an operation that transfer n token from alice to bob *) + let make_operation ?(nonce = 1) ?(level = 0) ?(amount = 0) () = + let level = Level.of_n (N.of_z (Z.of_int level) |> Option.get) in + let nonce = Nonce.of_n (N.of_z (Z.of_int nonce) |> Option.get) in + let amount = Amount.of_n (N.of_z (Z.of_int amount) |> Option.get) in + let operation = + Operation.Signed.ticket_transfer ~identity:alice ~level ~nonce + ~receiver:(Address.of_key_hash (Identity.key_hash bob)) + ~ticket_id ~amount + in + let operation_str = + Data_encoding.Binary.to_string_exn Operation.Signed.encoding operation + in + let (Signed_operation { initial = Initial_operation { hash; _ }; _ }) = + operation + in + (operation, operation_str, hash) -(* The parallel function given to the Protocol.apply *) -let parallel = List.filter_map + (* The parallel function given to the Protocol.apply *) + let parallel = List.filter_map -let assert_all_were_applied_with_receipts ~operations ~protocol ~receipts = - let (Protocol.Protocol { included_operations; _ }) = protocol in - let operations = - List.map - (fun (Operation.Signed.Signed_operation { initial; _ }) -> initial) - operations - in - let all_ops_are_included = - operations - |> List.for_all (fun op -> - Included_operation_set.mem op included_operations) - in - let op_hashes = - List.map - (fun (Operation.Initial.Initial_operation { hash; _ }) -> hash) - operations - |> List.sort Operation_hash.compare - in - let[@warning "-8"] op_hashes_from_receipts = - List.map - (fun (Receipt.Ticket_transfer_receipt { operation; _ }) -> operation) - receipts - |> List.sort Operation_hash.compare - in - let all_receipts_have_hashes_and_vice_versa = - List.equal Operation_hash.equal op_hashes op_hashes_from_receipts - in - all_receipts_have_hashes_and_vice_versa && all_ops_are_included + let assert_all_were_applied_with_receipts ~operations ~protocol ~receipts = + let (Protocol.Protocol { included_operations; _ }) = protocol in + let operations = + List.map + (fun (Operation.Signed.Signed_operation { initial; _ }) -> initial) + operations + in + let all_ops_are_included = + operations + |> List.for_all (fun op -> + Included_operation_set.mem op included_operations) + in + let op_hashes = + List.map + (fun (Operation.Initial.Initial_operation { hash; _ }) -> hash) + operations + |> List.sort Operation_hash.compare + in + let[@warning "-8"] op_hashes_from_receipts = + List.map + (fun (Receipt.Ticket_transfer_receipt { operation; _ }) -> operation) + receipts + |> List.sort Operation_hash.compare + in + let all_receipts_have_hashes_and_vice_versa = + List.equal Operation_hash.equal op_hashes op_hashes_from_receipts + in + all_receipts_have_hashes_and_vice_versa && all_ops_are_included -let test_apply_one_operation () = - let op, op_str, _ = make_operation () in - let protocol, receipts, _errors = - let payload = Protocol.prepare ~parallel ~payload:[ op_str ] in - Protocol.apply ~current_level:Level.zero ~payload ~tezos_operations:[] - Protocol.initial - in - Alcotest.(check bool) - "operation is included" true - (assert_all_were_applied_with_receipts ~operations:[ op ] ~receipts - ~protocol) + let test_apply_one_operation () = + let op, op_str, _ = make_operation () in + let protocol, receipts, _errors = + let payload = Protocol.prepare ~parallel ~payload:[ op_str ] in + Protocol.apply ~current_level:Level.zero ~payload ~tezos_operations:[] + Protocol.initial + in + Alcotest.(check bool) + "operation is included" true + (assert_all_were_applied_with_receipts ~operations:[ op ] ~receipts + ~protocol) -let test_many_operations () = - (* Creates a list of 10 different operations *) - let operations, payload, _op_hashes = - List.init 10 (fun nonce -> make_operation ~nonce ()) - |> List.fold_left - (fun (ops, payload, hashes) (op, op_str, op_hash) -> - (op :: ops, op_str :: payload, op_hash :: hashes)) - ([], [], []) - in - let protocol, receipts, _errors = - let payload = Protocol.prepare ~parallel ~payload in - Protocol.apply ~current_level:Level.zero ~payload ~tezos_operations:[] - Protocol.initial - in - Alcotest.(check bool) - "all operations have receipts and vice versa" true - (assert_all_were_applied_with_receipts ~operations ~protocol ~receipts) + let test_many_operations () = + (* Creates a list of 10 different operations *) + let operations, payload, _op_hashes = + List.init 10 (fun nonce -> make_operation ~nonce ()) + |> List.fold_left + (fun (ops, payload, hashes) (op, op_str, op_hash) -> + (op :: ops, op_str :: payload, op_hash :: hashes)) + ([], [], []) + in + let protocol, receipts, _errors = + let payload = Protocol.prepare ~parallel ~payload in + Protocol.apply ~current_level:Level.zero ~payload ~tezos_operations:[] + Protocol.initial + in + Alcotest.(check bool) + "all operations have receipts and vice versa" true + (assert_all_were_applied_with_receipts ~operations ~protocol ~receipts) -let test_duplicated_operation_same_level () = - let _, op_str, hash = make_operation () in + let test_duplicated_operation_same_level () = + let _, op_str, hash = make_operation () in - let _, receipts, _errors = - let payload = Protocol.prepare ~parallel ~payload:[ op_str; op_str ] in - Protocol.apply ~current_level:Level.zero ~payload ~tezos_operations:[] - Protocol.initial - in - let[@warning "-8"] (Receipt.Ticket_transfer_receipt { operation }) = - List.hd receipts - in - Alcotest.(check bool) - "there should only be one receipt" true - (List.length receipts = 1); - Alcotest.(check bool) - "the hash correspond" true - (Operation_hash.equal operation hash) + let _, receipts, _errors = + let payload = Protocol.prepare ~parallel ~payload:[ op_str; op_str ] in + Protocol.apply ~current_level:Level.zero ~payload ~tezos_operations:[] + Protocol.initial + in + let[@warning "-8"] (Receipt.Ticket_transfer_receipt { operation }) = + List.hd receipts + in + Alcotest.(check bool) + "there should only be one receipt" true + (List.length receipts = 1); + Alcotest.(check bool) + "the hash correspond" true + (Operation_hash.equal operation hash) -(* TODO: we can collapse a lot of these properties into - property based tests. We want to check that - - any time an operation is included, the corresponding receipt is generated - - receipts are never generated otherwise - - plus all the properties we're testing here. -*) -let test_duplicated_operation_different_level () = - let _, op_str, _ = make_operation () in - let _, receipts, _errors = - let payload = [ op_str ] in - let payload = Protocol.prepare ~parallel ~payload in - let protocol, _, _errors = - Protocol.apply ~current_level:Level.zero ~payload ~tezos_operations:[] - Protocol.initial - in - Protocol.apply ~current_level:(Level.next Level.zero) ~payload - ~tezos_operations:[] protocol - in - Alcotest.(check bool) - "second operation shouldn't be applied" true - (List.length receipts = 0) + (* TODO: we can collapse a lot of these properties into + property based tests. We want to check that + - any time an operation is included, the corresponding receipt is generated + - receipts are never generated otherwise + - plus all the properties we're testing here. + *) + let test_duplicated_operation_different_level () = + let _, op_str, _ = make_operation () in + let _, receipts, _errors = + let payload = [ op_str ] in + let payload = Protocol.prepare ~parallel ~payload in + let protocol, _, _errors = + Protocol.apply ~current_level:Level.zero ~payload ~tezos_operations:[] + Protocol.initial + in + Protocol.apply ~current_level:(Level.next Level.zero) ~payload + ~tezos_operations:[] protocol + in + Alcotest.(check bool) + "second operation shouldn't be applied" true + (List.length receipts = 0) -let test_duplicated_operation_after_includable_window () = - let _, op_str, _ = make_operation () in - let protocol, _receipts, _errors = - let payload = [ op_str ] in - let payload = Protocol.prepare ~parallel ~payload in - Protocol.apply ~current_level:Level.zero ~payload ~tezos_operations:[] - Protocol.initial - in - (* TODO: should we have an integrity check in Protocol.apply that checks - that the block being applied is a valid next block? *) - let protocol, _recipets, _errors = - Protocol.apply - ~current_level:(Level.of_n N.(zero + includable_operation_window)) - ~tezos_operations:[] ~payload:[] protocol - in - let _protocol, receipts, _errors = - let payload = [ op_str ] in - let payload = Protocol.prepare ~parallel ~payload in - Protocol.apply - ~current_level:(Level.of_n N.(zero + includable_operation_window + one)) - ~payload protocol ~tezos_operations:[] - in - Alcotest.(check bool) - "operation shouldn't be applied" true - (List.length receipts = 0) + let test_duplicated_operation_after_includable_window () = + let _, op_str, _ = make_operation () in + let protocol, _receipts, _errors = + let payload = [ op_str ] in + let payload = Protocol.prepare ~parallel ~payload in + Protocol.apply ~current_level:Level.zero ~payload ~tezos_operations:[] + Protocol.initial + in + (* TODO: should we have an integrity check in Protocol.apply that checks + that the block being applied is a valid next block? *) + let protocol, _recipets, _errors = + Protocol.apply + ~current_level:(Level.of_n N.(zero + includable_operation_window)) + ~tezos_operations:[] ~payload:[] protocol + in + let _protocol, receipts, _errors = + let payload = [ op_str ] in + let payload = Protocol.prepare ~parallel ~payload in + Protocol.apply + ~current_level:(Level.of_n N.(zero + includable_operation_window + one)) + ~payload protocol ~tezos_operations:[] + in + Alcotest.(check bool) + "operation shouldn't be applied" true + (List.length receipts = 0) -let test_invalid_string () = - let wrong_op_str = "waku waku" in - let _, receipts, _errors = - let payload = [ wrong_op_str ] in - let payload = Protocol.prepare ~parallel ~payload in - Protocol.apply ~current_level:Level.zero ~payload ~tezos_operations:[] - Protocol.initial - in - Alcotest.(check bool) "shouldn't be included" true (List.length receipts = 0) + let test_invalid_string () = + let wrong_op_str = "waku waku" in + let _, receipts, _errors = + let payload = [ wrong_op_str ] in + let payload = Protocol.prepare ~parallel ~payload in + Protocol.apply ~current_level:Level.zero ~payload ~tezos_operations:[] + Protocol.initial + in + Alcotest.(check bool) "shouldn't be included" true (List.length receipts = 0) -let test_invalid_signature () = - let source = - alice |> Identity.key_hash |> Address.of_key_hash |> Address.to_b58 - in - let key = alice |> Identity.key |> Key.to_b58 in - let nonce = Nonce.of_n N.one |> Nonce.show in - let level = Level.of_n N.one |> Level.show in - let operation = Format.sprintf {|{"sender": %s}|} source in - let initial = - Format.sprintf - {| - "nonce": %s, - "level": %s, - "operation": %s - |} - nonce level operation - in - let hash = BLAKE2b.hash "waku waku" in - let signature = Identity.sign ~hash alice |> Signature.to_b58 in - let signed = Format.sprintf {|[[%s, %s ], %s]|} key signature initial in + let test_invalid_signature () = + let source = + alice |> Identity.key_hash |> Address.of_key_hash |> Address.to_b58 + in + let key = alice |> Identity.key |> Key.to_b58 in + let nonce = Nonce.of_n N.one |> Nonce.show in + let level = Level.of_n N.one |> Level.show in + let operation = Format.sprintf {|{"sender": %s}|} source in + let initial = + Format.sprintf + {| + "nonce": %s, + "level": %s, + "operation": %s + |} + nonce level operation + in + let hash = BLAKE2b.hash "waku waku" in + let signature = Identity.sign ~hash alice |> Signature.to_b58 in + let signed = Format.sprintf {|[[%s, %s ], %s]|} key signature initial in - let _, receipts, _ = - let payload = [ signed ] in - let payload = Protocol.prepare ~parallel ~payload in - Protocol.apply ~current_level:Level.zero ~payload ~tezos_operations:[] - Protocol.initial - in - Alcotest.(check bool) "shouldn't be included" true (List.length receipts = 0) + let _, receipts, _ = + let payload = [ signed ] in + let payload = Protocol.prepare ~parallel ~payload in + Protocol.apply ~current_level:Level.zero ~payload ~tezos_operations:[] + Protocol.initial + in + Alcotest.(check bool) "shouldn't be included" true (List.length receipts = 0) -let test_valid_signature_but_different_key () = - let source = - alice |> Identity.key_hash |> Address.of_key_hash |> Address.to_b58 - in - let key = alice |> Identity.key |> Key.to_b58 in - let nonce = Nonce.of_n N.one |> Nonce.show in - let level = Level.of_n N.one |> Level.show in - let operation = Format.sprintf {|{"sender": %s}|} source in - let initial = - Format.sprintf - {| - "nonce": %s, - "level": %s, - "operation": %s - |} - nonce level operation - in - let hash = - Operation_hash.of_b58 "Do3mBf9sFinaGeQCKExQZGM1rqeP5A4NrS6TWvmyqkycgBvkGpjm" - |> Option.get |> Operation_hash.to_blake2b - in - let signature = Identity.sign ~hash alice |> Signature.to_b58 in - let signed = Format.sprintf {|[[%s, %s ], %s]|} key signature initial in + let test_valid_signature_but_different_key () = + let source = + alice |> Identity.key_hash |> Address.of_key_hash |> Address.to_b58 + in + let key = alice |> Identity.key |> Key.to_b58 in + let nonce = Nonce.of_n N.one |> Nonce.show in + let level = Level.of_n N.one |> Level.show in + let operation = Format.sprintf {|{"sender": %s}|} source in + let initial = + Format.sprintf + {| + "nonce": %s, + "level": %s, + "operation": %s + |} + nonce level operation + in + let hash = + Operation_hash.of_b58 "Do3mBf9sFinaGeQCKExQZGM1rqeP5A4NrS6TWvmyqkycgBvkGpjm" + |> Option.get |> Operation_hash.to_blake2b + in + let signature = Identity.sign ~hash alice |> Signature.to_b58 in + let signed = Format.sprintf {|[[%s, %s ], %s]|} key signature initial in - let _, receipts, _ = - let payload = [ signed ] in - let payload = Protocol.prepare ~parallel ~payload in - Protocol.apply ~current_level:Level.zero ~payload ~tezos_operations:[] - Protocol.initial - in - Alcotest.(check bool) "shouldn't be included" true (List.length receipts = 0) + let _, receipts, _ = + let payload = [ signed ] in + let payload = Protocol.prepare ~parallel ~payload in + Protocol.apply ~current_level:Level.zero ~payload ~tezos_operations:[] + Protocol.initial + in + Alcotest.(check bool) "shouldn't be included" true (List.length receipts = 0) -let test_receipt_implied_included_operations () = - let op, op_str, _ = make_operation () in - let protocol, receipts, _errors = - let payload = [ op_str ] in - let payload = Protocol.prepare ~parallel ~payload in - Protocol.apply ~current_level:Level.zero ~payload ~tezos_operations:[] - Protocol.initial - in - let (Protocol.Protocol { included_operations; _ }) = protocol in - let (Signed_operation { initial = op; _ }) = op in - let is_included = Included_operation_set.mem op included_operations in - Alcotest.(check bool) "the operation is included" true is_included; - Alcotest.(check bool) "there only one receipt" true (List.length receipts = 1) + let test_receipt_implied_included_operations () = + let op, op_str, _ = make_operation () in + let protocol, receipts, _errors = + let payload = [ op_str ] in + let payload = Protocol.prepare ~parallel ~payload in + Protocol.apply ~current_level:Level.zero ~payload ~tezos_operations:[] + Protocol.initial + in + let (Protocol.Protocol { included_operations; _ }) = protocol in + let (Signed_operation { initial = op; _ }) = op in + let is_included = Included_operation_set.mem op included_operations in + Alcotest.(check bool) "the operation is included" true is_included; + Alcotest.(check bool) "there only one receipt" true (List.length receipts = 1) -let test_included_operation_clean_after_window () = - let op, op_str, _ = make_operation () in - let protocol, _receipts, _errors = - let payload = [ op_str ] in - let payload = Protocol.prepare ~parallel ~payload in - let protocol, _receipts, _errors = - Protocol.apply ~current_level:Level.zero ~payload ~tezos_operations:[] - Protocol.initial - in + let test_included_operation_clean_after_window () = + let op, op_str, _ = make_operation () in + let protocol, _receipts, _errors = + let payload = [ op_str ] in + let payload = Protocol.prepare ~parallel ~payload in + let protocol, _receipts, _errors = + Protocol.apply ~current_level:Level.zero ~payload ~tezos_operations:[] + Protocol.initial + in - Protocol.apply - ~current_level:(Level.of_n N.(one + includable_operation_window)) - ~payload:[] ~tezos_operations:[] protocol - in - let (Protocol.Protocol { included_operations; _ }) = protocol in - let (Signed_operation { initial = op; _ }) = op in - let is_included = Included_operation_set.mem op included_operations in - Alcotest.(check bool) "included operations should be empty" false is_included + Protocol.apply + ~current_level:(Level.of_n N.(one + includable_operation_window)) + ~payload:[] ~tezos_operations:[] protocol + in + let (Protocol.Protocol { included_operations; _ }) = protocol in + let (Signed_operation { initial = op; _ }) = op in + let is_included = Included_operation_set.mem op included_operations in + Alcotest.(check bool) "included operations should be empty" false is_included -let amount = Alcotest.testable Amount.pp Amount.equal + let amount = Alcotest.testable Amount.pp Amount.equal -let test_cannot_create_amount_ex_nihilo () = - let _, op_str, _ = make_operation ~amount:32 () in - let protocol = Protocol.initial in - let (Protocol.Protocol { ledger; _ }) = protocol in - let bob_previous_balance = - ledger - |> Ledger.balance - (bob |> Identity.key_hash |> Address.of_key_hash) - ticket_id - in - let alice_previous_balance = - ledger - |> Ledger.balance - (alice |> Identity.key_hash |> Address.of_key_hash) - ticket_id - in - let protocol, _, _ = - let payload = [ op_str ] in - let payload = Protocol.prepare ~parallel ~payload in - Protocol.apply ~current_level:Level.zero ~payload ~tezos_operations:[] - protocol - in - let (Protocol.Protocol { ledger; _ }) = protocol in - let bob_balance = - ledger - |> Ledger.balance - (bob |> Identity.key_hash |> Address.of_key_hash) - ticket_id - in - let alice_balance = - ledger - |> Ledger.balance - (alice |> Identity.key_hash |> Address.of_key_hash) - ticket_id - in - Alcotest.(check amount) - "balance of alice has not changed" bob_previous_balance bob_balance; - Alcotest.(check amount) - "balance of bob has not changed" alice_previous_balance alice_balance + let test_cannot_create_amount_ex_nihilo () = + let _, op_str, _ = make_operation ~amount:32 () in + let protocol = Protocol.initial in + let (Protocol.Protocol { ledger; _ }) = protocol in + let bob_previous_balance = + ledger + |> Ledger.balance + (bob |> Identity.key_hash |> Address.of_key_hash) + ticket_id + in + let alice_previous_balance = + ledger + |> Ledger.balance + (alice |> Identity.key_hash |> Address.of_key_hash) + ticket_id + in + let protocol, _, _ = + let payload = [ op_str ] in + let payload = Protocol.prepare ~parallel ~payload in + Protocol.apply ~current_level:Level.zero ~payload ~tezos_operations:[] + protocol + in + let (Protocol.Protocol { ledger; _ }) = protocol in + let bob_balance = + ledger + |> Ledger.balance + (bob |> Identity.key_hash |> Address.of_key_hash) + ticket_id + in + let alice_balance = + ledger + |> Ledger.balance + (alice |> Identity.key_hash |> Address.of_key_hash) + ticket_id + in + Alcotest.(check amount) + "balance of alice has not changed" bob_previous_balance bob_balance; + Alcotest.(check amount) + "balance of bob has not changed" alice_previous_balance alice_balance -let run () = - let open Alcotest in - run "Protocol" ~and_exit:false - [ - ( "apply operations", - [ - test_case "apply one operation" `Quick test_apply_one_operation; - test_case "apply many operations" `Quick test_many_operations; - test_case "duplicated operation on same level" `Quick - test_duplicated_operation_same_level; - test_case "duplicated operation on different level" `Quick - test_duplicated_operation_different_level; - test_case "duplicated operation after includable window" `Quick - test_duplicated_operation_after_includable_window; - test_case "invalid string" `Quick test_invalid_string; - test_case "invalid signature" `Quick test_invalid_signature; - test_case "good signature, wrong key" `Quick - test_valid_signature_but_different_key; - test_case "one receipt imply one included operation" `Quick - test_receipt_implied_included_operations; - test_case "no more included operations after includable window" `Quick - test_included_operation_clean_after_window; - ] ); - ( "ledger operation", - [ - test_case "balance does not change, when not enough amount" `Quick - test_cannot_create_amount_ex_nihilo; - ] ); - ] + let run () = + let open Alcotest in + run "Protocol" ~and_exit:false + [ + ( "apply operations", + [ + test_case "apply one operation" `Quick test_apply_one_operation; + test_case "apply many operations" `Quick test_many_operations; + test_case "duplicated operation on same level" `Quick + test_duplicated_operation_same_level; + test_case "duplicated operation on different level" `Quick + test_duplicated_operation_different_level; + test_case "duplicated operation after includable window" `Quick + test_duplicated_operation_after_includable_window; + test_case "invalid string" `Quick test_invalid_string; + test_case "invalid signature" `Quick test_invalid_signature; + test_case "good signature, wrong key" `Quick + test_valid_signature_but_different_key; + test_case "one receipt imply one included operation" `Quick + test_receipt_implied_included_operations; + test_case "no more included operations after includable window" `Quick + test_included_operation_clean_after_window; + ] ); + ( "ledger operation", + [ + test_case "balance does not change, when not enough amount" `Quick + test_cannot_create_amount_ex_nihilo; + ] ); + ] *) diff --git a/deku-p/src/core/protocol/tests/test_protocol.mli b/deku-p/src/core/protocol/tests/test_protocol.mli index 733b2a3231..555189e103 100644 --- a/deku-p/src/core/protocol/tests/test_protocol.mli +++ b/deku-p/src/core/protocol/tests/test_protocol.mli @@ -1 +1 @@ -val run : unit -> unit +(* val run : unit -> unit *) diff --git a/deku-p/src/helper/deku_helper.ml b/deku-p/src/helper/deku_helper.ml deleted file mode 100644 index efcf64c1a4..0000000000 --- a/deku-p/src/helper/deku_helper.ml +++ /dev/null @@ -1,143 +0,0 @@ -open Deku_protocol -open Deku_stdlib -open Deku_concepts -open Deku_gossip -open Deku_crypto - -let post_directly_to_node ~env ~operation = - let host = "127.0.0.1" in - let port = 4440 in - let net = Eio.Stdenv.net env in - let content = Message.Content.operation operation in - let (Message { network; _ }) = Message.encode ~content in - let (Network_message { raw_header; raw_content }) = network in - let open Deku_network in - let message = Network_message.message ~raw_header ~raw_content in - Network_protocol.Client.connect ~net ~host ~port @@ fun connection -> - Network_protocol.Connection.write connection message - -let post_to_api ~sw ~env ~operation = - let node = "http://localhost:8080/api/v1/operations" |> Uri.of_string in - let json = - Data_encoding.Json.construct Operation.Signed.encoding operation - |> Data_encoding.Json.to_string - in - let body = Piaf.Body.of_string json in - let post_result = Piaf.Client.Oneshot.post ~body ~sw env node in - match post_result with - | Ok _ -> print_endline "operation submitted" - | Error _ -> print_endline "FAIL to submit operation" - -let make_identity secret = - secret |> Secret.of_b58 |> Option.get |> Identity.make - -type level_response = { level : Level.t } - -let make_level ~sw ~env () = - let response = - Piaf.Client.Oneshot.get ~sw env - (Uri.of_string "http://localhost:8080/api/v1/chain/level") - in - let body = - match response with - | Error _ -> failwith "cannot connect to the API" - | Ok res -> res.body - in - let string = Piaf.Body.to_string body in - let body = - match string with - | Error _ -> failwith "cannot parse body" - | Ok body -> body - in - let json = Data_encoding.Json.from_string body in - match json with - | Ok json -> - let level = Data_encoding.Json.destruct Level.encoding json in - level - | _ -> failwith "cannot decode level" - -let make_nonce () = - let rng = Stdlib.Random.State.make_self_init () in - Stdlib.Random.State.bits64 rng - |> Int64.abs |> Z.of_int64 |> N.of_z |> Option.get |> Nonce.of_n - -let main ~env ~sw:_ = - let identity = - make_identity "edsk4UWkJqpZrAm26qvJE8uY9ZFGFqQiFuBcDyEPASXeHxuD68WvvF" - in - let level = Level.zero in - let nonce = make_nonce () in - let _content2 = - {| - { "operation": - { "initial_storage": [ "Map", [] ], - "module": - "0061736d0100000001c3808080000d60017e017e60017e0060017e017f60027e7e017e60017f017e6000017e60027e7f0060037e7e7e017e60027e7f017e60027f7e017e60037e7e7e0060017f00600000028a888080005103656e760769735f6c656674000203656e76086475705f686f7374000103656e760470616972000303656e7606706169725f6e000403656e7606756e70616972000103656e76057a5f616464000303656e76057a5f737562000303656e76057a5f6d756c000303656e76036e6567000003656e76036c736c000303656e7606636f6e636174000303656e76036c7372000303656e7607636f6d70617265000303656e7603636172000003656e7603636472000003656e7604736f6d65000003656e76036e696c000503656e760474727565000503656e7608756e706169725f6e000603656e760566616c7365000503656e76046e6f6e65000503656e7604756e6974000503656e76047a65726f000503656e7609656d7074795f6d6170000503656e7609656d7074795f736574000503656e760d656d7074795f6269675f6d6170000503656e760673656e646572000503656e7606736f75726365000503656e76076d61705f676574000303656e76036d656d000303656e7606757064617465000703656e760469746572000603656e76036d6170000803656e760769665f6c656674000203656e760769665f6e6f6e65000203656e760769665f636f6e73000203656e760569736e6174000003656e76036e6f74000003656e76026f72000303656e7603616e64000303656e7603786f72000303656e760a64657265665f626f6f6c000203656e76036e6571000003656e76086661696c77697468000103656e76056765745f6e000903656e760465786563000303656e76056170706c79000303656e7605636f6e7374000403656e7603616273000003656e76026571000003656e76026774000003656e76026c74000003656e7607636c6f73757265000403656e76046c656674000003656e76057269676874000003656e7604636f6e73000303656e760f7472616e736665725f746f6b656e73000703656e760761646472657373000003656e7608636f6e7472616374000003656e760473656c66000503656e760c73656c665f61646472657373000503656e760e6765745f616e645f757064617465000a03656e760b726561645f7469636b6574000103656e76067469636b6574000303656e760c6a6f696e5f7469636b657473000003656e760c73706c69745f7469636b6574000303656e7606616d6f756e74000503656e760762616c616e6365000503656e760465646976000303656e76026765000003656e76026c65000003656e760473697a65000003656e7603696e74000003656e7610696d706c696369745f6163636f756e74000003656e7607626c616b653262000003656e76047061636b000003656e7606756e7061636b000003656e76066b656363616b000003656e7606736861323536000003656e760473686133000003656e760673686135313200000391808080001008060b0b0b0c0b0b05010b00000000000485808080000170010404058380808000010004069980808000047f0041000b7f0141a01f0b7f0141e8070b7f00418080020b07c580808000060470757368005a03706f700059046d61696e006008636c6f737572657301000d63616c6c5f63616c6c6261636b00511263616c6c5f63616c6c6261636b5f756e69740052098a80808000010041000b045f5e5d5c0afad780800010898080800000200020011100000b898080800000200020011101000bc48080800001037f4100210123012102230220006b22032402034041082303200320016a6a6c4108200220016a6c290300370300200141016a22012000470d000b200220006a24010bc48080800001037f230120006b22022401230221034100210103404108200220016a6c23034108200320016a6c6a290300370300200141016a22012000470d000b200320006a24020b8f80808000004108230120006a6c29030010010b948080800001027e10592100105921012000105a2001105a0bcb8080800002037f017e230120006a210323012201220241086c29030021040340410820016c200241016a220241086c290300370300200141016a210120012003490d000b410820036c20043703000bc28080800002027f017e4108230120006a22016c29030021030340410820016c210220024108200141016b22016c29030037030023012001490d000b410820016c20033703000b958080800001017f4108230122006c290300200041016a24010b978080800001017f4108230141016b22016c2000370300200124010b898080800000230120006a24010b8c8280800001017e2000105a10591004105941071012410710581059100410591059101c105a1059102204401016105a10561016105a105910591002105a1016105a41051058105910591002105a105910591002105a1016105a41061058105910591002105a1016105a41061058105910591002105a105910591002105a105910591002105a410310581016105a105910591002105a1016105a1016105a105910591002105a105910591002105a410410581016105a105910591002105a1016105a41051058105910591002105a105910591002105a105910591002105a105910591002105a105910591002105a0510564102105841031058410410584105105841061058410710584107105b0b10590b888380800001017e2000105a4100102f105a410110551059100d105a1059100d105a1059100e105a1059100d105a1059100d105a105910591007105a410a102f105a410210551059100d105a1059100e105a1059100d105a1059100e105a1059100e105a105910591007105a410b102f105a410310551059100d105a1059100e105a1059100d105a1059100d105a1059100e105a105910591007105a410c102f105a410410551059100d105a1059100e105a1059100e105a1059100d105a1059100e105a105910591007105a410d102f105a410510551059100d105a1059100d105a1059100e105a1059100e105a1059100d105a105910591007105a410e102f105a410610551059100d105a1059100d105a1059100d105a1059100d105a1059100e105a105910591007105a410f102f105a410710581059100e105a105910591007105a10564102105841031058410410584105105841061058105910591005105a105910591005105a105910591005105a105910591005105a105910591005105a105910591005105a10590bc88a80800001017e2000105a105910041059410810124108105810591004105910210440410210584105105841061058410710584104105b10591021044041031058410410584102105b1059102104404101105b410010551059100d105a1059100d105a1059100d105a1059100d105a1059100e105a4108102f105a105910591002105a41031055105610591059102d105a41021058105910591007105a10561059100d105a1059100d105a1059100d105a1059100d105a1059100e105a4102102f105a105910591002105a41021058105610591059102d105a1056105910591044105a1059102204404109102f105a1059102b000b1059100d105a054104105b1016105a0b05410210584101105b105910210440410210584102105b410010551059100d105a1059100d105a1059100e105a1059100d105a1059100d105a4108102f105a105910591002105a41031055105610591059102d105a41021058105910591007105a10561059100d105a1059100d105a1059100e105a1059100d105a1059100d105a4102102f105a105910591002105a41021058105610591059102d105a1056105910591044105a1059102204404109102f105a1059102b000b1059100d105a05410310584102105b410010551059100d105a1059100d105a1059100e105a1059100e105a1059100d105a4108102f105a105910591002105a41031055105610591059102d105a41021058105910591007105a10561059100d105a1059100d105a1059100e105a1059100e105a1059100d105a4102102f105a105910591002105a41021058105610591059102d105a1056105910591044105a1059102204404109102f105a1059102b000b1059100d105a0b0b054103105841041058410810584103105b10591021044041021058410310584102105b105910210440410310584102105b410010551059100d105a1059100e105a1059100d105a1059100d105a1059100e105a4108102f105a105910591002105a41031055105610591059102d105a41021058105910591007105a10561059100d105a1059100e105a1059100d105a1059100d105a1059100e105a4102102f105a105910591002105a41021058105610591059102d105a1056105910591044105a1059102204404109102f105a1059102b000b1059100d105a05410210584102105b410010551059100d105a1059100e105a1059100d105a1059100e105a1059100e105a4108102f105a105910591002105a41031055105610591059102d105a41021058105910591007105a10561059100d105a1059100e105a1059100d105a1059100e105a1059100e105a4102102f105a105910591002105a41021058105610591059102d105a1056105910591044105a1059102204404109102f105a1059102b000b1059100d105a0b0541041058410510584102105b105910210440410210584102105b410010551059100d105a1059100e105a1059100e105a1059100d105a1059100e105a4108102f105a105910591002105a41031055105610591059102d105a41021058105910591007105a10561059100d105a1059100e105a1059100e105a1059100d105a1059100e105a4102102f105a105910591002105a41021058105610591059102d105a1056105910591044105a1059102204404109102f105a1059102b000b1059100d105a05410310584102105b410010551059100e105a4108102f105a105910591002105a41031055105610591059102d105a41021058105910591007105a10561059100e105a4102102f105a105910591002105a41021058105610591059102d105a1056105910591044105a1059102204404109102f105a1059102b000b1059100d105a0b0b0b10590bb28180800001017e2000105a105910044100102f105a105910591002105a105910591002105a10591035105a034010591021044010591004105910041016105a4103105510591059100c105a10591031105a1059102904401056410210584102105b10591036105a054100102f105a41031058105910591006105a10591030105a410210554103105841031058105910591007105a105910591002105a105910591002105a10591035105a0b105910000d010b0b10590b9cc380800001017e2000105a41001034105a4101102f105a4102102f105a4103102f105a4104102f105a4105102f105a4106102f105a4107102f105a41011034105a410810554108105541081055410810554108105541081055410810554108105541081003105a10591059102e105a410810584101105b41021034105a41031034105a4109105541091055410910554109105541091055410910554109105541071003105a10591059102e105a410310584104105841051058410610584107105841081058410910584107105b4103105810591004101b105a410110551059100e105a1059100d105a10591021044010591021044041051058410610584103105b4102105541011055105910591002105a41041058105610591059102d105a1016105a410310551059100d105a1059100d105a10591059100c105a10591032105a410310551059100d105a1059100d105a410210551059100d105a1059100d105a1059100d105a1059100e105a1059100d105a10591059100c105a10591045105a105910591027105a105910290440410210551059100d105a1059100d105a410110551059100d105a1059100d105a1059100e105a1059100d105a1059100e105a105910591005105a410310581059100d105a1059100d105a410210551059100d105a1059100d105a1059100d105a1059100e105a1059100d105a105910591006105a10591030105a4104105841031058410010551059100e105a410110551059100d105a1059100e105a410210551059100d105a1059100d105a1059100e105a410310551059100d105a1059100d105a1059100d105a1059100e105a1059100e105a41061058105910591002105a410410581059100d105a1059100d105a1059100d105a1059100d105a105910591002105a105910591002105a105910591002105a105910591002105a410010551059100e105a410110551059100d105a1059100e105a410210551059100d105a1059100d105a1059100e105a1059100e105a41051058410410551059100d105a1059100d105a1059100e105a1059100d105a1059100d105a105910591002105a105910591002105a410310581059100d105a1059100d105a1059100d105a105910591002105a105910591002105a105910591002105a1059100f105a41021058105910591059101e105a054103105b0b054101105b410110551059100d105a1059100e105a1059102204404110102f105a1059102b000b41001055105910210440105910210440105910210440410310584102105b4102105541021055105910591002105a41041058105610591059102d105a410010551059100d105a1059100d105a1059100d105a1059100d105a1059100d105a410110551059100d105a1059100d105a1059100d105a1059100e105a1059100d105a10591059100c105a10591045105a1059102904404100102f105a410110551059100d105a1059100d105a1059100d105a1059100d105a1059100e105a105910591005105a410110551059100d105a1059100d105a1059100d105a1059100d105a1059100d105a410210551059100d105a1059100d105a1059100d105a1059100e105a1059100d105a105910591006105a10591030105a41021058410010551059100e105a410110551059100d105a1059100e105a410210551059100d105a1059100d105a1059100e105a410310551059100d105a1059100d105a1059100d105a1059100e105a1059100e105a41051058105910591002105a410410581059100d105a1059100d105a1059100d105a1059100d105a105910591002105a105910591002105a105910591002105a105910591002105a410010551059100e105a410110551059100d105a1059100e105a410210551059100d105a1059100d105a1059100e105a410310551059100d105a1059100d105a1059100d105a1059100e105a41051058410510581059100d105a1059100d105a1059100d105a1059100d105a1059100d105a105910591002105a105910591002105a105910591002105a105910591002105a105910591002105a4100105541021058105910591002105a41051058105610591059102d105a4101105541051058105610591059102d105a41021058410010551059100e105a410110551059100d105a1059100e105a410210551059100d105a1059100d105a1059100e105a410310551059100d105a1059100d105a1059100d105a1059100e105a410410581059100d105a1059100d105a1059100d105a1059100d105a1059100e105a41061058105910591002105a105910591002105a105910591002105a105910591002105a105910591002105a410010551059100e105a410110551059100d105a1059100e105a1059100e105a1059100e105a1059100e105a41031058105910591002105a410210551059100d105a1059100e105a1059100e105a1059100d105a105910591002105a410210551059100d105a1059100e105a1059100d105a105910591002105a410210581059100d105a1059100d105a105910591002105a105910591002105a4102105810561059100f105a41021058105910591059101e105a0510564102105841041058410510584105105b0b05105641061058410710584104105b4102105541011055105910591002105a41041058105610591059102d105a410210581059100d105a1059100d105a410110551059100d105a1059100d105a1059100d105a1059100e105a1059100d105a105910591005105a4103105841021058410010551059100e105a410110551059100d105a1059100e105a410210551059100d105a1059100d105a1059100e105a410310551059100d105a1059100d105a1059100d105a1059100e105a1059100e105a41061058105910591002105a410410581059100d105a1059100d105a1059100d105a1059100d105a105910591002105a105910591002105a105910591002105a105910591002105a1059100f105a41021058105910591059101e105a0b05410310584101105b1059102104404101105b4102105541021055105910591002105a41041058105610591059102d105a410010551059100d105a1059100d105a1059100d105a1059100e105a1059100e105a410110551059100d105a1059100d105a1059100d105a1059100e105a1059100d105a10591059100c105a10591045105a1059102904404100102f105a410110551059100d105a1059100d105a1059100e105a1059100d105a1059100d105a105910591005105a410110551059100d105a1059100d105a1059100d105a1059100e105a1059100e105a410210551059100d105a1059100d105a1059100d105a1059100e105a1059100d105a105910591006105a10591030105a41021058410010551059100e105a410110551059100d105a1059100e105a410210551059100d105a1059100d105a1059100e105a410310551059100d105a1059100d105a1059100d105a1059100e105a1059100e105a41051058105910591002105a410410581059100d105a1059100d105a1059100d105a1059100d105a105910591002105a105910591002105a105910591002105a105910591002105a410010551059100e105a410110551059100d105a1059100e105a410210551059100d105a1059100d105a1059100e105a1059100e105a410310551059100d105a1059100d105a1059100e105a1059100d105a1059100e105a41051058105910591002105a105910591002105a410310581059100d105a1059100d105a1059100d105a105910591002105a105910591002105a105910591002105a4100105541021058105910591002105a41051058105610591059102d105a4101105541051058105610591059102d105a41021058410010551059100e105a410110551059100d105a1059100e105a410210551059100d105a1059100d105a1059100e105a41051058410410551059100d105a1059100d105a1059100d105a1059100e105a1059100d105a105910591002105a410410581059100d105a1059100d105a1059100d105a1059100d105a105910591002105a105910591002105a105910591002105a105910591002105a410010551059100e105a410110551059100d105a1059100e105a1059100e105a1059100e105a1059100e105a41031058105910591002105a410210551059100d105a1059100e105a1059100e105a1059100d105a105910591002105a410210551059100d105a1059100e105a1059100d105a105910591002105a410210581059100d105a1059100d105a105910591002105a105910591002105a4102105810561059100f105a41021058105910591059101e105a0510564102105841041058410510584105105b0b054101105b4102105541021055105910591002105a41041058105610591059102d105a410010551059100d105a1059100d105a1059100e105a1059100e105a1059100e105a410110551059100d105a1059100d105a1059100d105a1059100e105a1059100d105a10591059100c105a10591045105a1059102904404100102f105a410110551059100d105a1059100d105a1059100e105a1059100e105a1059100d105a105910591005105a410110551059100d105a1059100d105a1059100e105a1059100e105a1059100e105a410210551059100d105a1059100d105a1059100d105a1059100e105a1059100d105a105910591006105a10591030105a41021058410010551059100e105a410110551059100d105a1059100e105a410210551059100d105a1059100d105a1059100e105a410310551059100d105a1059100d105a1059100d105a1059100e105a1059100e105a41051058105910591002105a410410581059100d105a1059100d105a1059100d105a1059100d105a105910591002105a105910591002105a105910591002105a105910591002105a410010551059100e105a410110551059100d105a1059100e105a410210551059100d105a1059100d105a1059100e105a1059100e105a1059100e105a41041058105910591002105a410310551059100d105a1059100d105a1059100e105a1059100d105a105910591002105a410310581059100d105a1059100d105a1059100d105a105910591002105a105910591002105a105910591002105a4100105541021058105910591002105a41051058105610591059102d105a4101105541051058105610591059102d105a41021058410010551059100e105a410110551059100d105a1059100e105a41041058410310551059100d105a1059100d105a1059100e105a1059100e105a1059100d105a105910591002105a410310551059100d105a1059100d105a1059100e105a1059100d105a105910591002105a410310581059100d105a1059100d105a1059100d105a105910591002105a105910591002105a105910591002105a410010551059100e105a410110551059100d105a1059100e105a1059100e105a1059100e105a1059100e105a41031058105910591002105a410210551059100d105a1059100e105a1059100e105a1059100d105a105910591002105a410210551059100d105a1059100e105a1059100d105a105910591002105a410210581059100d105a1059100d105a105910591002105a105910591002105a4102105810561059100f105a41021058105910591059101e105a0510564102105841041058410510584105105b0b0b0b05410310584101105b1059102104401059102104404101105b4102105541021055105910591002105a41041058105610591059102d105a410010551059100d105a1059100e105a1059100d105a1059100d105a1059100d105a410110551059100d105a1059100d105a1059100d105a1059100e105a1059100d105a10591059100c105a10591045105a1059102904404100102f105a410110551059100d105a1059100e105a1059100d105a1059100d105a1059100e105a105910591005105a410110551059100d105a1059100e105a1059100d105a1059100d105a1059100d105a410210551059100d105a1059100d105a1059100d105a1059100e105a1059100d105a105910591006105a10591030105a41021058410010551059100e105a410110551059100d105a1059100e105a410210551059100d105a1059100d105a1059100e105a410310551059100d105a1059100d105a1059100d105a1059100e105a1059100e105a41051058105910591002105a410410581059100d105a1059100d105a1059100d105a1059100d105a105910591002105a105910591002105a105910591002105a105910591002105a410010551059100e105a410110551059100d105a1059100e105a1059100e105a410210551059100d105a1059100e105a1059100d105a1059100e105a41041058410410551059100d105a1059100e105a1059100d105a1059100d105a1059100d105a105910591002105a105910591002105a105910591002105a410210581059100d105a1059100d105a105910591002105a105910591002105a4100105541021058105910591002105a41051058105610591059102d105a4101105541051058105610591059102d105a41021058410010551059100e105a410110551059100d105a1059100e105a1059100e105a410210551059100d105a1059100e105a1059100d105a1059100e105a410310551059100d105a1059100e105a1059100d105a1059100d105a1059100e105a41061058105910591002105a105910591002105a105910591002105a410210581059100d105a1059100d105a105910591002105a105910591002105a410010551059100e105a410110551059100d105a1059100e105a1059100e105a1059100e105a1059100e105a41031058105910591002105a410210551059100d105a1059100e105a1059100e105a1059100d105a105910591002105a410210551059100d105a1059100e105a1059100d105a105910591002105a410210581059100d105a1059100d105a105910591002105a105910591002105a4102105810561059100f105a41021058105910591059101e105a0510564102105841041058410510584105105b0b054101105b4102105541021055105910591002105a41041058105610591059102d105a410010551059100d105a1059100e105a1059100d105a1059100e105a1059100d105a410110551059100d105a1059100d105a1059100d105a1059100e105a1059100d105a10591059100c105a10591045105a1059102904404100102f105a410110551059100d105a1059100e105a1059100d105a1059100e105a1059100e105a105910591005105a410110551059100d105a1059100e105a1059100d105a1059100e105a1059100d105a410210551059100d105a1059100d105a1059100d105a1059100e105a1059100d105a105910591006105a10591030105a41021058410010551059100e105a410110551059100d105a1059100e105a410210551059100d105a1059100d105a1059100e105a410310551059100d105a1059100d105a1059100d105a1059100e105a1059100e105a41051058105910591002105a410410581059100d105a1059100d105a1059100d105a1059100d105a105910591002105a105910591002105a105910591002105a105910591002105a410010551059100e105a410110551059100d105a1059100e105a1059100e105a41031058410310551059100d105a1059100e105a1059100d105a1059100e105a1059100d105a105910591002105a410310551059100d105a1059100e105a1059100d105a1059100d105a105910591002105a105910591002105a410210581059100d105a1059100d105a105910591002105a105910591002105a4100105541021058105910591002105a41051058105610591059102d105a4101105541051058105610591059102d105a41021058410010551059100e105a410110551059100d105a1059100e105a1059100e105a410210551059100d105a1059100e105a1059100d105a1059100e105a1059100e105a41051058105910591002105a410310551059100d105a1059100e105a1059100d105a1059100d105a105910591002105a105910591002105a410210581059100d105a1059100d105a105910591002105a105910591002105a410010551059100e105a410110551059100d105a1059100e105a1059100e105a1059100e105a1059100e105a41031058105910591002105a410210551059100d105a1059100e105a1059100e105a1059100d105a105910591002105a410210551059100d105a1059100e105a1059100d105a105910591002105a410210581059100d105a1059100d105a105910591002105a105910591002105a4102105810561059100f105a41021058105910591059101e105a0510564102105841041058410510584105105b0b0b051059102104404101105b4102105541021055105910591002105a41041058105610591059102d105a410010551059100d105a1059100e105a1059100e105a1059100d105a1059100d105a410110551059100d105a1059100d105a1059100d105a1059100e105a1059100d105a10591059100c105a10591045105a1059102904404100102f105a410110551059100d105a1059100e105a1059100e105a1059100d105a1059100e105a105910591005105a410110551059100d105a1059100e105a1059100e105a1059100d105a1059100d105a410210551059100d105a1059100d105a1059100d105a1059100e105a1059100d105a105910591006105a10591030105a41021058410010551059100e105a410110551059100d105a1059100e105a410210551059100d105a1059100d105a1059100e105a410310551059100d105a1059100d105a1059100d105a1059100e105a1059100e105a41051058105910591002105a410410581059100d105a1059100d105a1059100d105a1059100d105a105910591002105a105910591002105a105910591002105a105910591002105a410010551059100e105a410110551059100d105a1059100e105a1059100e105a1059100e105a41031058410310551059100d105a1059100e105a1059100e105a1059100d105a1059100d105a105910591002105a105910591002105a410210551059100d105a1059100e105a1059100d105a105910591002105a410210581059100d105a1059100d105a105910591002105a105910591002105a4100105541021058105910591002105a41051058105610591059102d105a4101105541051058105610591059102d105a41021058410010551059100e105a410110551059100d105a1059100e105a1059100e105a1059100e105a410210551059100d105a1059100e105a1059100e105a1059100d105a1059100e105a41051058105910591002105a105910591002105a410210551059100d105a1059100e105a1059100d105a105910591002105a410210581059100d105a1059100d105a105910591002105a105910591002105a410010551059100e105a410110551059100d105a1059100e105a1059100e105a1059100e105a1059100e105a41031058105910591002105a410210551059100d105a1059100e105a1059100e105a1059100d105a105910591002105a410210551059100d105a1059100e105a1059100d105a105910591002105a410210581059100d105a1059100d105a105910591002105a105910591002105a4102105810561059100f105a41021058105910591059101e105a0510564102105841041058410510584105105b0b054101105b4102105541021055105910591002105a41041058105610591059102d105a410010551059100d105a1059100e105a1059100e105a1059100e105a1059100e105a410110551059100d105a1059100d105a1059100d105a1059100e105a1059100d105a10591059100c105a10591045105a1059102904404100102f105a410110551059100e105a105910591005105a410110551059100d105a1059100e105a1059100e105a1059100e105a1059100e105a410210551059100d105a1059100d105a1059100d105a1059100e105a1059100d105a105910591006105a10591030105a41021058410010551059100e105a410110551059100d105a1059100e105a410210551059100d105a1059100d105a1059100e105a410310551059100d105a1059100d105a1059100d105a1059100e105a1059100e105a41051058105910591002105a410410581059100d105a1059100d105a1059100d105a1059100d105a105910591002105a105910591002105a105910591002105a10564101105b105910591002105a4100105541021058105910591002105a41051058105610591059102d105a4101105541051058105610591059102d105a41021058410010551059100e105a41031058410210551059100d105a1059100e105a1059100e105a1059100e105a1059100d105a105910591002105a410210551059100d105a1059100e105a1059100e105a1059100d105a105910591002105a410210551059100d105a1059100e105a1059100d105a105910591002105a410210581059100d105a1059100d105a105910591002105a105910591002105a410010551059100e105a410110551059100d105a1059100e105a1059100e105a1059100e105a1059100e105a41031058105910591002105a410210551059100d105a1059100e105a1059100e105a1059100d105a105910591002105a410210551059100d105a1059100e105a1059100d105a105910591002105a410210581059100d105a1059100d105a105910591002105a105910591002105a4102105810561059100f105a41021058105910591059101e105a0510564102105841041058410510584105105b0b0b0b0b0b0541051058410610584103105b4102105541011055105910591002105a41041055105610591059102d105a1016105a410310551059100d105a1059100d105a10591059100c105a10591032105a410310551059100d105a1059100d105a410210551059100d105a1059100d105a1059100d105a1059100e105a1059100d105a10591059100c105a10591045105a105910591027105a105910290440410210551059100e105a1059100e105a1059102204404111102f105a1059102b000b4104105541011055105910591002105a41061058105610591059102d105a410410581059100d105a1059100d105a41001055410410551059100d105a1059100d105a1059100d105a1059100e105a1059100d105a105910591006105a1056410210551059100d105a1059100d105a1059100d105a1059100e105a1059100d105a105910591005105a4106105841051058410010551059100e105a410110551059100d105a1059100e105a410210551059100d105a1059100d105a1059100e105a410310551059100d105a1059100d105a1059100d105a1059100e105a1059100e105a4107105810591030105a105910591002105a410410581059100d105a1059100d105a1059100d105a1059100d105a105910591002105a105910591002105a105910591002105a105910591002105a1059100f105a41051058105910591059101e105a41021058410010551059100e105a410110551059100d105a1059100e105a410210551059100d105a1059100d105a1059100e105a410310551059100d105a1059100d105a1059100d105a1059100e105a1059100e105a41061058105910591002105a410410581059100d105a1059100d105a1059100d105a1059100d105a105910591002105a105910591002105a105910591002105a105910591002105a1059100f105a41021058105910591059101e105a05105641021058410410584104105b0b0b1010105a105910591002105a10590b", - "constants": - [ [ 0, [ "Int", "1" ] ], [ 1, [ "Int", "15" ] ], - [ 2, [ "Int", "100" ] ], [ 3, [ "Int", "1100" ] ], - [ 4, [ "Int", "12000" ] ], [ 5, [ "Int", "130000" ] ], - [ 6, [ "Int", "1400000" ] ], [ 7, [ "Int", "20000000" ] ], - [ 8, [ "Int", "115" ] ], [ 9, [ "String", "DIV by 0" ] ], - [ 10, [ "Int", "3" ] ], [ 11, [ "Int", "8" ] ], - [ 12, [ "Int", "47" ] ], [ 13, [ "Int", "260" ] ], - [ 14, [ "Int", "1400" ] ], [ 15, [ "Int", "7800" ] ], - [ 16, [ "String", "Operation is mandatory for minting" ] ], - [ 17, [ "String", "There is not recipient" ] ] ], - "entrypoints": {} }, "tickets": [] } -|} - in - (* Change this string with your appropriate needs*) - let _content = - {| - { "operation": - { "address": "tz1YCm2e83y4fWJG2Enf1EZVf3mSQykQJYMD", - "argument": - [ "Pair", - [ [ "Pair", - [ [ "Int", "1" ], - [ "Option", - [ "Some", - [ "Union", - [ "Left", - [ "Union", - [ "Left", [ "Union", [ "Right", [ "Unit" ] ] ] ] ] ] ] ] ] ] ], - [ "Pair", - [ [ "Union", [ "Left", [ "Union", [ "Right", [ "Unit" ] ] ] ] ], - [ "Option", [ "None", {} ] ] ] ] ] ] }, "tickets": [] } - |} - in - let operation = Data_encoding.Json.from_string _content2 in - let operation = - match operation with - | Ok operation -> - Data_encoding.Json.destruct Ocaml_wasm_vm.Operation_payload.encoding - operation - | _ -> failwith "impossible to decode operation" - in - print_endline (Ocaml_wasm_vm.Operation_payload.show operation); - - let (Deku_protocol.Operation.Signed.Signed_operation transaction as op) = - Operation.Signed.vm_transaction ~level ~nonce ~content:operation ~identity - in - let (Deku_protocol.Operation.Initial.Initial_operation { hash; _ }) = - transaction.initial - in - Format.printf "hash: %a\n%!" Operation_hash.pp hash; - let address = - Deku_ledger.Contract_address.of_user_operation_hash - (Deku_protocol.Operation_hash.to_blake2b hash) - |> Deku_ledger.Contract_address.to_b58 - in - (match operation.operation with - | Originate _ -> - print_newline (); - print_endline ("Address: " ^ address ^ "\n"); - print_newline () - | _ -> ()); - let _ = post_directly_to_node ~identity ~env ~operation:op in - () - -let () = - Eio_main.run @@ fun env -> - Eio.Switch.run @@ fun sw -> main ~env ~sw diff --git a/deku-p/src/helper/dune b/deku-p/src/helper/dune deleted file mode 100644 index 24b69743da..0000000000 --- a/deku-p/src/helper/dune +++ /dev/null @@ -1,14 +0,0 @@ -(executable - (public_name deku-helper) - (name deku_helper) - (libraries - deku_chain - deku_network - deku_gossip - deku_external_vm - cmdliner - logs.fmt - fmt.tty - piaf) - (preprocess - (pps ppx_let_binding))) diff --git a/docs/Deku-Canonical/deku_c_python.md b/docs/Deku-Canonical/deku_c_python.md index ae8d4ad418..46fc664341 100644 --- a/docs/Deku-Canonical/deku_c_python.md +++ b/docs/Deku-Canonical/deku_c_python.md @@ -29,6 +29,7 @@ $ pyenv activate deku ``` Then you can install the library using: + ```bash $ pip3 install . ``` @@ -39,10 +40,11 @@ directory](https://github.com/marigold-dev/deku/tree/main/deku-c/python-client). ## Examples Two examples are given [in the `examples/` directory](https://github.com/marigold-dev/deku/tree/main/deku-c/python-client/examples): -* `examples/bridge.py` demonstrates a deposit from Tezos to Deku using a pre-deployed contract on + +- `examples/bridge.py` demonstrates a deposit from Tezos to Deku using a pre-deployed contract on Ghostnet, as well as the withdraw operation. This contract uses a pre-defined implicit account on Ghostnet, but you may have to change it and/or use the [faucet](https://faucet.ghostnet.teztnets.xyz/) to refill it. -* `examples/contracts.py` shows the origination of a contract on Deku and invocation of an +- `examples/contracts.py` shows the origination of a contract on Deku and invocation of an entrypoint of this contract. You can then observe that the contract storage has changed using the [Deku CLI](https://deku.marigold.dev/docs/Deku-Canonical/deku_c_cli). diff --git a/examples/deku-plays-pokemon-bot/.gitignore b/examples/deku-plays-pokemon-bot/.gitignore new file mode 100644 index 0000000000..7ed7f450f5 --- /dev/null +++ b/examples/deku-plays-pokemon-bot/.gitignore @@ -0,0 +1,25 @@ +# See https://help.github.com/articles/ignoring-files/ for more about ignoring files. + +# dependencies +/node_modules +/.pnp +.pnp.js + +# testing +/coverage + +# production +/build + +# misc +.DS_Store +.env.local +.env.development.local +.env.test.local +.env.production.local + +npm-debug.log* +yarn-debug.log* +yarn-error.log* + +dist diff --git a/examples/deku-plays-pokemon-bot/index.ts b/examples/deku-plays-pokemon-bot/index.ts new file mode 100644 index 0000000000..c24afe5303 --- /dev/null +++ b/examples/deku-plays-pokemon-bot/index.ts @@ -0,0 +1,105 @@ +import { config } from "dotenv"; +config(); + +import { DekuPClient, fromMemorySigner, Vote } from "@marigold-dev/deku"; +import { InMemorySigner } from "@taquito/signer"; +import * as tmi from "tmi.js"; +import * as fs from "fs"; + +const twitch_secret = fs + .readFileSync("../../networks/betanets/dpp/TWITCH_TOKEN", { + encoding: "utf-8", + }) + .trim(); + +const deku_secret = JSON.parse( + fs + .readFileSync("../../networks/betanets/dpp/twitch_oracle.json", { + encoding: "utf-8", + }) + .trim() +).priv_key; + +const dekuSigner = fromMemorySigner(new InMemorySigner(deku_secret)); + +(async () => { + const deku = new DekuPClient({ + dekuRpc: "http://127.0.0.1:8080", + dekuSigner, + }); + + const client = new tmi.Client({ + channels: ["deku_plays_pokemon"], + options: { debug: true, messagesLogLevel: "debug" }, + identity: { + username: "deku_plays_pokemon", + password: twitch_secret, + }, + }); + + await client.connect(); + console.log("Connected to Twitch successfully"); + + const delegatedVote = async (input: Vote, username: string) => { + console.log(`Voting on behalf of ${username} for input ${input}`); + try { + const opHash = await deku.delegatedVote(input, username); + console.log("Submitted operation with hash: ", opHash); + } catch (error) { + console.error(error); + } + }; + + client.on("message", async (channel, tags, message, self) => { + message = message.trim(); + console.log(`Message received: ${tags["display-name"]}: ${message}`); + switch (true) { + case message.startsWith("!help"): + client.say( + channel, + `@${tags.username}, check out the instructions at https://deku-plays-pokemon.surge.sh!` + ); + break; + case message.startsWith("!attest"): + let tz1Address = message.split(" ")[1]; + await deku.attestDekuAddress(tags.username!, tz1Address); + client.say( + channel, + `@${tags.username}, registered your Deku address as ${tz1Address}` + ); + break; + case message.startsWith("!up"): + delegatedVote("Up", tags.username!); + break; + case message.startsWith("!down"): + delegatedVote("Down", tags.username!); + break; + case message.startsWith("!left"): + delegatedVote("Left", tags.username!); + break; + case message.startsWith("!right"): + delegatedVote("Right", tags.username!); + break; + case message.startsWith("!a"): + delegatedVote("A", tags.username!); + break; + case message.startsWith("!b"): + delegatedVote("B", tags.username!); + break; + case message.startsWith("!start"): + delegatedVote("Start", tags.username!); + break; + case message.startsWith("!select"): + delegatedVote("Select", tags.username!); + break; + case message.startsWith("!anarchy"): + delegatedVote("Anarchy", tags.username!); + break; + case message.startsWith("!democracy"): + delegatedVote("Democracy", tags.username!); + break; + default: + break; + } + }); +})(); diff --git a/examples/deku-plays-pokemon-bot/package.json b/examples/deku-plays-pokemon-bot/package.json new file mode 100644 index 0000000000..704780af52 --- /dev/null +++ b/examples/deku-plays-pokemon-bot/package.json @@ -0,0 +1,35 @@ +{ + "name": "deku-plays-pokemon-bot", + "version": "1.0.0", + "description": "", + "main": "index.js", + "scripts": { + "start": "ts-node-esm index.ts", + "build": "tsc index.ts", + "test": "jest --watch" + }, + "author": "", + "license": "MIT", + "dependencies": { + "@marigold-dev/deku": "0.1.3", + "@taquito/signer": "^14.0.0", + "@taquito/taquito": "^14.0.0", + "axios": "^0.27.2", + "dotenv": "^16.0.2", + "tmi.js": "^1.8.5" + }, + "devDependencies": { + "@tsconfig/node16": "^1.0.3", + "@types/express": "^4.17.14", + "@types/jest": "^28.1.3", + "@types/node": "^18.11.17", + "@types/tmi.js": "^1.8.2", + "jest": "^28.1.1", + "prettier": "^2.7.1", + "ts-jest": "^28.0.5", + "ts-node": "^10.9.1", + "tsconfig-paths": "^4.0.0", + "tsconfig-paths-jest": "^0.0.1", + "typescript": "^4.7.4" + } +} diff --git a/examples/deku-plays-pokemon-bot/tsconfig.json b/examples/deku-plays-pokemon-bot/tsconfig.json new file mode 100644 index 0000000000..02dbe4badb --- /dev/null +++ b/examples/deku-plays-pokemon-bot/tsconfig.json @@ -0,0 +1,21 @@ +{ + "compilerOptions": { + "outDir": "dist", + "target": "es2022", + "lib": ["ES2022"], + "allowJs": true, + "skipLibCheck": true, + "esModuleInterop": true, + "allowSyntheticDefaultImports": true, + "strict": true, + "forceConsistentCasingInFileNames": true, + "noFallthroughCasesInSwitch": true, + "module": "node16", + "moduleResolution": "node16", + "resolveJsonModule": true, + "isolatedModules": true, + "noEmit": true + }, + "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", "**/*.cjs", "**/*.mjs"], + "exclude": ["node_modules"] +} diff --git a/examples/deku-plays-pokemon-dapp/.gitignore b/examples/deku-plays-pokemon-dapp/.gitignore new file mode 100644 index 0000000000..4d29575de8 --- /dev/null +++ b/examples/deku-plays-pokemon-dapp/.gitignore @@ -0,0 +1,23 @@ +# See https://help.github.com/articles/ignoring-files/ for more about ignoring files. + +# dependencies +/node_modules +/.pnp +.pnp.js + +# testing +/coverage + +# production +/build + +# misc +.DS_Store +.env.local +.env.development.local +.env.test.local +.env.production.local + +npm-debug.log* +yarn-debug.log* +yarn-error.log* diff --git a/examples/deku-plays-pokemon-dapp/config-overrides.js b/examples/deku-plays-pokemon-dapp/config-overrides.js new file mode 100644 index 0000000000..9692120c94 --- /dev/null +++ b/examples/deku-plays-pokemon-dapp/config-overrides.js @@ -0,0 +1,18 @@ +const webpack = require("webpack"); + +module.exports = function override(config, env) { + console.log("override"); + let loaders = config.resolve; + loaders.fallback = { + stream: require.resolve("stream-browserify"), + buffer: require.resolve("buffer"), + os: require.resolve("os-browserify/browser"), + }; + config.plugins.push( + new webpack.ProvidePlugin({ + Buffer: ["buffer", "Buffer"], + }) + ); + + return config; +}; diff --git a/examples/deku-plays-pokemon-dapp/package.json b/examples/deku-plays-pokemon-dapp/package.json new file mode 100644 index 0000000000..194560ef23 --- /dev/null +++ b/examples/deku-plays-pokemon-dapp/package.json @@ -0,0 +1,56 @@ +{ + "name": "deku-plays-pokemon-dapp", + "version": "0.1.0", + "private": true, + "dependencies": { + "@airgap/beacon-sdk": "^3.3.0", + "@marigold-dev/deku": "0.1.3", + "@taquito/signer": "14.0.0", + "@testing-library/jest-dom": "^5.16.5", + "@testing-library/react": "^13.4.0", + "@testing-library/user-event": "^13.5.0", + "@types/jest": "^27.5.2", + "@types/node": "^16.18.3", + "@types/react": "^18.0.25", + "@types/react-dom": "^18.0.8", + "buffer": "^6.0.3", + "react": "^18.2.0", + "react-app-rewired": "^2.2.1", + "react-dom": "^18.2.0", + "react-scripts": "5.0.1", + "typescript": "^4.8.4", + "web-vitals": "^2.1.4" + }, + "scripts": { + "start": "react-app-rewired start", + "build": "react-app-rewired build", + "deploy": "react-app-rewired build && surge --domain deku-plays-pokemon.surge.sh build", + "test": "react-app-rewired test", + "eject": "react-app-rewired eject" + }, + "eslintConfig": { + "extends": [ + "react-app", + "react-app/jest" + ] + }, + "browserslist": { + "production": [ + ">0.2%", + "not dead", + "not op_mini all" + ], + "development": [ + "last 1 chrome version", + "last 1 firefox version", + "last 1 safari version" + ] + }, + "devDependencies": { + "@marigold-dev/deku-cli": "0.0.4", + "os-browserify": "^0.3.0", + "postcss-normalize": "^10.0.1", + "stream-browserify": "^3.0.0", + "surge": "^0.23.1" + } +} diff --git a/examples/deku-plays-pokemon-dapp/public/deku512.png b/examples/deku-plays-pokemon-dapp/public/deku512.png new file mode 100644 index 0000000000..29a301a1e6 Binary files /dev/null and b/examples/deku-plays-pokemon-dapp/public/deku512.png differ diff --git a/examples/deku-plays-pokemon-dapp/public/favicon.ico b/examples/deku-plays-pokemon-dapp/public/favicon.ico new file mode 100644 index 0000000000..1705a7c140 Binary files /dev/null and b/examples/deku-plays-pokemon-dapp/public/favicon.ico differ diff --git a/examples/deku-plays-pokemon-dapp/public/index.html b/examples/deku-plays-pokemon-dapp/public/index.html new file mode 100644 index 0000000000..645a3409dd --- /dev/null +++ b/examples/deku-plays-pokemon-dapp/public/index.html @@ -0,0 +1,44 @@ + + + + + + + + + + + + + Deku Plays Pokemon + + + + +
+ + + diff --git a/examples/deku-plays-pokemon-dapp/public/logo512.png b/examples/deku-plays-pokemon-dapp/public/logo512.png new file mode 100644 index 0000000000..a4e47a6545 Binary files /dev/null and b/examples/deku-plays-pokemon-dapp/public/logo512.png differ diff --git a/examples/deku-plays-pokemon-dapp/public/manifest.json b/examples/deku-plays-pokemon-dapp/public/manifest.json new file mode 100644 index 0000000000..312625117b --- /dev/null +++ b/examples/deku-plays-pokemon-dapp/public/manifest.json @@ -0,0 +1,20 @@ +{ + "short_name": "React App", + "name": "Create React App Sample", + "icons": [ + { + "src": "favicon.ico", + "sizes": "64x64 32x32 24x24 16x16", + "type": "image/x-icon" + }, + { + "src": "logo512.png", + "type": "image/png", + "sizes": "512x512" + } + ], + "start_url": ".", + "display": "standalone", + "theme_color": "#000000", + "background_color": "#ffffff" +} diff --git a/examples/deku-plays-pokemon-dapp/public/robots.txt b/examples/deku-plays-pokemon-dapp/public/robots.txt new file mode 100644 index 0000000000..e9e57dc4d4 --- /dev/null +++ b/examples/deku-plays-pokemon-dapp/public/robots.txt @@ -0,0 +1,3 @@ +# https://www.robotstxt.org/robotstxt.html +User-agent: * +Disallow: diff --git a/examples/deku-plays-pokemon-dapp/src/App.tsx b/examples/deku-plays-pokemon-dapp/src/App.tsx new file mode 100644 index 0000000000..60c8fd929d --- /dev/null +++ b/examples/deku-plays-pokemon-dapp/src/App.tsx @@ -0,0 +1,163 @@ +import { DAppClient, NetworkType } from "@airgap/beacon-sdk"; +import { DekuPClient } from "@marigold-dev/deku"; +import { fromBeaconSigner } from "@marigold-dev/deku"; +import { useState } from "react"; +const apiURL = "https://dpp.hines.house"; + +const connectBeaconWallet = async () => { + const dAppClient = new DAppClient({ + name: "Deku Plays Pokemon", + preferredNetwork: NetworkType.GHOSTNET, + }); + await dAppClient.requestPermissions({ + network: { type: NetworkType.GHOSTNET }, + }); + const signer = fromBeaconSigner(dAppClient); + const address = await signer.publicKeyHash(); + return { signer, address }; +}; + +export const App = () => { + const [twitch_handle, set_twitch_handle] = useState(""); + const [[deku_client, tz1_address], set_deku_client] = useState< + [DekuPClient | undefined, string] + >([undefined, "[your tz1 address]"]); + + const inputEnabled = !!deku_client; + + const connectButton = () => ( + <> + + +

(only works with AirGap and Temple Mobile for now)

+ + ); + const input_button = () => ( + <> + + + + ); + return ( +
+

Welcome to Deku Plays Pokemon!

+

Instructions

+ The primary way to play the game is in the{" "} + + twitch stream + + . You can input votes in the chat, but first you need to connect your + Tezos wallet to your Twitch account. +

Step 1: Get your Twitch handle

+

+ Make a Twitch account if you don't have + one already. +

+

+ Get your Twitch handle by clicking your profile pick and copying it to + your clipboard. +

+

+ + You must enter your twitch handle exactly as it appears in the Twitch + chat, else it will not work! + +

+

Step 2: Connecting your Wallet to Twitch

+

+ Click on the button below to connect via Beacon. +

+

+ Once connected, submit a signed operation attesting your Twitch handle + by pasting your handle into the input box and clicking "Submit". +

+ {inputEnabled ? input_button() : connectButton()} +

Step 3: Connect your Twitch account to your Wallet

+ Go to the{" "} + + twitch stream + {" "} + and attest your TZ1 address by entering this command into the chat: +
+ !attest {tz1_address} +

Step 4: Play the Game!

+ Vote on the next move Deku will perform by typing commands into the chat. + Votes are resolved after each block - about one second; however, the + Twitch stream adds about 10 seconds of lag. +
    +
  • + The follow commands vote on the next input: +
      +
    • + !up - send input Up +
    • +
    • + !down - send input Down +
    • +
    • + !left - send input Left +
    • +
    • + !right - send input Right +
    • +
    • + !a - send input A +
    • +
    • + !b - send input B +
    • +
    • + !start - send input Start +
    • +
    • + !select - send input Select +
    • +
    +
  • +
  • + You can also vote on the governance mode of the game: +
      +
    • + !anarchy - Votes for Anarchy mode, where the first + vote received is the one executed. May the fastest gun win! +
    • +
    • + !democracy - Votes for Democracy mode, where the + majority vote received is the one executed. Long live + civilization! +
    • +
    +
  • +
+
+ ); +}; diff --git a/examples/deku-plays-pokemon-dapp/src/config.ts b/examples/deku-plays-pokemon-dapp/src/config.ts new file mode 100644 index 0000000000..db2033fd4b --- /dev/null +++ b/examples/deku-plays-pokemon-dapp/src/config.ts @@ -0,0 +1,4 @@ +export const DEKU_RPC = process.env.DEKU_RPC || "http://localhost:8080"; +export const LIGO_RPC = process.env.DEKU_RPC || "http://localhost:9090"; +export const IS_DEV = process.env.NODE_ENV === "development"; +export const DAPP_URL = process.env.DAPP_URL || "http://localhost:3000"; diff --git a/examples/deku-plays-pokemon-dapp/src/index.scss b/examples/deku-plays-pokemon-dapp/src/index.scss new file mode 100644 index 0000000000..217941aa3a --- /dev/null +++ b/examples/deku-plays-pokemon-dapp/src/index.scss @@ -0,0 +1,32 @@ +// sytlesheet courtesy of https://github.com/LeoColomb/perfectmotherfuckingwebsite + +body { + max-width: 650px; + margin: 40px auto; + padding: 0 10px; + font: 18px/1.5 -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, + "Helvetica Neue", Arial, "Noto Sans", sans-serif, "Apple Color Emoji", + "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji"; + color: #444; +} + +h1, +h2, +h3 { + line-height: 1.2; +} + +@media (prefers-color-scheme: dark) { + body { + color: #c9d1d9; + background: #0d1117; + } + + a:link { + color: #58a6ff; + } + + a:visited { + color: #8e96f0; + } +} diff --git a/examples/deku-plays-pokemon-dapp/src/index.tsx b/examples/deku-plays-pokemon-dapp/src/index.tsx new file mode 100644 index 0000000000..29d34cabd1 --- /dev/null +++ b/examples/deku-plays-pokemon-dapp/src/index.tsx @@ -0,0 +1,9 @@ +import React from "react"; +import ReactDOM from "react-dom/client"; +import "./index.scss"; +import { App } from "./App"; + +const root = ReactDOM.createRoot( + document.getElementById("root") as HTMLElement +); +root.render(); diff --git a/examples/deku-plays-pokemon-dapp/src/react-app-env.d.ts b/examples/deku-plays-pokemon-dapp/src/react-app-env.d.ts new file mode 100644 index 0000000000..6431bc5fc6 --- /dev/null +++ b/examples/deku-plays-pokemon-dapp/src/react-app-env.d.ts @@ -0,0 +1 @@ +/// diff --git a/examples/deku-plays-pokemon-dapp/tsconfig.json b/examples/deku-plays-pokemon-dapp/tsconfig.json new file mode 100644 index 0000000000..9d379a3c4a --- /dev/null +++ b/examples/deku-plays-pokemon-dapp/tsconfig.json @@ -0,0 +1,20 @@ +{ + "compilerOptions": { + "target": "es5", + "lib": ["dom", "dom.iterable", "esnext"], + "allowJs": true, + "skipLibCheck": true, + "esModuleInterop": true, + "allowSyntheticDefaultImports": true, + "strict": true, + "forceConsistentCasingInFileNames": true, + "noFallthroughCasesInSwitch": true, + "module": "esnext", + "moduleResolution": "node", + "resolveJsonModule": true, + "isolatedModules": true, + "noEmit": true, + "jsx": "react-jsx" + }, + "include": ["src"] +} diff --git a/examples/deku-plays-pokemon-dapp/wallet.json b/examples/deku-plays-pokemon-dapp/wallet.json new file mode 100644 index 0000000000..34c0f64fb5 --- /dev/null +++ b/examples/deku-plays-pokemon-dapp/wallet.json @@ -0,0 +1,4 @@ +{ + "address": "tz1UAxwRXXDvpZ5sAanbbP8tjKBoa2dxKUHE", + "priv_key": "edsk4CsdbHDj3LemaNgKUF2zJHi8cjAx3vbMXGsueJWxE5DMEn6TcT" +} diff --git a/examples/number-go-up/package.json b/examples/number-go-up/package.json index 17fd78db44..626c6f4f0d 100644 --- a/examples/number-go-up/package.json +++ b/examples/number-go-up/package.json @@ -4,7 +4,7 @@ "private": true, "dependencies": { "@airgap/beacon-sdk": "^3.3.0", - "@marigold-dev/deku": "0.1.3", + "@marigold-dev/deku": "0.1.2", "@taquito/signer": "14.0.0", "@testing-library/jest-dom": "^5.16.5", "@testing-library/react": "^13.4.0", diff --git a/flake.nix b/flake.nix index 14413bc952..73166a6777 100644 --- a/flake.nix +++ b/flake.nix @@ -45,9 +45,9 @@ ]; systems = [ "x86_64-linux" - "aarch64-darwin" - "x86_64-darwin" - "aarch64-linux" + # "aarch64-darwin" + # "x86_64-darwin" + # "aarch64-linux" ]; perSystem = { config, @@ -122,6 +122,10 @@ # dune build @fmt --auto-promote does not comply with treefmt spec includes = ["*.ml" ".mli"]; }; + rust = { + command = "${rustfmt}/bin/rustfmt"; + includes = ["*.rs"]; + }; }; }; projectRootFile = "flake.nix"; @@ -141,7 +145,7 @@ sshUser = "root"; }; - checks = builtins.mapAttrs (system: deployLib: deployLib.deployChecks self.deploy) deploy-rs.lib; + # checks = builtins.mapAttrs (system: deployLib: deployLib.deployChecks self.deploy) deploy-rs.lib; }; }; } diff --git a/networks/betanets/dpp/TWITCH_TOKEN b/networks/betanets/dpp/TWITCH_TOKEN new file mode 100644 index 0000000000..b9d34a3ae8 Binary files /dev/null and b/networks/betanets/dpp/TWITCH_TOKEN differ diff --git a/networks/betanets/dpp/data b/networks/betanets/dpp/data new file mode 100644 index 0000000000..9d442c4072 Binary files /dev/null and b/networks/betanets/dpp/data differ diff --git a/networks/betanets/dpp/data.gb b/networks/betanets/dpp/data.gb new file mode 100644 index 0000000000..254385fdbc Binary files /dev/null and b/networks/betanets/dpp/data.gb differ diff --git a/networks/betanets/dpp/twitch_oracle.json b/networks/betanets/dpp/twitch_oracle.json new file mode 100644 index 0000000000..c3f60eb9c8 Binary files /dev/null and b/networks/betanets/dpp/twitch_oracle.json differ diff --git a/networks/betanets/fleet.nix b/networks/betanets/fleet.nix index 940909e6c2..77852eafba 100644 Binary files a/networks/betanets/fleet.nix and b/networks/betanets/fleet.nix differ diff --git a/nix/shell.nix b/nix/shell.nix index 562fcae8e0..ebe9973e6b 100644 --- a/nix/shell.nix +++ b/nix/shell.nix @@ -6,14 +6,40 @@ deploy-rs, }: with pkgs; -with ocamlPackages; +with ocamlPackages; let + rust-src = stdenv.mkDerivation { + inherit (rustc) src; + inherit (rustc.src) name; + phases = ["unpackPhase" "installPhase"]; + installPhase = ''cp -r library $out''; + }; +in mkShell { inputsFrom = [deku tuna]; + shellHook = '' + export RUST_SRC_PATH="${rust-src}" + ''; packages = [ + # Rust gameboy deps + pkg-config + cargo + rustc + libiconv + xorg.libX11 + xorg.libXcursor + xorg.libXrandr + xorg.libXi + xorg.libXext + libGLU + alsa-lib + # Formatters alejandra ocamlformat nodePackages.prettier + rustfmt + + rust-analyzer # Typescript for decookie nodePackages.typescript diff --git a/package.json b/package.json index 5c9cd7942f..2ca73b2d6d 100644 --- a/package.json +++ b/package.json @@ -8,6 +8,8 @@ "examples/deku-c-nodejs", "examples/deku-p-nodejs", "examples/deku-p-react", + "examples/deku-plays-pokemon-bot", + "examples/deku-plays-pokemon-dapp", "examples/number-go-up", "website" ], diff --git a/scripts/start_api.sh b/scripts/start_api.sh index 03b3012f65..356baac6d0 100755 --- a/scripts/start_api.sh +++ b/scripts/start_api.sh @@ -17,7 +17,7 @@ export DEKU_API_DATA_FOLDER="./flextesa_chain/data/0/" export DEKU_API_LOG_VERBOSITY=${DEKU_API_LOG_VERBOSITY:-info} ## The api needs its own vm -nix run ".#$vm" -- "$DEKU_API_VM" & +# nix run ".#$vm" -- "$DEKU_API_VM" & sleep 1 echo Start the API diff --git a/src/LICENSE b/src/LICENSE new file mode 100644 index 0000000000..5c93f45654 --- /dev/null +++ b/src/LICENSE @@ -0,0 +1,13 @@ + DO WHAT THE FUCK YOU WANT TO PUBLIC LICENSE + Version 2, December 2004 + + Copyright (C) 2004 Sam Hocevar + + Everyone is permitted to copy and distribute verbatim or modified + copies of this license document, and changing it is allowed as long + as the name is changed. + + DO WHAT THE FUCK YOU WANT TO PUBLIC LICENSE + TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION + + 0. You just DO WHAT THE FUCK YOU WANT TO. diff --git a/src/README.md b/src/README.md new file mode 100644 index 0000000000..6095d6ee2c --- /dev/null +++ b/src/README.md @@ -0,0 +1,73 @@ +# Gameboy + +Full-featured Cross-platform GameBoy emulator. **Forever boys!** + +![sample.gif](./res/imgs/sample.gif) + +You can start a game with the following command. The following example uses the built-in game "Boxes": + +```s +$ cargo run --release -- "./res/boxes.gb" +``` + +The following options are supported: + +```text +-a, --enable-audio Enable audio, default is false +-x, --scale-factor Scale the video by a factor of 1, 2, 4, or 8 +``` + +Gameboy is developed in Rust and has been thoroughly tested on Windows, Ubuntu, and Mac. + +# Controls + +``` + _n_________________ + |_|_______________|_| + | ,-------------. | + | | .---------. | | + | | | | | | + | | | | | | + | | | | | | + | | | | | | + | | `---------' | | + | `---------------' | + | _ GAME BOY | + Up | _| |_ ,-. | ----> Z +Left/Right <--- ||_ O _| ,-. "._,"| + Down | |_| "._," A | ----> X + | _ _ B | + | // // | + | // // \\\\\\ | ----> Enter/BackSpace + | ` ` \\\\\\ , + |________...______," +``` + +# Tests + +Thanks to [Blargg's Gameboy hardware test ROMs](https://github.com/retrio/gb-test-roms), I can easily verify my code. Run tests with the command: + +``` +$ cargo run --example blargg +``` + +| Test Name | Result | +| ------------ | ----------------------------------- | +| cpu_instrs | ![img](./res/imgs/cpu_instrs.png) | +| instr_timing | ![img](./res/imgs/instr_timing.png) | + +# References + +- [Gbdev](http://gbdev.gg8.se/wiki/articles/Main_Page) +- [Open Game Boy Documentation Project](https://mgba-emu.github.io/gbdoc/) +- [LR35902 Opcodes](https://rednex.github.io/rgbds/gbz80.7.html) +- [LR35902 Opcodes Table](http://www.pastraiser.com/cpu/gameboy/gameboy_opcodes.html) +- [Game Boy Memory Map](http://gameboy.mongenel.com/dmg/asmmemmap.html) +- [Game Boy Technical Data](http://bgb.bircd.org/pandocs.htm) +- [awesome-gbdev](https://github.com/gbdev/awesome-gbdev) +- [List of MBC roms](https://ladecadence.net/trastero/listado%20juegos%20gameboy.html) +- [Roms download](http://romhustler.net/roms/gbc/number) + +# Licenses + +WTFPL. diff --git a/src/apu.rs b/src/apu.rs new file mode 100644 index 0000000000..8fe8a66729 --- /dev/null +++ b/src/apu.rs @@ -0,0 +1,1018 @@ +use super::clock::Clock; +use super::cpu; +use super::memory::Memory; +use blip_buf::BlipBuf; +use std::cell::RefCell; +use std::rc::Rc; +use std::sync::{Arc, Mutex}; + +#[derive(Clone, Eq, PartialEq)] +enum Channel { + Square1, + Square2, + Wave, + Noise, + Mixer, +} + +// Name Addr 7654 3210 Function +// ----------------------------------------------------------------- +// Square 1 +// NR10 FF10 -PPP NSSS Sweep period, negate, shift +// NR11 FF11 DDLL LLLL Duty, Length load (64-L) +// NR12 FF12 VVVV APPP Starting volume, Envelope add mode, period +// NR13 FF13 FFFF FFFF Frequency LSB +// NR14 FF14 TL-- -FFF Trigger, Length enable, Frequency MSB +// +// Square 2 +// FF15 ---- ---- Not used +// NR21 FF16 DDLL LLLL Duty, Length load (64-L) +// NR22 FF17 VVVV APPP Starting volume, Envelope add mode, period +// NR23 FF18 FFFF FFFF Frequency LSB +// NR24 FF19 TL-- -FFF Trigger, Length enable, Frequency MSB +// +// Wave +// NR30 FF1A E--- ---- DAC power +// NR31 FF1B LLLL LLLL Length load (256-L) +// NR32 FF1C -VV- ---- Volume code (00=0%, 01=100%, 10=50%, 11=25%) +// NR33 FF1D FFFF FFFF Frequency LSB +// NR34 FF1E TL-- -FFF Trigger, Length enable, Frequency MSB +// +// Noise +// FF1F ---- ---- Not used +// NR41 FF20 --LL LLLL Length load (64-L) +// NR42 FF21 VVVV APPP Starting volume, Envelope add mode, period +// NR43 FF22 SSSS WDDD Clock shift, Width mode of LFSR, Divisor code +// NR44 FF23 TL-- ---- Trigger, Length enable +// +// Control/Status +// NR50 FF24 ALLL BRRR Vin L enable, Left vol, Vin R enable, Right vol +// NR51 FF25 NW21 NW21 Left enables, Right enables +// NR52 FF26 P--- NW21 Power control/status, Channel length statuses +// +// Not used +// FF27 ---- ---- +// .... ---- ---- +// FF2F ---- ---- +// +// Wave Table +// FF30 0000 1111 Samples 0 and 1 +// .... +// FF3F 0000 1111 Samples 30 and 31 +struct Register { + channel: Channel, + nrx0: u8, + nrx1: u8, + nrx2: u8, + nrx3: u8, + nrx4: u8, +} + +impl Register { + fn get_sweep_period(&self) -> u8 { + assert!(self.channel == Channel::Square1); + (self.nrx0 >> 4) & 0x07 + } + + fn get_negate(&self) -> bool { + assert!(self.channel == Channel::Square1); + self.nrx0 & 0x08 != 0x00 + } + + fn get_shift(&self) -> u8 { + assert!(self.channel == Channel::Square1); + self.nrx0 & 0x07 + } + + fn get_dac_power(&self) -> bool { + assert!(self.channel == Channel::Wave); + self.nrx0 & 0x80 != 0x00 + } + + fn get_duty(&self) -> u8 { + assert!(self.channel == Channel::Square1 || self.channel == Channel::Square2); + self.nrx1 >> 6 + } + + fn get_length_load(&self) -> u16 { + if self.channel == Channel::Wave { + (1 << 8) - u16::from(self.nrx1) + } else { + (1 << 6) - u16::from(self.nrx1 & 0x3f) + } + } + + fn get_starting_volume(&self) -> u8 { + assert!(self.channel != Channel::Wave); + self.nrx2 >> 4 + } + + fn get_volume_code(&self) -> u8 { + assert!(self.channel == Channel::Wave); + (self.nrx2 >> 5) & 0x03 + } + + fn get_envelope_add_mode(&self) -> bool { + assert!(self.channel != Channel::Wave); + self.nrx2 & 0x08 != 0x00 + } + + fn get_period(&self) -> u8 { + assert!(self.channel != Channel::Wave); + self.nrx2 & 0x07 + } + + fn get_frequency(&self) -> u16 { + assert!(self.channel != Channel::Noise); + u16::from(self.nrx4 & 0x07) << 8 | u16::from(self.nrx3) + } + + fn set_frequency(&mut self, f: u16) { + assert!(self.channel != Channel::Noise); + let h = ((f >> 8) & 0x07) as u8; + let l = f as u8; + self.nrx4 = (self.nrx4 & 0xf8) | h; + self.nrx3 = l; + } + + fn get_clock_shift(&self) -> u8 { + assert!(self.channel == Channel::Noise); + self.nrx3 >> 4 + } + + fn get_width_mode_of_lfsr(&self) -> bool { + assert!(self.channel == Channel::Noise); + self.nrx3 & 0x08 != 0x00 + } + + fn get_dividor_code(&self) -> u8 { + assert!(self.channel == Channel::Noise); + self.nrx3 & 0x07 + } + + fn get_trigger(&self) -> bool { + self.nrx4 & 0x80 != 0x00 + } + + fn set_trigger(&mut self, b: bool) { + if b { + self.nrx4 |= 0x80; + } else { + self.nrx4 &= 0x7f; + }; + } + + fn get_length_enable(&self) -> bool { + self.nrx4 & 0x40 != 0x00 + } + + fn get_l_vol(&self) -> u8 { + assert!(self.channel == Channel::Mixer); + (self.nrx0 >> 4) & 0x07 + } + + fn get_r_vol(&self) -> u8 { + assert!(self.channel == Channel::Mixer); + self.nrx0 & 0x07 + } + + fn get_power(&self) -> bool { + assert!(self.channel == Channel::Mixer); + self.nrx2 & 0x80 != 0x00 + } +} + +impl Register { + fn power_up(channel: Channel) -> Self { + let nrx1 = match channel { + Channel::Square1 | Channel::Square2 => 0x40, + _ => 0x00, + }; + Self { + channel, + nrx0: 0x00, + nrx1, + nrx2: 0x00, + nrx3: 0x00, + nrx4: 0x00, + } + } +} + +// Frame Sequencer +// The frame sequencer generates low frequency clocks for the modulation units. It is clocked by a 512 Hz timer. +// +// Step Length Ctr Vol Env Sweep +// --------------------------------------- +// 0 Clock - - +// 1 - - - +// 2 Clock - Clock +// 3 - - - +// 4 Clock - - +// 5 - - - +// 6 Clock - Clock +// 7 - Clock - +// --------------------------------------- +// Rate 256 Hz 64 Hz 128 Hz +struct FrameSequencer { + step: u8, +} + +impl FrameSequencer { + fn power_up() -> Self { + Self { step: 0x00 } + } + + fn next(&mut self) -> u8 { + self.step += 1; + self.step %= 8; + self.step + } +} + +// A length counter disables a channel when it decrements to zero. It contains an internal counter and enabled flag. +// Writing a byte to NRx1 loads the counter with 64-data (256-data for wave channel). The counter can be reloaded at any +// time. +// A channel is said to be disabled when the internal enabled flag is clear. When a channel is disabled, its volume unit +// receives 0, otherwise its volume unit receives the output of the waveform generator. Other units besides the length +// counter can enable/disable the channel as well. +// Each length counter is clocked at 256 Hz by the frame sequencer. When clocked while enabled by NRx4 and the counter +// is not zero, it is decremented. If it becomes zero, the channel is disabled. +struct LengthCounter { + reg: Rc>, + n: u16, +} + +impl LengthCounter { + fn power_up(reg: Rc>) -> Self { + Self { reg, n: 0x0000 } + } + + fn next(&mut self) { + if self.reg.borrow().get_length_enable() && self.n != 0 { + self.n -= 1; + if self.n == 0 { + self.reg.borrow_mut().set_trigger(false); + } + } + } + + fn reload(&mut self) { + if self.n == 0x0000 { + self.n = if self.reg.borrow().channel == Channel::Wave { + 1 << 8 + } else { + 1 << 6 + }; + } + } +} + +// A volume envelope has a volume counter and an internal timer clocked at 64 Hz by the frame sequencer. When the timer +// generates a clock and the envelope period is not zero, a new volume is calculated by adding or subtracting +// (as set by NRx2) one from the current volume. If this new volume within the 0 to 15 range, the volume is updated, +// otherwise it is left unchanged and no further automatic increments/decrements are made to the volume until the +// channel is triggered again. +// When the waveform input is zero the envelope outputs zero, otherwise it outputs the current volume. +// Writing to NRx2 causes obscure effects on the volume that differ on different Game Boy models (see obscure behavior). +struct VolumeEnvelope { + reg: Rc>, + timer: Clock, + volume: u8, +} + +impl VolumeEnvelope { + fn power_up(reg: Rc>) -> Self { + Self { + reg, + timer: Clock::power_up(8), + volume: 0x00, + } + } + + fn reload(&mut self) { + let p = self.reg.borrow().get_period(); + // The volume envelope and sweep timers treat a period of 0 as 8. + self.timer.period = if p == 0 { 8 } else { u32::from(p) }; + self.volume = self.reg.borrow().get_starting_volume(); + } + + fn next(&mut self) { + if self.reg.borrow().get_period() == 0 { + return; + } + if self.timer.next(1) == 0x00 { + return; + }; + // If this new volume within the 0 to 15 range, the volume is updated + let v = if self.reg.borrow().get_envelope_add_mode() { + self.volume.wrapping_add(1) + } else { + self.volume.wrapping_sub(1) + }; + if v <= 15 { + self.volume = v; + } + } +} + +// The first square channel has a frequency sweep unit, controlled by NR10. This has a timer, internal enabled flag, +// and frequency shadow register. It can periodically adjust square 1's frequency up or down. +// During a trigger event, several things occur: +// +// - Square 1's frequency is copied to the shadow register. +// - The sweep timer is reloaded. +// - The internal enabled flag is set if either the sweep period or shift are non-zero, cleared otherwise. +// - If the sweep shift is non-zero, frequency calculation and the overflow check are performed immediately. +// +// Frequency calculation consists of taking the value in the frequency shadow register, shifting it right by sweep +// shift, optionally negating the value, and summing this with the frequency shadow register to produce a new +// frequency. What is done with this new frequency depends on the context. +// +// The overflow check simply calculates the new frequency and if this is greater than 2047, square 1 is disabled. +// The sweep timer is clocked at 128 Hz by the frame sequencer. When it generates a clock and the sweep's internal +// enabled flag is set and the sweep period is not zero, a new frequency is calculated and the overflow check is +// performed. If the new frequency is 2047 or less and the sweep shift is not zero, this new frequency is written back +// to the shadow frequency and square 1's frequency in NR13 and NR14, then frequency calculation and overflow check are +// run AGAIN immediately using this new value, but this second new frequency is not written back. +// Square 1's frequency can be modified via NR13 and NR14 while sweep is active, but the shadow frequency won't be +// affected so the next time the sweep updates the channel's frequency this modification will be lost. +struct FrequencySweep { + reg: Rc>, + timer: Clock, + enable: bool, + shadow: u16, + newfeq: u16, +} + +impl FrequencySweep { + fn power_up(reg: Rc>) -> Self { + Self { + reg, + timer: Clock::power_up(8), + enable: false, + shadow: 0x0000, + newfeq: 0x0000, + } + } + + fn reload(&mut self) { + self.shadow = self.reg.borrow().get_frequency(); + let p = self.reg.borrow().get_sweep_period(); + // The volume envelope and sweep timers treat a period of 0 as 8. + self.timer.period = if p == 0 { 8 } else { u32::from(p) }; + self.enable = p != 0x00 || self.reg.borrow().get_shift() != 0x00; + if self.reg.borrow().get_shift() != 0x00 { + self.frequency_calculation(); + self.overflow_check(); + } + } + + fn frequency_calculation(&mut self) { + let offset = self.shadow >> self.reg.borrow().get_shift(); + if self.reg.borrow().get_negate() { + self.newfeq = self.shadow.wrapping_sub(offset); + } else { + self.newfeq = self.shadow.wrapping_add(offset); + } + } + + fn overflow_check(&mut self) { + if self.newfeq >= 2048 { + self.reg.borrow_mut().set_trigger(false); + } + } + + fn next(&mut self) { + if !self.enable || self.reg.borrow().get_sweep_period() == 0 { + return; + } + if self.timer.next(1) == 0x00 { + return; + } + self.frequency_calculation(); + self.overflow_check(); + + if self.newfeq < 2048 && self.reg.borrow().get_shift() != 0 { + self.reg.borrow_mut().set_frequency(self.newfeq); + self.shadow = self.newfeq; + self.frequency_calculation(); + self.overflow_check(); + } + } +} + +struct Blip { + data: BlipBuf, + from: u32, + ampl: i32, +} + +impl Blip { + fn power_up(data: BlipBuf) -> Self { + Self { + data, + from: 0x0000_0000, + ampl: 0x0000_0000, + } + } + + fn set(&mut self, time: u32, ampl: i32) { + self.from = time; + let d = ampl - self.ampl; + self.ampl = ampl; + self.data.add_delta(time, d); + } +} + +// A square channel's frequency timer period is set to (2048-frequency)*4. Four duty cycles are available, each +// waveform taking 8 frequency timer clocks to cycle through: +// +// Duty Waveform Ratio +// ------------------------- +// 0 00000001 12.5% +// 1 10000001 25% +// 2 10000111 50% +// 3 01111110 75% +struct ChannelSquare { + reg: Rc>, + timer: Clock, + lc: LengthCounter, + ve: VolumeEnvelope, + fs: FrequencySweep, + blip: Blip, + idx: u8, +} + +impl ChannelSquare { + fn power_up(blip: BlipBuf, mode: Channel) -> ChannelSquare { + let reg = Rc::new(RefCell::new(Register::power_up(mode.clone()))); + ChannelSquare { + reg: reg.clone(), + timer: Clock::power_up(8192), + lc: LengthCounter::power_up(reg.clone()), + ve: VolumeEnvelope::power_up(reg.clone()), + fs: FrequencySweep::power_up(reg.clone()), + blip: Blip::power_up(blip), + idx: 1, + } + } + + // This assumes no volume or sweep adjustments need to be done in the meantime + fn next(&mut self, cycles: u32) { + let pat = match self.reg.borrow().get_duty() { + 0 => 0b0000_0001, + 1 => 0b1000_0001, + 2 => 0b1000_0111, + 3 => 0b0111_1110, + _ => unreachable!(), + }; + let vol = i32::from(self.ve.volume); + for _ in 0..self.timer.next(cycles) { + let ampl = if !self.reg.borrow().get_trigger() || self.ve.volume == 0 { + 0x00 + } else if (pat >> self.idx) & 0x01 != 0x00 { + vol + } else { + vol * -1 + }; + self.blip.set(self.blip.from + self.timer.period, ampl); + self.idx = (self.idx + 1) % 8; + } + } +} + +impl Memory for ChannelSquare { + fn get(&self, a: u16) -> u8 { + match a { + 0xff10 | 0xff15 => self.reg.borrow().nrx0, + 0xff11 | 0xff16 => self.reg.borrow().nrx1, + 0xff12 | 0xff17 => self.reg.borrow().nrx2, + 0xff13 | 0xff18 => self.reg.borrow().nrx3, + 0xff14 | 0xff19 => self.reg.borrow().nrx4, + _ => unreachable!(), + } + } + + fn set(&mut self, a: u16, v: u8) { + match a { + 0xff10 | 0xff15 => self.reg.borrow_mut().nrx0 = v, + 0xff11 | 0xff16 => { + self.reg.borrow_mut().nrx1 = v; + self.lc.n = self.reg.borrow().get_length_load(); + } + 0xff12 | 0xff17 => self.reg.borrow_mut().nrx2 = v, + 0xff13 | 0xff18 => { + self.reg.borrow_mut().nrx3 = v; + self.timer.period = period(self.reg.clone()); + } + 0xff14 | 0xff19 => { + self.reg.borrow_mut().nrx4 = v; + self.timer.period = period(self.reg.clone()); + // Trigger Event + // + // Writing a value to NRx4 with bit 7 set causes the following things to occur: + // + // - Channel is enabled (see length counter). + // - If length counter is zero, it is set to 64 (256 for wave channel). + // - Frequency timer is reloaded with period. + // - Volume envelope timer is reloaded with period. + // - Channel volume is reloaded from NRx2. + // - Noise channel's LFSR bits are all set to 1. + // - Wave channel's position is set to 0 but sample buffer is NOT refilled. + // - Square 1's sweep does several things (see frequency sweep). + // + // Note that if the channel's DAC is off, after the above actions occur the channel will be immediately + // disabled again. + if self.reg.borrow().get_trigger() { + self.lc.reload(); + self.ve.reload(); + if self.reg.borrow().channel == Channel::Square1 { + self.fs.reload(); + } + } + } + _ => unreachable!(), + } + } +} + +// The wave channel plays a 32-entry wave table made up of 4-bit samples. Each byte encodes two samples, the first in +// the high bits. The wave channel has a sample buffer and position counter. +// The wave channel's frequency timer period is set to (2048-frequency)*2. When the timer generates a clock, the +// position counter is advanced one sample in the wave table, looping back to the beginning when it goes past the end, +// then a sample is read into the sample buffer from this NEW position. +// The DAC receives the current value from the upper/lower nibble of the sample buffer, shifted right by the volume +// control. +// +// Code Shift Volume +// ----------------------- +// 0 4 0% (silent) +// 1 0 100% +// 2 1 50% +// 3 2 25% +// Wave RAM can only be properly accessed when the channel is disabled (see obscure behavior). +struct ChannelWave { + reg: Rc>, + timer: Clock, + lc: LengthCounter, + blip: Blip, + waveram: [u8; 16], + waveidx: usize, +} + +impl ChannelWave { + fn power_up(blip: BlipBuf) -> ChannelWave { + let reg = Rc::new(RefCell::new(Register::power_up(Channel::Wave))); + ChannelWave { + reg: reg.clone(), + timer: Clock::power_up(8192), + lc: LengthCounter::power_up(reg.clone()), + blip: Blip::power_up(blip), + waveram: [0x00; 16], + waveidx: 0x00, + } + } + + fn next(&mut self, cycles: u32) { + let s = match self.reg.borrow().get_volume_code() { + 0 => 4, + 1 => 0, + 2 => 1, + 3 => 2, + _ => unreachable!(), + }; + for _ in 0..self.timer.next(cycles) { + let sample = if self.waveidx & 0x01 == 0x00 { + self.waveram[self.waveidx / 2] & 0x0f + } else { + self.waveram[self.waveidx / 2] >> 4 + }; + let ampl = if !self.reg.borrow().get_trigger() || !self.reg.borrow().get_dac_power() { + 0x00 + } else { + i32::from(sample >> s) + }; + self.blip.set(self.blip.from + self.timer.period, ampl); + self.waveidx = (self.waveidx + 1) % 32; + } + } +} + +impl Memory for ChannelWave { + fn get(&self, a: u16) -> u8 { + match a { + 0xff1a => self.reg.borrow().nrx0, + 0xff1b => self.reg.borrow().nrx1, + 0xff1c => self.reg.borrow().nrx2, + 0xff1d => self.reg.borrow().nrx3, + 0xff1e => self.reg.borrow().nrx4, + 0xff30..=0xff3f => self.waveram[a as usize - 0xff30], + _ => unreachable!(), + } + } + + fn set(&mut self, a: u16, v: u8) { + match a { + 0xff1a => self.reg.borrow_mut().nrx0 = v, + 0xff1b => { + self.reg.borrow_mut().nrx1 = v; + self.lc.n = self.reg.borrow().get_length_load(); + } + 0xff1c => self.reg.borrow_mut().nrx2 = v, + 0xff1d => { + self.reg.borrow_mut().nrx3 = v; + self.timer.period = period(self.reg.clone()); + } + 0xff1e => { + self.reg.borrow_mut().nrx4 = v; + self.timer.period = period(self.reg.clone()); + if self.reg.borrow().get_trigger() { + self.lc.reload(); + self.waveidx = 0x00; + } + } + 0xff30..=0xff3f => self.waveram[a as usize - 0xff30] = v, + _ => unreachable!(), + } + } +} + +// The linear feedback shift register (LFSR) generates a pseudo-random bit sequence. It has a 15-bit shift register +// with feedback. When clocked by the frequency timer, the low two bits (0 and 1) are XORed, all bits are shifted right +// by one, and the result of the XOR is put into the now-empty high bit. If width mode is 1 (NR43), the XOR result is +// ALSO put into bit 6 AFTER the shift, resulting in a 7-bit LFSR. The waveform output is bit 0 of the LFSR, INVERTED. +struct Lfsr { + reg: Rc>, + n: u16, +} + +impl Lfsr { + fn power_up(reg: Rc>) -> Self { + Self { reg, n: 0x0001 } + } + + fn next(&mut self) -> bool { + let s = if self.reg.borrow().get_width_mode_of_lfsr() { + 0x06 + } else { + 0x0e + }; + let src = self.n; + self.n <<= 1; + let bit = ((src >> s) ^ (self.n >> s)) & 0x0001; + self.n |= bit; + (src >> s) & 0x0001 != 0x0000 + } + + fn reload(&mut self) { + self.n = 0x0001 + } +} + +struct ChannelNoise { + reg: Rc>, + timer: Clock, + lc: LengthCounter, + ve: VolumeEnvelope, + lfsr: Lfsr, + blip: Blip, +} + +impl ChannelNoise { + fn power_up(blip: BlipBuf) -> ChannelNoise { + let reg = Rc::new(RefCell::new(Register::power_up(Channel::Noise))); + ChannelNoise { + reg: reg.clone(), + timer: Clock::power_up(4096), + lc: LengthCounter::power_up(reg.clone()), + ve: VolumeEnvelope::power_up(reg.clone()), + lfsr: Lfsr::power_up(reg.clone()), + blip: Blip::power_up(blip), + } + } + + fn next(&mut self, cycles: u32) { + for _ in 0..self.timer.next(cycles) { + let ampl = if !self.reg.borrow().get_trigger() || self.ve.volume == 0 { + 0x00 + } else if self.lfsr.next() { + i32::from(self.ve.volume) + } else { + i32::from(self.ve.volume) * -1 + }; + self.blip.set(self.blip.from + self.timer.period, ampl); + } + } +} + +impl Memory for ChannelNoise { + fn get(&self, a: u16) -> u8 { + match a { + 0xff1f => self.reg.borrow().nrx0, + 0xff20 => self.reg.borrow().nrx1, + 0xff21 => self.reg.borrow().nrx2, + 0xff22 => self.reg.borrow().nrx3, + 0xff23 => self.reg.borrow().nrx4, + _ => unreachable!(), + } + } + + fn set(&mut self, a: u16, v: u8) { + match a { + 0xff1f => self.reg.borrow_mut().nrx0 = v, + 0xff20 => { + self.reg.borrow_mut().nrx1 = v; + self.lc.n = self.reg.borrow().get_length_load(); + } + 0xff21 => self.reg.borrow_mut().nrx2 = v, + 0xff22 => { + self.reg.borrow_mut().nrx3 = v; + self.timer.period = period(self.reg.clone()); + } + 0xff23 => { + self.reg.borrow_mut().nrx4 = v; + if self.reg.borrow().get_trigger() { + self.lc.reload(); + self.ve.reload(); + self.lfsr.reload(); + } + } + _ => unreachable!(), + } + } +} + +pub struct Apu { + pub buffer: Arc>>, + reg: Register, + timer: Clock, + fs: FrameSequencer, + channel1: ChannelSquare, + channel2: ChannelSquare, + channel3: ChannelWave, + channel4: ChannelNoise, + sample_rate: u32, +} + +impl Apu { + pub fn power_up(sample_rate: u32) -> Self { + let blipbuf1 = create_blipbuf(sample_rate); + let blipbuf2 = create_blipbuf(sample_rate); + let blipbuf3 = create_blipbuf(sample_rate); + let blipbuf4 = create_blipbuf(sample_rate); + Self { + buffer: Arc::new(Mutex::new(Vec::new())), + reg: Register::power_up(Channel::Mixer), + timer: Clock::power_up(cpu::CLOCK_FREQUENCY / 512), + fs: FrameSequencer::power_up(), + channel1: ChannelSquare::power_up(blipbuf1, Channel::Square1), + channel2: ChannelSquare::power_up(blipbuf2, Channel::Square2), + channel3: ChannelWave::power_up(blipbuf3), + channel4: ChannelNoise::power_up(blipbuf4), + sample_rate, + } + } + + fn play(&mut self, l: &[f32], r: &[f32]) { + assert_eq!(l.len(), r.len()); + let mut buffer = self.buffer.lock().unwrap(); + for (l, r) in l.iter().zip(r) { + // Do not fill the buffer with more than 1 second of data + // This speeds up the resync after the turning on and off the speed limiter + if buffer.len() > self.sample_rate as usize { + return; + } + buffer.push((*l, *r)); + } + } + + pub fn next(&mut self, cycles: u32) { + if !self.reg.get_power() { + return; + } + + for _ in 0..self.timer.next(cycles) { + self.channel1.next(self.timer.period); + self.channel2.next(self.timer.period); + self.channel3.next(self.timer.period); + self.channel4.next(self.timer.period); + + let step = self.fs.next(); + if step == 0 || step == 2 || step == 4 || step == 6 { + self.channel1.lc.next(); + self.channel2.lc.next(); + self.channel3.lc.next(); + self.channel4.lc.next(); + } + if step == 7 { + self.channel1.ve.next(); + self.channel2.ve.next(); + self.channel4.ve.next(); + } + if step == 2 || step == 6 { + self.channel1.fs.next(); + self.channel1.timer.period = period(self.channel1.reg.clone()); + } + + self.channel1.blip.data.end_frame(self.timer.period); + self.channel2.blip.data.end_frame(self.timer.period); + self.channel3.blip.data.end_frame(self.timer.period); + self.channel4.blip.data.end_frame(self.timer.period); + self.channel1.blip.from -= self.timer.period; + self.channel2.blip.from -= self.timer.period; + self.channel3.blip.from -= self.timer.period; + self.channel4.blip.from -= self.timer.period; + self.mix(); + } + } + + fn mix(&mut self) { + let sc1 = self.channel1.blip.data.samples_avail(); + let sc2 = self.channel2.blip.data.samples_avail(); + let sc3 = self.channel3.blip.data.samples_avail(); + let sc4 = self.channel4.blip.data.samples_avail(); + assert_eq!(sc1, sc2); + assert_eq!(sc2, sc3); + assert_eq!(sc3, sc4); + + let sample_count = sc1 as usize; + let mut sum = 0; + + let l_vol = (f32::from(self.reg.get_l_vol()) / 7.0) * (1.0 / 15.0) * 0.25; + let r_vol = (f32::from(self.reg.get_r_vol()) / 7.0) * (1.0 / 15.0) * 0.25; + + while sum < sample_count { + let buf_l = &mut [0f32; 2048]; + let buf_r = &mut [0f32; 2048]; + let buf = &mut [0i16; 2048]; + + let count1 = self.channel1.blip.data.read_samples(buf, false); + for (i, v) in buf[..count1].iter().enumerate() { + if self.reg.nrx1 & 0x01 == 0x01 { + buf_l[i] += f32::from(*v) * l_vol; + } + if self.reg.nrx1 & 0x10 == 0x10 { + buf_r[i] += f32::from(*v) * r_vol; + } + } + + let count2 = self.channel2.blip.data.read_samples(buf, false); + for (i, v) in buf[..count2].iter().enumerate() { + if self.reg.nrx1 & 0x02 == 0x02 { + buf_l[i] += f32::from(*v) * l_vol; + } + if self.reg.nrx1 & 0x20 == 0x20 { + buf_r[i] += f32::from(*v) * r_vol; + } + } + + let count3 = self.channel3.blip.data.read_samples(buf, false); + for (i, v) in buf[..count3].iter().enumerate() { + if self.reg.nrx1 & 0x04 == 0x04 { + buf_l[i] += f32::from(*v) * l_vol; + } + if self.reg.nrx1 & 0x40 == 0x40 { + buf_r[i] += f32::from(*v) * r_vol; + } + } + + let count4 = self.channel4.blip.data.read_samples(buf, false); + for (i, v) in buf[..count4].iter().enumerate() { + if self.reg.nrx1 & 0x08 == 0x08 { + buf_l[i] += f32::from(*v) * l_vol; + } + if self.reg.nrx1 & 0x80 == 0x80 { + buf_r[i] += f32::from(*v) * r_vol; + } + } + + assert_eq!(count1, count2); + assert_eq!(count2, count3); + assert_eq!(count3, count4); + + self.play(&buf_l[..count1], &buf_r[..count1]); + sum += count1; + } + } +} + +// Registers are ORed with this when reading +const RD_MASK: [u8; 48] = [ + 0x80, 0x3f, 0x00, 0xff, 0xbf, 0xff, 0x3f, 0x00, 0xff, 0xbf, 0x7f, 0xff, 0x9f, 0xff, 0xbf, 0xff, + 0xff, 0x00, 0x00, 0xbf, 0x00, 0x00, 0x70, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, +]; + +impl Memory for Apu { + fn get(&self, a: u16) -> u8 { + let r = match a { + 0xff10..=0xff14 => self.channel1.get(a), + 0xff15..=0xff19 => self.channel2.get(a), + 0xff1a..=0xff1e => self.channel3.get(a), + 0xff1f..=0xff23 => self.channel4.get(a), + 0xff24 => self.reg.nrx0, + 0xff25 => self.reg.nrx1, + 0xff26 => { + let a = self.reg.nrx2 & 0xf0; + let b = if self.channel1.reg.borrow().get_trigger() { + 1 + } else { + 0 + }; + let c = if self.channel2.reg.borrow().get_trigger() { + 2 + } else { + 0 + }; + let d = if self.channel3.reg.borrow().get_trigger() + && self.channel3.reg.borrow().get_dac_power() + { + 4 + } else { + 0 + }; + let e = if self.channel4.reg.borrow().get_trigger() { + 8 + } else { + 0 + }; + a | b | c | d | e + } + 0xff27..=0xff2f => 0x00, + 0xff30..=0xff3f => self.channel3.get(a), + _ => unreachable!(), + }; + r | RD_MASK[a as usize - 0xff10] + } + + fn set(&mut self, a: u16, v: u8) { + if a != 0xff26 && !self.reg.get_power() { + return; + } + match a { + 0xff10..=0xff14 => self.channel1.set(a, v), + 0xff15..=0xff19 => self.channel2.set(a, v), + 0xff1a..=0xff1e => self.channel3.set(a, v), + 0xff1f..=0xff23 => self.channel4.set(a, v), + 0xff24 => self.reg.nrx0 = v, + 0xff25 => self.reg.nrx1 = v, + 0xff26 => { + self.reg.nrx2 = v; + // Powering APU off should write 0 to all regs + // Powering APU off shouldn't affect wave, that wave RAM is unchanged + if !self.reg.get_power() { + self.channel1.reg.borrow_mut().nrx0 = 0x00; + self.channel1.reg.borrow_mut().nrx1 = 0x00; + self.channel1.reg.borrow_mut().nrx2 = 0x00; + self.channel1.reg.borrow_mut().nrx3 = 0x00; + self.channel1.reg.borrow_mut().nrx4 = 0x00; + self.channel2.reg.borrow_mut().nrx0 = 0x00; + self.channel2.reg.borrow_mut().nrx1 = 0x00; + self.channel2.reg.borrow_mut().nrx2 = 0x00; + self.channel2.reg.borrow_mut().nrx3 = 0x00; + self.channel2.reg.borrow_mut().nrx4 = 0x00; + self.channel3.reg.borrow_mut().nrx0 = 0x00; + self.channel3.reg.borrow_mut().nrx1 = 0x00; + self.channel3.reg.borrow_mut().nrx2 = 0x00; + self.channel3.reg.borrow_mut().nrx3 = 0x00; + self.channel3.reg.borrow_mut().nrx4 = 0x00; + self.channel4.reg.borrow_mut().nrx0 = 0x00; + self.channel4.reg.borrow_mut().nrx1 = 0x00; + self.channel4.reg.borrow_mut().nrx2 = 0x00; + self.channel4.reg.borrow_mut().nrx3 = 0x00; + self.channel4.reg.borrow_mut().nrx4 = 0x00; + self.reg.nrx0 = 0x00; + self.reg.nrx1 = 0x00; + self.reg.nrx2 = 0x00; + self.reg.nrx3 = 0x00; + self.reg.nrx4 = 0x00; + } + } + 0xff27..=0xff2f => {} + 0xff30..=0xff3f => self.channel3.set(a, v), + _ => unreachable!(), + } + } +} + +fn create_blipbuf(sample_rate: u32) -> BlipBuf { + let mut blipbuf = BlipBuf::new(sample_rate); + blipbuf.set_rates(f64::from(cpu::CLOCK_FREQUENCY), f64::from(sample_rate)); + blipbuf +} + +fn period(reg: Rc>) -> u32 { + match reg.borrow().channel { + Channel::Square1 | Channel::Square2 => 4 * (2048 - u32::from(reg.borrow().get_frequency())), + Channel::Wave => 2 * (2048 - u32::from(reg.borrow().get_frequency())), + Channel::Noise => { + let d = match reg.borrow().get_dividor_code() { + 0 => 8, + n => (u32::from(n) + 1) * 16, + }; + d << reg.borrow().get_clock_shift() + } + Channel::Mixer => cpu::CLOCK_FREQUENCY / 512, + } +} diff --git a/src/cartridge.rs b/src/cartridge.rs new file mode 100644 index 0000000000..e2c0b5e6d9 --- /dev/null +++ b/src/cartridge.rs @@ -0,0 +1,429 @@ +// As the gameboys 16 bit address bus offers only limited space for ROM and RAM addressing, many games are using Memory +// Bank Controllers (MBCs) to expand the available address space by bank switching. These MBC chips are located in the +// game cartridge (ie. not in the gameboy itself). +// +// In each cartridge, the required (or preferred) MBC type should be specified in the byte at 0147h of the ROM, as +// described in the cartridge header. Several different MBC types are available. +// +// Reference: +// - http://gbdev.gg8.se/wiki/articles/The_Cartridge_Header +// - http://gbdev.gg8.se/wiki/articles/Memory_Bank_Controllers +use super::memory::Memory; +use std::fs::File; +use std::io::Read; +use std::path::Path; +use std::time::SystemTime; + +struct RealTimeClock { + s: u8, + m: u8, + h: u8, + dl: u8, + dh: u8, + zero: u64, +} + +impl RealTimeClock { + fn power_up() -> Self { + let zero = SystemTime::now() + .duration_since(SystemTime::UNIX_EPOCH) + .unwrap() + .as_secs(); + Self { + zero, + s: 0, + m: 0, + h: 0, + dl: 0, + dh: 0, + } + } + + fn tic(&mut self) { + let d = SystemTime::now() + .duration_since(SystemTime::UNIX_EPOCH) + .unwrap() + .as_secs() + - self.zero; + + self.s = (d % 60) as u8; + self.m = (d / 60 % 60) as u8; + self.h = (d / 3600 % 24) as u8; + let days = (d / 3600 / 24) as u16; + self.dl = (days % 256) as u8; + match days { + 0x0000..=0x00ff => {} + 0x0100..=0x01ff => { + self.dh |= 0x01; + } + _ => { + self.dh |= 0x01; + self.dh |= 0x80; + } + } + } +} + +impl Memory for RealTimeClock { + fn get(&self, a: u16) -> u8 { + match a { + 0x08 => self.s, + 0x09 => self.m, + 0x0a => self.h, + 0x0b => self.dl, + 0x0c => self.dh, + _ => panic!("No entry"), + } + } + + fn set(&mut self, a: u16, v: u8) { + match a { + 0x08 => self.s = v, + 0x09 => self.m = v, + 0x0a => self.h = v, + 0x0b => self.dl = v, + 0x0c => self.dh = v, + _ => panic!("No entry"), + } + } +} + +// Beside for the ability to access up to 2MB ROM (128 banks), and 64KB RAM (8 banks), the MBC3 also includes a +// built-in Real Time Clock (RTC). The RTC requires an external 32.768 kHz Quartz Oscillator, and an external +// battery (if it should continue to tick when the gameboy is turned off). +// 0000-3FFF - ROM Bank 00 (Read Only) +// Same as for MBC1. +// +// 4000-7FFF - ROM Bank 01-7F (Read Only) +// Same as for MBC1, except that accessing banks 20h, 40h, and 60h is supported now. +// +// A000-BFFF - RAM Bank 00-03, if any (Read/Write) +// A000-BFFF - RTC Register 08-0C (Read/Write) +// Depending on the current Bank Number/RTC Register selection (see below), this memory space is used to access an +// 8KByte external RAM Bank, or a single RTC Register. +// +// 0000-1FFF - RAM and Timer Enable (Write Only) +// Mostly the same as for MBC1, a value of 0Ah will enable reading and writing to external RAM - and to the RTC +// Registers! A value of 00h will disable either. +// +// 2000-3FFF - ROM Bank Number (Write Only) +// Same as for MBC1, except that the whole 7 bits of the RAM Bank Number are written directly to this address. As for +// the MBC1, writing a value of 00h, will select Bank 01h instead. All other values 01-7Fh select the corresponding +// ROM Banks. +// +// 4000-5FFF - RAM Bank Number - or - RTC Register Select (Write Only) +// As for the MBC1s RAM Banking Mode, writing a value in range for 00h-07h maps the corresponding external RAM Bank ( +// if any) into memory at A000-BFFF. When writing a value of 08h-0Ch, this will map the corresponding RTC register into +// memory at A000-BFFF. That register could then be read/written by accessing any address in that area, typically that +// is done by using address A000. +// +// 6000-7FFF - Latch Clock Data (Write Only) +// When writing 00h, and then 01h to this register, the current time becomes latched into the RTC registers. The +// latched data will not change until it becomes latched again, by repeating the write 00h->01h procedure. This is +// supposed for from the RTC registers. This can be proven by reading the latched (frozen) time from the RTC +// registers, and then unlatch the registers to show the clock itself continues to tick in background. +// +// The Clock Counter Registers +// 08h RTC S Seconds 0-59 (0-3Bh) +// 09h RTC M Minutes 0-59 (0-3Bh) +// 0Ah RTC H Hours 0-23 (0-17h) +// 0Bh RTC DL Lower 8 bits of Day Counter (0-FFh) +// 0Ch RTC DH Upper 1 bit of Day Counter, Carry Bit, Halt Flag +// Bit 0 Most significant bit of Day Counter (Bit 8) +// Bit 6 Halt (0=Active, 1=Stop Timer) +// Bit 7 Day Counter Carry Bit (1=Counter Overflow) +// The Halt Flag is supposed to be set before to the RTC Registers. +// +// The Day Counter +// The total 9 bits of the Day Counter allow to count days in range from 0-511 (0-1FFh). The Day Counter Carry Bit +// becomes set when this value overflows. In that case the Carry Bit remains set until the program does reset it. Note +// that you can store an offset to the Day Counter in battery RAM. For example, every time you read a non-zero Day +// Counter, add this Counter to the offset in RAM, and reset the Counter to zero. This method allows to count any +// number of days, making your program Year-10000-Proof, provided that the cartridge gets used at least every 511 days. +// +// Delays +// When accessing the RTC Registers it is recommended to execute a 4ms delay (4 Cycles in Normal Speed Mode) between +// the separate accesses. +pub struct Mbc3 { + rom: Vec, + ram: Vec, + rtc: RealTimeClock, + rom_bank: usize, + ram_bank: usize, + ram_enable: bool, +} + +impl Mbc3 { + pub fn power_up(rom: Vec, ram: Vec) -> Self { + Self { + rom, + ram, + rtc: RealTimeClock::power_up(), + rom_bank: 1, + ram_bank: 0, + ram_enable: false, + } + } +} + +impl Memory for Mbc3 { + fn get(&self, a: u16) -> u8 { + match a { + 0x0000..=0x3fff => self.rom[a as usize], + 0x4000..=0x7fff => { + let i = self.rom_bank * 0x4000 + a as usize - 0x4000; + self.rom[i] + } + 0xa000..=0xbfff => { + if self.ram_enable { + if self.ram_bank <= 0x03 { + let i = self.ram_bank * 0x2000 + a as usize - 0xa000; + self.ram[i] + } else { + self.rtc.get(self.ram_bank as u16) + } + } else { + 0x00 + } + } + _ => 0x00, + } + } + + fn set(&mut self, a: u16, v: u8) { + match a { + 0xa000..=0xbfff => { + if self.ram_enable { + if self.ram_bank <= 0x03 { + let i = self.ram_bank * 0x2000 + a as usize - 0xa000; + self.ram[i] = v; + } else { + self.rtc.set(self.ram_bank as u16, v) + } + } + } + 0x0000..=0x1fff => { + self.ram_enable = v & 0x0f == 0x0a; + } + 0x2000..=0x3fff => { + let n = (v & 0x7f) as usize; + let n = match n { + 0x00 => 0x01, + _ => n, + }; + self.rom_bank = n; + } + 0x4000..=0x5fff => { + let n = (v & 0x0f) as usize; + self.ram_bank = n; + } + 0x6000..=0x7fff => { + if v & 0x01 != 0 { + self.rtc.tic(); + } + } + _ => {} + } + } +} + +// Specifies which Memory Bank Controller (if any) is used in the cartridge, and if further external hardware exists in +// the cartridge. +// 00h ROM ONLY 19h MBC5 +// 01h MBC1 1Ah MBC5+RAM +// 02h MBC1+RAM 1Bh MBC5+RAM+BATTERY +// 03h MBC1+RAM+BATTERY 1Ch MBC5+RUMBLE +// 05h MBC2 1Dh MBC5+RUMBLE+RAM +// 06h MBC2+BATTERY 1Eh MBC5+RUMBLE+RAM+BATTERY +// 08h ROM+RAM 20h MBC6 +// 09h ROM+RAM+BATTERY 22h MBC7+SENSOR+RUMBLE+RAM+BATTERY +// 0Bh MMM01 +// 0Ch MMM01+RAM +// 0Dh MMM01+RAM+BATTERY +// 0Fh MBC3+TIMER+BATTERY +// 10h MBC3+TIMER+RAM+BATTERY FCh POCKET CAMERA +// 11h MBC3 FDh BANDAI TAMA5 +// 12h MBC3+RAM FEh HuC3 +// 13h MBC3+RAM+BATTERY FFh HuC1+RAM+BATTERY +pub fn power_up(path: impl AsRef) -> Box { + eprintln!("Loading cartridge from {:?}", path.as_ref()); + let mut f = File::open(path.as_ref()).unwrap(); + let mut rom = Vec::new(); + f.read_to_end(&mut rom).unwrap(); + if rom.len() < 0x150 { + panic!("Missing required information area which located at 0100-014F") + } + let rom_max = rom_size(rom[0x0148]); + if rom.len() > rom_max { + panic!("Rom size more than {}", rom_max); + } + let cart: Box = match rom[0x0147] { + 0x0f => Box::new(Mbc3::power_up(rom, vec![])), + 0x10 => { + let ram_max = ram_size(rom[0x0149]); + let sav_path = path.as_ref().to_path_buf().with_extension("sav"); + let ram = ram_read(sav_path.clone(), ram_max); + Box::new(Mbc3::power_up(rom, ram)) + } + 0x11 => Box::new(Mbc3::power_up(rom, vec![])), + 0x12 => { + let ram_max = ram_size(rom[0x0149]); + Box::new(Mbc3::power_up(rom, vec![0; ram_max])) + } + 0x13 => { + let ram_max = ram_size(rom[0x0149]); + let sav_path = path.as_ref().to_path_buf().with_extension("sav"); + let ram = ram_read(sav_path.clone(), ram_max); + Box::new(Mbc3::power_up(rom, ram)) + } + n => panic!("Unsupported cartridge type: 0x{:02x}", n), + }; + eprintln!("Cartridge name is {}", cart.title()); + eprintln!("Cartridge type is {}", mbc_info(cart.get(0x0147))); + ensure_logo(cart.as_ref()); + ensure_header_checksum(cart.as_ref()); + cart +} + +// Specifies the ROM Size of the cartridge. Typically calculated as "32KB shl N". +fn rom_size(b: u8) -> usize { + let bank = 16384; + match b { + 0x00 => bank * 2, + 0x01 => bank * 4, + 0x02 => bank * 8, + 0x03 => bank * 16, + 0x04 => bank * 32, + 0x05 => bank * 64, + 0x06 => bank * 128, + 0x07 => bank * 256, + 0x08 => bank * 512, + 0x52 => bank * 72, + 0x53 => bank * 80, + 0x54 => bank * 96, + n => panic!("Unsupported rom size: 0x{:02x}", n), + } +} + +// Specifies the size of the external RAM in the cartridge (if any). +fn ram_size(b: u8) -> usize { + match b { + 0x00 => 0, + 0x01 => 1024 * 2, + 0x02 => 1024 * 8, + 0x03 => 1024 * 32, + 0x04 => 1024 * 128, + 0x05 => 1024 * 64, + n => panic!("Unsupported ram size: 0x{:02x}", n), + } +} + +// Specifies the size of the external RAM in the cartridge (if any). +fn ram_read(path: impl AsRef, size: usize) -> Vec { + match File::open(path) { + Ok(mut ok) => { + let mut ram = Vec::new(); + ok.read_to_end(&mut ram).unwrap(); + ram + } + Err(_) => vec![0; size], + } +} + +// Readable form of MBC representation +fn mbc_info(b: u8) -> String { + String::from(match b { + 0x00 => "ROM ONLY", + 0x01 => "MBC1", + 0x02 => "MBC1+RAM", + 0x03 => "MBC1+RAM+BATTERY", + 0x05 => "MBC2", + 0x06 => "MBC2+BATTERY", + 0x08 => "ROM+RAM", + 0x09 => "ROM+RAM+BATTERY", + 0x0b => "MMM01", + 0x0c => "MMM01+RAM", + 0x0d => "MMM01+RAM+BATTERY", + 0x0f => "MBC3+TIMER+BATTERY", + 0x10 => "MBC3+TIMER+RAM+BATTERY", + 0x11 => "MBC3", + 0x12 => "MBC3+RAM", + 0x13 => "MBC3+RAM+BATTERY", + 0x15 => "MBC4", + 0x16 => "MBC4+RAM", + 0x17 => "MBC4+RAM+BATTERY", + 0x19 => "MBC5", + 0x1a => "MBC5+RAM", + 0x1b => "MBC5+RAM+BATTERY", + 0x1c => "MBC5+RUMBLE", + 0x1d => "MBC5+RUMBLE+RAM", + 0x1e => "MBC5+RUMBLE+RAM+BATTERY", + 0xfc => "POCKET CAMERA", + 0xfd => "BANDAI TAMA5", + 0xfe => "HuC3", + 0x1f => "HuC1+RAM+BATTERY", + n => panic!("Unsupported cartridge type: 0x{:02x}", n), + }) +} + +// These bytes define the bitmap of the Nintendo logo that is displayed when the gameboy gets turned on. +// The reason for joining is because if the pirates copy the cartridge, they must also copy Nintendo's LOGO, +// which infringes the trademark law. In the early days, the copyright law is not perfect for the determination of +// electronic data. +// The hexdump of this bitmap is: +const NINTENDO_LOGO: [u8; 48] = [ + 0xCE, 0xED, 0x66, 0x66, 0xCC, 0x0D, 0x00, 0x0B, 0x03, 0x73, 0x00, 0x83, 0x00, 0x0C, 0x00, 0x0D, + 0x00, 0x08, 0x11, 0x1F, 0x88, 0x89, 0x00, 0x0E, 0xDC, 0xCC, 0x6E, 0xE6, 0xDD, 0xDD, 0xD9, 0x99, + 0xBB, 0xBB, 0x67, 0x63, 0x6E, 0x0E, 0xEC, 0xCC, 0xDD, 0xDC, 0x99, 0x9F, 0xBB, 0xB9, 0x33, 0x3E, +]; + +// Ensure Nintendo Logo. +fn ensure_logo(cart: &dyn Cartridge) { + for i in 0..48 { + if cart.get(0x0104 + i as u16) != NINTENDO_LOGO[i as usize] { + panic!("Nintendo logo is incorrect") + } + } +} + +// In position 0x14d, contains an 8 bit checksum across the cartridge header bytes 0134-014C. The checksum is +// calculated as follows: +// +// x=0:FOR i=0134h TO 014Ch:x=x-MEM[i]-1:NEXT +// +// The lower 8 bits of the result must be the same than the value in this entry. The GAME WON'T WORK if this +// checksum is incorrect. +fn ensure_header_checksum(cart: &dyn Cartridge) { + let mut v: u8 = 0; + for i in 0x0134..0x014d { + v = v.wrapping_sub(cart.get(i)).wrapping_sub(1); + } + if cart.get(0x014d) != v { + panic!("Cartridge's header checksum is incorrect") + } +} + +pub trait Cartridge: Memory + Send { + // Title of the game in UPPER CASE ASCII. If it is less than 16 characters then the remaining bytes are filled with + // 00's. When inventing the CGB, Nintendo has reduced the length of this area to 15 characters, and some months + // later they had the fantastic idea to reduce it to 11 characters only. The new meaning of the ex-title bytes is + // described below. + fn title(&self) -> String { + let mut buf = String::new(); + let ic = 0x0134; + let oc = if self.get(0x0143) == 0x80 { + 0x013e + } else { + 0x0143 + }; + for i in ic..oc { + match self.get(i) { + 0 => break, + v => buf.push(v as char), + } + } + buf + } +} + +impl Cartridge for Mbc3 {} diff --git a/src/clock.rs b/src/clock.rs new file mode 100644 index 0000000000..9dea492e2b --- /dev/null +++ b/src/clock.rs @@ -0,0 +1,18 @@ +// Clock is outputed 1 cycle every N cycles. +pub struct Clock { + pub period: u32, + pub n: u32, +} + +impl Clock { + pub fn power_up(period: u32) -> Self { + Self { period, n: 0x00 } + } + + pub fn next(&mut self, cycles: u32) -> u32 { + self.n += cycles; + let rs = self.n / self.period; + self.n = self.n % self.period; + rs + } +} diff --git a/src/convention.rs b/src/convention.rs new file mode 100644 index 0000000000..e67ea05902 --- /dev/null +++ b/src/convention.rs @@ -0,0 +1,7 @@ +#[derive(Clone, Copy, Eq, PartialEq)] +pub enum Term { + GB, // Original GameBoy (GameBoy Classic) + GBP, // GameBoy Pocket/GameBoy Light + GBC, // GameBoy Color + SGB, // Super GameBoy +} diff --git a/src/cpu.rs b/src/cpu.rs new file mode 100644 index 0000000000..7df59fee17 --- /dev/null +++ b/src/cpu.rs @@ -0,0 +1,1753 @@ +// The chip behind the NINTENDO GAME BOY: The sharp LR35902. +use super::convention::Term; +use super::memory::Memory; +use super::register::Flag::{C, H, N, Z}; +use super::register::Register; +use std::cell::RefCell; +use std::rc::Rc; +use std::thread; +use std::time; + +pub const CLOCK_FREQUENCY: u32 = 4_194_304; +pub const STEP_TIME: u32 = 16; +pub const STEP_CYCLES: u32 = (STEP_TIME as f64 / (1000_f64 / CLOCK_FREQUENCY as f64)) as u32; + +// Nintendo documents describe the CPU & instructions speed in machine cycles while this document describes them in +// clock cycles. Here is the translation: +// 1 machine cycle = 4 clock cycles +// GB CPU Speed NOP Instruction +// Machine Cycles 1.05MHz 1 cycle +// Clock Cycles 4.19MHz 4 cycles +// +// 0 1 2 3 4 5 6 7 8 9 a b c d e f +const OP_CYCLES: [u32; 256] = [ + 1, 3, 2, 2, 1, 1, 2, 1, 5, 2, 2, 2, 1, 1, 2, 1, // 0 + 0, 3, 2, 2, 1, 1, 2, 1, 3, 2, 2, 2, 1, 1, 2, 1, // 1 + 2, 3, 2, 2, 1, 1, 2, 1, 2, 2, 2, 2, 1, 1, 2, 1, // 2 + 2, 3, 2, 2, 3, 3, 3, 1, 2, 2, 2, 2, 1, 1, 2, 1, // 3 + 1, 1, 1, 1, 1, 1, 2, 1, 1, 1, 1, 1, 1, 1, 2, 1, // 4 + 1, 1, 1, 1, 1, 1, 2, 1, 1, 1, 1, 1, 1, 1, 2, 1, // 5 + 1, 1, 1, 1, 1, 1, 2, 1, 1, 1, 1, 1, 1, 1, 2, 1, // 6 + 2, 2, 2, 2, 2, 2, 0, 2, 1, 1, 1, 1, 1, 1, 2, 1, // 7 + 1, 1, 1, 1, 1, 1, 2, 1, 1, 1, 1, 1, 1, 1, 2, 1, // 8 + 1, 1, 1, 1, 1, 1, 2, 1, 1, 1, 1, 1, 1, 1, 2, 1, // 9 + 1, 1, 1, 1, 1, 1, 2, 1, 1, 1, 1, 1, 1, 1, 2, 1, // a + 1, 1, 1, 1, 1, 1, 2, 1, 1, 1, 1, 1, 1, 1, 2, 1, // b + 2, 3, 3, 4, 3, 4, 2, 4, 2, 4, 3, 0, 3, 6, 2, 4, // c + 2, 3, 3, 0, 3, 4, 2, 4, 2, 4, 3, 0, 3, 0, 2, 4, // d + 3, 3, 2, 0, 0, 4, 2, 4, 4, 1, 4, 0, 0, 0, 2, 4, // e + 3, 3, 2, 1, 0, 4, 2, 4, 3, 2, 4, 1, 0, 0, 2, 4, // f +]; + +// 0 1 2 3 4 5 6 7 8 9 a b c d e f +const CB_CYCLES: [u32; 256] = [ + 2, 2, 2, 2, 2, 2, 4, 2, 2, 2, 2, 2, 2, 2, 4, 2, // 0 + 2, 2, 2, 2, 2, 2, 4, 2, 2, 2, 2, 2, 2, 2, 4, 2, // 1 + 2, 2, 2, 2, 2, 2, 4, 2, 2, 2, 2, 2, 2, 2, 4, 2, // 2 + 2, 2, 2, 2, 2, 2, 4, 2, 2, 2, 2, 2, 2, 2, 4, 2, // 3 + 2, 2, 2, 2, 2, 2, 3, 2, 2, 2, 2, 2, 2, 2, 3, 2, // 4 + 2, 2, 2, 2, 2, 2, 3, 2, 2, 2, 2, 2, 2, 2, 3, 2, // 5 + 2, 2, 2, 2, 2, 2, 3, 2, 2, 2, 2, 2, 2, 2, 3, 2, // 6 + 2, 2, 2, 2, 2, 2, 3, 2, 2, 2, 2, 2, 2, 2, 3, 2, // 7 + 2, 2, 2, 2, 2, 2, 4, 2, 2, 2, 2, 2, 2, 2, 4, 2, // 8 + 2, 2, 2, 2, 2, 2, 4, 2, 2, 2, 2, 2, 2, 2, 4, 2, // 9 + 2, 2, 2, 2, 2, 2, 4, 2, 2, 2, 2, 2, 2, 2, 4, 2, // a + 2, 2, 2, 2, 2, 2, 4, 2, 2, 2, 2, 2, 2, 2, 4, 2, // b + 2, 2, 2, 2, 2, 2, 4, 2, 2, 2, 2, 2, 2, 2, 4, 2, // c + 2, 2, 2, 2, 2, 2, 4, 2, 2, 2, 2, 2, 2, 2, 4, 2, // d + 2, 2, 2, 2, 2, 2, 4, 2, 2, 2, 2, 2, 2, 2, 4, 2, // e + 2, 2, 2, 2, 2, 2, 4, 2, 2, 2, 2, 2, 2, 2, 4, 2, // f +]; + +pub struct Cpu { + pub reg: Register, + pub mem: Rc>, + pub halted: bool, + pub ei: bool, +} + +// The GameBoy CPU is based on a subset of the Z80 microprocessor. A summary of these commands is given below. +// If 'Flags affected' is not given for a command then none are affected. +impl Cpu { + fn imm(&mut self) -> u8 { + let v = self.mem.borrow().get(self.reg.pc); + self.reg.pc += 1; + v + } + + fn imm_word(&mut self) -> u16 { + let v = self.mem.borrow().get_word(self.reg.pc); + self.reg.pc += 2; + v + } + + fn stack_add(&mut self, v: u16) { + self.reg.sp -= 2; + self.mem.borrow_mut().set_word(self.reg.sp, v); + } + + fn stack_pop(&mut self) -> u16 { + let r = self.mem.borrow().get_word(self.reg.sp); + self.reg.sp += 2; + r + } + + // Add n to A. + // n = A,B,C,D,E,H,L,(HL),# + // + // Flags affected: + // Z - Set if result is zero. + // N - Reset. + // H - Set if carry from bit 3. + // C - Set if carry from bit 7. + fn alu_add(&mut self, n: u8) { + let a = self.reg.a; + let r = a.wrapping_add(n); + self.reg.set_flag(C, u16::from(a) + u16::from(n) > 0xff); + self.reg.set_flag(H, (a & 0x0f) + (n & 0x0f) > 0x0f); + self.reg.set_flag(N, false); + self.reg.set_flag(Z, r == 0x00); + self.reg.a = r; + } + + // Add n + Carry flag to A. + // n = A,B,C,D,E,H,L,(HL),# + // + // Flags affected: + // Z - Set if result is zero. + // N - Reset. + // H - Set if carry from bit 3. + // C - Set if carry from bit 7. + fn alu_adc(&mut self, n: u8) { + let a = self.reg.a; + let c = u8::from(self.reg.get_flag(C)); + let r = a.wrapping_add(n).wrapping_add(c); + self.reg + .set_flag(C, u16::from(a) + u16::from(n) + u16::from(c) > 0xff); + self.reg + .set_flag(H, (a & 0x0f) + (n & 0x0f) + (c & 0x0f) > 0x0f); + self.reg.set_flag(N, false); + self.reg.set_flag(Z, r == 0x00); + self.reg.a = r; + } + + // Subtract n from A. + // n = A,B,C,D,E,H,L,(HL),# + // + // Flags affected: + // Z - Set if result is zero. + // N - Set. + // H - Set if no borrow from bit 4. + // C - Set if no borrow + fn alu_sub(&mut self, n: u8) { + let a = self.reg.a; + let r = a.wrapping_sub(n); + self.reg.set_flag(C, u16::from(a) < u16::from(n)); + self.reg.set_flag(H, (a & 0x0f) < (n & 0x0f)); + self.reg.set_flag(N, true); + self.reg.set_flag(Z, r == 0x00); + self.reg.a = r; + } + + // Subtract n + Carry flag from A. + // n = A,B,C,D,E,H,L,(HL),# + // + // Flags affected: + // Z - Set if result is zero. + // N - Set. + // H - Set if no borrow from bit 4. + // C - Set if no borrow. + fn alu_sbc(&mut self, n: u8) { + let a = self.reg.a; + let c = u8::from(self.reg.get_flag(C)); + let r = a.wrapping_sub(n).wrapping_sub(c); + self.reg + .set_flag(C, u16::from(a) < u16::from(n) + u16::from(c)); + self.reg.set_flag(H, (a & 0x0f) < (n & 0x0f) + c); + self.reg.set_flag(N, true); + self.reg.set_flag(Z, r == 0x00); + self.reg.a = r; + } + + // Logically AND n with A, result in A. + // n = A,B,C,D,E,H,L,(HL),# + // + // Flags affected: + // Z - Set if result is zero. + // N - Reset. + // H - Set. + // C - Reset + fn alu_and(&mut self, n: u8) { + let r = self.reg.a & n; + self.reg.set_flag(C, false); + self.reg.set_flag(H, true); + self.reg.set_flag(N, false); + self.reg.set_flag(Z, r == 0x00); + self.reg.a = r; + } + + // Logical OR n with register A, result in A. + // n = A,B,C,D,E,H,L,(HL),# + // + // Flags affected: + // Z - Set if result is zero. + // N - Reset. + // H - Reset. + // C - Reset. + fn alu_or(&mut self, n: u8) { + let r = self.reg.a | n; + self.reg.set_flag(C, false); + self.reg.set_flag(H, false); + self.reg.set_flag(N, false); + self.reg.set_flag(Z, r == 0x00); + self.reg.a = r; + } + + // Logical exclusive OR n with register A, result in A. + // n = A,B,C,D,E,H,L,(HL),# + // + // Flags affected: + // Z - Set if result is zero. + // N - Reset. + // H - Reset. + // C - Reset. + fn alu_xor(&mut self, n: u8) { + let r = self.reg.a ^ n; + self.reg.set_flag(C, false); + self.reg.set_flag(H, false); + self.reg.set_flag(N, false); + self.reg.set_flag(Z, r == 0x00); + self.reg.a = r; + } + + // Compare A with n. This is basically an A - n subtraction instruction but the results are thrown away. + // n = A,B,C,D,E,H,L,(HL),# + // + // Flags affected: + // Z - Set if result is zero. (Set if A = n.) + // N - Set. + // H - Set if no borrow from bit 4. + // C - Set for no borrow. (Set if A < n.) + fn alu_cp(&mut self, n: u8) { + let r = self.reg.a; + self.alu_sub(n); + self.reg.a = r; + } + + // Increment register n. + // n = A,B,C,D,E,H,L,(HL) + // + // Flags affected: + // Z - Set if result is zero. + // N - Reset. + // H - Set if carry from bit 3. + // C - Not affected. + fn alu_inc(&mut self, a: u8) -> u8 { + let r = a.wrapping_add(1); + self.reg.set_flag(H, (a & 0x0f) + 0x01 > 0x0f); + self.reg.set_flag(N, false); + self.reg.set_flag(Z, r == 0x00); + r + } + + // Decrement register n. + // n = A,B,C,D,E,H,L,(HL) + // + // Flags affected: + // Z - Set if reselt is zero. + // N - Set. + // H - Set if no borrow from bit 4. + // C - Not affected + fn alu_dec(&mut self, a: u8) -> u8 { + let r = a.wrapping_sub(1); + self.reg.set_flag(H, a.trailing_zeros() >= 4); + self.reg.set_flag(N, true); + self.reg.set_flag(Z, r == 0); + r + } + + // Add n to HL + // n = BC,DE,HL,SP + // + // Flags affected: + // Z - Not affected. + // N - Reset. + // H - Set if carry from bit 11. + // C - Set if carry from bit 15. + fn alu_add_hl(&mut self, n: u16) { + let a = self.reg.get_hl(); + let r = a.wrapping_add(n); + self.reg.set_flag(C, a > 0xffff - n); + self.reg.set_flag(H, (a & 0x0fff) + (n & 0x0fff) > 0x0fff); + self.reg.set_flag(N, false); + self.reg.set_hl(r); + } + + // Add n to Stack Pointer (SP). + // n = one byte signed immediate value (#). + // + // Flags affected: + // Z - Reset. + // N - Reset. + // H - Set or reset according to operation. + // C - Set or reset according to operation. + fn alu_add_sp(&mut self) { + let a = self.reg.sp; + let b = i16::from(self.imm() as i8) as u16; + self.reg.set_flag(C, (a & 0x00ff) + (b & 0x00ff) > 0x00ff); + self.reg.set_flag(H, (a & 0x000f) + (b & 0x000f) > 0x000f); + self.reg.set_flag(N, false); + self.reg.set_flag(Z, false); + self.reg.sp = a.wrapping_add(b); + } + + // Swap upper & lower nibles of n. + // n = A,B,C,D,E,H,L,(HL) + // + // Flags affected: + // Z - Set if result is zero. + // N - Reset. + // H - Reset. + // C - Reset. + fn alu_swap(&mut self, a: u8) -> u8 { + self.reg.set_flag(C, false); + self.reg.set_flag(H, false); + self.reg.set_flag(N, false); + self.reg.set_flag(Z, a == 0x00); + (a >> 4) | (a << 4) + } + + // Decimal adjust register A. This instruction adjusts register A so that the correct representation of Binary + // Coded Decimal (BCD) is obtained. + // + // Flags affected: + // Z - Set if register A is zero. + // N - Not affected. + // H - Reset. + // C - Set or reset according to operation + fn alu_daa(&mut self) { + let mut a = self.reg.a; + let mut adjust = if self.reg.get_flag(C) { 0x60 } else { 0x00 }; + if self.reg.get_flag(H) { + adjust |= 0x06; + }; + if !self.reg.get_flag(N) { + if a & 0x0f > 0x09 { + adjust |= 0x06; + }; + if a > 0x99 { + adjust |= 0x60; + }; + a = a.wrapping_add(adjust); + } else { + a = a.wrapping_sub(adjust); + } + self.reg.set_flag(C, adjust >= 0x60); + self.reg.set_flag(H, false); + self.reg.set_flag(Z, a == 0x00); + self.reg.a = a; + } + + // Complement A register. (Flip all bits.) + // + // Flags affected: + // Z - Not affected. + // N - Set. + // H - Set. + // C - Not affected. + fn alu_cpl(&mut self) { + self.reg.a = !self.reg.a; + self.reg.set_flag(H, true); + self.reg.set_flag(N, true); + } + + // Complement carry flag. If C flag is set, then reset it. If C flag is reset, then set it. + // Flags affected: + // + // Z - Not affected. + // N - Reset. + // H - Reset. + // C - Complemented. + fn alu_ccf(&mut self) { + let v = !self.reg.get_flag(C); + self.reg.set_flag(C, v); + self.reg.set_flag(H, false); + self.reg.set_flag(N, false); + } + + // Set Carry flag. + // + // Flags affected: + // Z - Not affected. + // N - Reset. + // H - Reset. + // C - Set. + fn alu_scf(&mut self) { + self.reg.set_flag(C, true); + self.reg.set_flag(H, false); + self.reg.set_flag(N, false); + } + + // Rotate A left. Old bit 7 to Carry flag. + // + // Flags affected: + // Z - Set if result is zero. + // N - Reset. + // H - Reset. + // C - Contains old bit 7 data. + fn alu_rlc(&mut self, a: u8) -> u8 { + let c = (a & 0x80) >> 7 == 0x01; + let r = (a << 1) | u8::from(c); + self.reg.set_flag(C, c); + self.reg.set_flag(H, false); + self.reg.set_flag(N, false); + self.reg.set_flag(Z, r == 0x00); + r + } + + // Rotate A left through Carry flag. + // + // Flags affected: + // Z - Set if result is zero. + // N - Reset. + // H - Reset. + // C - Contains old bit 7 data. + fn alu_rl(&mut self, a: u8) -> u8 { + let c = (a & 0x80) >> 7 == 0x01; + let r = (a << 1) + u8::from(self.reg.get_flag(C)); + self.reg.set_flag(C, c); + self.reg.set_flag(H, false); + self.reg.set_flag(N, false); + self.reg.set_flag(Z, r == 0x00); + r + } + + // Rotate A right. Old bit 0 to Carry flag. + // + // Flags affected: + // Z - Set if result is zero. + // N - Reset. + // H - Reset. + // C - Contains old bit 0 data + fn alu_rrc(&mut self, a: u8) -> u8 { + let c = a & 0x01 == 0x01; + let r = if c { 0x80 | (a >> 1) } else { a >> 1 }; + self.reg.set_flag(C, c); + self.reg.set_flag(H, false); + self.reg.set_flag(N, false); + self.reg.set_flag(Z, r == 0x00); + r + } + + // Rotate A right through Carry flag. + // + // Flags affected: + // Z - Set if result is zero. + // N - Reset. + // H - Reset. + // C - Contains old bit 0 data. + fn alu_rr(&mut self, a: u8) -> u8 { + let c = a & 0x01 == 0x01; + let r = if self.reg.get_flag(C) { + 0x80 | (a >> 1) + } else { + a >> 1 + }; + self.reg.set_flag(C, c); + self.reg.set_flag(H, false); + self.reg.set_flag(N, false); + self.reg.set_flag(Z, r == 0x00); + r + } + + // Shift n left into Carry. LSB of n set to 0. + // n = A,B,C,D,E,H,L,(HL) + // + // Flags affected: + // Z - Set if result is zero. + // N - Reset. + // H - Reset. + // C - Contains old bit 7 data + fn alu_sla(&mut self, a: u8) -> u8 { + let c = (a & 0x80) >> 7 == 0x01; + let r = a << 1; + self.reg.set_flag(C, c); + self.reg.set_flag(H, false); + self.reg.set_flag(N, false); + self.reg.set_flag(Z, r == 0x00); + r + } + + // Shift n right into Carry. MSB doesn't change. + // n = A,B,C,D,E,H,L,(HL) + // + // Flags affected: + // Z - Set if result is zero. + // N - Reset. + // H - Reset. + // C - Contains old bit 0 data. + fn alu_sra(&mut self, a: u8) -> u8 { + let c = a & 0x01 == 0x01; + let r = (a >> 1) | (a & 0x80); + self.reg.set_flag(C, c); + self.reg.set_flag(H, false); + self.reg.set_flag(N, false); + self.reg.set_flag(Z, r == 0x00); + r + } + + // Shift n right into Carry. MSB set to 0. + // n = A,B,C,D,E,H,L,(HL) + // + // Flags affected: + // Z - Set if result is zero. + // N - Reset. + // H - Reset. + // C - Contains old bit 0 data. + fn alu_srl(&mut self, a: u8) -> u8 { + let c = a & 0x01 == 0x01; + let r = a >> 1; + self.reg.set_flag(C, c); + self.reg.set_flag(H, false); + self.reg.set_flag(N, false); + self.reg.set_flag(Z, r == 0x00); + r + } + + // Test bit b in register r. + // b = 0 - 7, r = A,B,C,D,E,H,L,(HL) + // + // Flags affected: + // Z - Set if bit b of register r is 0. + // N - Reset. + // H - Set. + // C - Not affected + fn alu_bit(&mut self, a: u8, b: u8) { + let r = a & (1 << b) == 0x00; + self.reg.set_flag(H, true); + self.reg.set_flag(N, false); + self.reg.set_flag(Z, r); + } + + // Set bit b in register r. + // b = 0 - 7, r = A,B,C,D,E,H,L,(HL) + // + // Flags affected: None. + fn alu_set(&mut self, a: u8, b: u8) -> u8 { + a | (1 << b) + } + + // Reset bit b in register r. + // b = 0 - 7, r = A,B,C,D,E,H,L,(HL) + // + // Flags affected: None. + fn alu_res(&mut self, a: u8, b: u8) -> u8 { + a & !(1 << b) + } + + // Add n to current address and jump to it. + // n = one byte signed immediate value + fn alu_jr(&mut self, n: u8) { + let n = n as i8; + self.reg.pc = ((u32::from(self.reg.pc) as i32) + i32::from(n)) as u16; + } +} + +impl Cpu { + pub fn power_up(term: Term, mem: Rc>) -> Self { + Self { + reg: Register::power_up(term), + mem, + halted: false, + ei: true, + } + } + + // The IME (interrupt master enable) flag is reset by DI and prohibits all interrupts. It is set by EI and + // acknowledges the interrupt setting by the IE register. + // 1. When an interrupt is generated, the IF flag will be set. + // 2. If the IME flag is set & the corresponding IE flag is set, the following 3 steps are performed. + // 3. Reset the IME flag and prevent all interrupts. + // 4. The PC (program counter) is pushed onto the stack. + // 5. Jump to the starting address of the interrupt. + fn hi(&mut self) -> u32 { + if !self.halted && !self.ei { + return 0; + } + let intf = self.mem.borrow().get(0xff0f); + let inte = self.mem.borrow().get(0xffff); + let ii = intf & inte; + if ii == 0x00 { + return 0; + } + self.halted = false; + if !self.ei { + return 0; + } + self.ei = false; + + // Consumer an interrupter, the rest is written back to the register + let n = ii.trailing_zeros(); + let intf = intf & !(1 << n); + self.mem.borrow_mut().set(0xff0f, intf); + + self.stack_add(self.reg.pc); + // Set the PC to correspond interrupt process program: + // V-Blank: 0x40 + // LCD: 0x48 + // TIMER: 0x50 + // JOYPAD: 0x60 + // Serial: 0x58 + self.reg.pc = 0x0040 | ((n as u16) << 3); + 4 + } + + fn ex(&mut self) -> u32 { + let opcode = self.imm(); + let mut cbcode: u8 = 0; + match opcode { + // LD r8, d8 + 0x06 => self.reg.b = self.imm(), + 0x0e => self.reg.c = self.imm(), + 0x16 => self.reg.d = self.imm(), + 0x1e => self.reg.e = self.imm(), + 0x26 => self.reg.h = self.imm(), + 0x2e => self.reg.l = self.imm(), + 0x36 => { + let a = self.reg.get_hl(); + let v = self.imm(); + self.mem.borrow_mut().set(a, v); + } + 0x3e => self.reg.a = self.imm(), + + // LD (r16), A + 0x02 => self.mem.borrow_mut().set(self.reg.get_bc(), self.reg.a), + 0x12 => self.mem.borrow_mut().set(self.reg.get_de(), self.reg.a), + + // LD A, (r16) + 0x0a => self.reg.a = self.mem.borrow().get(self.reg.get_bc()), + 0x1a => self.reg.a = self.mem.borrow().get(self.reg.get_de()), + + // LD (HL+), A + 0x22 => { + let a = self.reg.get_hl(); + self.mem.borrow_mut().set(a, self.reg.a); + self.reg.set_hl(a + 1); + } + // LD (HL-), A + 0x32 => { + let a = self.reg.get_hl(); + self.mem.borrow_mut().set(a, self.reg.a); + self.reg.set_hl(a - 1); + } + // LD A, (HL+) + 0x2a => { + let v = self.reg.get_hl(); + self.reg.a = self.mem.borrow().get(v); + self.reg.set_hl(v + 1); + } + // LD A, (HL-) + 0x3a => { + let v = self.reg.get_hl(); + self.reg.a = self.mem.borrow().get(v); + self.reg.set_hl(v - 1); + } + + // LD r8, r8 + 0x40 => {} + 0x41 => self.reg.b = self.reg.c, + 0x42 => self.reg.b = self.reg.d, + 0x43 => self.reg.b = self.reg.e, + 0x44 => self.reg.b = self.reg.h, + 0x45 => self.reg.b = self.reg.l, + 0x46 => self.reg.b = self.mem.borrow().get(self.reg.get_hl()), + 0x47 => self.reg.b = self.reg.a, + 0x48 => self.reg.c = self.reg.b, + 0x49 => {} + 0x4a => self.reg.c = self.reg.d, + 0x4b => self.reg.c = self.reg.e, + 0x4c => self.reg.c = self.reg.h, + 0x4d => self.reg.c = self.reg.l, + 0x4e => self.reg.c = self.mem.borrow().get(self.reg.get_hl()), + 0x4f => self.reg.c = self.reg.a, + 0x50 => self.reg.d = self.reg.b, + 0x51 => self.reg.d = self.reg.c, + 0x52 => {} + 0x53 => self.reg.d = self.reg.e, + 0x54 => self.reg.d = self.reg.h, + 0x55 => self.reg.d = self.reg.l, + 0x56 => self.reg.d = self.mem.borrow().get(self.reg.get_hl()), + 0x57 => self.reg.d = self.reg.a, + 0x58 => self.reg.e = self.reg.b, + 0x59 => self.reg.e = self.reg.c, + 0x5a => self.reg.e = self.reg.d, + 0x5b => {} + 0x5c => self.reg.e = self.reg.h, + 0x5d => self.reg.e = self.reg.l, + 0x5e => self.reg.e = self.mem.borrow().get(self.reg.get_hl()), + 0x5f => self.reg.e = self.reg.a, + 0x60 => self.reg.h = self.reg.b, + 0x61 => self.reg.h = self.reg.c, + 0x62 => self.reg.h = self.reg.d, + 0x63 => self.reg.h = self.reg.e, + 0x64 => {} + 0x65 => self.reg.h = self.reg.l, + 0x66 => self.reg.h = self.mem.borrow().get(self.reg.get_hl()), + 0x67 => self.reg.h = self.reg.a, + 0x68 => self.reg.l = self.reg.b, + 0x69 => self.reg.l = self.reg.c, + 0x6a => self.reg.l = self.reg.d, + 0x6b => self.reg.l = self.reg.e, + 0x6c => self.reg.l = self.reg.h, + 0x6d => {} + 0x6e => self.reg.l = self.mem.borrow().get(self.reg.get_hl()), + 0x6f => self.reg.l = self.reg.a, + 0x70 => self.mem.borrow_mut().set(self.reg.get_hl(), self.reg.b), + 0x71 => self.mem.borrow_mut().set(self.reg.get_hl(), self.reg.c), + 0x72 => self.mem.borrow_mut().set(self.reg.get_hl(), self.reg.d), + 0x73 => self.mem.borrow_mut().set(self.reg.get_hl(), self.reg.e), + 0x74 => self.mem.borrow_mut().set(self.reg.get_hl(), self.reg.h), + 0x75 => self.mem.borrow_mut().set(self.reg.get_hl(), self.reg.l), + 0x77 => self.mem.borrow_mut().set(self.reg.get_hl(), self.reg.a), + 0x78 => self.reg.a = self.reg.b, + 0x79 => self.reg.a = self.reg.c, + 0x7a => self.reg.a = self.reg.d, + 0x7b => self.reg.a = self.reg.e, + 0x7c => self.reg.a = self.reg.h, + 0x7d => self.reg.a = self.reg.l, + 0x7e => self.reg.a = self.mem.borrow().get(self.reg.get_hl()), + 0x7f => {} + + // LDH (a8), A + 0xe0 => { + let a = 0xff00 | u16::from(self.imm()); + self.mem.borrow_mut().set(a, self.reg.a); + } + // LDH A, (a8) + 0xf0 => { + let a = 0xff00 | u16::from(self.imm()); + self.reg.a = self.mem.borrow().get(a); + } + + // LD (C), A + 0xe2 => self + .mem + .borrow_mut() + .set(0xff00 | u16::from(self.reg.c), self.reg.a), + // LD A, (C) + 0xf2 => self.reg.a = self.mem.borrow().get(0xff00 | u16::from(self.reg.c)), + + // LD (a16), A + 0xea => { + let a = self.imm_word(); + self.mem.borrow_mut().set(a, self.reg.a); + } + // LD A, (a16) + 0xfa => { + let a = self.imm_word(); + self.reg.a = self.mem.borrow().get(a); + } + + // LD r16, d16 + 0x01 | 0x11 | 0x21 | 0x31 => { + let v = self.imm_word(); + match opcode { + 0x01 => self.reg.set_bc(v), + 0x11 => self.reg.set_de(v), + 0x21 => self.reg.set_hl(v), + 0x31 => self.reg.sp = v, + _ => {} + } + } + + // LD SP, HL + 0xf9 => self.reg.sp = self.reg.get_hl(), + // LD SP, d8 + 0xf8 => { + let a = self.reg.sp; + let b = i16::from(self.imm() as i8) as u16; + self.reg.set_flag(C, (a & 0x00ff) + (b & 0x00ff) > 0x00ff); + self.reg.set_flag(H, (a & 0x000f) + (b & 0x000f) > 0x000f); + self.reg.set_flag(N, false); + self.reg.set_flag(Z, false); + self.reg.set_hl(a.wrapping_add(b)); + } + // LD (d16), SP + 0x08 => { + let a = self.imm_word(); + self.mem.borrow_mut().set_word(a, self.reg.sp); + } + + // PUSH + 0xc5 => self.stack_add(self.reg.get_bc()), + 0xd5 => self.stack_add(self.reg.get_de()), + 0xe5 => self.stack_add(self.reg.get_hl()), + 0xf5 => self.stack_add(self.reg.get_af()), + + // POP + 0xc1 | 0xf1 | 0xd1 | 0xe1 => { + let v = self.stack_pop(); + match opcode { + 0xc1 => self.reg.set_bc(v), + 0xd1 => self.reg.set_de(v), + 0xe1 => self.reg.set_hl(v), + 0xf1 => self.reg.set_af(v), + _ => {} + } + } + + // ADD A, r8/d8 + 0x80 => self.alu_add(self.reg.b), + 0x81 => self.alu_add(self.reg.c), + 0x82 => self.alu_add(self.reg.d), + 0x83 => self.alu_add(self.reg.e), + 0x84 => self.alu_add(self.reg.h), + 0x85 => self.alu_add(self.reg.l), + 0x86 => { + let v = self.mem.borrow().get(self.reg.get_hl()); + self.alu_add(v); + } + 0x87 => self.alu_add(self.reg.a), + 0xc6 => { + let v = self.imm(); + self.alu_add(v); + } + + // ADC A, r8/d8 + 0x88 => self.alu_adc(self.reg.b), + 0x89 => self.alu_adc(self.reg.c), + 0x8a => self.alu_adc(self.reg.d), + 0x8b => self.alu_adc(self.reg.e), + 0x8c => self.alu_adc(self.reg.h), + 0x8d => self.alu_adc(self.reg.l), + 0x8e => { + let a = self.mem.borrow().get(self.reg.get_hl()); + self.alu_adc(a); + } + 0x8f => self.alu_adc(self.reg.a), + 0xce => { + let v = self.imm(); + self.alu_adc(v); + } + + // SUB A, r8/d8 + 0x90 => self.alu_sub(self.reg.b), + 0x91 => self.alu_sub(self.reg.c), + 0x92 => self.alu_sub(self.reg.d), + 0x93 => self.alu_sub(self.reg.e), + 0x94 => self.alu_sub(self.reg.h), + 0x95 => self.alu_sub(self.reg.l), + 0x96 => { + let a = self.mem.borrow().get(self.reg.get_hl()); + self.alu_sub(a); + } + 0x97 => self.alu_sub(self.reg.a), + 0xd6 => { + let v = self.imm(); + self.alu_sub(v); + } + + // SBC A, r8/d8 + 0x98 => self.alu_sbc(self.reg.b), + 0x99 => self.alu_sbc(self.reg.c), + 0x9a => self.alu_sbc(self.reg.d), + 0x9b => self.alu_sbc(self.reg.e), + 0x9c => self.alu_sbc(self.reg.h), + 0x9d => self.alu_sbc(self.reg.l), + 0x9e => { + let a = self.mem.borrow().get(self.reg.get_hl()); + self.alu_sbc(a); + } + 0x9f => self.alu_sbc(self.reg.a), + 0xde => { + let v = self.imm(); + self.alu_sbc(v); + } + + // AND A, r8/d8 + 0xa0 => self.alu_and(self.reg.b), + 0xa1 => self.alu_and(self.reg.c), + 0xa2 => self.alu_and(self.reg.d), + 0xa3 => self.alu_and(self.reg.e), + 0xa4 => self.alu_and(self.reg.h), + 0xa5 => self.alu_and(self.reg.l), + 0xa6 => { + let a = self.mem.borrow().get(self.reg.get_hl()); + self.alu_and(a); + } + 0xa7 => self.alu_and(self.reg.a), + 0xe6 => { + let v = self.imm(); + self.alu_and(v); + } + + // OR A, r8/d8 + 0xb0 => self.alu_or(self.reg.b), + 0xb1 => self.alu_or(self.reg.c), + 0xb2 => self.alu_or(self.reg.d), + 0xb3 => self.alu_or(self.reg.e), + 0xb4 => self.alu_or(self.reg.h), + 0xb5 => self.alu_or(self.reg.l), + 0xb6 => { + let a = self.mem.borrow().get(self.reg.get_hl()); + self.alu_or(a); + } + 0xb7 => self.alu_or(self.reg.a), + 0xf6 => { + let v = self.imm(); + self.alu_or(v); + } + + // XOR A, r8/d8 + 0xa8 => self.alu_xor(self.reg.b), + 0xa9 => self.alu_xor(self.reg.c), + 0xaa => self.alu_xor(self.reg.d), + 0xab => self.alu_xor(self.reg.e), + 0xac => self.alu_xor(self.reg.h), + 0xad => self.alu_xor(self.reg.l), + 0xae => { + let a = self.mem.borrow().get(self.reg.get_hl()); + self.alu_xor(a); + } + 0xaf => self.alu_xor(self.reg.a), + 0xee => { + let v = self.imm(); + self.alu_xor(v); + } + + // CP A, r8/d8 + 0xb8 => self.alu_cp(self.reg.b), + 0xb9 => self.alu_cp(self.reg.c), + 0xba => self.alu_cp(self.reg.d), + 0xbb => self.alu_cp(self.reg.e), + 0xbc => self.alu_cp(self.reg.h), + 0xbd => self.alu_cp(self.reg.l), + 0xbe => { + let a = self.mem.borrow().get(self.reg.get_hl()); + self.alu_cp(a); + } + 0xbf => self.alu_cp(self.reg.a), + 0xfe => { + let v = self.imm(); + self.alu_cp(v); + } + + // INC r8 + 0x04 => self.reg.b = self.alu_inc(self.reg.b), + 0x0c => self.reg.c = self.alu_inc(self.reg.c), + 0x14 => self.reg.d = self.alu_inc(self.reg.d), + 0x1c => self.reg.e = self.alu_inc(self.reg.e), + 0x24 => self.reg.h = self.alu_inc(self.reg.h), + 0x2c => self.reg.l = self.alu_inc(self.reg.l), + 0x34 => { + let a = self.reg.get_hl(); + let v = self.mem.borrow().get(a); + let h = self.alu_inc(v); + self.mem.borrow_mut().set(a, h); + } + 0x3c => self.reg.a = self.alu_inc(self.reg.a), + + // DEC r8 + 0x05 => self.reg.b = self.alu_dec(self.reg.b), + 0x0d => self.reg.c = self.alu_dec(self.reg.c), + 0x15 => self.reg.d = self.alu_dec(self.reg.d), + 0x1d => self.reg.e = self.alu_dec(self.reg.e), + 0x25 => self.reg.h = self.alu_dec(self.reg.h), + 0x2d => self.reg.l = self.alu_dec(self.reg.l), + 0x35 => { + let a = self.reg.get_hl(); + let v = self.mem.borrow().get(a); + let h = self.alu_dec(v); + self.mem.borrow_mut().set(a, h); + } + 0x3d => self.reg.a = self.alu_dec(self.reg.a), + + // ADD HL, r16 + 0x09 => self.alu_add_hl(self.reg.get_bc()), + 0x19 => self.alu_add_hl(self.reg.get_de()), + 0x29 => self.alu_add_hl(self.reg.get_hl()), + 0x39 => self.alu_add_hl(self.reg.sp), + + // ADD SP, d8 + 0xe8 => self.alu_add_sp(), + + // INC r16 + 0x03 => { + let v = self.reg.get_bc().wrapping_add(1); + self.reg.set_bc(v); + } + 0x13 => { + let v = self.reg.get_de().wrapping_add(1); + self.reg.set_de(v); + } + 0x23 => { + let v = self.reg.get_hl().wrapping_add(1); + self.reg.set_hl(v); + } + 0x33 => { + let v = self.reg.sp.wrapping_add(1); + self.reg.sp = v; + } + + // DEC r16 + 0x0b => { + let v = self.reg.get_bc().wrapping_sub(1); + self.reg.set_bc(v); + } + 0x1b => { + let v = self.reg.get_de().wrapping_sub(1); + self.reg.set_de(v); + } + 0x2b => { + let v = self.reg.get_hl().wrapping_sub(1); + self.reg.set_hl(v); + } + 0x3b => { + let v = self.reg.sp.wrapping_sub(1); + self.reg.sp = v; + } + + // DAA + 0x27 => self.alu_daa(), + + // CPL + 0x2f => self.alu_cpl(), + + // CCF + 0x3f => self.alu_ccf(), + + // SCF + 0x37 => self.alu_scf(), + + // NOP + 0x00 => {} + + // HALT + 0x76 => self.halted = true, + + // STOP + 0x10 => {} + + // DI/EI + 0xf3 => self.ei = false, + 0xfb => self.ei = true, + + // RLCA + 0x07 => { + self.reg.a = self.alu_rlc(self.reg.a); + self.reg.set_flag(Z, false); + } + + // RLA + 0x17 => { + self.reg.a = self.alu_rl(self.reg.a); + self.reg.set_flag(Z, false); + } + + // RRCA + 0x0f => { + self.reg.a = self.alu_rrc(self.reg.a); + self.reg.set_flag(Z, false); + } + + // RRA + 0x1f => { + self.reg.a = self.alu_rr(self.reg.a); + self.reg.set_flag(Z, false); + } + + // JUMP + 0xc3 => self.reg.pc = self.imm_word(), + 0xe9 => self.reg.pc = self.reg.get_hl(), + + // JUMP IF + 0xc2 | 0xca | 0xd2 | 0xda => { + let pc = self.imm_word(); + let cond = match opcode { + 0xc2 => !self.reg.get_flag(Z), + 0xca => self.reg.get_flag(Z), + 0xd2 => !self.reg.get_flag(C), + 0xda => self.reg.get_flag(C), + _ => panic!(""), + }; + if cond { + self.reg.pc = pc; + } + } + + // JR + 0x18 => { + let n = self.imm(); + self.alu_jr(n); + } + + // JR IF + 0x20 | 0x28 | 0x30 | 0x38 => { + let cond = match opcode { + 0x20 => !self.reg.get_flag(Z), + 0x28 => self.reg.get_flag(Z), + 0x30 => !self.reg.get_flag(C), + 0x38 => self.reg.get_flag(C), + _ => panic!(""), + }; + let n = self.imm(); + if cond { + self.alu_jr(n); + } + } + + // CALL + 0xcd => { + let nn = self.imm_word(); + self.stack_add(self.reg.pc); + self.reg.pc = nn; + } + + // CALL IF + 0xc4 | 0xcc | 0xd4 | 0xdc => { + let cond = match opcode { + 0xc4 => !self.reg.get_flag(Z), + 0xcc => self.reg.get_flag(Z), + 0xd4 => !self.reg.get_flag(C), + 0xdc => self.reg.get_flag(C), + _ => panic!(""), + }; + let nn = self.imm_word(); + if cond { + self.stack_add(self.reg.pc); + self.reg.pc = nn; + } + } + + // RST + 0xc7 => { + self.stack_add(self.reg.pc); + self.reg.pc = 0x00; + } + 0xcf => { + self.stack_add(self.reg.pc); + self.reg.pc = 0x08; + } + 0xd7 => { + self.stack_add(self.reg.pc); + self.reg.pc = 0x10; + } + 0xdf => { + self.stack_add(self.reg.pc); + self.reg.pc = 0x18; + } + 0xe7 => { + self.stack_add(self.reg.pc); + self.reg.pc = 0x20; + } + 0xef => { + self.stack_add(self.reg.pc); + self.reg.pc = 0x28; + } + 0xf7 => { + self.stack_add(self.reg.pc); + self.reg.pc = 0x30; + } + 0xff => { + self.stack_add(self.reg.pc); + self.reg.pc = 0x38; + } + + // RET + 0xc9 => self.reg.pc = self.stack_pop(), + + // RET IF + 0xc0 | 0xc8 | 0xd0 | 0xd8 => { + let cond = match opcode { + 0xc0 => !self.reg.get_flag(Z), + 0xc8 => self.reg.get_flag(Z), + 0xd0 => !self.reg.get_flag(C), + 0xd8 => self.reg.get_flag(C), + _ => panic!(""), + }; + if cond { + self.reg.pc = self.stack_pop(); + } + } + + // RETI + 0xd9 => { + self.reg.pc = self.stack_pop(); + self.ei = true; + } + + // Extended Bit Operations + 0xcb => { + cbcode = self.mem.borrow().get(self.reg.pc); + self.reg.pc += 1; + match cbcode { + // RLC r8 + 0x00 => self.reg.b = self.alu_rlc(self.reg.b), + 0x01 => self.reg.c = self.alu_rlc(self.reg.c), + 0x02 => self.reg.d = self.alu_rlc(self.reg.d), + 0x03 => self.reg.e = self.alu_rlc(self.reg.e), + 0x04 => self.reg.h = self.alu_rlc(self.reg.h), + 0x05 => self.reg.l = self.alu_rlc(self.reg.l), + 0x06 => { + let a = self.reg.get_hl(); + let v = self.mem.borrow().get(a); + let h = self.alu_rlc(v); + self.mem.borrow_mut().set(a, h); + } + 0x07 => self.reg.a = self.alu_rlc(self.reg.a), + + // RRC r8 + 0x08 => self.reg.b = self.alu_rrc(self.reg.b), + 0x09 => self.reg.c = self.alu_rrc(self.reg.c), + 0x0a => self.reg.d = self.alu_rrc(self.reg.d), + 0x0b => self.reg.e = self.alu_rrc(self.reg.e), + 0x0c => self.reg.h = self.alu_rrc(self.reg.h), + 0x0d => self.reg.l = self.alu_rrc(self.reg.l), + 0x0e => { + let a = self.reg.get_hl(); + let v = self.mem.borrow().get(a); + let h = self.alu_rrc(v); + self.mem.borrow_mut().set(a, h); + } + 0x0f => self.reg.a = self.alu_rrc(self.reg.a), + + // RL r8 + 0x10 => self.reg.b = self.alu_rl(self.reg.b), + 0x11 => self.reg.c = self.alu_rl(self.reg.c), + 0x12 => self.reg.d = self.alu_rl(self.reg.d), + 0x13 => self.reg.e = self.alu_rl(self.reg.e), + 0x14 => self.reg.h = self.alu_rl(self.reg.h), + 0x15 => self.reg.l = self.alu_rl(self.reg.l), + 0x16 => { + let a = self.reg.get_hl(); + let v = self.mem.borrow().get(a); + let h = self.alu_rl(v); + self.mem.borrow_mut().set(a, h); + } + 0x17 => self.reg.a = self.alu_rl(self.reg.a), + + // RR r8 + 0x18 => self.reg.b = self.alu_rr(self.reg.b), + 0x19 => self.reg.c = self.alu_rr(self.reg.c), + 0x1a => self.reg.d = self.alu_rr(self.reg.d), + 0x1b => self.reg.e = self.alu_rr(self.reg.e), + 0x1c => self.reg.h = self.alu_rr(self.reg.h), + 0x1d => self.reg.l = self.alu_rr(self.reg.l), + 0x1e => { + let a = self.reg.get_hl(); + let v = self.mem.borrow().get(a); + let h = self.alu_rr(v); + self.mem.borrow_mut().set(a, h); + } + 0x1f => self.reg.a = self.alu_rr(self.reg.a), + + // SLA r8 + 0x20 => self.reg.b = self.alu_sla(self.reg.b), + 0x21 => self.reg.c = self.alu_sla(self.reg.c), + 0x22 => self.reg.d = self.alu_sla(self.reg.d), + 0x23 => self.reg.e = self.alu_sla(self.reg.e), + 0x24 => self.reg.h = self.alu_sla(self.reg.h), + 0x25 => self.reg.l = self.alu_sla(self.reg.l), + 0x26 => { + let a = self.reg.get_hl(); + let v = self.mem.borrow().get(a); + let h = self.alu_sla(v); + self.mem.borrow_mut().set(a, h); + } + 0x27 => self.reg.a = self.alu_sla(self.reg.a), + + // SRA r8 + 0x28 => self.reg.b = self.alu_sra(self.reg.b), + 0x29 => self.reg.c = self.alu_sra(self.reg.c), + 0x2a => self.reg.d = self.alu_sra(self.reg.d), + 0x2b => self.reg.e = self.alu_sra(self.reg.e), + 0x2c => self.reg.h = self.alu_sra(self.reg.h), + 0x2d => self.reg.l = self.alu_sra(self.reg.l), + 0x2e => { + let a = self.reg.get_hl(); + let v = self.mem.borrow().get(a); + let h = self.alu_sra(v); + self.mem.borrow_mut().set(a, h); + } + 0x2f => self.reg.a = self.alu_sra(self.reg.a), + + // SWAP r8 + 0x30 => self.reg.b = self.alu_swap(self.reg.b), + 0x31 => self.reg.c = self.alu_swap(self.reg.c), + 0x32 => self.reg.d = self.alu_swap(self.reg.d), + 0x33 => self.reg.e = self.alu_swap(self.reg.e), + 0x34 => self.reg.h = self.alu_swap(self.reg.h), + 0x35 => self.reg.l = self.alu_swap(self.reg.l), + 0x36 => { + let a = self.reg.get_hl(); + let v = self.mem.borrow().get(a); + let h = self.alu_swap(v); + self.mem.borrow_mut().set(a, h); + } + 0x37 => self.reg.a = self.alu_swap(self.reg.a), + + // SRL r8 + 0x38 => self.reg.b = self.alu_srl(self.reg.b), + 0x39 => self.reg.c = self.alu_srl(self.reg.c), + 0x3a => self.reg.d = self.alu_srl(self.reg.d), + 0x3b => self.reg.e = self.alu_srl(self.reg.e), + 0x3c => self.reg.h = self.alu_srl(self.reg.h), + 0x3d => self.reg.l = self.alu_srl(self.reg.l), + 0x3e => { + let a = self.reg.get_hl(); + let v = self.mem.borrow().get(a); + let h = self.alu_srl(v); + self.mem.borrow_mut().set(a, h); + } + 0x3f => self.reg.a = self.alu_srl(self.reg.a), + + // BIT b, r8 + 0x40 => self.alu_bit(self.reg.b, 0), + 0x41 => self.alu_bit(self.reg.c, 0), + 0x42 => self.alu_bit(self.reg.d, 0), + 0x43 => self.alu_bit(self.reg.e, 0), + 0x44 => self.alu_bit(self.reg.h, 0), + 0x45 => self.alu_bit(self.reg.l, 0), + 0x46 => { + let a = self.reg.get_hl(); + let v = self.mem.borrow().get(a); + self.alu_bit(v, 0); + } + 0x47 => self.alu_bit(self.reg.a, 0), + 0x48 => self.alu_bit(self.reg.b, 1), + 0x49 => self.alu_bit(self.reg.c, 1), + 0x4a => self.alu_bit(self.reg.d, 1), + 0x4b => self.alu_bit(self.reg.e, 1), + 0x4c => self.alu_bit(self.reg.h, 1), + 0x4d => self.alu_bit(self.reg.l, 1), + 0x4e => { + let a = self.reg.get_hl(); + let v = self.mem.borrow().get(a); + self.alu_bit(v, 1); + } + 0x4f => self.alu_bit(self.reg.a, 1), + 0x50 => self.alu_bit(self.reg.b, 2), + 0x51 => self.alu_bit(self.reg.c, 2), + 0x52 => self.alu_bit(self.reg.d, 2), + 0x53 => self.alu_bit(self.reg.e, 2), + 0x54 => self.alu_bit(self.reg.h, 2), + 0x55 => self.alu_bit(self.reg.l, 2), + 0x56 => { + let a = self.reg.get_hl(); + let v = self.mem.borrow().get(a); + self.alu_bit(v, 2); + } + 0x57 => self.alu_bit(self.reg.a, 2), + 0x58 => self.alu_bit(self.reg.b, 3), + 0x59 => self.alu_bit(self.reg.c, 3), + 0x5a => self.alu_bit(self.reg.d, 3), + 0x5b => self.alu_bit(self.reg.e, 3), + 0x5c => self.alu_bit(self.reg.h, 3), + 0x5d => self.alu_bit(self.reg.l, 3), + 0x5e => { + let a = self.reg.get_hl(); + let v = self.mem.borrow().get(a); + self.alu_bit(v, 3); + } + 0x5f => self.alu_bit(self.reg.a, 3), + 0x60 => self.alu_bit(self.reg.b, 4), + 0x61 => self.alu_bit(self.reg.c, 4), + 0x62 => self.alu_bit(self.reg.d, 4), + 0x63 => self.alu_bit(self.reg.e, 4), + 0x64 => self.alu_bit(self.reg.h, 4), + 0x65 => self.alu_bit(self.reg.l, 4), + 0x66 => { + let a = self.reg.get_hl(); + let v = self.mem.borrow().get(a); + self.alu_bit(v, 4); + } + 0x67 => self.alu_bit(self.reg.a, 4), + 0x68 => self.alu_bit(self.reg.b, 5), + 0x69 => self.alu_bit(self.reg.c, 5), + 0x6a => self.alu_bit(self.reg.d, 5), + 0x6b => self.alu_bit(self.reg.e, 5), + 0x6c => self.alu_bit(self.reg.h, 5), + 0x6d => self.alu_bit(self.reg.l, 5), + 0x6e => { + let a = self.reg.get_hl(); + let v = self.mem.borrow().get(a); + self.alu_bit(v, 5); + } + 0x6f => self.alu_bit(self.reg.a, 5), + 0x70 => self.alu_bit(self.reg.b, 6), + 0x71 => self.alu_bit(self.reg.c, 6), + 0x72 => self.alu_bit(self.reg.d, 6), + 0x73 => self.alu_bit(self.reg.e, 6), + 0x74 => self.alu_bit(self.reg.h, 6), + 0x75 => self.alu_bit(self.reg.l, 6), + 0x76 => { + let a = self.reg.get_hl(); + let v = self.mem.borrow().get(a); + self.alu_bit(v, 6); + } + 0x77 => self.alu_bit(self.reg.a, 6), + 0x78 => self.alu_bit(self.reg.b, 7), + 0x79 => self.alu_bit(self.reg.c, 7), + 0x7a => self.alu_bit(self.reg.d, 7), + 0x7b => self.alu_bit(self.reg.e, 7), + 0x7c => self.alu_bit(self.reg.h, 7), + 0x7d => self.alu_bit(self.reg.l, 7), + 0x7e => { + let a = self.reg.get_hl(); + let v = self.mem.borrow().get(a); + self.alu_bit(v, 7); + } + 0x7f => self.alu_bit(self.reg.a, 7), + + // RES b, r8 + 0x80 => self.reg.b = self.alu_res(self.reg.b, 0), + 0x81 => self.reg.c = self.alu_res(self.reg.c, 0), + 0x82 => self.reg.d = self.alu_res(self.reg.d, 0), + 0x83 => self.reg.e = self.alu_res(self.reg.e, 0), + 0x84 => self.reg.h = self.alu_res(self.reg.h, 0), + 0x85 => self.reg.l = self.alu_res(self.reg.l, 0), + 0x86 => { + let a = self.reg.get_hl(); + let v = self.mem.borrow().get(a); + let h = self.alu_res(v, 0); + self.mem.borrow_mut().set(a, h); + } + 0x87 => self.reg.a = self.alu_res(self.reg.a, 0), + 0x88 => self.reg.b = self.alu_res(self.reg.b, 1), + 0x89 => self.reg.c = self.alu_res(self.reg.c, 1), + 0x8a => self.reg.d = self.alu_res(self.reg.d, 1), + 0x8b => self.reg.e = self.alu_res(self.reg.e, 1), + 0x8c => self.reg.h = self.alu_res(self.reg.h, 1), + 0x8d => self.reg.l = self.alu_res(self.reg.l, 1), + 0x8e => { + let a = self.reg.get_hl(); + let v = self.mem.borrow().get(a); + let h = self.alu_res(v, 1); + self.mem.borrow_mut().set(a, h); + } + 0x8f => self.reg.a = self.alu_res(self.reg.a, 1), + 0x90 => self.reg.b = self.alu_res(self.reg.b, 2), + 0x91 => self.reg.c = self.alu_res(self.reg.c, 2), + 0x92 => self.reg.d = self.alu_res(self.reg.d, 2), + 0x93 => self.reg.e = self.alu_res(self.reg.e, 2), + 0x94 => self.reg.h = self.alu_res(self.reg.h, 2), + 0x95 => self.reg.l = self.alu_res(self.reg.l, 2), + 0x96 => { + let a = self.reg.get_hl(); + let v = self.mem.borrow().get(a); + let h = self.alu_res(v, 2); + self.mem.borrow_mut().set(a, h); + } + 0x97 => self.reg.a = self.alu_res(self.reg.a, 2), + 0x98 => self.reg.b = self.alu_res(self.reg.b, 3), + 0x99 => self.reg.c = self.alu_res(self.reg.c, 3), + 0x9a => self.reg.d = self.alu_res(self.reg.d, 3), + 0x9b => self.reg.e = self.alu_res(self.reg.e, 3), + 0x9c => self.reg.h = self.alu_res(self.reg.h, 3), + 0x9d => self.reg.l = self.alu_res(self.reg.l, 3), + 0x9e => { + let a = self.reg.get_hl(); + let v = self.mem.borrow().get(a); + let h = self.alu_res(v, 3); + self.mem.borrow_mut().set(a, h); + } + 0x9f => self.reg.a = self.alu_res(self.reg.a, 3), + 0xa0 => self.reg.b = self.alu_res(self.reg.b, 4), + 0xa1 => self.reg.c = self.alu_res(self.reg.c, 4), + 0xa2 => self.reg.d = self.alu_res(self.reg.d, 4), + 0xa3 => self.reg.e = self.alu_res(self.reg.e, 4), + 0xa4 => self.reg.h = self.alu_res(self.reg.h, 4), + 0xa5 => self.reg.l = self.alu_res(self.reg.l, 4), + 0xa6 => { + let a = self.reg.get_hl(); + let v = self.mem.borrow().get(a); + let h = self.alu_res(v, 4); + self.mem.borrow_mut().set(a, h); + } + 0xa7 => self.reg.a = self.alu_res(self.reg.a, 4), + 0xa8 => self.reg.b = self.alu_res(self.reg.b, 5), + 0xa9 => self.reg.c = self.alu_res(self.reg.c, 5), + 0xaa => self.reg.d = self.alu_res(self.reg.d, 5), + 0xab => self.reg.e = self.alu_res(self.reg.e, 5), + 0xac => self.reg.h = self.alu_res(self.reg.h, 5), + 0xad => self.reg.l = self.alu_res(self.reg.l, 5), + 0xae => { + let a = self.reg.get_hl(); + let v = self.mem.borrow().get(a); + let h = self.alu_res(v, 5); + self.mem.borrow_mut().set(a, h); + } + 0xaf => self.reg.a = self.alu_res(self.reg.a, 5), + 0xb0 => self.reg.b = self.alu_res(self.reg.b, 6), + 0xb1 => self.reg.c = self.alu_res(self.reg.c, 6), + 0xb2 => self.reg.d = self.alu_res(self.reg.d, 6), + 0xb3 => self.reg.e = self.alu_res(self.reg.e, 6), + 0xb4 => self.reg.h = self.alu_res(self.reg.h, 6), + 0xb5 => self.reg.l = self.alu_res(self.reg.l, 6), + 0xb6 => { + let a = self.reg.get_hl(); + let v = self.mem.borrow().get(a); + let h = self.alu_res(v, 6); + self.mem.borrow_mut().set(a, h); + } + 0xb7 => self.reg.a = self.alu_res(self.reg.a, 6), + 0xb8 => self.reg.b = self.alu_res(self.reg.b, 7), + 0xb9 => self.reg.c = self.alu_res(self.reg.c, 7), + 0xba => self.reg.d = self.alu_res(self.reg.d, 7), + 0xbb => self.reg.e = self.alu_res(self.reg.e, 7), + 0xbc => self.reg.h = self.alu_res(self.reg.h, 7), + 0xbd => self.reg.l = self.alu_res(self.reg.l, 7), + 0xbe => { + let a = self.reg.get_hl(); + let v = self.mem.borrow().get(a); + let h = self.alu_res(v, 7); + self.mem.borrow_mut().set(a, h); + } + 0xbf => self.reg.a = self.alu_res(self.reg.a, 7), + + // SET b, r8 + 0xc0 => self.reg.b = self.alu_set(self.reg.b, 0), + 0xc1 => self.reg.c = self.alu_set(self.reg.c, 0), + 0xc2 => self.reg.d = self.alu_set(self.reg.d, 0), + 0xc3 => self.reg.e = self.alu_set(self.reg.e, 0), + 0xc4 => self.reg.h = self.alu_set(self.reg.h, 0), + 0xc5 => self.reg.l = self.alu_set(self.reg.l, 0), + 0xc6 => { + let a = self.reg.get_hl(); + let v = self.mem.borrow().get(a); + let h = self.alu_set(v, 0); + self.mem.borrow_mut().set(a, h); + } + 0xc7 => self.reg.a = self.alu_set(self.reg.a, 0), + 0xc8 => self.reg.b = self.alu_set(self.reg.b, 1), + 0xc9 => self.reg.c = self.alu_set(self.reg.c, 1), + 0xca => self.reg.d = self.alu_set(self.reg.d, 1), + 0xcb => self.reg.e = self.alu_set(self.reg.e, 1), + 0xcc => self.reg.h = self.alu_set(self.reg.h, 1), + 0xcd => self.reg.l = self.alu_set(self.reg.l, 1), + 0xce => { + let a = self.reg.get_hl(); + let v = self.mem.borrow().get(a); + let h = self.alu_set(v, 1); + self.mem.borrow_mut().set(a, h); + } + 0xcf => self.reg.a = self.alu_set(self.reg.a, 1), + 0xd0 => self.reg.b = self.alu_set(self.reg.b, 2), + 0xd1 => self.reg.c = self.alu_set(self.reg.c, 2), + 0xd2 => self.reg.d = self.alu_set(self.reg.d, 2), + 0xd3 => self.reg.e = self.alu_set(self.reg.e, 2), + 0xd4 => self.reg.h = self.alu_set(self.reg.h, 2), + 0xd5 => self.reg.l = self.alu_set(self.reg.l, 2), + 0xd6 => { + let a = self.reg.get_hl(); + let v = self.mem.borrow().get(a); + let h = self.alu_set(v, 2); + self.mem.borrow_mut().set(a, h); + } + 0xd7 => self.reg.a = self.alu_set(self.reg.a, 2), + 0xd8 => self.reg.b = self.alu_set(self.reg.b, 3), + 0xd9 => self.reg.c = self.alu_set(self.reg.c, 3), + 0xda => self.reg.d = self.alu_set(self.reg.d, 3), + 0xdb => self.reg.e = self.alu_set(self.reg.e, 3), + 0xdc => self.reg.h = self.alu_set(self.reg.h, 3), + 0xdd => self.reg.l = self.alu_set(self.reg.l, 3), + 0xde => { + let a = self.reg.get_hl(); + let v = self.mem.borrow().get(a); + let h = self.alu_set(v, 3); + self.mem.borrow_mut().set(a, h); + } + 0xdf => self.reg.a = self.alu_set(self.reg.a, 3), + 0xe0 => self.reg.b = self.alu_set(self.reg.b, 4), + 0xe1 => self.reg.c = self.alu_set(self.reg.c, 4), + 0xe2 => self.reg.d = self.alu_set(self.reg.d, 4), + 0xe3 => self.reg.e = self.alu_set(self.reg.e, 4), + 0xe4 => self.reg.h = self.alu_set(self.reg.h, 4), + 0xe5 => self.reg.l = self.alu_set(self.reg.l, 4), + 0xe6 => { + let a = self.reg.get_hl(); + let v = self.mem.borrow().get(a); + let h = self.alu_set(v, 4); + self.mem.borrow_mut().set(a, h); + } + 0xe7 => self.reg.a = self.alu_set(self.reg.a, 4), + 0xe8 => self.reg.b = self.alu_set(self.reg.b, 5), + 0xe9 => self.reg.c = self.alu_set(self.reg.c, 5), + 0xea => self.reg.d = self.alu_set(self.reg.d, 5), + 0xeb => self.reg.e = self.alu_set(self.reg.e, 5), + 0xec => self.reg.h = self.alu_set(self.reg.h, 5), + 0xed => self.reg.l = self.alu_set(self.reg.l, 5), + 0xee => { + let a = self.reg.get_hl(); + let v = self.mem.borrow().get(a); + let h = self.alu_set(v, 5); + self.mem.borrow_mut().set(a, h); + } + 0xef => self.reg.a = self.alu_set(self.reg.a, 5), + 0xf0 => self.reg.b = self.alu_set(self.reg.b, 6), + 0xf1 => self.reg.c = self.alu_set(self.reg.c, 6), + 0xf2 => self.reg.d = self.alu_set(self.reg.d, 6), + 0xf3 => self.reg.e = self.alu_set(self.reg.e, 6), + 0xf4 => self.reg.h = self.alu_set(self.reg.h, 6), + 0xf5 => self.reg.l = self.alu_set(self.reg.l, 6), + 0xf6 => { + let a = self.reg.get_hl(); + let v = self.mem.borrow().get(a); + let h = self.alu_set(v, 6); + self.mem.borrow_mut().set(a, h); + } + 0xf7 => self.reg.a = self.alu_set(self.reg.a, 6), + 0xf8 => self.reg.b = self.alu_set(self.reg.b, 7), + 0xf9 => self.reg.c = self.alu_set(self.reg.c, 7), + 0xfa => self.reg.d = self.alu_set(self.reg.d, 7), + 0xfb => self.reg.e = self.alu_set(self.reg.e, 7), + 0xfc => self.reg.h = self.alu_set(self.reg.h, 7), + 0xfd => self.reg.l = self.alu_set(self.reg.l, 7), + 0xfe => { + let a = self.reg.get_hl(); + let v = self.mem.borrow().get(a); + let h = self.alu_set(v, 7); + self.mem.borrow_mut().set(a, h); + } + 0xff => self.reg.a = self.alu_set(self.reg.a, 7), + } + } + 0xd3 => panic!("Opcode 0xd3 is not implemented"), + 0xdb => panic!("Opcode 0xdb is not implemented"), + 0xdd => panic!("Opcode 0xdd is not implemented"), + 0xe3 => panic!("Opcode 0xe3 is not implemented"), + 0xe4 => panic!("Opcode 0xd4 is not implemented"), + 0xeb => panic!("Opcode 0xeb is not implemented"), + 0xec => panic!("Opcode 0xec is not implemented"), + 0xed => panic!("Opcode 0xed is not implemented"), + 0xf4 => panic!("Opcode 0xf4 is not implemented"), + 0xfc => panic!("Opcode 0xfc is not implemented"), + 0xfd => panic!("Opcode 0xfd is not implemented"), + }; + + let ecycle = match opcode { + 0x20 | 0x30 => { + if self.reg.get_flag(Z) { + 0x00 + } else { + 0x01 + } + } + 0x28 | 0x38 => { + if self.reg.get_flag(Z) { + 0x01 + } else { + 0x00 + } + } + 0xc0 | 0xd0 => { + if self.reg.get_flag(Z) { + 0x00 + } else { + 0x03 + } + } + 0xc8 | 0xcc | 0xd8 | 0xdc => { + if self.reg.get_flag(Z) { + 0x03 + } else { + 0x00 + } + } + 0xc2 | 0xd2 => { + if self.reg.get_flag(Z) { + 0x00 + } else { + 0x01 + } + } + 0xca | 0xda => { + if self.reg.get_flag(Z) { + 0x01 + } else { + 0x00 + } + } + 0xc4 | 0xd4 => { + if self.reg.get_flag(Z) { + 0x00 + } else { + 0x03 + } + } + _ => 0x00, + }; + if opcode == 0xcb { + CB_CYCLES[cbcode as usize] + } else { + OP_CYCLES[opcode as usize] + ecycle + } + } + + pub fn next(&mut self) -> u32 { + let mac = { + let c = self.hi(); + if c != 0 { + c + } else if self.halted { + OP_CYCLES[0] + } else { + self.ex() + } + }; + mac * 4 + } +} + +// Real time cpu provided to simulate real hardware speed. +pub struct Rtc { + pub cpu: Cpu, + step_cycles: u32, + step_zero: time::Instant, + step_flip: bool, +} + +impl Rtc { + pub fn power_up(term: Term, mem: Rc>) -> Self { + let cpu = Cpu::power_up(term, mem); + Self { + cpu, + step_cycles: 0, + step_zero: time::Instant::now(), + step_flip: false, + } + } + + // Function next simulates real hardware execution speed, by limiting the frequency of the function cpu.next(). + pub fn next(&mut self) -> u32 { + if self.step_cycles > STEP_CYCLES { + self.step_flip = true; + self.step_cycles -= STEP_CYCLES; + let now = time::Instant::now(); + let d = now.duration_since(self.step_zero); + let s = u64::from(STEP_TIME.saturating_sub(d.as_millis() as u32)); + thread::sleep(time::Duration::from_millis(s)); + self.step_zero = self + .step_zero + .checked_add(time::Duration::from_millis(u64::from(STEP_TIME))) + .unwrap(); + + // If now is after the just updated target frame time, reset to + // avoid drift. + if now.checked_duration_since(self.step_zero).is_some() { + self.step_zero = now; + } + } + let cycles = self.cpu.next(); + self.step_cycles += cycles; + cycles + } + + pub fn flip(&mut self) -> bool { + let r = self.step_flip; + if r { + self.step_flip = false; + } + r + } +} diff --git a/src/gpu.rs b/src/gpu.rs new file mode 100644 index 0000000000..3b1336c4f7 --- /dev/null +++ b/src/gpu.rs @@ -0,0 +1,863 @@ +use super::convention::Term; +use super::intf::{Flag, Intf}; +use super::memory::Memory; +use std::cell::RefCell; +use std::rc::Rc; + +#[derive(Eq, PartialEq)] +pub enum HdmaMode { + // When using this transfer method, all data is transferred at once. The execution of the program is halted until + // the transfer has completed. Note that the General Purpose DMA blindly attempts to copy the data, even if the + // CD controller is currently accessing VRAM. So General Purpose DMA should be used only if the Display is disabled, + // or during V-Blank, or (for rather short blocks) during H-Blank. The execution of the program continues when the + // transfer has been completed, and FF55 then contains a value of FFh. + Gdma, + // The H-Blank DMA transfers 10h bytes of data during each H-Blank, ie. at LY=0-143, no data is transferred during + // V-Blank (LY=144-153), but the transfer will then continue at LY=00. The execution of the program is halted + // during the separate transfers, but the program execution continues during the 'spaces' between each data block. + // Note that the program should not change the Destination VRAM bank (FF4F), or the Source ROM/RAM bank (in case + // data is transferred from bankable memory) until the transfer has completed! (The transfer should be paused as + // described below while the banks are switched) Reading from Register FF55 returns the remaining length (divided + // by 10h, minus 1), a value of 0FFh indicates that the transfer has completed. It is also possible to terminate + // an active H-Blank transfer by writing zero to Bit 7 of FF55. In that case reading from FF55 will return how many + // $10 "blocks" remained (minus 1) in the lower 7 bits, but Bit 7 will be read as "1". Stopping the transfer + // doesn't set HDMA1-4 to $FF. + Hdma, +} + +pub struct Hdma { + // These two registers specify the address at which the transfer will read data from. Normally, this should be + // either in ROM, SRAM or WRAM, thus either in range 0000-7FF0 or A000-DFF0. [Note : this has yet to be tested on + // Echo RAM, OAM, FEXX, IO and HRAM]. Trying to specify a source address in VRAM will cause garbage to be copied. + // The four lower bits of this address will be ignored and treated as 0. + pub src: u16, + // These two registers specify the address within 8000-9FF0 to which the data will be copied. Only bits 12-4 are + // respected; others are ignored. The four lower bits of this address will be ignored and treated as 0. + pub dst: u16, + pub active: bool, + pub mode: HdmaMode, + pub remain: u8, +} + +impl Hdma { + pub fn power_up() -> Self { + Self { + src: 0x0000, + dst: 0x8000, + active: false, + mode: HdmaMode::Gdma, + remain: 0x00, + } + } +} + +impl Memory for Hdma { + fn get(&self, a: u16) -> u8 { + match a { + 0xff51 => (self.src >> 8) as u8, + 0xff52 => self.src as u8, + 0xff53 => (self.dst >> 8) as u8, + 0xff54 => self.dst as u8, + 0xff55 => self.remain | if self.active { 0x00 } else { 0x80 }, + _ => panic!(""), + } + } + + fn set(&mut self, a: u16, v: u8) { + match a { + 0xff51 => self.src = (u16::from(v) << 8) | (self.src & 0x00ff), + 0xff52 => self.src = (self.src & 0xff00) | u16::from(v & 0xf0), + 0xff53 => self.dst = 0x8000 | (u16::from(v & 0x1f) << 8) | (self.dst & 0x00ff), + 0xff54 => self.dst = (self.dst & 0xff00) | u16::from(v & 0xf0), + 0xff55 => { + if self.active && self.mode == HdmaMode::Hdma { + if v & 0x80 == 0x00 { + self.active = false; + }; + return; + } + self.active = true; + self.remain = v & 0x7f; + self.mode = if v & 0x80 != 0x00 { + HdmaMode::Hdma + } else { + HdmaMode::Gdma + }; + } + _ => panic!(""), + }; + } +} + +// LCDC is the main LCD Control register. Its bits toggle what elements are displayed on the screen, and how. +pub struct Lcdc { + data: u8, +} + +#[rustfmt::skip] +impl Lcdc { + pub fn power_up() -> Self { + Self { data: 0b0100_1000 } + } + + // LCDC.7 - LCD Display Enable + // This bit controls whether the LCD is on and the PPU is active. Setting it to 0 turns both off, which grants + // immediate and full access to VRAM, OAM, etc. + fn bit7(&self) -> bool { self.data & 0b1000_0000 != 0x00 } + + // LCDC.6 - Window Tile Map Display Select + // This bit controls which background map the Window uses for rendering. When it's reset, the $9800 tilemap is used, + // otherwise it's the $9C00 one. + fn bit6(&self) -> bool { self.data & 0b0100_0000 != 0x00 } + + // LCDC.5 - Window Display Enable + // This bit controls whether the window shall be displayed or not. (TODO : what happens when toggling this + // mid-scanline ?) This bit is overridden on DMG by bit 0 if that bit is reset. + // Note that on CGB models, setting this bit to 0 then back to 1 mid-frame may cause the second write to be ignored. + fn bit5(&self) -> bool { self.data & 0b0010_0000 != 0x00 } + + // LCDC.4 - BG & Window Tile Data Select + // This bit controls which addressing mode the BG and Window use to pick tiles. + // Sprites aren't affected by this, and will always use $8000 addressing mode. + fn bit4(&self) -> bool { self.data & 0b0001_0000 != 0x00 } + + // LCDC.3 - BG Tile Map Display Select + // This bit works similarly to bit 6: if the bit is reset, the BG uses tilemap $9800, otherwise tilemap $9C00. + fn bit3(&self) -> bool { self.data & 0b0000_1000 != 0x00 } + + // LCDC.2 - OBJ Size + // This bit controls the sprite size (1 tile or 2 stacked vertically). + // Be cautious when changing this mid-frame from 8x8 to 8x16 : "remnants" of the sprites intended for 8x8 could + // "leak" into the 8x16 zone and cause artifacts. + fn bit2(&self) -> bool { self.data & 0b0000_0100 != 0x00 } + + // LCDC.1 - OBJ Display Enable + // This bit toggles whether sprites are displayed or not. + // This can be toggled mid-frame, for example to avoid sprites being displayed on top of a status bar or text box. + // (Note: toggling mid-scanline might have funky results on DMG? Investigation needed.) + fn bit1(&self) -> bool { self.data & 0b0000_0010 != 0x00 } + + + // LCDC.0 - BG/Window Display/Priority + // LCDC.0 has different meanings depending on Gameboy type and Mode: + // Monochrome Gameboy, SGB and CGB in Non-CGB Mode: BG Display + // When Bit 0 is cleared, both background and window become blank (white), and the Window Display Bit is ignored in + // that case. Only Sprites may still be displayed (if enabled in Bit 1). + // CGB in CGB Mode: BG and Window Master Priority + // When Bit 0 is cleared, the background and window lose their priority - the sprites will be always displayed on + // top of background and window, independently of the priority flags in OAM and BG Map attributes. + fn bit0(&self) -> bool { self.data & 0b0000_0001 != 0x00 } +} + +// LCD Status Register. +pub struct Stat { + // Bit 6 - LYC=LY Coincidence Interrupt (1=Enable) (Read/Write) + enable_ly_interrupt: bool, + // Bit 5 - Mode 2 OAM Interrupt (1=Enable) (Read/Write) + enable_m2_interrupt: bool, + // Bit 4 - Mode 1 V-Blank Interrupt (1=Enable) (Read/Write) + enable_m1_interrupt: bool, + // Bit 3 - Mode 0 H-Blank Interrupt (1=Enable) (Read/Write) + enable_m0_interrupt: bool, + // Bit 1-0 - Mode Flag (Mode 0-3, see below) (Read Only) + // 0: During H-Blank + // 1: During V-Blank + // 2: During Searching OAM + // 3: During Transferring Data to LCD Driver + mode: u8, +} + +impl Stat { + pub fn power_up() -> Self { + Self { + enable_ly_interrupt: false, + enable_m2_interrupt: false, + enable_m1_interrupt: false, + enable_m0_interrupt: false, + mode: 0x00, + } + } +} + +// This register is used to address a byte in the CGBs Background Palette Memory. Each two byte in that memory define a +// color value. The first 8 bytes define Color 0-3 of Palette 0 (BGP0), and so on for BGP1-7. +// Bit 0-5 Index (00-3F) +// Bit 7 Auto Increment (0=Disabled, 1=Increment after Writing) +// Data can be read/written to/from the specified index address through Register FF69. When the Auto Increment bit is +// set then the index is automatically incremented after each to FF69. Auto Increment has no effect when +// from FF69, so the index must be manually incremented in that case. Writing to FF69 during rendering still +// causes auto-increment to occur. +// Unlike the following, this register can be accessed outside V-Blank and H-Blank. +struct Bgpi { + i: u8, + auto_increment: bool, +} + +impl Bgpi { + fn power_up() -> Self { + Self { + i: 0x00, + auto_increment: false, + } + } + + fn get(&self) -> u8 { + let a = if self.auto_increment { 0x80 } else { 0x00 }; + a | self.i + } + + fn set(&mut self, v: u8) { + self.auto_increment = v & 0x80 != 0x00; + self.i = v & 0x3f; + } +} + +pub enum GrayShades { + White = 0xff, + Light = 0xc0, + Dark = 0x60, + Black = 0x00, +} + +// Bit7 OBJ-to-BG Priority (0=OBJ Above BG, 1=OBJ Behind BG color 1-3) +// (Used for both BG and Window. BG color 0 is always behind OBJ) +// Bit6 Y flip (0=Normal, 1=Vertically mirrored) +// Bit5 X flip (0=Normal, 1=Horizontally mirrored) +// Bit4 Palette number **Non CGB Mode Only** (0=OBP0, 1=OBP1) +// Bit3 Tile VRAM-Bank **CGB Mode Only** (0=Bank 0, 1=Bank 1) +// Bit2-0 Palette number **CGB Mode Only** (OBP0-7) +struct Attr { + priority: bool, + yflip: bool, + xflip: bool, + palette_number_0: usize, + bank: bool, + palette_number_1: usize, +} + +impl From for Attr { + fn from(u: u8) -> Self { + Self { + priority: u & (1 << 7) != 0, + yflip: u & (1 << 6) != 0, + xflip: u & (1 << 5) != 0, + palette_number_0: u as usize & (1 << 4), + bank: u & (1 << 3) != 0, + palette_number_1: u as usize & 0x07, + } + } +} + +pub const SCREEN_W: usize = 160; +pub const SCREEN_H: usize = 144; + +pub struct Gpu { + // Digital image with mode RGB. Size = 144 * 160 * 3. + // 3--------- + // ---------- + // ---------- + // ---------- 160 + // 144 + pub data: [[[u8; 3]; SCREEN_W]; SCREEN_H], + pub intf: Rc>, + pub term: Term, + pub h_blank: bool, + pub v_blank: bool, + + lcdc: Lcdc, + stat: Stat, + // Scroll Y (R/W), Scroll X (R/W) + // Specifies the position in the 256x256 pixels BG map (32x32 tiles) which is to be displayed at the upper/left LCD + // display position. Values in range from 0-255 may be used for X/Y each, the video controller automatically wraps + // back to the upper (left) position in BG map when drawing exceeds the lower (right) border of the BG map area. + sy: u8, + sx: u8, + // Window Y Position (R/W), Window X Position minus 7 (R/W) + wy: u8, + wx: u8, + // The LY indicates the vertical line to which the present data is transferred to the LCD Driver. The LY can take + // on any value between 0 through 153. The values between 144 and 153 indicate the V-Blank period. Writing will + // reset the counter. + ly: u8, + // The Gameboy permanently compares the value of the LYC and LY registers. When both values are identical, the + // coincident bit in the STAT register becomes set, and (if enabled) a STAT interrupt is requested. + lc: u8, + + // This register assigns gray shades to the color numbers of the BG and Window tiles. + bgp: u8, + // This register assigns gray shades for sprite palette 0. It works exactly as BGP (FF47), except that the lower + // two bits aren't used because sprite data 00 is transparent. + op0: u8, + // This register assigns gray shades for sprite palette 1. It works exactly as BGP (FF47), except that the lower + // two bits aren't used because sprite data 00 is transparent. + op1: u8, + + cbgpi: Bgpi, + // This register allows to read/write data to the CGBs Background Palette Memory, addressed through Register FF68. + // Each color is defined by two bytes (Bit 0-7 in first byte). + // Bit 0-4 Red Intensity (00-1F) + // Bit 5-9 Green Intensity (00-1F) + // Bit 10-14 Blue Intensity (00-1F) + // Much like VRAM, data in Palette Memory cannot be read/written during the time when the LCD Controller is + // reading from it. (That is when the STAT register indicates Mode 3). Note: All background colors are initialized + // as white by the boot ROM, but it's a good idea to initialize at least one color yourself (for example if you + // include a soft-reset mechanic). + // + // Note: Type [[[u8; 3]; 4]; 8] equals with [u8; 64]. + cbgpd: [[[u8; 3]; 4]; 8], + + cobpi: Bgpi, + cobpd: [[[u8; 3]; 4]; 8], + + ram: [u8; 0x4000], + ram_bank: usize, + // VRAM Sprite Attribute Table (OAM) + // Gameboy video controller can display up to 40 sprites either in 8x8 or in 8x16 pixels. Because of a limitation of + // hardware, only ten sprites can be displayed per scan line. Sprite patterns have the same format as BG tiles, but + // they are taken from the Sprite Pattern Table located at $8000-8FFF and have unsigned numbering. + // Sprite attributes reside in the Sprite Attribute Table (OAM - Object Attribute Memory) at $FE00-FE9F. Each of the 40 + // entries consists of four bytes with the following meanings: + // Byte0 - Y Position + // Specifies the sprites vertical position on the screen (minus 16). An off-screen value (for example, Y=0 or + // Y>=160) hides the sprite. + // + // Byte1 - X Position + // Specifies the sprites horizontal position on the screen (minus 8). An off-screen value (X=0 or X>=168) hides the + // sprite, but the sprite still affects the priority ordering - a better way to hide a sprite is to set its + // Y-coordinate off-screen. + // + // Byte2 - Tile/Pattern Number + // Specifies the sprites Tile Number (00-FF). This (unsigned) value selects a tile from memory at 8000h-8FFFh. In + // CGB Mode this could be either in VRAM Bank 0 or 1, depending on Bit 3 of the following byte. In 8x16 mode, the + // lower bit of the tile number is ignored. IE: the upper 8x8 tile is "NN AND FEh", and the lower 8x8 tile + // is "NN OR 01h". + // + // Byte3 - Attributes/Flags: + // Bit7 OBJ-to-BG Priority (0=OBJ Above BG, 1=OBJ Behind BG color 1-3) + // (Used for both BG and Window. BG color 0 is always behind OBJ) + // Bit6 Y flip (0=Normal, 1=Vertically mirrored) + // Bit5 X flip (0=Normal, 1=Horizontally mirrored) + // Bit4 Palette number **Non CGB Mode Only** (0=OBP0, 1=OBP1) + // Bit3 Tile VRAM-Bank **CGB Mode Only** (0=Bank 0, 1=Bank 1) + // Bit2-0 Palette number **CGB Mode Only** (OBP0-7) + oam: [u8; 0xa0], + + prio: [(bool, usize); SCREEN_W], + // The LCD controller operates on a 222 Hz = 4.194 MHz dot clock. An entire frame is 154 scanlines, 70224 dots, or + // 16.74 ms. On scanlines 0 through 143, the LCD controller cycles through modes 2, 3, and 0 once every 456 dots. + // Scanlines 144 through 153 are mode 1. + dots: u32, +} + +impl Gpu { + pub fn power_up(term: Term, intf: Rc>) -> Self { + Self { + data: [[[0xffu8; 3]; SCREEN_W]; SCREEN_H], + intf, + term, + h_blank: false, + v_blank: false, + + lcdc: Lcdc::power_up(), + stat: Stat::power_up(), + sy: 0x00, + sx: 0x00, + wx: 0x00, + wy: 0x00, + ly: 0x00, + lc: 0x00, + bgp: 0x00, + op0: 0x00, + op1: 0x01, + cbgpi: Bgpi::power_up(), + cbgpd: [[[0u8; 3]; 4]; 8], + cobpi: Bgpi::power_up(), + cobpd: [[[0u8; 3]; 4]; 8], + ram: [0x00; 0x4000], + ram_bank: 0x00, + oam: [0x00; 0xa0], + prio: [(true, 0); SCREEN_W], + dots: 0, + } + } + + fn get_ram0(&self, a: u16) -> u8 { + self.ram[a as usize - 0x8000] + } + + fn get_ram1(&self, a: u16) -> u8 { + self.ram[a as usize - 0x6000] + } + + // This register assigns gray shades to the color numbers of the BG and Window tiles. + // Bit 7-6 - Shade for Color Number 3 + // Bit 5-4 - Shade for Color Number 2 + // Bit 3-2 - Shade for Color Number 1 + // Bit 1-0 - Shade for Color Number 0 + // The four possible gray shades are: + // 0 White + // 1 Light gray + // 2 Dark gray + // 3 Black + fn get_gray_shades(v: u8, i: usize) -> GrayShades { + match v >> (2 * i) & 0x03 { + 0x00 => GrayShades::White, + 0x01 => GrayShades::Light, + 0x02 => GrayShades::Dark, + _ => GrayShades::Black, + } + } + + // Grey scale. + fn set_gre(&mut self, x: usize, g: u8) { + self.data[self.ly as usize][x] = [g, g, g]; + } + + // When developing graphics on PCs, note that the RGB values will have different appearance on CGB displays as on + // VGA/HDMI monitors calibrated to sRGB color. Because the GBC is not lit, the highest intensity will produce Light + // Gray color rather than White. The intensities are not linear; the values 10h-1Fh will all appear very bright, + // while medium and darker colors are ranged at 00h-0Fh. + // The CGB display's pigments aren't perfectly saturated. This means the colors mix quite oddly; increasing + // intensity of only one R,G,B color will also influence the other two R,G,B colors. For example, a color setting + // of 03EFh (Blue=0, Green=1Fh, Red=0Fh) will appear as Neon Green on VGA displays, but on the CGB it'll produce a + // decently washed out Yellow. See image on the right. + fn set_rgb(&mut self, x: usize, r: u8, g: u8, b: u8) { + assert!(r <= 0x1f); + assert!(g <= 0x1f); + assert!(b <= 0x1f); + let r = u32::from(r); + let g = u32::from(g); + let b = u32::from(b); + let lr = ((r * 13 + g * 2 + b) >> 1) as u8; + let lg = ((g * 3 + b) << 1) as u8; + let lb = ((r * 3 + g * 2 + b * 11) >> 1) as u8; + self.data[self.ly as usize][x] = [lr, lg, lb]; + } + + pub fn next(&mut self, cycles: u32) { + if !self.lcdc.bit7() { + return; + } + self.h_blank = false; + + // The LCD controller operates on a 222 Hz = 4.194 MHz dot clock. An entire frame is 154 scanlines, 70224 dots, + // or 16.74 ms. On scanlines 0 through 143, the LCD controller cycles through modes 2, 3, and 0 once every 456 + // dots. Scanlines 144 through 153 are mode 1. + // + // 1 scanline = 456 dots + // + // The following are typical when the display is enabled: + // Mode 2 2_____2_____2_____2_____2_____2___________________2____ + // Mode 3 _33____33____33____33____33____33__________________3___ + // Mode 0 ___000___000___000___000___000___000________________000 + // Mode 1 ____________________________________11111111111111_____ + if cycles == 0 { + return; + } + let c = (cycles - 1) / 80 + 1; + for i in 0..c { + if i == (c - 1) { + self.dots += cycles % 80 + } else { + self.dots += 80 + } + let d = self.dots; + self.dots %= 456; + if d != self.dots { + self.ly = (self.ly + 1) % 154; + if self.stat.enable_ly_interrupt && self.ly == self.lc { + self.intf.borrow_mut().hi(Flag::LCDStat); + } + } + if self.ly >= 144 { + if self.stat.mode == 1 { + continue; + } + self.stat.mode = 1; + self.v_blank = true; + self.intf.borrow_mut().hi(Flag::VBlank); + if self.stat.enable_m1_interrupt { + self.intf.borrow_mut().hi(Flag::LCDStat); + } + } else if self.dots <= 80 { + if self.stat.mode == 2 { + continue; + } + self.stat.mode = 2; + if self.stat.enable_m2_interrupt { + self.intf.borrow_mut().hi(Flag::LCDStat); + } + } else if self.dots <= (80 + 172) { + self.stat.mode = 3; + } else { + if self.stat.mode == 0 { + continue; + } + self.stat.mode = 0; + self.h_blank = true; + if self.stat.enable_m0_interrupt { + self.intf.borrow_mut().hi(Flag::LCDStat); + } + // Render scanline + if self.term == Term::GBC || self.lcdc.bit0() { + self.draw_bg(); + } + if self.lcdc.bit1() { + self.draw_sprites(); + } + } + } + } + + fn draw_bg(&mut self) { + let show_window = self.lcdc.bit5() && self.wy <= self.ly; + let tile_base = if self.lcdc.bit4() { 0x8000 } else { 0x8800 }; + + let wx = self.wx.wrapping_sub(7); + let py = if show_window { + self.ly.wrapping_sub(self.wy) + } else { + self.sy.wrapping_add(self.ly) + }; + let ty = (u16::from(py) >> 3) & 31; + + for x in 0..SCREEN_W { + let px = if show_window && x as u8 >= wx { + x as u8 - wx + } else { + self.sx.wrapping_add(x as u8) + }; + let tx = (u16::from(px) >> 3) & 31; + + // Background memory base addr. + let bg_base = if show_window && x as u8 >= wx { + if self.lcdc.bit6() { + 0x9c00 + } else { + 0x9800 + } + } else if self.lcdc.bit3() { + 0x9c00 + } else { + 0x9800 + }; + + // Tile data + // Each tile is sized 8x8 pixels and has a color depth of 4 colors/gray shades. + // Each tile occupies 16 bytes, where each 2 bytes represent a line: + // Byte 0-1 First Line (Upper 8 pixels) + // Byte 2-3 Next Line + // etc. + let tile_addr = bg_base + ty * 32 + tx; + let tile_number = self.get_ram0(tile_addr); + let tile_offset = if self.lcdc.bit4() { + i16::from(tile_number) + } else { + i16::from(tile_number as i8) + 128 + } as u16 + * 16; + let tile_location = tile_base + tile_offset; + let tile_attr = Attr::from(self.get_ram1(tile_addr)); + + let tile_y = if tile_attr.yflip { 7 - py % 8 } else { py % 8 }; + let tile_y_data: [u8; 2] = if self.term == Term::GBC && tile_attr.bank { + let a = self.get_ram1(tile_location + u16::from(tile_y * 2)); + let b = self.get_ram1(tile_location + u16::from(tile_y * 2) + 1); + [a, b] + } else { + let a = self.get_ram0(tile_location + u16::from(tile_y * 2)); + let b = self.get_ram0(tile_location + u16::from(tile_y * 2) + 1); + [a, b] + }; + let tile_x = if tile_attr.xflip { 7 - px % 8 } else { px % 8 }; + + // Palettes + let color_l = if tile_y_data[0] & (0x80 >> tile_x) != 0 { + 1 + } else { + 0 + }; + let color_h = if tile_y_data[1] & (0x80 >> tile_x) != 0 { + 2 + } else { + 0 + }; + let color = color_h | color_l; + + // Priority + self.prio[x] = (tile_attr.priority, color); + + if self.term == Term::GBC { + let r = self.cbgpd[tile_attr.palette_number_1][color][0]; + let g = self.cbgpd[tile_attr.palette_number_1][color][1]; + let b = self.cbgpd[tile_attr.palette_number_1][color][2]; + self.set_rgb(x as usize, r, g, b); + } else { + let color = Self::get_gray_shades(self.bgp, color) as u8; + self.set_gre(x, color); + } + } + } + + // Gameboy video controller can display up to 40 sprites either in 8x8 or in 8x16 pixels. Because of a limitation + // of hardware, only ten sprites can be displayed per scan line. Sprite patterns have the same format as BG tiles, + // but they are taken from the Sprite Pattern Table located at $8000-8FFF and have unsigned numbering. + // + // Sprite attributes reside in the Sprite Attribute Table (OAM - Object Attribute Memory) at $FE00-FE9F. Each of + // the 40 entries consists of four bytes with the following meanings: + // Byte0 - Y Position + // Specifies the sprites vertical position on the screen (minus 16). An off-screen value (for example, Y=0 or + // Y>=160) hides the sprite. + // + // Byte1 - X Position + // Specifies the sprites horizontal position on the screen (minus 8). An off-screen value (X=0 or X>=168) hides + // the sprite, but the sprite still affects the priority ordering - a better way to hide a sprite is to set its + // Y-coordinate off-screen. + // + // Byte2 - Tile/Pattern Number + // Specifies the sprites Tile Number (00-FF). This (unsigned) value selects a tile from memory at 8000h-8FFFh. In + // CGB Mode this could be either in VRAM Bank 0 or 1, depending on Bit 3 of the following byte. In 8x16 mode, the + // lower bit of the tile number is ignored. IE: the upper 8x8 tile is "NN AND FEh", and the lower 8x8 tile is + // "NN OR 01h". + // + // Byte3 - Attributes/Flags: + // Bit7 OBJ-to-BG Priority (0=OBJ Above BG, 1=OBJ Behind BG color 1-3) + // (Used for both BG and Window. BG color 0 is always behind OBJ) + // Bit6 Y flip (0=Normal, 1=Vertically mirrored) + // Bit5 X flip (0=Normal, 1=Horizontally mirrored) + // Bit4 Palette number **Non CGB Mode Only** (0=OBP0, 1=OBP1) + // Bit3 Tile VRAM-Bank **CGB Mode Only** (0=Bank 0, 1=Bank 1) + // Bit2-0 Palette number **CGB Mode Only** (OBP0-7) + fn draw_sprites(&mut self) { + // Sprite tile size 8x8 or 8x16(2 stacked vertically). + let sprite_size = if self.lcdc.bit2() { 16 } else { 8 }; + for i in 0..40 { + let sprite_addr = 0xfe00 + (i as u16) * 4; + let py = self.get(sprite_addr).wrapping_sub(16); + let px = self.get(sprite_addr + 1).wrapping_sub(8); + let tile_number = + self.get(sprite_addr + 2) & if self.lcdc.bit2() { 0xfe } else { 0xff }; + let tile_attr = Attr::from(self.get(sprite_addr + 3)); + + // If this is true the scanline is out of the area we care about + if py <= 0xff - sprite_size + 1 { + if self.ly < py || self.ly > py + sprite_size - 1 { + continue; + } + } else { + if self.ly > py.wrapping_add(sprite_size) - 1 { + continue; + } + } + if px >= (SCREEN_W as u8) && px <= (0xff - 7) { + continue; + } + + let tile_y = if tile_attr.yflip { + sprite_size - 1 - self.ly.wrapping_sub(py) + } else { + self.ly.wrapping_sub(py) + }; + let tile_y_addr = 0x8000u16 + u16::from(tile_number) * 16 + u16::from(tile_y) * 2; + let tile_y_data: [u8; 2] = if self.term == Term::GBC && tile_attr.bank { + let b1 = self.get_ram1(tile_y_addr); + let b2 = self.get_ram1(tile_y_addr + 1); + [b1, b2] + } else { + let b1 = self.get_ram0(tile_y_addr); + let b2 = self.get_ram0(tile_y_addr + 1); + [b1, b2] + }; + + for x in 0..8 { + if px.wrapping_add(x) >= (SCREEN_W as u8) { + continue; + } + let tile_x = if tile_attr.xflip { 7 - x } else { x }; + + // Palettes + let color_l = if tile_y_data[0] & (0x80 >> tile_x) != 0 { + 1 + } else { + 0 + }; + let color_h = if tile_y_data[1] & (0x80 >> tile_x) != 0 { + 2 + } else { + 0 + }; + let color = color_h | color_l; + if color == 0 { + continue; + } + + // Confirm the priority of background and sprite. + let prio = self.prio[px.wrapping_add(x) as usize]; + let skip = if self.term == Term::GBC && !self.lcdc.bit0() { + prio.1 == 0 + } else if prio.0 { + prio.1 != 0 + } else { + tile_attr.priority && prio.1 != 0 + }; + if skip { + continue; + } + + if self.term == Term::GBC { + let r = self.cobpd[tile_attr.palette_number_1][color][0]; + let g = self.cobpd[tile_attr.palette_number_1][color][1]; + let b = self.cobpd[tile_attr.palette_number_1][color][2]; + self.set_rgb(px.wrapping_add(x) as usize, r, g, b); + } else { + let color = if tile_attr.palette_number_0 == 1 { + Self::get_gray_shades(self.op1, color) as u8 + } else { + Self::get_gray_shades(self.op0, color) as u8 + }; + self.set_gre(px.wrapping_add(x) as usize, color); + } + } + } + } +} + +impl Memory for Gpu { + fn get(&self, a: u16) -> u8 { + match a { + 0x8000..=0x9fff => self.ram[self.ram_bank * 0x2000 + a as usize - 0x8000], + 0xfe00..=0xfe9f => self.oam[a as usize - 0xfe00], + 0xff40 => self.lcdc.data, + 0xff41 => { + let bit6 = if self.stat.enable_ly_interrupt { + 0x40 + } else { + 0x00 + }; + let bit5 = if self.stat.enable_m2_interrupt { + 0x20 + } else { + 0x00 + }; + let bit4 = if self.stat.enable_m1_interrupt { + 0x10 + } else { + 0x00 + }; + let bit3 = if self.stat.enable_m0_interrupt { + 0x08 + } else { + 0x00 + }; + let bit2 = if self.ly == self.lc { 0x04 } else { 0x00 }; + bit6 | bit5 | bit4 | bit3 | bit2 | self.stat.mode + } + 0xff42 => self.sy, + 0xff43 => self.sx, + 0xff44 => self.ly, + 0xff45 => self.lc, + 0xff47 => self.bgp, + 0xff48 => self.op0, + 0xff49 => self.op1, + 0xff4a => self.wy, + 0xff4b => self.wx, + 0xff4f => 0xfe | self.ram_bank as u8, + 0xff68 => self.cbgpi.get(), + 0xff69 => { + let r = self.cbgpi.i as usize >> 3; + let c = self.cbgpi.i as usize >> 1 & 0x3; + if self.cbgpi.i & 0x01 == 0x00 { + let a = self.cbgpd[r][c][0]; + let b = self.cbgpd[r][c][1] << 5; + a | b + } else { + let a = self.cbgpd[r][c][1] >> 3; + let b = self.cbgpd[r][c][2] << 2; + a | b + } + } + 0xff6a => self.cobpi.get(), + 0xff6b => { + let r = self.cobpi.i as usize >> 3; + let c = self.cobpi.i as usize >> 1 & 0x3; + if self.cobpi.i & 0x01 == 0x00 { + let a = self.cobpd[r][c][0]; + let b = self.cobpd[r][c][1] << 5; + a | b + } else { + let a = self.cobpd[r][c][1] >> 3; + let b = self.cobpd[r][c][2] << 2; + a | b + } + } + _ => panic!(""), + } + } + + fn set(&mut self, a: u16, v: u8) { + match a { + 0x8000..=0x9fff => self.ram[self.ram_bank * 0x2000 + a as usize - 0x8000] = v, + 0xfe00..=0xfe9f => self.oam[a as usize - 0xfe00] = v, + 0xff40 => { + self.lcdc.data = v; + if !self.lcdc.bit7() { + self.dots = 0; + self.ly = 0; + self.stat.mode = 0; + // Clean screen. + self.data = [[[0xffu8; 3]; SCREEN_W]; SCREEN_H]; + self.v_blank = true; + } + } + 0xff41 => { + self.stat.enable_ly_interrupt = v & 0x40 != 0x00; + self.stat.enable_m2_interrupt = v & 0x20 != 0x00; + self.stat.enable_m1_interrupt = v & 0x10 != 0x00; + self.stat.enable_m0_interrupt = v & 0x08 != 0x00; + } + 0xff42 => self.sy = v, + 0xff43 => self.sx = v, + 0xff44 => {} + 0xff45 => self.lc = v, + 0xff47 => self.bgp = v, + 0xff48 => self.op0 = v, + 0xff49 => self.op1 = v, + 0xff4a => self.wy = v, + 0xff4b => self.wx = v, + 0xff4f => self.ram_bank = (v & 0x01) as usize, + 0xff68 => self.cbgpi.set(v), + 0xff69 => { + let r = self.cbgpi.i as usize >> 3; + let c = self.cbgpi.i as usize >> 1 & 0x03; + if self.cbgpi.i & 0x01 == 0x00 { + self.cbgpd[r][c][0] = v & 0x1f; + self.cbgpd[r][c][1] = (self.cbgpd[r][c][1] & 0x18) | (v >> 5); + } else { + self.cbgpd[r][c][1] = (self.cbgpd[r][c][1] & 0x07) | ((v & 0x03) << 3); + self.cbgpd[r][c][2] = (v >> 2) & 0x1f; + } + if self.cbgpi.auto_increment { + self.cbgpi.i += 0x01; + self.cbgpi.i &= 0x3f; + } + } + 0xff6a => self.cobpi.set(v), + 0xff6b => { + let r = self.cobpi.i as usize >> 3; + let c = self.cobpi.i as usize >> 1 & 0x03; + if self.cobpi.i & 0x01 == 0x00 { + self.cobpd[r][c][0] = v & 0x1f; + self.cobpd[r][c][1] = (self.cobpd[r][c][1] & 0x18) | (v >> 5); + } else { + self.cobpd[r][c][1] = (self.cobpd[r][c][1] & 0x07) | ((v & 0x03) << 3); + self.cobpd[r][c][2] = (v >> 2) & 0x1f; + } + if self.cobpi.auto_increment { + self.cobpi.i += 0x01; + self.cobpi.i &= 0x3f; + } + } + _ => panic!(""), + } + } +} diff --git a/src/intf.rs b/src/intf.rs new file mode 100644 index 0000000000..8531937c5d --- /dev/null +++ b/src/intf.rs @@ -0,0 +1,29 @@ +// FF0F - IF - Interrupt Flag (R/W) +// Bit 0: V-Blank Interrupt Request (INT 40h) (1=Request) +// Bit 1: LCD STAT Interrupt Request (INT 48h) (1=Request) +// Bit 2: Timer Interrupt Request (INT 50h) (1=Request) +// Bit 3: Serial Interrupt Request (INT 58h) (1=Request) +// Bit 4: Joypad Interrupt Request (INT 60h) (1=Request) +#[rustfmt::skip] +#[derive(Clone)] +pub enum Flag { + VBlank = 0, + LCDStat = 1, + Timer = 2, + Serial = 3, + Joypad = 4, +} + +pub struct Intf { + pub data: u8, +} + +impl Intf { + pub fn power_up() -> Self { + Self { data: 0x00 } + } + + pub fn hi(&mut self, flag: Flag) { + self.data |= 1 << flag as u8; + } +} diff --git a/src/joypad.rs b/src/joypad.rs new file mode 100644 index 0000000000..04c3b0cea0 --- /dev/null +++ b/src/joypad.rs @@ -0,0 +1,78 @@ +// The eight gameboy buttons/direction keys are arranged in form of a 2x4 matrix. Select either button or direction +// keys by writing to this register, then read-out bit 0-3. +// +// FF00 - P1/JOYP - Joypad (R/W) +// +// Bit 7 - Not used +// Bit 6 - Not used +// Bit 5 - P15 Select Button Keys (0=Select) +// Bit 4 - P14 Select Direction Keys (0=Select) +// Bit 3 - P13 Input Down or Start (0=Pressed) (Read Only) +// Bit 2 - P12 Input Up or Select (0=Pressed) (Read Only) +// Bit 1 - P11 Input Left or Button B (0=Pressed) (Read Only) +// Bit 0 - P10 Input Right or Button A (0=Pressed) (Read Only) +// +// Note: Most programs are repeatedly reading from this port several times (the first reads used as short delay, +// allowing the inputs to stabilize, and only the value from the last read actually used). +use super::intf::{Flag, Intf}; +use super::memory::Memory; +use std::cell::RefCell; +use std::rc::Rc; + +#[rustfmt::skip] +#[derive(Clone, Copy, Debug, PartialEq)] +pub enum JoypadKey { + Right = 0b0000_0001, + Left = 0b0000_0010, + Up = 0b0000_0100, + Down = 0b0000_1000, + A = 0b0001_0000, + B = 0b0010_0000, + Select = 0b0100_0000, + Start = 0b1000_0000, +} + +pub struct Joypad { + intf: Rc>, + matrix: u8, + select: u8, +} + +impl Joypad { + pub fn power_up(intf: Rc>) -> Self { + Self { + intf, + matrix: 0xff, + select: 0x00, + } + } +} + +impl Joypad { + pub fn keydown(&mut self, key: JoypadKey) { + self.matrix &= !(key as u8); + self.intf.borrow_mut().hi(Flag::Joypad); + } + + pub fn keyup(&mut self, key: JoypadKey) { + self.matrix |= key as u8; + } +} + +impl Memory for Joypad { + fn get(&self, a: u16) -> u8 { + assert_eq!(a, 0xff00); + if (self.select & 0b0001_0000) == 0x00 { + return self.select | (self.matrix & 0x0f); + } + if (self.select & 0b0010_0000) == 0x00 { + return self.select | (self.matrix >> 4); + } + self.select + } + + fn set(&mut self, a: u16, v: u8) { + assert_eq!(a, 0xff00); + self.select = v; + } +} diff --git a/src/lib.rs b/src/lib.rs new file mode 100644 index 0000000000..e536c8a0ce --- /dev/null +++ b/src/lib.rs @@ -0,0 +1,14 @@ +pub mod apu; +pub mod cartridge; +pub mod clock; +pub mod convention; +pub mod cpu; +pub mod gpu; +pub mod intf; +pub mod joypad; +pub mod memory; +pub mod mmunit; +pub mod motherboard; +pub mod register; +pub mod serial; +pub mod timer; diff --git a/src/main.rs b/src/main.rs new file mode 100644 index 0000000000..46cc463d45 --- /dev/null +++ b/src/main.rs @@ -0,0 +1,473 @@ +use std::io::{prelude::*, BufReader}; +use std::net::{TcpListener}; +use std::sync::mpsc::{self, Receiver, Sender, TryRecvError}; +use std::thread; +// Note: Game BoyTM, Game Boy PocketTM, Super Game BoyTM and Game Boy ColorTM are registered trademarks of +// Nintendo CO., LTD. © 1989 to 1999 by Nintendo CO., LTD. + +use gameboy::gpu::{SCREEN_H, SCREEN_W}; +use gameboy::joypad; +use gameboy::motherboard::MotherBoard; +use std::io::{BufRead}; + +#[cfg(not(feature = "audio"))] +fn initialize_audio(_: &gameboy::motherboard::MotherBoard) { + panic!("audio is not supported"); +} + +#[cfg(feature = "audio")] +fn initialize_audio(mbrd: &gameboy::motherboard::MotherBoard) { + use gameboy::apu::Apu; + let device = cpal::default_output_device().unwrap(); + let format = device.default_output_format().unwrap(); + eprintln!( + "Open the audio player: {}. Sample rate: {:?}", + device.name(), + format.sample_rate.0 + ); + let format = cpal::Format { + channels: 2, + sample_rate: format.sample_rate, + data_type: cpal::SampleFormat::F32, + }; + + let event_loop = cpal::EventLoop::new(); + let stream_id = event_loop.build_output_stream(&device, &format).unwrap(); + event_loop.play_stream(stream_id); + + let apu = Apu::power_up(format.sample_rate.0); + let apu_data = apu.buffer.clone(); + mbrd.mmu.borrow_mut().apu = Some(apu); + + std::thread::spawn(move || { + event_loop.run(move |_, stream_data| { + let mut apu_data = apu_data.lock().unwrap(); + if let cpal::StreamData::Output { buffer } = stream_data { + let len = std::cmp::min(buffer.len() / 2, apu_data.len()); + match buffer { + cpal::UnknownTypeOutputBuffer::F32(mut buffer) => { + for (i, (data_l, data_r)) in apu_data.drain(..len).enumerate() { + buffer[i * 2] = data_l; + buffer[i * 2 + 1] = data_r; + } + } + cpal::UnknownTypeOutputBuffer::U16(mut buffer) => { + for (i, (data_l, data_r)) in apu_data.drain(..len).enumerate() { + buffer[i * 2] = (data_l * f32::from(std::i16::MAX) + + f32::from(std::u16::MAX) / 2.0) + as u16; + buffer[i * 2 + 1] = (data_r * f32::from(std::i16::MAX) + + f32::from(std::u16::MAX) / 2.0) + as u16; + } + } + cpal::UnknownTypeOutputBuffer::I16(mut buffer) => { + for (i, (data_l, data_r)) in apu_data.drain(..len).enumerate() { + buffer[i * 2] = (data_l * f32::from(std::i16::MAX)) as i16; + buffer[i * 2 + 1] = (data_r * f32::from(std::i16::MAX)) as i16; + } + } + } + } + }); + }); +} + +// For playing the game with keyboard input +fn main_type0(mut mbrd: MotherBoard, c_audio: bool) { + // Initialize audio related + if c_audio { + initialize_audio(&mbrd); + } + + let mut option = minifb::WindowOptions::default(); + option.resize = false; + let rom_name = mbrd.mmu.borrow().cartridge.title(); + let mut window = minifb::Window::new( + format!("Gameboy - {}", rom_name).as_str(), + SCREEN_W, + SCREEN_H, + option, + ) + .unwrap(); + let mut window_buffer = vec![0x00; SCREEN_W * SCREEN_H]; + window.update_with_buffer(window_buffer.as_slice()).unwrap(); + + loop { + // Stop the program, if the GUI is closed by the user + if !window.is_open() { + break; + } + + // Execute an instruction + mbrd.next(); + + // Update the window + if mbrd.check_and_reset_gpu_updated() { + let mut i: usize = 0; + for l in mbrd.mmu.borrow().gpu.data.iter() { + for w in l.iter() { + let b = u32::from(w[0]) << 16; + let g = u32::from(w[1]) << 8; + let r = u32::from(w[2]); + let a = 0xff00_0000; + + window_buffer[i] = a | b | g | r; + i += 1; + } + } + window.update_with_buffer(window_buffer.as_slice()).unwrap(); + } + + if !mbrd.cpu.flip() { + continue; + } + + // Handle keyboard events + if window.is_key_down(minifb::Key::Escape) { + break; + } + let keys = vec![ + (minifb::Key::Right, gameboy::joypad::JoypadKey::Right), + (minifb::Key::Up, gameboy::joypad::JoypadKey::Up), + (minifb::Key::Left, gameboy::joypad::JoypadKey::Left), + (minifb::Key::Down, gameboy::joypad::JoypadKey::Down), + (minifb::Key::Z, gameboy::joypad::JoypadKey::A), + (minifb::Key::X, gameboy::joypad::JoypadKey::B), + (minifb::Key::Space, gameboy::joypad::JoypadKey::Select), + (minifb::Key::Enter, gameboy::joypad::JoypadKey::Start), + ]; + for (rk, vk) in &keys { + if window.is_key_down(*rk) { + mbrd.mmu.borrow_mut().joypad.keydown(vk.clone()); + } else { + mbrd.mmu.borrow_mut().joypad.keyup(vk.clone()); + } + } + } +} + +#[derive(Debug)] +enum Command { + Input(Option), + InputAndAdvance((Option, u32)), +} + +#[derive(Debug)] +enum CommandError { + FailedToParseMessage(), +} + +#[derive(Debug)] +enum Response { + State(u128), +} + +fn response_to_string(response: Response) -> String { + match response { + Response::State(n) => n.to_string(), + } +} + +fn joypad_input_of_string(buffer: &str) -> Option { + match &buffer[..] { + "A" => Some(joypad::JoypadKey::A), + "B" => Some(joypad::JoypadKey::B), + "Start" => Some(joypad::JoypadKey::Start), + "Select" => Some(joypad::JoypadKey::Select), + "Up" => Some(joypad::JoypadKey::Up), + "Down" => Some(joypad::JoypadKey::Down), + "Left" => Some(joypad::JoypadKey::Left), + "Right" => Some(joypad::JoypadKey::Right), + _ => None, + } +} + +fn command_of_string(buffer: String) -> Option { + let tokens: Vec<&str> = buffer.split(" ").collect(); + match tokens.get(0) { + Some(&"Input") => { + let joypad = joypad_input_of_string(tokens.get(1)?); + Some(Command::Input(joypad)) + } + Some(&"Input_and_advance") => { + let joypad = joypad_input_of_string(tokens.get(1)?); + let advance = tokens.get(2)?; + let advance: Result = advance.parse(); + match advance { + Ok(advance) => Some(Command::InputAndAdvance((joypad, advance))), + Err(_) => None, + } + } + Some(_) | None => None, + } +} + +fn main_test() {} + +fn setup_channels(port: u16) -> (Receiver, Sender) { + let (server_tx, server_rx) = mpsc::channel::(); + let (emulator_tx, emulator_rx) = mpsc::channel::(); + thread::spawn(move || loop { + let address = "127.0.0.1:".to_string() + port.to_string().as_str(); + let listener = TcpListener::bind(address).unwrap(); + + for stream in listener.incoming() { + let mut stream = stream.unwrap(); + eprintln!("Got connection to {:?}", stream.local_addr()); + let mut buf_reader = BufReader::new(&mut stream); + let mut buffer = String::new(); + buf_reader.read_line(&mut buffer).unwrap(); + eprintln!("Got request '{}'", buffer.trim()); + + match command_of_string(buffer.trim().to_string()) { + Some(command) => { + server_tx.send(command).unwrap(); + let response = emulator_rx.recv().unwrap(); + let response = response_to_string(response); + stream.write((response + "\n").as_bytes()).unwrap(); + stream.flush().unwrap(); + } + None => eprintln!("Error: unable to parse message {}", buffer), + } + } + }); + (server_rx, emulator_tx) +} + +// measured in cycles +const INPUT_DURATION: u32 = 80000; + +// For playing the game on-chain +fn main_type1(mut _mbrd: MotherBoard) { + panic!("unfinished!"); + // let mut option = minifb::WindowOptions::default(); + // option.resize = false; + // let rom_name = mbrd.mmu.borrow().cartridge.title(); + // let mut window = + // minifb::Window::new(format!("Gameboy - {}", rom_name).as_str(), SCREEN_W, SCREEN_H, option).unwrap(); + // let mut window_buffer = vec![0x00; SCREEN_W * SCREEN_H]; + // window.update_with_buffer(window_buffer.as_slice()).unwrap(); + + // let mut cycles: u32 = 0; + // let mut key_down_cycles: u32 = 0; + // let mut key_down: Option<(u32, joypad::JoypadKey)> = None; + // loop { + // cycles += 1; + // key_down_cycles += 1; + // let mut buffer = String::new(); + // io::stdin().read_line(&mut buffer).unwrap(); + // match command_of_string(buffer) { + // Command::Input(key) => (), + // _ => () + // } + + // // Execute an instruction + // mbrd.next(); + + // // Update the window + // mbrd.check_and_reset_gpu_updated(); + + // if !mbrd.cpu.flip() { + // continue; + // } + + // // Handle keyboard events + // if window.is_key_down(minifb::Key::Escape) { + // break; + // } + // let keys = vec![ + // (minifb::Key::Right, gameboy::joypad::JoypadKey::Right), + // (minifb::Key::Up, gameboy::joypad::JoypadKey::Up), + // (minifb::Key::Left, gameboy::joypad::JoypadKey::Left), + // (minifb::Key::Down, gameboy::joypad::JoypadKey::Down), + // (minifb::Key::Z, gameboy::joypad::JoypadKey::A), + // (minifb::Key::X, gameboy::joypad::JoypadKey::B), + // (minifb::Key::Space, gameboy::joypad::JoypadKey::Select), + // (minifb::Key::Enter, gameboy::joypad::JoypadKey::Start), + // ]; + // for (rk, vk) in &keys { + // match key_down { + // Some((last_cycle, key)) if key.eq(vk) && key_down_cycles > last_cycle + INPUT_DURATION => { + // println!("Key up: {:?}", key); + // mbrd.mmu.borrow_mut().joypad.keyup(vk.clone()); + // key_down = None; + // key_down_cycles = 0; + // } + // Some((_, key)) if key.eq(vk) => (), + // None if window.is_key_down(*rk) => { + // println!("Key down at {} cycles", key_down_cycles); + // key_down = Some((key_down_cycles.clone(), vk.clone())); + // mbrd.mmu.borrow_mut().joypad.keydown(vk.clone()); + // } + // Some(_) | None => mbrd.mmu.borrow_mut().joypad.keyup(vk.clone()), + // }; + // } + // } +} + +fn press_key(mbrd: &MotherBoard, input_key: Option) { + let keys = vec![ + (gameboy::joypad::JoypadKey::Right), + (gameboy::joypad::JoypadKey::Up), + (gameboy::joypad::JoypadKey::Left), + (gameboy::joypad::JoypadKey::Down), + (gameboy::joypad::JoypadKey::A), + (gameboy::joypad::JoypadKey::B), + (gameboy::joypad::JoypadKey::Select), + (gameboy::joypad::JoypadKey::Start), + ]; + for key in keys { + match input_key { + Some(input_key) if key.eq(&input_key) => { + mbrd.mmu.borrow_mut().joypad.keydown(key.clone()) + } + Some(_) | None => mbrd.mmu.borrow_mut().joypad.keyup(key.clone()), + } + } +} + +// For playing the game as the block producer +fn main_type2(mut mbrd: MotherBoard, c_audio: bool) { + let (in_channel, out_channel) = setup_channels(2222); + + // Initialize audio related + if c_audio { + initialize_audio(&mbrd); + } + + let mut option = minifb::WindowOptions::default(); + option.resize = false; + let rom_name = mbrd.mmu.borrow().cartridge.title(); + let mut window = minifb::Window::new( + format!("Gameboy - {}", rom_name).as_str(), + SCREEN_W, + SCREEN_H, + option, + ) + .unwrap(); + let mut window_buffer = vec![0x00; SCREEN_W * SCREEN_H]; + window.update_with_buffer(window_buffer.as_slice()).unwrap(); + + let mut cycles: u128 = 0; + let mut key_down: Option<(u32, joypad::JoypadKey)> = None; + loop { + cycles += 1; + + // Stop the program, if the GUI is closed by the user + if !window.is_open() { + break; + } + + // Execute an instruction + mbrd.next(); + + // Update the window + if mbrd.check_and_reset_gpu_updated() { + let mut i: usize = 0; + for l in mbrd.mmu.borrow().gpu.data.iter() { + for w in l.iter() { + let b = u32::from(w[0]) << 16; + let g = u32::from(w[1]) << 8; + let r = u32::from(w[2]); + let a = 0xff00_0000; + + window_buffer[i] = a | b | g | r; + i += 1; + } + } + window.update_with_buffer(window_buffer.as_slice()).unwrap(); + } + + match key_down { + Some((last_cycle, key)) => { + key_down = Some((last_cycle + 1, key)); + } + None => + // Handle incoming commands + // TODO: the point of putting it in this match is + // so that inputs resolve before we take on new commands. + // not sure if this works. + { + match in_channel.try_recv() { + Ok(Command::Input(key)) => { + match key { + Some(key) => { + eprintln!("Received Input {:?}", key); + key_down = Some((0, key)) + } + None => (), + } + out_channel.send(Response::State(cycles)).unwrap(); + } + Ok(Command::InputAndAdvance(_)) => { + panic!("Received Input_and_advance, but that command is not supported in type 2") + } + Err(TryRecvError::Empty) => (), + Err(TryRecvError::Disconnected) => panic!("Thread channel disconnected"), + } + } + }; + + if !mbrd.cpu.flip() { + continue; + } + + // Handle keyboard events + if window.is_key_down(minifb::Key::Escape) { + break; + } + + match key_down { + Some((last_cycle, key)) => { + if last_cycle > INPUT_DURATION { + println!("Releasing key {:?}", key); + press_key(&mbrd, None); + key_down = None + } else { + press_key(&mbrd, Some(key)); + } + } + None => { + press_key(&mbrd, None); + } + } + } +} + +#[cfg(feature = "gui")] +fn main() { + let mut rom = String::from(""); + let mut c_audio = false; + let mut runtime_type = 0; + { + let mut ap = argparse::ArgumentParser::new(); + ap.set_description("Gameboy emulator"); + ap.refer(&mut c_audio).add_option( + &["-a", "--enable-audio"], + argparse::StoreTrue, + "Enable audio", + ); + ap.refer(&mut runtime_type).add_option( + &["-t", "--runtime-type"], + argparse::Store, + "Runtime type", + ); + ap.refer(&mut rom) + .add_argument("rom", argparse::Store, "Rom name"); + + ap.parse_args_or_exit(); + } + + eprintln!("Running in runtime type {}", runtime_type); + + let mbrd = MotherBoard::power_up(rom); + + match runtime_type { + 0 => main_type0(mbrd, c_audio), + 1 => main_type1(mbrd), + 2 => main_type2(mbrd, c_audio), + 3 => main_test(), + _ => panic!("You must choose type 1 or type 2"), + } +} diff --git a/src/memory.rs b/src/memory.rs new file mode 100644 index 0000000000..d7159fc6dd --- /dev/null +++ b/src/memory.rs @@ -0,0 +1,29 @@ +// General Memory Map +// 0000-3FFF 16KB ROM Bank 00 (in cartridge, fixed at bank 00) +// 4000-7FFF 16KB ROM Bank 01..NN (in cartridge, switchable bank number) +// 8000-9FFF 8KB Video RAM (VRAM) (switchable bank 0-1 in CGB Mode) +// A000-BFFF 8KB External RAM (in cartridge, switchable bank, if any) +// C000-CFFF 4KB Work RAM Bank 0 (WRAM) +// D000-DFFF 4KB Work RAM Bank 1 (WRAM) (switchable bank 1-7 in CGB Mode) +// E000-FDFF Same as C000-DDFF (ECHO) (typically not used) +// FE00-FE9F Sprite Attribute Table (OAM) +// FEA0-FEFF Not Usable +// FF00-FF7F I/O Ports +// FF80-FFFE High RAM (HRAM) +// FFFF Interrupt Enable Register +// +// See: http://bgb.bircd.org/pandocs.htm#cgbregisters +pub trait Memory { + fn get(&self, a: u16) -> u8; + + fn set(&mut self, a: u16, v: u8); + + fn get_word(&self, a: u16) -> u16 { + u16::from(self.get(a)) | (u16::from(self.get(a + 1)) << 8) + } + + fn set_word(&mut self, a: u16, v: u16) { + self.set(a, (v & 0xFF) as u8); + self.set(a + 1, (v >> 8) as u8) + } +} diff --git a/src/mmunit.rs b/src/mmunit.rs new file mode 100644 index 0000000000..0faacc8c66 --- /dev/null +++ b/src/mmunit.rs @@ -0,0 +1,247 @@ +// A memory management unit (MMU), sometimes called paged memory management unit (PMMU), is a computer hardware unit +// having all memory references passed through itself, primarily performing the translation of virtual memory addresses +// to physical addresses. +use super::apu::Apu; +use super::cartridge::{self, Cartridge}; +use super::convention::Term; +use super::gpu::{Gpu, Hdma, HdmaMode}; +use super::intf::Intf; +use super::joypad::Joypad; +use super::memory::Memory; +use super::serial::Serial; +use super::timer::Timer; +use std::cell::RefCell; +use std::path::Path; +use std::rc::Rc; + +#[derive(Clone, Copy, Eq, PartialEq)] +pub enum Speed { + Normal = 0x01, + Double = 0x02, +} + +pub struct Mmunit { + pub cartridge: Box, + pub apu: Option, + pub gpu: Gpu, + pub joypad: Joypad, + pub serial: Serial, + pub shift: bool, + pub speed: Speed, + pub term: Term, + pub timer: Timer, + inte: u8, + intf: Rc>, + hdma: Hdma, + hram: [u8; 0x7f], + wram: [u8; 0x8000], + wram_bank: usize, +} + +impl Mmunit { + pub fn power_up(path: impl AsRef) -> Self { + let cart = cartridge::power_up(path); + let term = match cart.get(0x0143) & 0x80 { + 0x80 => Term::GBC, + _ => Term::GB, + }; + let intf = Rc::new(RefCell::new(Intf::power_up())); + let mut r = Self { + cartridge: cart, + apu: None, + gpu: Gpu::power_up(term, intf.clone()), + joypad: Joypad::power_up(intf.clone()), + serial: Serial::power_up(intf.clone()), + shift: false, + speed: Speed::Normal, + term, + timer: Timer::power_up(intf.clone()), + inte: 0x00, + intf: intf.clone(), + hdma: Hdma::power_up(), + hram: [0x00; 0x7f], + wram: [0x00; 0x8000], + wram_bank: 0x01, + }; + r.set(0xff05, 0x00); + r.set(0xff06, 0x00); + r.set(0xff07, 0x00); + r.set(0xff10, 0x80); + r.set(0xff11, 0xbf); + r.set(0xff12, 0xf3); + r.set(0xff14, 0xbf); + r.set(0xff16, 0x3f); + r.set(0xff16, 0x3f); + r.set(0xff17, 0x00); + r.set(0xff19, 0xbf); + r.set(0xff1a, 0x7f); + r.set(0xff1b, 0xff); + r.set(0xff1c, 0x9f); + r.set(0xff1e, 0xff); + r.set(0xff20, 0xff); + r.set(0xff21, 0x00); + r.set(0xff22, 0x00); + r.set(0xff23, 0xbf); + r.set(0xff24, 0x77); + r.set(0xff25, 0xf3); + r.set(0xff26, 0xf1); + r.set(0xff40, 0x91); + r.set(0xff42, 0x00); + r.set(0xff43, 0x00); + r.set(0xff45, 0x00); + r.set(0xff47, 0xfc); + r.set(0xff48, 0xff); + r.set(0xff49, 0xff); + r.set(0xff4a, 0x00); + r.set(0xff4b, 0x00); + r + } +} + +impl Mmunit { + pub fn next(&mut self, cycles: u32) -> u32 { + let cpu_divider = self.speed as u32; + let vram_cycles = self.run_dma(); + let gpu_cycles = cycles / cpu_divider + vram_cycles; + let cpu_cycles = cycles + vram_cycles * cpu_divider; + self.timer.next(cpu_cycles); + self.gpu.next(gpu_cycles); + self.apu.as_mut().map_or((), |s| s.next(gpu_cycles)); + gpu_cycles + } + + pub fn switch_speed(&mut self) { + if self.shift { + if self.speed == Speed::Double { + self.speed = Speed::Normal; + } else { + self.speed = Speed::Double; + } + } + self.shift = false; + } + + fn run_dma(&mut self) -> u32 { + if !self.hdma.active { + return 0; + } + match self.hdma.mode { + HdmaMode::Gdma => { + let len = u32::from(self.hdma.remain) + 1; + for _ in 0..len { + self.run_dma_hrampart(); + } + self.hdma.active = false; + len * 8 + } + HdmaMode::Hdma => { + if !self.gpu.h_blank { + return 0; + } + self.run_dma_hrampart(); + if self.hdma.remain == 0x7f { + self.hdma.active = false; + } + 8 + } + } + } + + fn run_dma_hrampart(&mut self) { + let mmu_src = self.hdma.src; + for i in 0..0x10 { + let b: u8 = self.get(mmu_src + i); + self.gpu.set(self.hdma.dst + i, b); + } + self.hdma.src += 0x10; + self.hdma.dst += 0x10; + if self.hdma.remain == 0 { + self.hdma.remain = 0x7f; + } else { + self.hdma.remain -= 1; + } + } +} + +impl Memory for Mmunit { + fn get(&self, a: u16) -> u8 { + match a { + 0x0000..=0x7fff => self.cartridge.get(a), + 0x8000..=0x9fff => self.gpu.get(a), + 0xa000..=0xbfff => self.cartridge.get(a), + 0xc000..=0xcfff => self.wram[a as usize - 0xc000], + 0xd000..=0xdfff => self.wram[a as usize - 0xd000 + 0x1000 * self.wram_bank], + 0xe000..=0xefff => self.wram[a as usize - 0xe000], + 0xf000..=0xfdff => self.wram[a as usize - 0xf000 + 0x1000 * self.wram_bank], + 0xfe00..=0xfe9f => self.gpu.get(a), + 0xfea0..=0xfeff => 0x00, + 0xff00 => self.joypad.get(a), + 0xff01..=0xff02 => self.serial.get(a), + 0xff04..=0xff07 => self.timer.get(a), + 0xff0f => self.intf.borrow().data, + 0xff10..=0xff3f => match &self.apu { + Some(some) => some.get(a), + None => 0x00, + }, + 0xff4d => { + let a = if self.speed == Speed::Double { + 0x80 + } else { + 0x00 + }; + let b = if self.shift { 0x01 } else { 0x00 }; + a | b + } + 0xff40..=0xff45 | 0xff47..=0xff4b | 0xff4f => self.gpu.get(a), + 0xff51..=0xff55 => self.hdma.get(a), + 0xff68..=0xff6b => self.gpu.get(a), + 0xff70 => self.wram_bank as u8, + 0xff80..=0xfffe => self.hram[a as usize - 0xff80], + 0xffff => self.inte, + _ => 0x00, + } + } + + fn set(&mut self, a: u16, v: u8) { + match a { + 0x0000..=0x7fff => self.cartridge.set(a, v), + 0x8000..=0x9fff => self.gpu.set(a, v), + 0xa000..=0xbfff => self.cartridge.set(a, v), + 0xc000..=0xcfff => self.wram[a as usize - 0xc000] = v, + 0xd000..=0xdfff => self.wram[a as usize - 0xd000 + 0x1000 * self.wram_bank] = v, + 0xe000..=0xefff => self.wram[a as usize - 0xe000] = v, + 0xf000..=0xfdff => self.wram[a as usize - 0xf000 + 0x1000 * self.wram_bank] = v, + 0xfe00..=0xfe9f => self.gpu.set(a, v), + 0xfea0..=0xfeff => {} + 0xff00 => self.joypad.set(a, v), + 0xff01..=0xff02 => self.serial.set(a, v), + 0xff04..=0xff07 => self.timer.set(a, v), + 0xff10..=0xff3f => self.apu.as_mut().map_or((), |s| s.set(a, v)), + 0xff46 => { + // Writing to this register launches a DMA transfer from ROM or RAM to OAM memory (sprite attribute + // table). + // See: http://gbdev.gg8.se/wiki/articles/Video_Display#FF46_-_DMA_-_DMA_Transfer_and_Start_Address_.28R.2FW.29 + assert!(v <= 0xf1); + let base = u16::from(v) << 8; + for i in 0..0xa0 { + let b = self.get(base + i); + self.set(0xfe00 + i, b); + } + } + 0xff4d => self.shift = (v & 0x01) == 0x01, + 0xff40..=0xff45 | 0xff47..=0xff4b | 0xff4f => self.gpu.set(a, v), + 0xff51..=0xff55 => self.hdma.set(a, v), + 0xff68..=0xff6b => self.gpu.set(a, v), + 0xff0f => self.intf.borrow_mut().data = v, + 0xff70 => { + self.wram_bank = match v & 0x7 { + 0 => 1, + n => n as usize, + }; + } + 0xff80..=0xfffe => self.hram[a as usize - 0xff80] = v, + 0xffff => self.inte = v, + _ => {} + } + } +} diff --git a/src/motherboard.rs b/src/motherboard.rs new file mode 100644 index 0000000000..0cc426ad1e --- /dev/null +++ b/src/motherboard.rs @@ -0,0 +1,34 @@ +use super::cpu::Rtc; +use super::memory::Memory; +use super::mmunit::Mmunit; +use std::cell::RefCell; +use std::path::Path; +use std::rc::Rc; + +pub struct MotherBoard { + pub mmu: Rc>, + pub cpu: Rtc, +} + +impl MotherBoard { + pub fn power_up(path: impl AsRef) -> Self { + let mmu = Rc::new(RefCell::new(Mmunit::power_up(path))); + let cpu = Rtc::power_up(mmu.borrow().term, mmu.clone()); + Self { mmu, cpu } + } + + pub fn next(&mut self) -> u32 { + if self.mmu.borrow().get(self.cpu.cpu.reg.pc) == 0x10 { + self.mmu.borrow_mut().switch_speed(); + } + let cycles = self.cpu.next(); + self.mmu.borrow_mut().next(cycles); + cycles + } + + pub fn check_and_reset_gpu_updated(&mut self) -> bool { + let result = self.mmu.borrow().gpu.v_blank; + self.mmu.borrow_mut().gpu.v_blank = false; + result + } +} diff --git a/src/register.rs b/src/register.rs new file mode 100644 index 0000000000..e8e9b5db7f --- /dev/null +++ b/src/register.rs @@ -0,0 +1,139 @@ +use super::convention::Term; + +// The GameBoy has instructions & registers similar to the Intel 8080, Intel 8085, & Zilog Z80 microprocessors. It has +// eight 8-bit registers A,B,C,D,E,F,H,L and two 16-bit registers SP & PC +// +// ------------- +// | A Flags | ---> Program Status Word +// | B C | ---> B +// | D E | ---> D +// | H L | ---> H +// | SP | ---> Stack Pointer +// | PC | ---> Program Counter +// ------------- +#[derive(Clone, Default)] +pub struct Register { + pub a: u8, + pub f: u8, // The F register is indirectly accessible by the programer. + pub b: u8, + pub c: u8, + pub d: u8, + pub e: u8, + pub h: u8, + pub l: u8, + pub sp: u16, + pub pc: u16, +} + +// Some instructions, however, allow you to use the registers A,B,C,D,E,H,L as 16-bit registers by pairing them up +// in the following manner: AF,BC,DE,HL. +impl Register { + pub fn get_af(&self) -> u16 { + (u16::from(self.a) << 8) | u16::from(self.f) + } + + pub fn get_bc(&self) -> u16 { + (u16::from(self.b) << 8) | u16::from(self.c) + } + + pub fn get_de(&self) -> u16 { + (u16::from(self.d) << 8) | u16::from(self.e) + } + + pub fn get_hl(&self) -> u16 { + (u16::from(self.h) << 8) | u16::from(self.l) + } + + pub fn set_af(&mut self, v: u16) { + self.a = (v >> 8) as u8; + self.f = (v & 0x00f0) as u8; + } + + pub fn set_bc(&mut self, v: u16) { + self.b = (v >> 8) as u8; + self.c = (v & 0x00ff) as u8; + } + + pub fn set_de(&mut self, v: u16) { + self.d = (v >> 8) as u8; + self.e = (v & 0x00ff) as u8; + } + + pub fn set_hl(&mut self, v: u16) { + self.h = (v >> 8) as u8; + self.l = (v & 0x00ff) as u8; + } +} + +// The Fleg Register consists of the following bits: Z, N, H, C, 0, 0, 0, 0. +pub enum Flag { + // Zero Flag. This bit is set when the result of a math operationis zero or two values match when using the CP + // instruction. + Z = 0b1000_0000, + // Subtract Flag. This bit is set if a subtraction was performed in the last math instruction. + N = 0b0100_0000, + // Half Carry Flag. This bit is set if a carry occurred from the lowernibble in the last math operation. + H = 0b0010_0000, + // Carry Flag. This bit is set if a carry occurred from the last math operation or if register A is the smaller + // valuewhen executing the CP instruction. + C = 0b0001_0000, +} + +impl Flag { + pub fn og(self) -> u8 { + self as u8 + } + + pub fn bw(self) -> u8 { + !self.og() + } +} + +impl Register { + pub fn get_flag(&self, f: Flag) -> bool { + (self.f & f as u8) != 0 + } + + pub fn set_flag(&mut self, f: Flag, v: bool) { + if v { + self.f |= f.og(); + } else { + self.f &= f.bw(); + } + } +} + +impl Register { + pub fn power_up(term: Term) -> Self { + let mut r = Self::default(); + match term { + Term::GB => { + r.a = 0x01; + } + Term::GBP => { + r.a = 0xff; + } + Term::GBC => { + r.a = 0x11; + } + Term::SGB => { + r.a = 0x01; + } + } + r.f = 0xb0; + r.b = 0x00; + r.c = 0x13; + r.d = 0x00; + r.e = 0xd8; + r.h = 0x01; + r.l = 0x4d; + // The GameBoy stack pointer is initialized to 0xfffe on power up but a programmer should not rely on this + // setting and rather should explicitly set its value. + r.sp = 0xfffe; + // On power up, the GameBoy Program Counter is initialized to 0x0100 and the instruction found at this location + // in ROM is executed. The Program Counter from this point on is controlled, indirectly, by the program + // instructions themselves that were generated by the programmer of the ROM cart. + r.pc = 0x0100; + r + } +} diff --git a/src/serial.rs b/src/serial.rs new file mode 100644 index 0000000000..2723812592 --- /dev/null +++ b/src/serial.rs @@ -0,0 +1,49 @@ +// Communication between two Gameboys happens one byte at a time. One Gameboy acts as the master, uses its internal +// clock, and thus controls when the exchange happens. The other one uses an external clock (i.e., the one inside the +// other Gameboy) and has no control over when the transfer happens. If it hasn't gotten around to loading up the next +// data byte at the time the transfer begins, the last one will go out again. Alternately, if it's ready to send the +// next byte but the last one hasn't gone out yet, it has no choice but to wait. +// +// See: http://gbdev.gg8.se/wiki/articles/Serial_Data_Transfer_(Link_Cable) +use super::intf::Intf; +use std::cell::RefCell; +use std::rc::Rc; + +pub struct Serial { + _intf: Rc>, + + // Before a transfer, it holds the next byte that will go out. + // During a transfer, it has a blend of the outgoing and incoming bytes. Each cycle, the leftmost bit is shifted + // out (and over the wire) and the incoming bit is shifted in from the other side: + data: u8, + // Bit 7 - Transfer Start Flag (0=No transfer is in progress or requested, 1=Transfer in progress, or requested) + // Bit 1 - Clock Speed (0=Normal, 1=Fast) ** CGB Mode Only ** + // Bit 0 - Shift Clock (0=External Clock, 1=Internal Clock) + control: u8, +} + +impl Serial { + pub fn power_up(intf: Rc>) -> Self { + Self { + _intf: intf, + data: 0x00, + control: 0x00, + } + } + + pub fn get(&self, a: u16) -> u8 { + match a { + 0xff01 => self.data, + 0xff02 => self.control, + _ => panic!("Only supports addresses 0xff01, 0xff02"), + } + } + + pub fn set(&mut self, a: u16, v: u8) { + match a { + 0xff01 => self.data = v, + 0xff02 => self.control = v, + _ => panic!("Only supports addresses 0xff01, 0xff02"), + }; + } +} diff --git a/src/timer.rs b/src/timer.rs new file mode 100644 index 0000000000..39da2cdbb8 --- /dev/null +++ b/src/timer.rs @@ -0,0 +1,106 @@ +// Sometimes it's useful to have a timer that interrupts at regular intervals for routines that require periodic or +// percise updates. The timer in the GameBoy has a selectable frequency of 4096, 16384, 65536, or 262144 Hertz. +// This frequency increments the Timer Counter (TIMA). When it overflows, it generates an interrupt. It is then loaded +// with the contents of Timer Modulo (TMA). +// +// See: http://gbdev.gg8.se/wiki/articles/Timer_and_Divider_Registers +use super::clock::Clock; +use super::intf::{Flag, Intf}; +use std::cell::RefCell; +use std::rc::Rc; + +#[derive(Default)] +struct Register { + // This register is incremented at rate of 16384Hz (~16779Hz on SGB). Writing any value to this register resets it + // to 00h. + // Note: The divider is affected by CGB double speed mode, and will increment at 32768Hz in double speed. + div: u8, + // This timer is incremented by a clock frequency specified by the TAC register ($FF07). When the value overflows + // (gets bigger than FFh) then it will be reset to the value specified in TMA (FF06), and an interrupt will be + // requested, as described below. + tima: u8, + // When the TIMA overflows, this data will be loaded. + tma: u8, + // Bit 2 - Timer Enable + // Bits 1-0 - Input Clock Select + // 00: CPU Clock / 1024 (DMG, CGB: 4096 Hz, SGB: ~4194 Hz) + // 01: CPU Clock / 16 (DMG, CGB: 262144 Hz, SGB: ~268400 Hz) + // 10: CPU Clock / 64 (DMG, CGB: 65536 Hz, SGB: ~67110 Hz) + // 11: CPU Clock / 256 (DMG, CGB: 16384 Hz, SGB: ~16780 Hz) + tac: u8, +} + +// Each time when the timer overflows (ie. when TIMA gets bigger than FFh), then an interrupt is requested by +// setting Bit 2 in the IF Register (FF0F). When that interrupt is enabled, then the CPU will execute it by calling +// the timer interrupt vector at 0050h. +pub struct Timer { + intf: Rc>, + reg: Register, + div_clock: Clock, + tma_clock: Clock, +} + +impl Timer { + pub fn power_up(intf: Rc>) -> Self { + Timer { + intf, + reg: Register::default(), + div_clock: Clock::power_up(256), + tma_clock: Clock::power_up(1024), + } + } + + pub fn get(&self, a: u16) -> u8 { + match a { + 0xff04 => self.reg.div, + 0xff05 => self.reg.tima, + 0xff06 => self.reg.tma, + 0xff07 => self.reg.tac, + _ => panic!("Unsupported address"), + } + } + + pub fn set(&mut self, a: u16, v: u8) { + match a { + 0xff04 => { + self.reg.div = 0x00; + self.div_clock.n = 0x00; + } + 0xff05 => self.reg.tima = v, + 0xff06 => self.reg.tma = v, + 0xff07 => { + if (self.reg.tac & 0x03) != (v & 0x03) { + self.tma_clock.n = 0x00; + self.tma_clock.period = match v & 0x03 { + 0x00 => 1024, + 0x01 => 16, + 0x02 => 64, + 0x03 => 256, + _ => panic!(""), + }; + self.reg.tima = self.reg.tma; + } + self.reg.tac = v; + } + _ => panic!("Unsupported address"), + } + } + + pub fn next(&mut self, cycles: u32) { + // Increment div at rate of 16384Hz. Because the clock cycles is 4194304, so div increment every 256 cycles. + self.reg.div = self.reg.div.wrapping_add(self.div_clock.next(cycles) as u8); + + // Increment tima at rate of Clock / freq + // Timer Enable + if (self.reg.tac & 0x04) != 0x00 { + let n = self.tma_clock.next(cycles); + for _ in 0..n { + self.reg.tima = self.reg.tima.wrapping_add(1); + if self.reg.tima == 0x00 { + self.reg.tima = self.reg.tma; + self.intf.borrow_mut().hi(Flag::Timer); + } + } + } + } +} diff --git a/start.sh b/start.sh index 8fbdb3af7e..78bca7743e 100755 --- a/start.sh +++ b/start.sh @@ -27,10 +27,11 @@ export DEKU_DEFAULT_BLOCK_SIZE=${DEKU_DEFAULT_BLOCK_SIZE:-10000} export DEKU_LOG_VERBOSITY=${DEKU_LOG_VERBOSITY:-info} export DEKU_API_LOG_VERBOSITY=${DEKU_API_LOG_VERBOSITY:-info} +export DEKU_TWITCH_ORACLE_ADDRESS="tz1cTyRNTn3c83gKkrGXKtYWTeVfKaxxt8s5" # Starting only one API node export DEKU_API_NODE_URI="127.0.0.1:4440" export DEKU_API_PORT=8080 -export DEKU_API_DATABASE_URI="sqlite3:/tmp/api_database.db" +export DEKU_API_DATABASE_URI="sqlite3:./flextesa_chain/dpp_api_database.db" export DEKU_API_DOMAINS=8 export DEKU_API_VM="./flextesa_chain/data/0/api_vm_pipe" export DEKU_API_DATA_FOLDER="./flextesa_chain/data/0/" @@ -53,11 +54,11 @@ start_node() { # Starts the Node _build/install/default/bin/deku-node \ - --default-block-size=10000 \ + --default-block-size=10 \ --port "444$N" \ --database-uri "sqlite3:./flextesa_chain/data/$N/database.db" \ --data-folder "./flextesa_chain/data/$N" \ - --color=always 2> >(awk "{ print \"$N: \" \$0 }" >&2) & + --color=always 2> >(awk "{ print \"$N: \" \$0 }" >&2) 2>&1 | tee "logs_$N" & sleep 0.1 } @@ -67,7 +68,7 @@ then ## The api needs its own vm _build/install/default/bin/deku-api \ - --color=always 2> >(awk "{ print \"A: \" \$0 }" >&2) & + --color=always 2> >(awk "{ print \"A: \" \$0 }" >&2) 2>&1 | tee "logs_api" & for N in 0 1 2 3; do start_node $N diff --git a/start_bot.sh b/start_bot.sh new file mode 100755 index 0000000000..63738cc6f6 --- /dev/null +++ b/start_bot.sh @@ -0,0 +1,3 @@ +#!/usr/bin/env bash +yarn build:client +yarn --cwd ./examples/deku-plays-pokemon-bot start | tee "logs_bot" & diff --git a/start_gb.sh b/start_gb.sh new file mode 100755 index 0000000000..81c3874e0b --- /dev/null +++ b/start_gb.sh @@ -0,0 +1,3 @@ +#!/usr/bin/env bash + +cargo run --release -- ./networks/betanets/dpp/data.gb -t 2 -a 2>&1 | tee "logs_gb" diff --git a/wallet.json b/wallet.json new file mode 100644 index 0000000000..c2b093c60c --- /dev/null +++ b/wallet.json @@ -0,0 +1,4 @@ +{ + "address": "tz1QuWpetnA1FXBqPu59mNJjFinZwjCG7xZe", + "priv_key": "edsk2zF1fvNPm4yrF7kvqJXZqCkPZquQS3x89bqKASAHwRLfMzB2Y2" +} diff --git a/yarn.lock b/yarn.lock index f1ab36d03b..c31066e8c8 100644 --- a/yarn.lock +++ b/yarn.lock @@ -4046,6 +4046,16 @@ npmlog "^6.0.2" write-file-atomic "^4.0.1" +"@marigold-dev/deku@0.1.2": + version "0.1.2" + resolved "https://registry.npmjs.org/@marigold-dev/deku/-/deku-0.1.2.tgz" + integrity sha512-rdYiz6nVPx15jkrfhat6+EGRVWAkwxZgIZOvZDEIeqDdUnJSz4x2ORoix3q9GgGGp/nMnAauzeuyvxJ8+CWNCQ== + dependencies: + "@taquito/taquito" "^13.0.1" + "@tzstamp/helpers" "^0.3.4" + blakejs "^1.2.1" + bs58check "^2.1.2" + "@mdx-js/mdx@^1.6.22": version "1.6.22" resolved "https://registry.npmjs.org/@mdx-js/mdx/-/mdx-1.6.22.tgz" @@ -5588,6 +5598,11 @@ resolved "https://registry.npmjs.org/@types/node/-/node-17.0.45.tgz" integrity sha512-w+tIMs3rq2afQdsPJlODhoUEKzFP1ayaoyl1CcnwtIlsVe7K7bA1NGm4s3PraqTLlXnbIN84zuBlxBWo1u9BLw== +"@types/node@^18.11.17": + version "18.11.17" + resolved "https://registry.yarnpkg.com/@types/node/-/node-18.11.17.tgz#5c009e1d9c38f4a2a9d45c0b0c493fe6cdb4bcb5" + integrity sha512-HJSUJmni4BeDHhfzn6nF0sVmd1SMezP7/4F0Lq+aXzmp2xm9O7WXrUtHW/CHlYVtZUbByEvWidHqRtcJXGF2Ng== + "@types/normalize-package-data@^2.4.0": version "2.4.1" resolved "https://registry.npmjs.org/@types/normalize-package-data/-/normalize-package-data-2.4.1.tgz" @@ -5749,6 +5764,11 @@ dependencies: "@types/jest" "*" +"@types/tmi.js@^1.8.2": + version "1.8.2" + resolved "https://registry.yarnpkg.com/@types/tmi.js/-/tmi.js-1.8.2.tgz#7345399a4b74c39b9baa98540a397e24fa3a9e5c" + integrity sha512-meCZPCqs8zuCvw8Wl+0UXy2D6L/31Vskc99B/dZeQtIZvzX3EEzWSLTmRRxwI36m5hQFi3xjoLcnmEL8hBPqHA== + "@types/trusted-types@^2.0.2": version "2.0.2" resolved "https://registry.npmjs.org/@types/trusted-types/-/trusted-types-2.0.2.tgz" @@ -5966,7 +5986,7 @@ "@typescript-eslint/utils@5.40.1": version "5.40.1" - resolved "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-5.40.1.tgz" + resolved "https://registry.yarnpkg.com/@typescript-eslint/utils/-/utils-5.40.1.tgz#3204fb73a559d3b7bab7dc9d3c44487c2734a9ca" integrity sha512-a2TAVScoX9fjryNrW6BZRnreDUszxqm9eQ9Esv8n5nXApMW0zeANUYlwh/DED04SC/ifuBvXgZpIK5xeJHQ3aw== dependencies: "@types/json-schema" "^7.0.9" @@ -12519,7 +12539,7 @@ jsdom@^16.6.0: jsesc@^2.5.1: version "2.5.2" - resolved "https://registry.npmjs.org/jsesc/-/jsesc-2.5.2.tgz" + resolved "https://registry.yarnpkg.com/jsesc/-/jsesc-2.5.2.tgz#80564d2e483dacf6e8ef209650a67df3f0c283a4" integrity sha512-OYu7XEzjkCQ3C5Ps3QIZsQfNpqoJyZZA99wd9aWd05NCtC5pWOkShK2mkL6HXQR6/Cy2lbNdPlZBpuQHXE63gA== jsesc@~0.5.0: @@ -17410,6 +17430,14 @@ tiny-warning@^1.0.0: resolved "https://registry.npmjs.org/tiny-warning/-/tiny-warning-1.0.3.tgz" integrity sha512-lBN9zLN/oAf68o3zNXYrdCt1kP8WsiGW8Oo2ka41b2IM5JL/S1CTyX1rW0mb/zSuJun0ZUrDxx4sqvYS2FWzPA== +tmi.js@^1.8.5: + version "1.8.5" + resolved "https://registry.npmjs.org/tmi.js/-/tmi.js-1.8.5.tgz" + integrity sha512-A9qrydfe1e0VWM9MViVhhxVgvLpnk7pFShVUWePsSTtoi+A1X+Zjdoa7OJd7/YsgHXGj3GkNEvnWop/1WwZuew== + dependencies: + node-fetch "^2.6.1" + ws "^8.2.0" + tmp@^0.0.33: version "0.0.33" resolved "https://registry.npmjs.org/tmp/-/tmp-0.0.33.tgz" @@ -18794,6 +18822,11 @@ ws@^7.3.1, ws@^7.4.6: resolved "https://registry.npmjs.org/ws/-/ws-7.5.9.tgz" integrity sha512-F+P9Jil7UiSKSkppIiD94dN07AwvFixvLIj1Og1Rl9GGMuNipJnV9JzjD6XuqmAeiswGvUmNLjr5cFuXwNS77Q== +ws@^8.2.0: + version "8.11.0" + resolved "https://registry.npmjs.org/ws/-/ws-8.11.0.tgz" + integrity sha512-HPG3wQd9sNQoT9xHyNCXoDUa+Xw/VevmY9FoHyQ+g+rrMn4j6FB4np7Z0OhdTgjx6MgQLK7jwSy1YecU1+4Asg== + xdg-basedir@^4.0.0: version "4.0.0" resolved "https://registry.npmjs.org/xdg-basedir/-/xdg-basedir-4.0.0.tgz"