├── .github └── workflows │ └── rust.yml ├── .gitignore ├── Cargo.lock ├── Cargo.toml ├── LICENSE ├── README.md ├── builtins.md ├── changelog.md ├── compiler ├── Cargo.toml └── src │ ├── builtins.rs │ ├── compiler.rs │ ├── compiler_types.rs │ ├── context.rs │ ├── globals.rs │ ├── leveldata.rs │ ├── lib.rs │ ├── parse_levelstring.rs │ ├── value.rs │ └── value_storage.rs ├── default.nix ├── docgen ├── Cargo.toml └── src │ ├── documentation.rs │ └── lib.rs ├── editorlive ├── Cargo.toml └── src │ ├── editorlive_mac.rs │ ├── editorlive_unavailable.rs │ ├── editorlive_win.rs │ └── lib.rs ├── errors ├── Cargo.toml └── src │ ├── compiler_info.rs │ └── lib.rs ├── levelstring ├── Cargo.toml └── src │ └── lib.rs ├── libraries ├── gamescene │ └── lib.spwn └── std │ ├── array.spwn │ ├── block.spwn │ ├── chroma.spwn │ ├── chroma │ └── colors.spwn │ ├── color.spwn │ ├── constants.spwn │ ├── control_flow.spwn │ ├── counter.spwn │ ├── dictionary.spwn │ ├── events.spwn │ ├── fileio.spwn │ ├── frames.spwn │ ├── general_triggers.spwn │ ├── group.spwn │ ├── http.spwn │ ├── item.spwn │ ├── level_info.spwn │ ├── lib.spwn │ ├── log.spwn │ ├── macro.spwn │ ├── number.spwn │ ├── object.spwn │ ├── path.spwn │ ├── range.spwn │ ├── regex.spwn │ ├── set.spwn │ ├── string.spwn │ ├── util.spwn │ └── vector.spwn ├── optimizer ├── Cargo.toml └── src │ ├── dead_code.rs │ ├── group_toggling.rs │ ├── lib.rs │ ├── optimize.rs │ ├── spawn_optimisation.rs │ └── trigger_dedup.rs ├── parser ├── Cargo.toml └── src │ ├── ast.rs │ ├── fmt.rs │ ├── lib.rs │ └── parser.rs ├── pckp ├── Cargo.lock ├── Cargo.toml └── src │ ├── config_file.rs │ ├── download.rs │ ├── error.rs │ ├── lib.rs │ ├── package.rs │ └── version.rs ├── shared ├── Cargo.toml └── src │ └── lib.rs ├── spwn-web ├── Cargo.toml ├── index.html └── src │ └── lib.rs ├── spwn ├── Cargo.lock ├── Cargo.toml ├── benches │ └── time.rs ├── src │ ├── lib.rs │ ├── main.rs │ └── tests.rs └── wix │ ├── commands.sh │ ├── generate_wix_lib_file.py │ ├── libraries.wxs │ └── main.wxs └── test ├── bf ├── bf_interpreter.spwn ├── bfold.spwn ├── brainfugd_pro.spwn ├── brainfugd_tutorial.txt ├── brainfugd_video_example.spwn ├── text_display.spwn └── text_display_pro.spwn ├── binaryCalculator.spwn ├── bugtest.spwn ├── collection.spwn ├── inclrange.spwn ├── jsontest.json ├── level_reading ├── draw.spwn └── main.spwn ├── number_literals_max.spwn ├── orthographic_rotation ├── full_cube.spwn ├── index.spwn ├── message.txt ├── reference_grid.spwn └── voxel.spwn ├── path_test.spwn ├── pckp ├── main.spwn └── pckp.yaml ├── pckup.spwn ├── physics.spwn ├── readfiletest.spwn ├── readfiletest.txt ├── set ├── graphs.spwn └── tile.spwn ├── sync_group_idea.spwn ├── test.spwn ├── test2.spwn ├── tomltest.toml ├── unsafe └── main.spwn ├── vector └── main.spwn └── yamltest.yaml /.github/workflows/rust.yml: -------------------------------------------------------------------------------- 1 | name: Rust 2 | 3 | on: 4 | push: 5 | branches: [master] 6 | pull_request: 7 | branches: [master] 8 | 9 | env: 10 | CARGO_TERM_COLOR: always 11 | 12 | jobs: 13 | build-ubuntu: 14 | runs-on: ubuntu-latest 15 | 16 | steps: 17 | - uses: actions/checkout@v2 18 | - name: Build 19 | run: cargo build --release --verbose 20 | - name: Run tests 21 | run: cargo test --verbose --profile test 22 | # - name: Install rust WASM 23 | # run: rustup target add wasm32-unknown-unknown 24 | # - name: Test if the code will build to a WASM binary 25 | # run: cargo check --target wasm32-unknown-unknown 26 | 27 | upload-ubuntu: 28 | runs-on: ubuntu-latest 29 | 30 | steps: 31 | - uses: actions/checkout@v2 32 | 33 | - run: mkdir -p build_artifacts 34 | - run: cp -r libraries build_artifacts 35 | - run: cargo build --release 36 | - run: mv target/release/spwn build_artifacts 37 | 38 | - uses: actions/upload-artifact@v2 39 | with: 40 | name: ubuntu-linux-artifacts 41 | path: build_artifacts 42 | 43 | # upload-wasm: 44 | # runs-on: ubuntu-latest 45 | 46 | # steps: 47 | # - uses: actions/checkout@v2 48 | 49 | # - run: mkdir -p build_artifacts 50 | # - run: cp -r libraries build_artifacts 51 | # - run: rustup target add wasm32-unknown-unknown 52 | # - run: cargo build --target wasm32-unknown-unknown 53 | # - run: mv target/wasm32-unknown-unknown/release/spwn build_artifacts 54 | 55 | # - uses: actions/upload-artifact@v2 56 | # with: 57 | # name: wasm32-artifacts 58 | # path: build_artifacts 59 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | target 2 | .vscode 3 | **/*.rs.bk 4 | .DS_Store 5 | std-docs 6 | gamescene-docs 7 | spwn-web/pkg 8 | spwn-web/ansi_up.js 9 | *.sublime-workspace 10 | pckp_libraries 11 | .pckp_tmp 12 | test/flow_level 13 | result/ 14 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [workspace] 2 | members = [ 3 | "spwn", 4 | "parser", 5 | "shared", 6 | "errors", 7 | "levelstring", 8 | "editorlive", 9 | "compiler", 10 | "optimizer", 11 | "docgen", 12 | "spwn-web", 13 | "pckp" 14 | ] 15 | 16 | [profile.dev] 17 | debug-assertions = true 18 | opt-level = 2 19 | 20 | [profile.test] 21 | debug-assertions = false 22 | opt-level = 2 -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright 2022 Spu7Nix 2 | 3 | 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: 4 | 5 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 6 | 7 | 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. 8 | 9 | -------------------------------------------------------------------------------- /changelog.md: -------------------------------------------------------------------------------- 1 | # Current Version: 0.8 beta 2 | 3 | ## Breaking Changes 4 | 5 | - Keyword `switch` is now `match` 6 | - Keyword `case` is removed (in favor of `==` patterns) 7 | - Unary `..` operator is no longer allowed (for example `..10` now needs to be written as `0..10`) 8 | - The `has` operator has been replaced with `in` (with the order flipped) 9 | 10 | ## New Features 11 | 12 | - Return types for macros with `(arguments) -> return_type { ... }` syntax 13 | - You can spread an array in another array with `..` syntax: 14 | - `spwn eval` subcommand for running code in the console 15 | 16 | ```rs 17 | b = [3, 4] 18 | $.assert([1, 2, ..b, 5] == [1, 2, 3, 4, 5]) 19 | ``` 20 | 21 | - Unzip arrays with `*[...]` syntax: 22 | 23 | ```rs 24 | a = [1, "a"] 25 | b = [2, "b"] 26 | c = [3, "c"] 27 | $.assert([\*[a, b, c]] == [[1, 2, 3], ["a", "b", "c"]]) 28 | ``` 29 | 30 | - Destructuring additions, including dictionary destruction, and destruction of the syntax mentioned above (`..` and `*[...]`) 31 | - **Big expansion to the pattern system**: 32 | - `is` operator for matching any value to a pattern (`10 is @number // true`) 33 | - using most boolean operators as unary operators now yield a pattern: 34 | - `==val` will match any value that equals `val` 35 | - `!=val` will match any value that does not equal `val` 36 | - `>val` will match any value that is greater than `val` 37 | - `=val` will match any value that is greater than or equal to `val` 39 | - `<=val` will match any value that is less than or equal to `val` 40 | - `in val` will match any value that is in `val` 41 | - `&` operator for combining two patterns, so that the resulting pattern requires the value to match both patterns 42 | - `_` pattern, which matches any value (wildcard) 43 | - pattern ternary operator, which returns a value if it matches a pattern, and otherwise returns the default value: 44 | ```rs 45 | 10 if is >5 else 0 // 10 46 | 4 if is >5 else 0 // 0 47 | ``` 48 | - You can now remove stuff from dictionaries with `dict.delete(key)` 49 | 50 | ## STD Library Features 51 | 52 | - `@chroma` type for color values, this type is now used in for example color triggers instead of RGB arguments 53 | - `level` global variable for reading the objects in the current level 54 | - `@log` and `@runtime_log` types for debug logging to the console or at runtime 55 | - `@set` type for making groups of objects read from the level that can be rotated, scaled, and pasted in the level 56 | - Changes and improvements to many existing types (see the [docs](https://spu7nix.net/spwn/#/std-docs/std-docs)) 57 | -------------------------------------------------------------------------------- /compiler/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "compiler" 3 | version = "0.0.8" 4 | edition = "2021" 5 | 6 | # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html 7 | 8 | [dependencies] 9 | text_io = "0.1.8" 10 | fancy-regex = "0.7.1" 11 | slyce = "0.3.1" 12 | 13 | 14 | internment = "0.5.4" 15 | ariadne = "0.1.3" # errors 16 | base64 = "0.13.0" 17 | serde = { version = "1.0.104", features = ["derive"] } 18 | serde_json = "1.0.48" 19 | serde_yaml = "0.8.19" 20 | toml = "0.5.7" 21 | itertools = "0.10.1" 22 | 23 | ahash = "0.7.6" 24 | distance = "0.4.0" 25 | 26 | include_dir = "0.6.2" 27 | 28 | parser = { path = "../parser" } 29 | shared = { path = "../shared" } 30 | errors = { path = "../errors" } 31 | 32 | slotmap = "1.0.6" 33 | 34 | 35 | [target.'cfg(not(target_arch = "wasm32"))'.dependencies] 36 | rand = "0.8.4" 37 | reqwest = { version = "0.11", features = ["blocking"] } -------------------------------------------------------------------------------- /compiler/src/lib.rs: -------------------------------------------------------------------------------- 1 | #[cfg(not(debug_assertions))] 2 | #[macro_use(include_dir)] 3 | extern crate include_dir; 4 | 5 | pub mod builtins; 6 | pub mod compiler; 7 | pub mod compiler_types; 8 | pub mod context; 9 | pub mod globals; 10 | pub mod leveldata; 11 | pub mod parse_levelstring; 12 | pub mod value; 13 | pub mod value_storage; 14 | 15 | pub const STD_PATH: &str = "std"; 16 | -------------------------------------------------------------------------------- /compiler/src/parse_levelstring.rs: -------------------------------------------------------------------------------- 1 | use crate::builtins::{Block, Group, Id, Item}; 2 | use crate::{builtins::Color, leveldata::ObjParam, value::Value}; 3 | use errors::RuntimeError; 4 | use parser::ast::ObjectMode; 5 | 6 | pub fn parse_levelstring(ls: &str) -> Result, RuntimeError> { 7 | let mut obj_strings = ls.split(';'); 8 | obj_strings.next(); // skip the header 9 | let mut objs = Vec::new(); 10 | for obj_string in obj_strings { 11 | if obj_string.is_empty() { 12 | continue; 13 | } 14 | let key_val = obj_string.split(',').collect::>(); 15 | let mut group_51 = false; 16 | 17 | { 18 | let mut key_val_iter = key_val.iter(); 19 | while let Some(key) = key_val_iter.next() { 20 | let val = key_val_iter.next().unwrap(); 21 | if *key == "52" && *val == "1" { 22 | group_51 = true; 23 | } 24 | } 25 | } 26 | 27 | let mut obj = Vec::new(); 28 | let mut obj_id = 0; 29 | 30 | let mut key_val_iter = key_val.iter(); 31 | 32 | while let Some(key) = key_val_iter.next() { 33 | let key = key.parse::().unwrap(); 34 | let val = key_val_iter.next().unwrap(); 35 | 36 | let prop = match key { 37 | 1 => { 38 | obj_id = val.parse::().unwrap(); 39 | ObjParam::Number(obj_id as f64) 40 | } 41 | 4 | 5 | 11 | 13 | 15 | 16 | 17 | 34 | 41 | 42 | 48 | 56 | 58 | 59 | 60 | 62 42 | | 64 | 65 | 66 | 67 | 70 | 81 | 86 | 87 | 89 | 93 | 94 | 96 | 98 | 104 | 100 43 | | 102 | 103 | 106 | 36 => ObjParam::Bool(val.trim() == "1"), 44 | 21 | 22 | 23 | 50 => ObjParam::Color(Color { 45 | id: Id::Specific(val.parse::().unwrap()), 46 | }), 47 | 31 | 43 | 44 | 49 => ObjParam::Text(val.to_string()), 48 | 71 => ObjParam::Group(Group { 49 | id: Id::Specific(val.parse::().unwrap()), 50 | }), 51 | 95 => ObjParam::Block(Block { 52 | id: Id::Specific(val.parse::().unwrap()), 53 | }), 54 | 55 | 57 => ObjParam::GroupList( 56 | val.split('.') 57 | .map(|g| Group { 58 | id: Id::Specific(g.parse::().unwrap()), 59 | }) 60 | .collect::>(), 61 | ), 62 | 80 => match obj_id { 63 | 1815 => ObjParam::Block(Block { 64 | id: Id::Specific(val.parse::().unwrap()), 65 | }), 66 | _ => ObjParam::Item(Item { 67 | id: Id::Specific(val.parse::().unwrap()), 68 | }), 69 | }, 70 | 51 => match obj_id { 71 | 1006 => { 72 | if group_51 { 73 | ObjParam::Group(Group { 74 | id: Id::Specific(val.parse::().unwrap()), 75 | }) 76 | } else { 77 | ObjParam::Color(Color { 78 | id: Id::Specific(val.parse::().unwrap()), 79 | }) 80 | } 81 | } 82 | 899 => ObjParam::Color(Color { 83 | id: Id::Specific(val.parse::().unwrap()), 84 | }), 85 | _ => ObjParam::Group(Group { 86 | id: Id::Specific(val.parse::().unwrap()), 87 | }), 88 | }, 89 | _ => ObjParam::Number(val.parse::().unwrap()), 90 | }; 91 | obj.push((key, prop)); 92 | } 93 | objs.push(Value::Obj(obj, ObjectMode::Object)); 94 | } 95 | Ok(objs) 96 | } 97 | -------------------------------------------------------------------------------- /default.nix: -------------------------------------------------------------------------------- 1 | { pkgs ? import {}, lib ? pkgs.lib }: 2 | 3 | { 4 | spwn = pkgs.rustPlatform.buildRustPackage { 5 | name = "spwn"; 6 | src = ./.; 7 | nativeBuildInputs = with pkgs; [ 8 | pkg-config 9 | ]; 10 | buildInputs = with pkgs; [ 11 | openssl 12 | ]; 13 | cargoLock = { 14 | lockFile = ./Cargo.lock; 15 | }; 16 | 17 | meta = with lib; { 18 | description = "A language for Geometry Dash triggers"; 19 | license = licenses.mit; 20 | }; 21 | }; 22 | } 23 | -------------------------------------------------------------------------------- /docgen/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "docgen" 3 | version = "0.0.8" 4 | edition = "2021" 5 | 6 | # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html 7 | 8 | [dependencies] 9 | 10 | internment = "0.5.4" 11 | shared = { path = "../shared" } 12 | errors = { path = "../errors" } 13 | compiler = { path = "../compiler" } 14 | 15 | ahash = "0.7.6" 16 | -------------------------------------------------------------------------------- /docgen/src/lib.rs: -------------------------------------------------------------------------------- 1 | pub mod documentation; 2 | -------------------------------------------------------------------------------- /editorlive/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "editorlive" 3 | version = "0.0.8" 4 | edition = "2018" 5 | 6 | # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html 7 | 8 | [dependencies] 9 | 10 | 11 | [target.'cfg(target_os = "macos")'.dependencies] 12 | core-foundation-sys = "0.8.2" 13 | libc = "0.2.81" 14 | 15 | [target.'cfg(target_os = "windows")'.dependencies] 16 | named_pipe = "0.4.1" 17 | -------------------------------------------------------------------------------- /editorlive/src/editorlive_mac.rs: -------------------------------------------------------------------------------- 1 | extern crate libc; 2 | 3 | use std::ptr::null; 4 | 5 | use core_foundation_sys::data as cfd; 6 | use core_foundation_sys::messageport as mp; 7 | use core_foundation_sys::string as cfst; 8 | 9 | fn new_port() -> Result { 10 | unsafe { 11 | let cfstr = cfst::CFStringCreateWithCString( 12 | null(), 13 | "314GDL\0".as_ptr() as *const libc::c_char, 14 | cfst::kCFStringEncodingUTF8, 15 | ); 16 | let port = mp::CFMessagePortCreateRemote(null(), cfstr); 17 | if port.is_null() || mp::CFMessagePortIsValid(port) == 0 { 18 | return Err("Could not make a connection to GD".to_string()); 19 | } 20 | Ok(port) 21 | } 22 | } 23 | fn create_data(value: &str) -> Result { 24 | unsafe { 25 | let cdr = cfd::CFDataCreate(null(), value.as_ptr() as *const u8, value.len() as isize); 26 | if cdr.is_null() { 27 | return Err("Could not create data".to_string()); 28 | } 29 | Ok(cdr) 30 | } 31 | } 32 | pub fn editor_paste(message: &str) -> Result { 33 | unsafe { 34 | let data = create_data(message); 35 | let port = new_port(); 36 | match (data.clone(), port) { 37 | (Err(a), _) | (_, Err(a)) => { 38 | return Err(a); 39 | } 40 | _ => { 41 | let port = new_port().unwrap(); 42 | let result = mp::CFMessagePortSendRequest( 43 | port, 44 | 0x1, 45 | data.unwrap(), 46 | 1.0, 47 | 10.0, 48 | null(), 49 | &mut create_data("").unwrap(), 50 | ); 51 | if result != 0 { 52 | return Err(format!("Could not send message, error code {}", result)); 53 | } 54 | 55 | Ok(true) 56 | } 57 | } 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /editorlive/src/editorlive_unavailable.rs: -------------------------------------------------------------------------------- 1 | pub fn editor_paste(_: &str) -> Result { 2 | Err(String::from( 3 | "Your device does not currently support live editing", 4 | )) 5 | } 6 | -------------------------------------------------------------------------------- /editorlive/src/editorlive_win.rs: -------------------------------------------------------------------------------- 1 | use named_pipe::PipeClient; 2 | use std::ffi::OsStr; 3 | use std::io::Write; 4 | use std::time::Duration; 5 | 6 | pub fn editor_paste(message: &str) -> Result { 7 | let pipe_name = OsStr::new("\\\\.\\pipe\\GDPipe"); 8 | 9 | match PipeClient::connect_ms(pipe_name, 5) { 10 | Ok(mut client) => { 11 | client.set_write_timeout(Some(Duration::new(1,0))); 12 | let split = message.split(';').collect::>(); 13 | for iter in split.chunks(2) { 14 | let mut data = iter.join(";").to_string(); 15 | 16 | if data.ends_with(';') { 17 | data.pop(); 18 | } 19 | match client.write(format!("{};",data).as_bytes()) { 20 | Ok(_) => (), 21 | Err(e) => { 22 | return Err(format!("Could not send a message to GD with this error: {:?}", e)); 23 | } 24 | }; 25 | } 26 | Ok(true) 27 | } 28 | Err(_) => Err("Could not make a connection to GD, try injecting the live editor library into geometry dash".to_string()) 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /editorlive/src/lib.rs: -------------------------------------------------------------------------------- 1 | #[cfg_attr(target_os = "macos", path = "editorlive_mac.rs")] 2 | #[cfg_attr(windows, path = "editorlive_win.rs")] 3 | #[cfg_attr( 4 | not(any(target_os = "macos", windows)), 5 | path = "editorlive_unavailable.rs" 6 | )] 7 | pub mod editorlive; 8 | -------------------------------------------------------------------------------- /errors/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "errors" 3 | version = "0.0.8" 4 | edition = "2018" 5 | 6 | # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html 7 | 8 | [dependencies] 9 | 10 | ariadne = "0.1.3" # errors 11 | internment = "0.5.4" 12 | 13 | shared = { path = "../shared" } 14 | -------------------------------------------------------------------------------- /errors/src/compiler_info.rs: -------------------------------------------------------------------------------- 1 | use internment::LocalIntern; 2 | use shared::FileRange; 3 | use std::path::PathBuf; 4 | #[derive(Debug, Clone, PartialEq, Eq)] 5 | pub struct CompilerInfo { 6 | pub depth: u8, 7 | pub call_stack: Vec, 8 | pub current_module: String, // empty string means script 9 | pub position: CodeArea, 10 | } 11 | 12 | impl CompilerInfo { 13 | pub fn new() -> Self { 14 | CompilerInfo { 15 | depth: 0, 16 | call_stack: Vec::new(), 17 | 18 | current_module: String::new(), 19 | position: CodeArea::new(), 20 | } 21 | } 22 | 23 | pub fn from_area(a: CodeArea) -> Self { 24 | CompilerInfo { 25 | position: a, 26 | ..Self::new() 27 | } 28 | } 29 | 30 | pub fn with_area(self, a: CodeArea) -> Self { 31 | CompilerInfo { 32 | position: a, 33 | ..self 34 | } 35 | } 36 | 37 | pub fn add_to_call_stack(&mut self, new: CodeArea) { 38 | self.call_stack.push(self.position); 39 | self.position = new; 40 | } 41 | } 42 | 43 | impl Default for CompilerInfo { 44 | fn default() -> Self { 45 | Self::new() 46 | } 47 | } 48 | 49 | #[derive(Debug, Clone, Copy, PartialEq, Eq)] 50 | pub struct CodeArea { 51 | pub file: LocalIntern, 52 | pub pos: FileRange, 53 | } 54 | 55 | impl CodeArea { 56 | pub fn new() -> Self { 57 | CodeArea { 58 | file: LocalIntern::new(shared::SpwnSource::File(PathBuf::new())), 59 | pos: (0, 0), 60 | } 61 | } 62 | } 63 | 64 | impl Default for CodeArea { 65 | fn default() -> Self { 66 | Self::new() 67 | } 68 | } 69 | use ariadne::Span; 70 | 71 | impl Span for CodeArea { 72 | type SourceId = shared::SpwnSource; 73 | 74 | fn source(&self) -> &Self::SourceId { 75 | &self.file 76 | } 77 | fn start(&self) -> usize { 78 | self.pos.0 79 | } 80 | fn end(&self) -> usize { 81 | self.pos.1 82 | } 83 | } 84 | -------------------------------------------------------------------------------- /levelstring/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "levelstring" 3 | version = "0.0.8" 4 | edition = "2021" 5 | 6 | # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html 7 | 8 | [dependencies] 9 | 10 | quick-xml = "0.20.0" 11 | base64 = "0.13.0" 12 | libflate = "1.1.0" 13 | crc32fast = "1.2.0" 14 | 15 | aes = "0.7.4" 16 | block-modes = "0.8.1" 17 | -------------------------------------------------------------------------------- /libraries/gamescene/lib.spwn: -------------------------------------------------------------------------------- 1 | extract obj_props 2 | 3 | // Triggers and groups 4 | follow_x_group = ?g 5 | follow_y_group = ?g 6 | hidden_group = ?g 7 | 8 | -> hidden_group.alpha(0) 9 | -> follow_x_group.lock_to_player(lock_x = true, lock_y = false) 10 | -> follow_x_group.move(x = 0, y = 5, duration = 0.01) 11 | -> follow_y_group.follow_player_y() 12 | -> hide_player() 13 | 14 | // Portals 15 | $.add(obj { 16 | OBJ_ID: obj_ids.portals.DUAL_ON, 17 | X: 0, 18 | Y: 30, 19 | GROUPS: hidden_group, 20 | }) 21 | $.add(obj { 22 | OBJ_ID: obj_ids.portals.WAVE, 23 | X: 0, 24 | Y: 30, 25 | GROUPS: hidden_group, 26 | }) 27 | $.add(obj { 28 | OBJ_ID: obj_ids.portals.SIZE_MINI, 29 | X: 0, 30 | Y: 30, 31 | GROUPS: hidden_group, 32 | }) 33 | 34 | // Top and bottom blocks 35 | $.add(obj { 36 | OBJ_ID: 1, 37 | X: 0, 38 | Y: 33, 39 | GROUPS: [ hidden_group, follow_x_group ], 40 | }) 41 | $.add(obj { 42 | OBJ_ID: 1, 43 | X: 0, 44 | Y: -12, 45 | GROUPS: [ hidden_group, follow_x_group ], 46 | }) 47 | 48 | 49 | // Collision blocks 50 | player_block = ?b 51 | collide_block = ?b 52 | 53 | $.add(obj { 54 | OBJ_ID: obj_ids.special.COLLISION_BLOCK, 55 | DYNAMIC_BLOCK: true, 56 | BLOCK_A: player_block, 57 | X: 0, 58 | Y: 0, 59 | GROUPS: [ hidden_group, follow_x_group, follow_y_group ], 60 | }) 61 | $.add(obj { 62 | OBJ_ID: obj_ids.special.COLLISION_BLOCK, 63 | DYNAMIC_BLOCK: false, 64 | BLOCK_A: collide_block, 65 | X: 0, 66 | Y: 37, 67 | GROUPS: [ hidden_group, follow_x_group ], 68 | }) 69 | 70 | // D block 71 | $.add(obj { 72 | OBJ_ID: obj_ids.special.D_BLOCK, 73 | SCALING: 2, 74 | X: 0, 75 | Y: 15, 76 | GROUPS: [ hidden_group, follow_x_group ], 77 | }) 78 | 79 | return { 80 | button_a: #[desc("Returns an event for when button A is pressed (the right side by default)")] () { 81 | return collision(player_block, collide_block) 82 | }, 83 | button_b: #[desc("Returns an event for when button B is pressed (the left side by default)")] () { 84 | return touch(dual_side = true) 85 | }, 86 | button_a_end: #[desc("Returns an event for when button A is released (the right side by default)")] () { 87 | return collision_exit(player_block, collide_block) 88 | }, 89 | button_b_end: #[desc("Returns an event for when button B is released (the left side by default)")] () { 90 | return touch_end(dual_side = true) 91 | }, 92 | hidden_group: #[desc("A group that is hidden (alpha = 0)")] hidden_group, 93 | } 94 | 95 | -------------------------------------------------------------------------------- /libraries/std/block.spwn: -------------------------------------------------------------------------------- 1 | #[no_std, cache_output] 2 | events = import "events.spwn" 3 | 4 | extract import "util.spwn" 5 | impl @block { 6 | 7 | _range_: create_range_macro(@block), 8 | create_tracker_item: #[desc("Returns an item ID that is 1 when the blocks are colliding and 0 when they are not"), example(u" 9 | // in some minigame 10 | type @player 11 | player = @player::{ block: 1b, group: 1g, jump: () {}} 12 | ground = 2b 13 | on_ground = counter(player.block.create_tracker_item(ground)) 14 | on(touch(), !{ 15 | //jump 16 | if on_ground == 1 { // player can only jump while touching the ground 17 | player.jump() 18 | } 19 | }) 20 | ")] 21 | ( 22 | self, 23 | #[desc("Block ID to check against")] other: @block 24 | ) -> @item { 25 | item = ?i 26 | events.collision(self, other).on(!{ 27 | item.add(1) 28 | }) 29 | 30 | events.collision_exit(self, other).on(!{ 31 | item.add(-1) 32 | }) 33 | 34 | return item 35 | }, 36 | } 37 | -------------------------------------------------------------------------------- /libraries/std/chroma/colors.spwn: -------------------------------------------------------------------------------- 1 | #[no_std, cache_output] 2 | 3 | impl @chroma { 4 | ALICE_BLUE: @chroma::from_rgb8(240, 248, 255), 5 | ANTIQUE_WHITE: @chroma::from_rgb8(250, 235, 215), 6 | AQUA: @chroma::from_rgb8(0, 255, 255), 7 | AQUA_MARINE: @chroma::from_rgb8(127, 255, 212), 8 | AZURE: @chroma::from_rgb8(240, 255, 255), 9 | BEIGE: @chroma::from_rgb8(245, 245, 220), 10 | BISQUE: @chroma::from_rgb8(255, 228, 196), 11 | BLACK: @chroma::from_rgb8(0, 0, 0), 12 | BLANCHED_ALMOND: @chroma::from_rgb8(255, 235, 205), 13 | BLUE: @chroma::from_rgb8(0, 0, 255), 14 | BLUE_VIOLET: @chroma::from_rgb8(138, 43, 226), 15 | BROWN: @chroma::from_rgb8(165, 42, 42), 16 | BURLY_WOOD: @chroma::from_rgb8(222, 184, 135), 17 | CADET_BLUE: @chroma::from_rgb8(95, 158, 160), 18 | CHARTREUSE: @chroma::from_rgb8(127, 255, 0), 19 | CHOCOLATE: @chroma::from_rgb8(210, 105, 30), 20 | CORAL: @chroma::from_rgb8(255, 127, 80), 21 | CORNFLOWER_BLUE: @chroma::from_rgb8(100, 149, 237), 22 | CORNSILK: @chroma::from_rgb8(255, 248, 220), 23 | CRIMSON: @chroma::from_rgb8(220, 20, 60), 24 | CYAN: @chroma::from_rgb8(0, 255, 255), 25 | DARK_BLUE: @chroma::from_rgb8(0, 0, 139), 26 | DARK_CYAN: @chroma::from_rgb8(0, 139, 139), 27 | DARK_GOLDEN_ROD: @chroma::from_rgb8(184, 134, 11), 28 | DARK_GRAY: @chroma::from_rgb8(169, 169, 169), 29 | DARK_GREY: @chroma::from_rgb8(169, 169, 169), 30 | DARK_GREEN: @chroma::from_rgb8(0, 100, 0), 31 | DARK_KHAKI: @chroma::from_rgb8(189, 183, 107), 32 | DARK_MAGENTA: @chroma::from_rgb8(139, 0, 139), 33 | DARK_OLIVE_GREEN: @chroma::from_rgb8(85, 107, 47), 34 | DARK_ORANGE: @chroma::from_rgb8(255, 140, 0), 35 | DARK_ORCHID: @chroma::from_rgb8(153, 50, 204), 36 | DARK_RED: @chroma::from_rgb8(139, 0, 0), 37 | DARK_SALMON: @chroma::from_rgb8(233, 150, 122), 38 | DARK_SEA_GREEN: @chroma::from_rgb8(143, 188, 139), 39 | DARK_SLATE_BLUE: @chroma::from_rgb8(72, 61, 139), 40 | DARK_SLATE_GRAY: @chroma::from_rgb8(47, 79, 79), 41 | DARK_SLATE_GREY: @chroma::from_rgb8(47, 79, 79), 42 | DARK_TURQUOISE: @chroma::from_rgb8(0, 206, 209), 43 | DARK_VIOLET: @chroma::from_rgb8(148, 0, 211), 44 | DEEP_PINK: @chroma::from_rgb8(255, 20, 147), 45 | DEEP_SKY_BLUE: @chroma::from_rgb8(0, 191, 255), 46 | DIM_GRAY: @chroma::from_rgb8(105, 105, 105), 47 | DIM_GREY: @chroma::from_rgb8(105, 105, 105), 48 | DODGER_BLUE: @chroma::from_rgb8(30, 144, 255), 49 | FIRE_BRICK: @chroma::from_rgb8(178, 34, 34), 50 | FLORAL_WHITE: @chroma::from_rgb8(255, 250, 240), 51 | FOREST_GREEN: @chroma::from_rgb8(34, 139, 34), 52 | FUCHSIA: @chroma::from_rgb8(255, 0, 255), 53 | GAINSBORO: @chroma::from_rgb8(220, 220, 220), 54 | GHOST_WHITE: @chroma::from_rgb8(248, 248, 255), 55 | GOLD: @chroma::from_rgb8(255, 215, 0), 56 | GOLDEN_ROD: @chroma::from_rgb8(218, 165, 32), 57 | GRAY: @chroma::from_rgb8(128, 128, 128), 58 | GREY: @chroma::from_rgb8(128, 128, 128), 59 | GREEN: @chroma::from_rgb8(0, 128, 0), 60 | GREEN_YELLOW: @chroma::from_rgb8(173, 255, 47), 61 | HONEY_DEW: @chroma::from_rgb8(240, 255, 240), 62 | HOT_PINK: @chroma::from_rgb8(255, 105, 180), 63 | INDIAN_RED: @chroma::from_rgb8(205, 92, 92), 64 | INDIGO: @chroma::from_rgb8(75, 0, 130), 65 | IVORY: @chroma::from_rgb8(255, 255, 240), 66 | KHAKI: @chroma::from_rgb8(240, 230, 140), 67 | LAVENDER: @chroma::from_rgb8(230, 230, 250), 68 | LAVENDER_BLUSH: @chroma::from_rgb8(255, 240, 245), 69 | LAWN_GREEN: @chroma::from_rgb8(124, 252, 0), 70 | LEMON_CHIFFON: @chroma::from_rgb8(255, 250, 205), 71 | LIGHT_BLUE: @chroma::from_rgb8(173, 216, 230), 72 | LIGHT_CORAL: @chroma::from_rgb8(240, 128, 128), 73 | LIGHT_CYAN: @chroma::from_rgb8(224, 255, 255), 74 | LIGHT_GOLDEN_ROD_YELLOW: @chroma::from_rgb8(250, 250, 210), 75 | LIGHT_GRAY: @chroma::from_rgb8(211, 211, 211), 76 | LIGHT_GREY: @chroma::from_rgb8(211, 211, 211), 77 | LIGHT_GREEN: @chroma::from_rgb8(144, 238, 144), 78 | LIGHT_PINK: @chroma::from_rgb8(255, 182, 193), 79 | LIGHT_SALMON: @chroma::from_rgb8(255, 160, 122), 80 | LIGHT_SEA_GREEN: @chroma::from_rgb8(32, 178, 170), 81 | LIGHT_SKY_BLUE: @chroma::from_rgb8(135, 206, 250), 82 | LIGHT_SLATE_GRAY: @chroma::from_rgb8(119, 136, 153), 83 | LIGHT_SLATE_GREY: @chroma::from_rgb8(119, 136, 153), 84 | LIGHT_STEEL_BLUE: @chroma::from_rgb8(176, 196, 222), 85 | LIGHT_YELLOW: @chroma::from_rgb8(255, 255, 224), 86 | LIME: @chroma::from_rgb8(0, 255, 0), 87 | LIME_GREEN: @chroma::from_rgb8(50, 205, 50), 88 | LINEN: @chroma::from_rgb8(250, 240, 230), 89 | MAGENTA: @chroma::from_rgb8(255, 0, 255), 90 | MAROON: @chroma::from_rgb8(128, 0, 0), 91 | MEDIUM_AQUA_MARINE: @chroma::from_rgb8(102, 205, 170), 92 | MEDIUM_BLUE: @chroma::from_rgb8(0, 0, 205), 93 | MEDIUM_ORCHID: @chroma::from_rgb8(186, 85, 211), 94 | MEDIUM_PURPLE: @chroma::from_rgb8(147, 112, 219), 95 | MEDIUM_SEA_GREEN: @chroma::from_rgb8(60, 179, 113), 96 | MEDIUM_SLATE_BLUE: @chroma::from_rgb8(123, 104, 238), 97 | MEDIUM_SPRING_GREEN: @chroma::from_rgb8(0, 250, 154), 98 | MEDIUM_TURQUOISE: @chroma::from_rgb8(72, 209, 204), 99 | MEDIUM_VIOLET_RED: @chroma::from_rgb8(199, 21, 133), 100 | MIDNIGHT_BLUE: @chroma::from_rgb8(25, 25, 112), 101 | MINT_CREAM: @chroma::from_rgb8(245, 255, 250), 102 | MISTY_ROSE: @chroma::from_rgb8(255, 228, 225), 103 | MOCCASIN: @chroma::from_rgb8(255, 228, 181), 104 | NAVAJO_WHITE: @chroma::from_rgb8(255, 222, 173), 105 | NAVY: @chroma::from_rgb8(0, 0, 128), 106 | OLD_LACE: @chroma::from_rgb8(253, 245, 230), 107 | OLIVE: @chroma::from_rgb8(128, 128, 0), 108 | OLIVE_DRAB: @chroma::from_rgb8(107, 142, 35), 109 | ORANGE: @chroma::from_rgb8(255, 165, 0), 110 | ORANGE_RED: @chroma::from_rgb8(255, 69, 0), 111 | ORCHID: @chroma::from_rgb8(218, 112, 214), 112 | PALE_GOLDEN_ROD: @chroma::from_rgb8(238, 232, 170), 113 | PALE_GREEN: @chroma::from_rgb8(152, 251, 152), 114 | PALE_TURQUOISE: @chroma::from_rgb8(175, 238, 238), 115 | PALE_VIOLET_RED: @chroma::from_rgb8(219, 112, 147), 116 | PAPAYA_WHIP: @chroma::from_rgb8(255, 239, 213), 117 | PEACH_PUFF: @chroma::from_rgb8(255, 218, 185), 118 | PERU: @chroma::from_rgb8(205, 133, 63), 119 | PINK: @chroma::from_rgb8(255, 192, 203), 120 | PLUM: @chroma::from_rgb8(221, 160, 221), 121 | POWDER_BLUE: @chroma::from_rgb8(176, 224, 230), 122 | PURPLE: @chroma::from_rgb8(128, 0, 128), 123 | REBECCA_PURPLE: @chroma::from_rgb8(102, 51, 153), 124 | RED: @chroma::from_rgb8(255, 0, 0), 125 | ROSY_BROWN: @chroma::from_rgb8(188, 143, 143), 126 | ROYAL_BLUE: @chroma::from_rgb8(65, 105, 225), 127 | SADDLE_BROWN: @chroma::from_rgb8(139, 69, 19), 128 | SALMON: @chroma::from_rgb8(250, 128, 114), 129 | SANDY_BROWN: @chroma::from_rgb8(244, 164, 96), 130 | SEA_GREEN: @chroma::from_rgb8(46, 139, 87), 131 | SEA_SHELL: @chroma::from_rgb8(255, 245, 238), 132 | SIENNA: @chroma::from_rgb8(160, 82, 45), 133 | SILVER: @chroma::from_rgb8(192, 192, 192), 134 | SKY_BLUE: @chroma::from_rgb8(135, 206, 235), 135 | SLATE_BLUE: @chroma::from_rgb8(106, 90, 205), 136 | SLATE_GRAY: @chroma::from_rgb8(112, 128, 144), 137 | SLATE_GREY: @chroma::from_rgb8(112, 128, 144), 138 | SNOW: @chroma::from_rgb8(255, 250, 250), 139 | SPRING_GREEN: @chroma::from_rgb8(0, 255, 127), 140 | STEEL_BLUE: @chroma::from_rgb8(70, 130, 180), 141 | TAN: @chroma::from_rgb8(210, 180, 140), 142 | TEAL: @chroma::from_rgb8(0, 128, 128), 143 | THISTLE: @chroma::from_rgb8(216, 191, 216), 144 | TOMATO: @chroma::from_rgb8(255, 99, 71), 145 | TURQUOISE: @chroma::from_rgb8(64, 224, 208), 146 | VIOLET: @chroma::from_rgb8(238, 130, 238), 147 | WHEAT: @chroma::from_rgb8(245, 222, 179), 148 | WHITE: @chroma::from_rgb8(255, 255, 255), 149 | WHITE_SMOKE: @chroma::from_rgb8(245, 245, 245), 150 | YELLOW: @chroma::from_rgb8(255, 255, 0), 151 | YELLOW_GREEN: @chroma::from_rgb8(154, 205, 50), 152 | } 153 | -------------------------------------------------------------------------------- /libraries/std/color.spwn: -------------------------------------------------------------------------------- 1 | #[no_std, cache_output] 2 | 3 | constants = import "constants.spwn" 4 | chroma = import "chroma.spwn" 5 | 6 | extract constants.obj_props 7 | extract import "control_flow.spwn" 8 | extract import "util.spwn" 9 | impl @color { 10 | 11 | _range_: #[desc("Implementation of the range operator (`..`) for colors"), example(u" 12 | for color in 1c..10c { 13 | -> color.set(rgb(0,0,0), 0.5) 14 | } 15 | ")] create_range_macro(@color), 16 | 17 | set: 18 | #[desc("Implementation of the color trigger"), example(u" 19 | BG.set(rgb(0, 0, 0), 0.5) // turns the background color black 20 | ")] 21 | ( 22 | self, 23 | #[desc("Target chroma")] c: @chroma, 24 | #[desc("Duration of color change")] duration: @number = 0, 25 | #[desc("Toggle blending on target color")] blending: @bool = false 26 | ) -> @NULL { 27 | $.add( trigger{ 28 | OBJ_ID: 899, 29 | DURATION: duration, 30 | TRIGGER_RED: c.r8(), 31 | TRIGGER_GREEN: c.g8(), 32 | TRIGGER_BLUE: c.b8(), 33 | OPACITY: c.a(), 34 | BLENDING: blending, 35 | TARGET_COLOR: self, 36 | 36: 1, 37 | }) 38 | wait(duration) 39 | }, 40 | 41 | copy: 42 | #[desc("Makes a color channel copy another"), example(u" 43 | 1c.copy(2c, duration = 1) // transitions color 1 into copying color 2 over 1 second 44 | ")] 45 | ( 46 | self, 47 | #[desc("Target channel")] c: @color, 48 | #[desc("Duration of color change")] duration: @number = 0, 49 | #[desc("Copy color HVS")] hvs: @string = "0a1a1a0a0", 50 | #[desc("Toggle blending on target color")] blending: @bool = false, 51 | #[desc("Channel opacity")] opacity: @number = 1, 52 | #[desc("Copy target opacity")] copy_opacity: @bool = false, 53 | ) -> @NULL { 54 | $.add( trigger{ 55 | OBJ_ID: 899, 56 | DURATION: duration, 57 | COPIED_COLOR_ID: c, 58 | COPIED_COLOR_HVS: hvs, 59 | COPY_OPACITY: copy_opacity, 60 | OPACITY: opacity, 61 | BLENDING: blending, 62 | TARGET_COLOR: self, 63 | 36: 1, 64 | }) 65 | wait(duration) 66 | }, 67 | 68 | pulse_hsv: #[desc("Implementation of the pulse trigger for colors with hsv"), example(u" 69 | BG.pulse_hsv(180, 1, 1, fade_out = 0.5) // pulses the background with the complementary color 70 | ")] 71 | ( 72 | self, 73 | #[desc("Hue")] h: @number, 74 | #[desc("Saturation")] s: @number, 75 | #[desc("Brightness")] b: @number, 76 | #[desc("Saturation checked")] s_checked: @bool = false, 77 | #[desc("Brightness checked")] b_checked: @bool = false, 78 | #[desc("Fade-in duration")] fade_in: @number = 0, 79 | #[desc("Duration to hold the color")] hold: @number = 0, 80 | #[desc("Fade-out duration")] fade_out: @number = 0, 81 | #[desc("Whether to prioritize this pulse over simultaneous pulses")] exclusive: @bool = false, 82 | ) -> @NULL { 83 | $.add( trigger{ 84 | OBJ_ID: 1006, 85 | COPIED_COLOR_HVS: 86 | h as @string + "a" + s as @string + "a" + b as @string + "a" 87 | + s_checked as @number as @string + "a" + b_checked as @number as @string, 88 | EXCLUSIVE: exclusive, 89 | FADE_IN: fade_in, 90 | HOLD: hold, 91 | FADE_OUT: fade_out, 92 | TARGET: self, 93 | PULSE_HSV: true, 94 | }) 95 | wait(fade_in + hold + fade_out) 96 | }, 97 | 98 | pulse: #[desc("Implementation of the pulse trigger for colors"), example(u" 99 | BG.pulse(rgb8(255, 0, 0), fade_out = 0.5) // pulses the background red 100 | ")] 101 | ( 102 | self, 103 | #[desc("Chroma for pulse color")] c: @chroma, 104 | #[desc("Fade-in duration")] fade_in: @number = 0, 105 | #[desc("Duration to hold the color")] hold: @number = 0, 106 | #[desc("Fade-out duration")] fade_out: @number = 0, 107 | #[desc("Whether to prioritize this pulse over simultaneous pulses")] exclusive: @bool = false, 108 | ) -> @NULL { 109 | $.add( trigger{ 110 | OBJ_ID: 1006, 111 | TRIGGER_RED: c.r8(), 112 | TRIGGER_GREEN: c.g8(), 113 | TRIGGER_BLUE: c.b8(), 114 | EXCLUSIVE: exclusive, 115 | FADE_IN: fade_in, 116 | HOLD: hold, 117 | FADE_OUT: fade_out, 118 | TARGET: self, 119 | PULSE_HSV: false, 120 | }) 121 | wait(fade_in + hold + fade_out) 122 | }, 123 | } 124 | -------------------------------------------------------------------------------- /libraries/std/control_flow.spwn: -------------------------------------------------------------------------------- 1 | #[no_std, cache_output] 2 | 3 | constants = import "constants.spwn" 4 | 5 | extract constants.obj_props 6 | extract constants.comparisons 7 | 8 | wait = #[desc("Adds a delay before the next triggers"), example(u" 9 | BG.set(rgb8(255, 0, 0)) // turn background red 10 | wait(2) // wait 2 seconds 11 | BG.set(rgb8(0, 255, 0)) // turn background green 12 | ")] ( 13 | #[desc("Delay time in seconds (leave empty for minimum delay)")] 14 | time: @number | @epsilon = @epsilon::{} 15 | ) -> @NULL { 16 | if time.type == @epsilon || time > 0 { 17 | $.add( trigger{ 18 | OBJ_ID: 1268, 19 | SPAWN_DURATION: time, 20 | TARGET: !{ 21 | -> return 22 | }, 23 | }) 24 | } else { 25 | -> return 26 | } 27 | 28 | } 29 | 30 | call_with_delay = #[desc("Call a function after a delay"), example(u" 31 | BG.set(rgb8(255, 0, 0)) // turn background red 32 | call_with_delay(2, !{ 33 | BG.set(rgb8(0, 255, 0)) // turn background green 2 seconds later 34 | }) 35 | ")] ( 36 | #[desc("Delay time in seconds (leave empty for minimum delay)")] time: @number | @epsilon = @epsilon::{}, 37 | #[desc("Function to call after the delay")] function: @trigger_function | @group 38 | ) -> @NULL { 39 | $.add(trigger{ 40 | OBJ_ID: 1268, 41 | SPAWN_DURATION: time, 42 | TARGET: function, 43 | }) 44 | } 45 | 46 | -> return { 47 | wait: wait, 48 | call_with_delay: call_with_delay, 49 | 50 | 51 | suppress_signal: #[desc("Stops signal from coming past for some time"), example(u" 52 | f = !{ 53 | suppress_signal(1) 54 | 10g.move(10, 0) 55 | } 56 | 57 | f! // moves 58 | wait(0.4) 59 | f! // does nothing 60 | wait(0.4) 61 | f! // does nothing 62 | wait(0.4) 63 | f! // moves 64 | ")] ( 65 | #[desc("Time to suppress signal")] delay: @number 66 | ) -> @NULL { 67 | //if checker is 0, a signal can come through 68 | //if checker is 1, it will be suppressed 69 | checker = @counter::new(0, reset = false) 70 | -> (){ 71 | wait(delay) 72 | if checker.item == 1 { 73 | checker.item.add(-1) 74 | } 75 | }() 76 | if checker.item == 0 { 77 | checker.item.add(1) 78 | -> return 79 | } 80 | 81 | 82 | }, 83 | 84 | suppress_signal_forever: #[desc("Stops signal from coming past after call"), example(u" 85 | f = !{ 86 | suppress_signal_forever() 87 | 10g.move(10, 0) 88 | } 89 | f! // moves 90 | wait(0.4) 91 | f! // does nothing 92 | wait(1000) 93 | f! // does nothing 94 | ")] () -> @NULL { 95 | 96 | checker = @counter::new(0, reset = false) 97 | if checker.item == 0 { 98 | checker.item.add(1) 99 | -> return 100 | } 101 | 102 | 103 | }, 104 | 105 | // suppress_signal_quick: #[desc("Stops signal from coming past for some time (better for quick/glitchy signals)")] ( 106 | // #[desc("Time to suppress signal")] delay: @number, 107 | // ){ 108 | // //if checker is 0, a signal can come through 109 | // //if checker is 1, it will be suppressed 110 | // checker = @counter::new(0, reset = false) 111 | 112 | // -> (){ 113 | // checker.item.add(1) 114 | // wait(delay) 115 | // if checker.item == 1 { 116 | // -> return 117 | // } 118 | // wait() 119 | // if checker.item > 1 { 120 | // checker -= 1 121 | // } 122 | 123 | // }() 124 | 125 | 126 | 127 | // }, 128 | 129 | suppress_signal_until: #[desc("Suppresses the signal until the desired group or trigger function is called"), example(u" 130 | func = !{ 131 | suppress_signal_until(5g) 132 | @log::runtime::flash() 133 | } 134 | 135 | func! // does nothing 136 | 5g! 137 | wait(0.4) 138 | func! // flashes 139 | ")] ( 140 | #[desc("Group or trigger function to call to stop suppression (default: current context)")] 141 | group: @group | @trigger_function = null, 142 | ) -> @NULL { 143 | context_func = match group { 144 | @NULL: $.trigger_fn_context(), 145 | else: group, 146 | } 147 | 148 | if context_func == 0g { -> return } 149 | 150 | // 0 = suppressed 151 | // 1 = not suppressed 152 | checker = @counter::new(bits = 1, reset = false) 153 | 154 | -> (){ 155 | $.extend_trigger_func(context_func, (){ 156 | if checker == 0 { checker++ } 157 | }) 158 | }() 159 | 160 | if checker > 0 { 161 | -> return 162 | } 163 | }, 164 | 165 | for_loop: #[desc("Implementation of a spawn loop with a counter"), example(u" 166 | for_loop(0..10, (i) { 167 | if i < 5 { 168 | 10g.move(-10, 0) 169 | } else { 170 | 10g.move(10, 0) 171 | } 172 | }) 173 | ")] 174 | ( 175 | #[desc("Range of values (for example 0..10)")] range: @range, 176 | #[desc("Macro of the code that gets looped, should take the iterator (a counter) as the first argument.")] code: (@counter -> @NULL) | (() -> @NULL), // supposed to be @counter -> @NULL, but @counter isnt defined lol 177 | #[desc("Delay between loops (less than 0.05 may be unstable)")] delay: @number | @epsilon = @epsilon::{}, 178 | //#[desc("Whether to reset the iterator after looping (only disable if the loop is only triggered once)")] reset: @bool = true, 179 | ) -> @NULL { 180 | i = @counter::new(range.start) 181 | 182 | func = !{ 183 | if code is (@counter -> @NULL) { 184 | code(i) 185 | } else { 186 | code() 187 | } 188 | wait(delay) 189 | i.add(range.step_size) 190 | if range.step_size > 0 { 191 | if i.item < range.end { 192 | func! 193 | } 194 | if i.item > range.end - 1 { 195 | -> return 196 | } 197 | } else { 198 | if i.item > range.end { 199 | func! 200 | } 201 | if i.item < range.end + 1 { 202 | -> return 203 | } 204 | } 205 | } 206 | 207 | func! 208 | 209 | }, 210 | 211 | while_loop: #[desc("Implementation of a conditional spawn loop"), example(u" 212 | c = counter(11) 213 | 214 | while_loop(() => c > 4, () { 215 | c -= 2 216 | }) 217 | 218 | // c is now 3 219 | ")] 220 | ( 221 | #[desc("While loop condition, should return a boolean")] expr: () -> @bool, 222 | #[desc("Macro of the code that gets looped")] code: () -> @NULL, 223 | #[desc("Delay between loops (less than 0.05 may be unstable)")] delay: @number | @epsilon = @epsilon::{}, 224 | ) -> @NULL { 225 | func = !{ 226 | if expr() { 227 | code() 228 | call_with_delay(delay, func) 229 | } else { 230 | -> return 231 | } 232 | } 233 | 234 | func! 235 | }, 236 | 237 | do_while_loop: #[desc("Implementation of a conditional spawn loop"), example(u" 238 | c = counter(4) 239 | 240 | do_while_loop(() => c > 10, () { 241 | c -= 2 242 | }) 243 | 244 | // c is now 2 245 | ")] 246 | ( 247 | #[desc("While loop condition, should -> return a boolean")] expr: () -> @bool, 248 | #[desc("Macro of the code that gets looped")] code: () -> @NULL, 249 | #[desc("Delay between loops (less than 0.05 may be unstable)")] delay: @number | @epsilon = @epsilon::{}, 250 | ) -> @NULL { 251 | func = !{ 252 | code() 253 | if expr() { 254 | call_with_delay(delay, func) 255 | } else { 256 | -> return 257 | } 258 | } 259 | 260 | func! 261 | }, 262 | 263 | 264 | } 265 | -------------------------------------------------------------------------------- /libraries/std/dictionary.spwn: -------------------------------------------------------------------------------- 1 | #[no_std, cache_output] 2 | import "array.spwn" 3 | 4 | impl @dictionary { 5 | is_empty: #[desc("Returns true if there are no entries in the dictionary, false otherwise."), example(u" 6 | dict = {} 7 | $.assert(dict.is_empty()) 8 | ")] 9 | (self) -> @bool { 10 | // no way to get number of entries currently afaik 11 | // so this 'loop' will get skipped over if empty, 12 | // or otherwise early exit on the first entry if not 13 | for kv in self { 14 | return false; 15 | } 16 | return true; 17 | }, 18 | keys: #[desc("Gets the dictionary's keys."), example(u" 19 | wares = { 20 | apple: 10, 21 | gold: 1000, 22 | peanuts: 5, 23 | } 24 | $.assert('apple' in wares.keys()) 25 | ")] 26 | (self) -> [@string] { 27 | let ret = [] 28 | for kv in self { 29 | ret.push(kv[0]) 30 | } 31 | return ret 32 | }, 33 | values: #[desc("Gets the dictionary's values."), example(u" 34 | wares = { 35 | apple: 10, 36 | gold: 1000, 37 | peanuts: 5, 38 | } 39 | $.assert(wares.values().sort() == [5, 10, 1000]) 40 | ")] 41 | (self) -> [_] { 42 | let ret = [] 43 | for kv in self { 44 | ret.push(kv[1]) 45 | } 46 | return ret 47 | }, 48 | items: #[desc("Gets the dictionary's items."), example(u" 49 | wares = { 50 | apple: 10, 51 | gold: 1000, 52 | peanuts: 5, 53 | } 54 | $.assert(['apple', 10] in wares.items()) 55 | ")] 56 | (self) -> [@array] { 57 | let ret = [] 58 | for kv in self { 59 | ret.push(kv) 60 | } 61 | return ret 62 | }, 63 | set: #[desc("Sets an item in the dictionary."), example(u" 64 | let wares = { 65 | apple: 10, 66 | gold: 1000, 67 | } 68 | wares.set('peanuts', 5) 69 | $.assert(wares.peanuts == 5) 70 | ")] 71 | (self, key: @string, val) -> @NULL { 72 | if key in self { 73 | self[key] = val 74 | } else { 75 | let self[key] = val 76 | } 77 | }, 78 | get: #[desc("Gets an item from the dictionary."), example(u" 79 | let wares = { 80 | apple: 10, 81 | gold: 1000, 82 | peanuts: 5, 83 | } 84 | 85 | $.assert(wares.get('peanuts') == 5) 86 | $.assert(wares.get('silver', default = 42) == 42) 87 | ")] 88 | (self, key: @string, default = @NULL::{}) -> _ { 89 | if key in self { 90 | return self[key] 91 | } else { 92 | if default == @NULL::{} { 93 | throw "Key doesn't exist and no fallback was provided" 94 | } else { 95 | return default 96 | } 97 | } 98 | }, 99 | delete: #[desc("Deletes an item from the dictionary."), example(u" 100 | let wares = { 101 | apple: 10, 102 | gold: 1000, 103 | peanuts: 5, 104 | } 105 | wares.delete('peanuts') 106 | 107 | $.assert('peanuts' in wares == false) 108 | ")] (self, key: @string) -> @NULL { 109 | if key in self { $.remove_index(self, key) } 110 | }, 111 | clear: #[desc("Clears the dictionary."), example(u" 112 | let wares = { 113 | apple: 10, 114 | gold: 1000, 115 | peanuts: 5, 116 | } 117 | wares.clear() 118 | 119 | $.assert(wares.is_empty()) 120 | ")] 121 | (self) -> @NULL { 122 | self = {} 123 | }, 124 | contains_value: #[desc("Checks if the dictionary contains a value."), example(u" 125 | let wares = { 126 | apple: 10, 127 | gold: 1000, 128 | peanuts: 5, 129 | } 130 | 131 | $.assert(wares.contains_value(5)) 132 | ")] 133 | (self, value) -> @bool { 134 | return self.values().contains(value) 135 | }, 136 | map: #[desc("Calls a defined callback function on each key-value pair of a dictionary, and returns an array that contains the results."), example(u" 137 | dict = {key1: 0, key2: 1, key3: 2} 138 | $.assert('k' in dict.map((k, v) => k[v])) 139 | ")] 140 | (self, cb: (_, _) -> _) -> @array { 141 | let output = []; 142 | for iter in self { 143 | output.push(cb(iter[0], iter[1])); 144 | } 145 | return output; 146 | }, 147 | } 148 | -------------------------------------------------------------------------------- /libraries/std/events.spwn: -------------------------------------------------------------------------------- 1 | #[no_std, cache_output] 2 | 3 | constants = import "constants.spwn" 4 | extract constants.obj_props 5 | 6 | extract import "control_flow.spwn" 7 | extract import "general_triggers.spwn" 8 | 9 | #[desc("An API for runtime events, like player input, collisions, etc.")] 10 | type @event 11 | 12 | impl @event { 13 | new: #[constructor, desc("Creates a new event"), example(u" 14 | my_event = @event::new() 15 | 16 | my_event.on(!{ 17 | @log::runtime::flash() 18 | }) 19 | 20 | wait(5) 21 | my_event.emit() 22 | ")] ( 23 | #[desc("Group used for emitting and listening to the event")] group: @group = null, 24 | ){ 25 | return @event::{ 26 | io: group if group != null else ?g, 27 | } 28 | }, 29 | on: #[desc("Triggers a function every time an event fires"), example(u" 30 | on(touch(), !{ 31 | 10g.move(10, 0) 32 | }) 33 | 34 | // you can also use it as a decorator 35 | [[ on(touch()) ]] !{ 36 | 10g.move(10, 0) 37 | } 38 | ")] ( 39 | self, 40 | #[desc("Function to trigger")] function: @trigger_function|@group, 41 | #[desc("Force suppression until current context")] suppress: @bool = false, 42 | ) -> @NULL { 43 | $.extend_trigger_func(self.io, (){ 44 | if suppress { suppress_signal_until() } 45 | function! 46 | }) 47 | }, 48 | emit: #[desc("Triggers all listeners of this event"), example(u" 49 | wait(5) 50 | event.emit() 51 | ", run_test = false)] ( 52 | self, 53 | ) { 54 | self.io! 55 | }, 56 | } 57 | 58 | return { 59 | on: ( 60 | event: @event, 61 | function, 62 | suppress = false, 63 | ) => event.on(function, suppress = suppress), 64 | 65 | touch: #[desc("Implementation of the touch trigger (returns an event)"), example(u" 66 | on(touch(), !{ 67 | 10g.move(10, 0) 68 | }) 69 | ")] ( 70 | #[desc("Dual mode (only check for touch on the dual side)")] dual_side: @bool = false 71 | ) -> @event { 72 | event = @event::new() 73 | $.add( trigger{ 74 | OBJ_ID: 1595, 75 | HOLD_MODE: true, 76 | TOGGLE_MODE: 1, 77 | TARGET: event.io, 78 | DUAL_MODE: dual_side, 79 | }) 80 | return event 81 | }, 82 | 83 | touch_end: #[desc("Returns an event for when a touch ends"), example(u" 84 | on(touch_end(), !{ 85 | 10g.move(10, 0) 86 | }) 87 | ")] ( 88 | #[desc("Dual mode (only check for touch on the dual side)")] dual_side: @bool = false 89 | ) -> @event { 90 | event = @event::new() 91 | $.add( trigger{ 92 | OBJ_ID: 1595, 93 | HOLD_MODE: true, 94 | TOGGLE_MODE: 2, 95 | TARGET: event.io, 96 | DUAL_MODE: dual_side, 97 | }) 98 | return event 99 | }, 100 | 101 | collision: #[desc("Implementation of the collision trigger (returns an event)"), example(u" 102 | on(collision(1b, 2b), !{ 103 | BG.set(rgb(0, 0, 0)) 104 | }) 105 | ")] ( 106 | #[desc("Block A ID")] a: @block, 107 | #[desc("Block B ID")] b: @block 108 | ) -> @event { 109 | event = @event::new() 110 | $.add( trigger{ 111 | OBJ_ID: 1815, 112 | BLOCK_A: a, 113 | BLOCK_B: b, 114 | ACTIVATE_GROUP: true, 115 | ACTIVATE_ON_EXIT: false, 116 | TARGET: event.io, 117 | }) 118 | return event 119 | }, 120 | 121 | collision_exit: #[desc("Returns an event for when a collision exits"), example(u" 122 | on(collision_exit(1b, 2b), !{ 123 | BG.set(rgb(0, 0, 0)) 124 | }) 125 | ")] ( 126 | #[desc("Block A ID")] a: @block, 127 | #[desc("Block B ID")] b: @block 128 | ) -> @event { 129 | event = @event::new() 130 | $.add( trigger{ 131 | OBJ_ID: 1815, 132 | BLOCK_A: a, 133 | BLOCK_B: b, 134 | ACTIVATE_GROUP: true, 135 | ACTIVATE_ON_EXIT: true, 136 | TARGET: event.io, 137 | }) 138 | return event 139 | }, 140 | 141 | 142 | death: #[desc("Returns an event for when the player dies"), example(u" 143 | on(death(), !{ 144 | BG.set(rgb(0, 0, 0)) 145 | }) 146 | ")] () -> @event { 147 | event = @event::new() 148 | $.add( trigger{ 149 | OBJ_ID: 1812, 150 | ACTIVATE_GROUP: true, 151 | TARGET: event.io, 152 | }) 153 | return event 154 | }, 155 | 156 | count: #[desc("Implementation of a count trigger (returns an event)"), example(u" 157 | on(count(4i, 3), !{ 158 | BG.set(rgb(0, 0, 0)) 159 | }) 160 | ")] (it: @item, hits: @number, multi: @bool = true) -> @event { 161 | event = @event::new() 162 | $.add( trigger{ 163 | OBJ_ID: 1611, 164 | ACTIVATE_GROUP: true, 165 | TARGET: event.io, 166 | ITEM: it, 167 | COUNT: hits, 168 | COUNT_MULTI_ACTIVATE: multi, 169 | }) 170 | return event 171 | }, 172 | 173 | x_position: #[desc("Returns an event for when the player reaches a specific x position"), example(u" 174 | BG.set(rgb8(255, 0, 0)) // turn background red 175 | on(x_position(300), !{ 176 | BG.set(rgb8(0, 255, 0)) // turn background green when x position is 300 177 | }) 178 | ")] ( 179 | #[desc("X position")] position:@number, 180 | ) { 181 | event = @event::new() 182 | $.add(spawn_trigger(event.io).with(X, position).with(Y, 2145)) 183 | return event 184 | } 185 | } 186 | -------------------------------------------------------------------------------- /libraries/std/fileio.spwn: -------------------------------------------------------------------------------- 1 | #[no_std, cache_output] 2 | // brought to you by camden314 3 | 4 | #[desc("A type for reading files")] 5 | type @file 6 | 7 | impl @file { 8 | new: #[ 9 | constructor, 10 | desc("Creates a new file IO object") 11 | example("@file::new('C:/path/to/file.txt')", run_test = false) 12 | ] ( 13 | #[desc("Path to file (at the moment this is only stable with absolute paths)")] path: @string 14 | ) -> @file { 15 | let ret = @file::{}; 16 | 17 | ret.filedata = $.readfile(path); 18 | ret.size = ret.filedata.length; 19 | let ret.fseek = 0; 20 | return ret; 21 | }, 22 | seek: #[desc("Sets a position in the file to read from"), example(u" 23 | f = @file::new('data.txt') 24 | f.seek(10) 25 | data = f.read(5) // reads characters 10 to 15 26 | ", run_test = false)] (self, s: @number) -> @NULL { 27 | if s < 0 { 28 | throw "Negative seek position " + s as @string; 29 | } 30 | self.fseek = s; 31 | }, 32 | read: #[desc("Reads the data in the file from the seek position to the end (or for a specified amount of characters)"), example(u" 33 | data = @file::new('data.txt').read() 34 | ", run_test = false)](self, s=-1) -> @string { 35 | let size = s 36 | if s < 0 { 37 | size = self.size; 38 | } 39 | if self.fseek >= self.size { 40 | return ""; 41 | } else { 42 | oldseek = self.fseek; 43 | self.fseek += size; 44 | return $.substr(self.filedata, oldseek, [self.fseek, self.size].min()); 45 | } 46 | }, 47 | } 48 | -------------------------------------------------------------------------------- /libraries/std/frames.spwn: -------------------------------------------------------------------------------- 1 | #[no_std, cache_output] 2 | 3 | constants = import "constants.spwn" 4 | extract constants.obj_ids 5 | extract constants.obj_props 6 | extract import 'events.spwn' 7 | extract import 'control_flow.spwn' 8 | extract import 'util.spwn' 9 | import "counter.spwn" 10 | 11 | frames_setup = cache_macro(() { 12 | block_a = ?b 13 | block_b = ?b 14 | group_toggle = ?g 15 | group_first = ?g 16 | group_second = ?g 17 | group_frame = ?g 18 | 19 | $.add(obj{ 20 | OBJ_ID: special.COLLISION_BLOCK, 21 | X: 0, 22 | Y: 2400, 23 | BLOCK_A: block_a, 24 | GROUPS: group_toggle, 25 | DYNAMIC_BLOCK: true, 26 | }) 27 | $.add(obj{ 28 | OBJ_ID: special.COLLISION_BLOCK, 29 | X: 0, 30 | Y: 2400, 31 | BLOCK_A: block_b, 32 | }) 33 | 34 | collision(block_a, block_b).on(!{ 35 | group_toggle.toggle_off() 36 | group_first! 37 | group_frame! 38 | }) 39 | collision_exit(block_a, block_b).on(!{ 40 | group_toggle.toggle_on() 41 | group_second! 42 | group_frame! 43 | }) 44 | 45 | on(x_position(1), !{ 46 | group_toggle.toggle_off() 47 | }) 48 | 49 | return { 50 | block_a: block_a, 51 | block_b: block_b, 52 | group_toggle: group_toggle, 53 | group_first: group_first, 54 | group_second: group_second, 55 | group_frame: group_frame, 56 | } 57 | }) 58 | 59 | return { 60 | frames: #[desc("Waits for an arbitrary amount of frames"), example(u" 61 | @log::runtime::flash(rgb(1,0,0)) // flash the screen 62 | frames(60) // wait 60 frames (1 second on 60fps) 63 | @log::runtime::flash(rgb(0,0,1)) // flash the screen 64 | ")] ( 65 | #[desc("The amount of frames to wait")] frames: @number|@counter, 66 | ) -> @NULL { 67 | frames_count = @counter::new(bits = $.ceil($.log(frames, 2))) 68 | 69 | let frames = match frames { 70 | @number: frames.floor(), 71 | @counter: frames.clone(), 72 | } 73 | 74 | extern = !{ 75 | if frames_count < frames { 76 | frames_count++ 77 | } else if frames_count == frames { 78 | frames_count++ 79 | -> return 80 | } 81 | } 82 | 83 | current_context = $.trigger_fn_context() 84 | 85 | $.extend_trigger_func(frames_setup().group_frame, (){ 86 | suppress_signal_until(current_context) 87 | extern! 88 | }) 89 | }, 90 | 91 | frame: #[desc("Returns an event that runs on every frame"), example(u" 92 | on(frame(), !{ 93 | 10g.move(1, 0) // moves every frame (on 60fps the block would move by 6 blocks per second) 94 | }) 95 | ")] () -> @event { 96 | event = @event::new() 97 | $.extend_trigger_func( 98 | frames_setup().group_frame, 99 | () => event.emit(), 100 | ) 101 | return event 102 | }, 103 | } 104 | -------------------------------------------------------------------------------- /libraries/std/http.spwn: -------------------------------------------------------------------------------- 1 | #[no_std, cache_output] 2 | 3 | #[desc("A type sending http requests")] 4 | type @http 5 | impl @http { 6 | get: #[desc("Makes a get request to the provided URL. A dictionary of headers can optionally be passed in as a second argument")] 7 | (url: @string, headers: @dictionary = {}, body = "") { 8 | return $.http_request( 9 | "get", 10 | url, headers, body 11 | ) 12 | }, 13 | post: #[desc("Makes a post request to the provided URL. A dictionary of headers can optionally be passed in as a second argument")] 14 | (url: @string, headers: @dictionary = {}, body = "") { 15 | return $.http_request( 16 | "post", 17 | url, headers, body 18 | ) 19 | }, 20 | put: #[desc("Makes a put request to the provided URL. A dictionary of headers can optionally be passed in as a second argument")] 21 | (url: @string, headers: @dictionary = {}, body = "") { 22 | return $.http_request( 23 | "put", 24 | url, headers, body 25 | ) 26 | }, 27 | delete: #[desc("Makes a put request to the provided URL. A dictionary of headers can optionally be passed in as a second argument")] 28 | (url: @string, headers: @dictionary = {}, body = "") { 29 | return $.http_request( 30 | "delete", 31 | url, headers, body 32 | ) 33 | }, 34 | patch: #[desc("Makes a patch request to the provided URL. A dictionary of headers can optionally be passed in as a second argument")] 35 | (url: @string, headers: @dictionary = {}, body = "") { 36 | return $.http_request( 37 | "patch", 38 | url, headers, body 39 | ) 40 | }, 41 | head: #[desc("Makes a head request to the provided URL. A dictionary of headers can optionally be passed in as a second argument")] 42 | (url: @string, headers: @dictionary = {}, body = "") { 43 | return $.http_request( 44 | "head", 45 | url, headers, body 46 | ) 47 | }, 48 | } -------------------------------------------------------------------------------- /libraries/std/item.spwn: -------------------------------------------------------------------------------- 1 | #[no_std, cache_output] 2 | constants = import "constants.spwn" 3 | extract constants.obj_props 4 | extract constants.comparisons 5 | 6 | import "events.spwn" 7 | extract import "util.spwn" 8 | 9 | impl @item { 10 | 11 | add: 12 | #[desc("Implementation of the pickup trigger"), example("10i.add(5)")] 13 | (self, #[desc("Amount to add")] amount: @number) -> @NULL { 14 | $.add( trigger{ 15 | OBJ_ID: 1817, 16 | COUNT: amount, 17 | ITEM: self, 18 | }) 19 | }, 20 | 21 | if_is: 22 | #[desc("Implementation of the instant count trigger"), example(u" 23 | 10i.if_is(EQUAL_TO, 5, !{ 24 | BG.pulse(rgb8(255, 0, 0), fade_out = 0.5) 25 | }) 26 | ")] 27 | ( 28 | self, 29 | #[desc("Comparison mode")] comparison: @comparison, 30 | #[desc("Number to compare with")] other: @number, 31 | #[desc("Target function if comparison is 'true'")] function: @trigger_function 32 | ) -> @NULL { 33 | $.add( trigger{ 34 | OBJ_ID: 1811, 35 | TARGET: function, 36 | COUNT: other, 37 | ACTIVATE_GROUP: true, 38 | COMPARISON: comparison.id, 39 | ITEM: self, 40 | }) 41 | }, 42 | 43 | count: 44 | #[desc("Implementation of a count trigger (returns an event)"), example(u" 45 | on(4i.count(3), !{ 46 | BG.set(rgb(0, 0, 0)) 47 | }) 48 | ")] 49 | ( 50 | self, 51 | #[desc("Number to check against")] number: @number = 0 52 | ) -> @event { 53 | event = @event::new() 54 | $.add( trigger{ 55 | OBJ_ID: 1611, 56 | TARGET: event.io, 57 | COUNT: number, 58 | ACTIVATE_GROUP: true, 59 | ITEM: self, 60 | COUNT_MULTI_ACTIVATE: true, 61 | }) 62 | return event 63 | }, 64 | 65 | compare: 66 | #[desc("Compare the ID to a number"), example(u" 67 | // be sure to use the comparison operators instead of this macro 68 | // e.g. `if 10i > 3 {` 69 | 70 | if 10i.compare(5) == 1 { // if the ID is greater than 5 71 | @log::runtime::flash(rgb8(255, 0, 0)) 72 | } 73 | ")] 74 | (self, number: @number) -> in [ -1, 0, 1 ] { 75 | self.if_is(SMALLER_THAN, number, !{ 76 | -> return -1 77 | }) 78 | self.if_is(EQUAL_TO, number, !{ 79 | -> return 0 80 | }) 81 | self.if_is(LARGER_THAN, number, !{ 82 | -> return 1 83 | }) 84 | }, 85 | 86 | _more_than_: 87 | #[desc("Implementation of (`>`) for item IDs"), example(u" 88 | if 4i > 3 { 89 | BG.set(rgb(0, 0, 0)) 90 | } 91 | ")] 92 | (self, number: @number) -> @bool { 93 | return self.compare(number) == 1 94 | }, 95 | 96 | _less_than_: 97 | #[desc("Implementation of (`<`) for item IDs"), example(u" 98 | if 4i < 3 { 99 | BG.set(rgb(0, 0, 0)) 100 | } 101 | ")] 102 | (self, number: @number) -> @bool { 103 | return self.compare(number) == -1 104 | }, 105 | 106 | _equal_: 107 | #[desc("Implementation of (`==`) for item IDs"), example(u" 108 | if 4i == 3 { 109 | BG.set(rgb(0, 0, 0)) 110 | } 111 | ")] 112 | (self, number: @number) -> @bool { 113 | return self.compare(number) == 0 114 | }, 115 | 116 | _not_equal_: 117 | #[desc("Implementation of (`!=`) for item IDs"), example(u" 118 | if 4i != 3 { 119 | BG.set(rgb(0, 0, 0)) 120 | } 121 | ")] 122 | (self, number: @number) -> @bool { 123 | return self.compare(number) in [ -1, 1 ] 124 | }, 125 | 126 | _more_or_equal_: 127 | #[desc("Implementation of (`>=`) for item IDs"), example(u" 128 | if 4i >= 3 { 129 | BG.set(rgb(0, 0, 0)) 130 | } 131 | ")] 132 | (self, number: @number) -> @bool { 133 | return self.compare(number) in [ 0, 1 ] 134 | }, 135 | 136 | _less_or_equal_: 137 | #[desc("Implementation of (`<=`) for item IDs"), example(u" 138 | if 4i <= 3 { 139 | BG.set(rgb(0, 0, 0)) 140 | } 141 | ")] 142 | (self, number: @number) -> @bool { 143 | return self.compare(number) in [ -1, 0 ] 144 | }, 145 | 146 | _range_: #[desc("Implementation of the range operator (`..`) for item IDs"), example(u" 147 | for item in 1i..10i { 148 | item.add(10) 149 | } 150 | ")] create_range_macro(@item), 151 | } 152 | -------------------------------------------------------------------------------- /libraries/std/level_info.spwn: -------------------------------------------------------------------------------- 1 | #[no_std, cache_output] 2 | 3 | let constants = import "constants.spwn" 4 | 5 | MATCHER = @pattern | (_ -> @bool) 6 | matches = (v, p: MATCHER) -> @bool => match p { 7 | @pattern: v is p, 8 | @macro: p(v) 9 | } 10 | 11 | return { 12 | objects: $.level_objects(), 13 | 14 | get_objects: #[desc("Returns an array of all the objects in the level with a property whose value matches the pattern or macro"), example(u" 15 | objects_over_x_50 = level.get_objects(obj_props.X, >50) 16 | objects_with_group_2 = level.get_objects(obj_props.GROUPS, g => 2g in g) 17 | ", run_test = false)] ( 18 | prop: @object_key, 19 | pat: MATCHER, 20 | ) -> [@object] { 21 | return $.level_objects().filter(o => prop in o && matches(o[prop], pat)) 22 | }, 23 | 24 | get_marker: #[desc("Returns the first text object found with the given text, or null if none are found"), example(u' 25 | extract obj_props 26 | 27 | thing_marker = level.get_marker("thing marker") 28 | $.add( move_trigger(1g,10,0).with(X, thing_marker[X] ) ) 29 | ', run_test = false)] ( 30 | text: @string, 31 | ) -> @object | @NULL { 32 | for i in $.level_objects() { 33 | if constants.obj_props.TEXT in i && i[constants.obj_props.TEXT] == $.b64encode(text) { 34 | return i 35 | } 36 | } 37 | return null 38 | }, 39 | 40 | } 41 | -------------------------------------------------------------------------------- /libraries/std/lib.spwn: -------------------------------------------------------------------------------- 1 | #[no_std, cache_output] 2 | let constants = import "constants.spwn" 3 | 4 | //type implementations 5 | import "counter.spwn" 6 | import "group.spwn" 7 | import "color.spwn" 8 | import "item.spwn" 9 | import "block.spwn" 10 | import "array.spwn" 11 | import "macro.spwn" 12 | import "object.spwn" 13 | import "dictionary.spwn" 14 | import "string.spwn" 15 | import "fileio.spwn" 16 | import "regex.spwn" 17 | import "number.spwn" 18 | import "vector.spwn" 19 | import "http.spwn" 20 | import "range.spwn" 21 | import "set.spwn" 22 | import "log.spwn" 23 | import "path.spwn" 24 | level_info = import "level_info.spwn" 25 | 26 | chroma = import "chroma.spwn" 27 | general = import "general_triggers.spwn" 28 | events = import "events.spwn" 29 | ctrl_flow = import "control_flow.spwn" 30 | frames = import "frames.spwn" 31 | 32 | return { 33 | ..constants.easing_types, 34 | ..constants.comparisons, 35 | ..constants.colors, 36 | ..general, 37 | ..events, 38 | ..ctrl_flow, 39 | ..frames, 40 | 41 | PI: constants.PI, 42 | EULER: constants.EULER, 43 | 44 | counter: @counter::new, 45 | obj_props: constants.obj_props, 46 | obj_ids: constants.obj_ids, 47 | open: @file::new, 48 | path: @path::new, 49 | regex: @regex::new, 50 | rgb: @chroma::from_rgb, 51 | rgb8: @chroma::from_rgb8, 52 | hex: @chroma::from_hex, 53 | hsv: @chroma::from_hsv, 54 | hsv2: @chroma::from_hsv2, 55 | blend_modes: chroma.BLEND_MODES, 56 | vec: @vector::new, 57 | vec2: @vector::new2, 58 | vec3: @vector::new3, 59 | 60 | level: level_info, 61 | } 62 | 63 | 64 | -------------------------------------------------------------------------------- /libraries/std/log.spwn: -------------------------------------------------------------------------------- 1 | #[no_std, cache_output] 2 | 3 | util = import 'util.spwn' 4 | extract import 'constants.spwn'.colors 5 | import 'chroma.spwn' 6 | import 'dictionary.spwn' 7 | import 'string.spwn' 8 | 9 | let data = { 10 | time_labels: {}, 11 | counts: {}, 12 | } 13 | 14 | default = 'default' 15 | 16 | log_time = (label: @string, time: @number) { 17 | $.print( 18 | "{}: {}".fmt([ 19 | label, 20 | util.prettify_time(time) 21 | ]) 22 | ) 23 | } 24 | 25 | #[desc("Runtime logging utility, please call this type with `@log::runtime`.")] 26 | type @runtime_log 27 | impl @runtime_log { 28 | flash: #[desc('Flashes the background'), example(u" 29 | while_loop(() => true, (){ 30 | 5g! 31 | @log::runtime::flash() 32 | }, delay = 1) 33 | ")]( 34 | color: @chroma = @chroma::from_rgb(1,1,1), 35 | fade_in: @number = 0.0, 36 | hold: @number = 0.0, 37 | fade_out: @number = 0.5, 38 | ) -> @NULL { 39 | -> BG.pulse(color, fade_in, hold, fade_out) 40 | }, 41 | } 42 | 43 | #[desc("Logging utility type.")] 44 | type @log 45 | impl @log { 46 | runtime: @runtime_log, 47 | time: #[desc("Saves the current time to a given label."), example(u" 48 | @log::time('heavy calculation') 49 | // 50 | @log::time_end('heavy calculation') 51 | ")] (label: @string = default) -> @NULL { 52 | instant = $.time() 53 | data.time_labels.set(label, instant) 54 | }, 55 | time_log: #[desc("Logs the time between now and the start of the label."), example(u" 56 | @log::time('for') 57 | 58 | for i in 0..10 { 59 | @log::time_log('for') 60 | } 61 | ")] (label: @string) -> @NULL { 62 | instant = $.time() 63 | if !(label in data.time_labels) { 64 | throw "Label {} not found".fmt(label) 65 | } 66 | log_time(label, instant - data.time_labels.get(label)) 67 | }, 68 | time_end: #[desc("Logs the time between now and the start of the label and destroys it."), example(u" 69 | @log::time('huge loop') 70 | 71 | for i in 0..100000 {} 72 | 73 | @log::time_end('huge loop') 74 | ", run_test = false)] (label = default) -> @NULL { 75 | instant = $.time() 76 | if !(label in data.time_labels) { 77 | throw "Label {} does not exist".fmt($.display(label)) 78 | } 79 | log_time(label, instant - data.time_labels.get(label)) 80 | data.time_labels.delete(label) 81 | }, 82 | count: #[desc("Logs how many times the label got counted."), example(u" 83 | for i in 0..10 { 84 | @log::count('my loop') 85 | } 86 | ")] (label: @string = default) -> @NULL { 87 | count = data.counts.get(label) + 1 if label in data.counts else 1 88 | data.counts.set(label, count) 89 | $.print("{0}: {1}".fmt([label, count])) 90 | }, 91 | reset_count: #[desc("Resets the counted amount of the label."), example(u" 92 | for i in 0..10 { 93 | @log::count('loop') 94 | } 95 | 96 | @log::reset_count('loop') 97 | 98 | for i in 0..10 { 99 | @log::count('loop') 100 | } 101 | ")] (label: @string = default) -> @NULL { 102 | data.counts.set(label, 0) 103 | }, 104 | } 105 | -------------------------------------------------------------------------------- /libraries/std/macro.spwn: -------------------------------------------------------------------------------- 1 | #[no_std, cache_output] 2 | 3 | impl @macro { 4 | call: (self, args: @array) { 5 | return $.call(self, args) 6 | }, 7 | partial_call: (self, args: @array) { 8 | pass_args = args[:self.args.length] + [null] * (self.args.length - args.length) 9 | return $.call(self, pass_args) 10 | }, 11 | } 12 | 13 | -------------------------------------------------------------------------------- /libraries/std/number.spwn: -------------------------------------------------------------------------------- 1 | #[no_std, cache_output] 2 | 3 | import 'string.spwn' 4 | extract import 'constants.spwn' 5 | 6 | impl @number { 7 | abs: #[desc("Returns the absolute value of the number"), example("$.assert((-1).abs() == 1)")] 8 | (self) -> @number => self if self >= 0 else -self, 9 | sign: #[desc("Returns the sign of the number, also works with -0"), example("$.assert((-69).sign() == -1)")] 10 | (self) -> @number => self if self == 0 else ((-1) if self < 0 else 1), 11 | round: #[desc("Returns the nearest integer to the number"), example("$.assert((1.5).round() == 2)")] 12 | (self) -> @number => $.round(self), 13 | ceil: #[desc("Returns the smallest integer greater than or equal to the number"), example("$.assert((1.5).ceil() == 2)")] 14 | (self) -> @number => $.ceil(self), 15 | floor: #[desc("Returns the largest integer less than or equal to the number"), example("$.assert((1.5).floor() == 1)")] 16 | (self) -> @number => $.floor(self), 17 | sqrt: #[desc("Returns the square root of the number"), example("$.assert(4.sqrt() == 2)")] 18 | (self) -> @number => $.sqrt(self), 19 | log: #[desc("Returns the logarithm of the number (default: natural logarithm)"), example("$.assert(2.log() == 0.6931471805599453)")] 20 | (self, base = EULER) -> @number => $.log(self, base), 21 | lerp: #[desc("Returns the linear interpolation between the two numbers"), example("$.assert((0.7).lerp(0,2) == 1.4)")] 22 | (self, lower, higher) -> @number => lower + (higher - lower) * self, 23 | map: #[desc("Maps a number linearily from one interval to another"), example("$.assert(2.map(1, 4, 5, 11) == 7)")] 24 | (self, istart: @number, istop: @number, ostart: @number, ostop: @number) -> @number { 25 | return ostart + (ostop - ostart) * ((self - istart) / (istop - istart)) 26 | }, 27 | clamp: #[desc("Constrains a number between two numbers"), example("$.assert(2.clamp(7,10) == 7)")] 28 | (self, min: @number, max: @number) -> @number { 29 | return $.min($.max(self,min),max) 30 | }, 31 | wrap: #[desc("Wraps a number between two numbers"), example("$.assert(8.clamp(7,10) == 8 && 11.clamp(7,10) == 10)")] 32 | (self, min: @number, max: @number) -> @number { 33 | return ((self - min) % (max - min)) + min 34 | }, 35 | ordinal: #[desc("Returns the number in ordinal form"), example("$.assert(1.ordinal() == '1st')")] 36 | (self) -> @string { 37 | number = self.floor() if self >= 0 else self.ceil() 38 | numb_abs = number.abs() 39 | return match true { 40 | ==(((numb_abs % 100) / 10).floor() == 1): "{}th", 41 | ==(numb_abs % 10 == 1): "{}st", 42 | ==(numb_abs % 10 == 2): "{}nd", 43 | ==(numb_abs % 10 == 3): "{}rd", 44 | else: "{}th", 45 | }.fmt(number) 46 | }, 47 | to_fixed: #[desc("Turns the number into a string with the given precision"), example("$.assert(1.to_fixed(2) == '1.00')")] 48 | (self, precision: @number = 0) -> @string { 49 | let precision = precision.ceil() 50 | let string = self as @string 51 | let dot_index = string.index(".") 52 | if dot_index == null { 53 | dot_index = string.length 54 | if string.length - dot_index + precision <= 0 { return string } 55 | string += "." 56 | } 57 | string = string.r_pad(dot_index + precision + 1, "0") 58 | string = string.substr(0, dot_index + precision + 1) 59 | return string.r_trim(".") // hopefully chaining will be fixed soon 60 | }, 61 | to_precision: #[desc("Turns the number to a specific precision"), example("$.assert((0.12345).to_precision(3) == 0.123)")] 62 | (self, precision: @number = 0) -> @number => self.to_fixed(precision) as @number, 63 | } 64 | 65 | 66 | -------------------------------------------------------------------------------- /libraries/std/object.spwn: -------------------------------------------------------------------------------- 1 | #[no_std, cache_output] 2 | extract import "constants.spwn" 3 | impl @object { 4 | set: (self, key: @object_key, value) -> @NULL { 5 | $.edit_obj(self, key, value); 6 | }, 7 | with: (self, key: @object_key, value) -> @object { 8 | let new_obj = self 9 | new_obj.set(key, value) 10 | return new_obj 11 | }, 12 | has_key: (self, key: @object_key) -> @bool { 13 | return key in self 14 | }, 15 | add_groups: (self, groups: @group | [@group]) -> @NULL { 16 | let grps = match groups { 17 | @group: [groups], 18 | [@group]: groups 19 | }; 20 | 21 | if obj_props.GROUPS in self { 22 | 23 | grps += match self[obj_props.GROUPS] { 24 | @group: [self[obj_props.GROUPS]], 25 | [@group]: self[obj_props.GROUPS] 26 | }; 27 | } 28 | self.set(obj_props.GROUPS, grps); 29 | }, 30 | add: (self) -> @NULL { 31 | $.add(self); 32 | }, 33 | } 34 | -------------------------------------------------------------------------------- /libraries/std/range.spwn: -------------------------------------------------------------------------------- 1 | #[no_std, cache_output] 2 | 3 | fix_step = (value: @number) => value.abs().floor() if is !=0 else 1 4 | 5 | impl @range { 6 | reverse: #[desc("Reverses the range."), example(u" 7 | let range = 10..5..50 8 | $.assert(range.reverse() == 50..5..10) 9 | ")] 10 | (self) -> @range { 11 | return self.end..self.step_size..self.start 12 | }, 13 | _plus_: #[desc("Adds a value to the range."), example(u" 14 | let range = 10..5..50 15 | $.assert(range + 10 == 20..5..60) 16 | ")] 17 | (self, value: @number) -> @range { 18 | return self.start + value..self.step_size..self.end + value 19 | }, 20 | _minus_: #[desc("Subtracts a value from the range."), example(u" 21 | let range = 10..5..50 22 | $.assert(range - 10 == 0..5..40) 23 | ")] 24 | (self, value: @number) -> @range { 25 | return self.start - value..self.step_size..self.end - value 26 | }, 27 | _times_: #[desc("Multiplies the range by a value."), example(u" 28 | let range = 10..5..50 29 | $.assert(range * 10 == 100..50..500) 30 | ")] 31 | (self, value: @number) -> @range { 32 | return self.start * value..fix_step(self.step_size * value)..self.end * value 33 | }, 34 | _divided_by_: #[desc("Divides the range by a value."), example(u" 35 | let range = 10..5..50 36 | $.assert(range / 10 == 1..1..5) // sadly steps on ranges must be integers 37 | $.assert(range / -10 == -1..1..-5) 38 | ")] 39 | (self, value: @number) -> @range { 40 | return match value { 41 | ==0: (){ 42 | throw "Cannot divide range by zero." 43 | }(), 44 | else: self.start / value..fix_step(self.step_size / value)..self.end / value, 45 | } 46 | }, 47 | _pow_: #[desc("Raises the range to a power."), example(u" 48 | let range = 10..5..50 49 | $.assert(range ^ 2 == 100..25..2500) 50 | ")] 51 | (self, value: @number) -> @range { 52 | return self.start ^ value..fix_step(self.step_size ^ value)..self.end ^ value 53 | }, 54 | // this is not possible currently 55 | // _in_: #[desc("Returns true if the value is in the range."), example(u" 56 | // let range = 10..5..50 57 | // $.assert(10 in range) 58 | // $.assert(!(5 in range)) 59 | // ")] 60 | // (self, value: @number) -> @bool { 61 | // return value in @array(self) 62 | // }, 63 | } 64 | -------------------------------------------------------------------------------- /libraries/std/regex.spwn: -------------------------------------------------------------------------------- 1 | #[no_std, cache_output] 2 | type @regex 3 | 4 | impl @regex { 5 | new: #[constructor, desc("Create a new instance of regex")] 6 | (#[desc("A regex string. Make sure to use two backslashes to escape selectors instead of one or it will error")] re: @string) -> @regex { 7 | return @regex::{ 8 | regex: re 9 | } 10 | }, 11 | matches: #[desc("Checks if the regex matches a string argument")] 12 | (self, m: @string) -> @bool { 13 | return $.regex(self.regex, m, "match", null) 14 | }, 15 | replace: #[desc("Regex replace the contents of a string")] 16 | (self, to_replace: @string, replacer: @string) -> @string { 17 | let t_rep = to_replace; 18 | return $.regex(self.regex, t_rep, "replace", replacer) 19 | }, 20 | find_all: #[desc("Regex find all matching indices of the string argument")] 21 | (self, m: @string) -> [[@number]] { 22 | return $.regex(self.regex, m, "find_all", null) 23 | }, 24 | find_groups: #[desc("Regex find all groups of the string argument, their range, text, and name")] 25 | (self, m: @string) -> [@dictionary] { 26 | return $.regex(self.regex, m, "find_groups", null) 27 | }, 28 | } 29 | -------------------------------------------------------------------------------- /libraries/std/set.spwn: -------------------------------------------------------------------------------- 1 | #[no_std, cache_output] 2 | 3 | type @set 4 | constants = import "constants.spwn" 5 | extract constants.obj_props 6 | extract import "util.spwn" 7 | 8 | rot_vec = (x, y, r) => [x * $.cos(r) - y * $.sin(r), x * $.sin(r) + y * $.cos(r)] 9 | rot = (x, y, cx, cy, r) { 10 | v = rot_vec(x - cx, y - cy, r) 11 | return [v[0] + cx, v[1] + cy] 12 | } 13 | 14 | impl @set { 15 | new: #[constructor, desc("Creates a new empty object set"), example(u" 16 | let object_set = @set::new() 17 | ")] 18 | () -> @set { 19 | return @set::{ 20 | objects: [] 21 | } 22 | }, 23 | from: #[desc("Creates a new object set from am optional array of objects, with an optional center x and center y position"), example(u" 24 | let object_set = @set::from(objects=my_objects, center_x=30, center_y=15) 25 | //`@set::from(my_objects, 5, 10)` is also valid 26 | ", run_test = false)] 27 | ( 28 | objects: [@object] = [], 29 | center_x: @number | @NULL = null, 30 | center_y: @number | @NULL = null, 31 | ) -> @set { 32 | if objects == [] { return @set::new() } 33 | 34 | let objects = objects 35 | for i in 0..objects.length { 36 | if !objects[i].has_key(X) { objects[i].set(X, 0) } 37 | if !objects[i].has_key(Y) { objects[i].set(Y, 0) } 38 | } 39 | 40 | let [center_x, center_y] = [center_x, center_y] 41 | if center_x == null { 42 | center_x = objects.sum(o => o[X]) / objects.length 43 | } 44 | if center_y == null { 45 | center_y = objects.sum(o => o[Y]) / objects.length 46 | } 47 | return @set::{ 48 | objects: objects.map(o => o.with(X, o[X] - center_x).with(Y, o[Y] - center_y) ) 49 | } 50 | }, 51 | is_empty: #[desc("Returns true if this set contains no objects, otherwise false.")] 52 | (self) { 53 | return self.objects.is_empty(); 54 | }, 55 | push: #[desc("Adds a new object to the set")] 56 | (self, object: @object) { 57 | let to_push = object; 58 | $.append(self.objects, to_push) 59 | }, 60 | map: #[desc("Calls a defined callback function on each object of the set, and returns a new set that contains the modified objects"), example(u" 61 | let shifted_objects = objects.map(o => o[obj_props.X] + 30) 62 | ", run_test = false)] 63 | ( 64 | self, 65 | cb: @object -> @object 66 | ) -> @set { 67 | return @set::from( 68 | self.objects.map( cb ), 69 | 0, 70 | 0, 71 | ) 72 | }, 73 | place: #[desc("Adds all objects in the set into the level, with a given X and Y offset position")] 74 | ( 75 | self, 76 | x: @number, 77 | y: @number, 78 | ) -> @NULL { 79 | self.objects.map(o => $.add( 80 | o.with(X, o[X] + x).with(Y, o[Y] + y) 81 | )) 82 | }, 83 | rotated: #[desc("Rotates every object in the set, returning a new set with the modified objects"), example(u" 84 | let rotated_objects = objects.rotated(90, around_x=30, around_y=30) 85 | ", run_test = false)] 86 | ( 87 | self, 88 | degrees: @number, 89 | around_x: @number = 0, 90 | around_y: @number = 0, 91 | lock: @bool = false, 92 | ) -> @set { 93 | return self.map( 94 | (o) { 95 | v = rot(o[X], o[Y], around_x, around_y, - 3.1415926535897932 / 180 * degrees) 96 | return o.with(X, v[0]).with(Y, v[1]).with(ROTATION, (o[ROTATION] if ROTATION in o else 0) + (degrees if !lock else 0)) 97 | } 98 | ) 99 | }, 100 | with: #[desc("Adds or modifies the given prop and value pair for every object in the set, returning a new set with the modified objects")] 101 | ( 102 | self, 103 | prop: @object_key | @number, 104 | value, 105 | ) -> @set { 106 | return self.map( 107 | o => o.with(prop, value) 108 | ) 109 | }, 110 | replace: #[desc("Replaces every object that has a specific prop value, with another specified value (or a fallback default value), returning a new set with the modified objects"), example(u" 111 | let replaced = objects.replace(obj_props.EDITOR_LAYER_1, ==1, 0, 1) 112 | ", run_test = false)] 113 | ( 114 | self, 115 | prop: @object_key | @number, 116 | pat: MATCHER, 117 | new, 118 | default, 119 | ) -> @set { 120 | return self.map( 121 | (o) { 122 | value = o[prop] if prop in o else default 123 | if matches(value, pat) { 124 | return o.with( 125 | prop, 126 | ( new(value) if new is @macro else new ) 127 | ) 128 | } 129 | return o 130 | } 131 | ) 132 | }, 133 | 134 | replace_group: #[desc("Replaces a given group with another given group, returning a new set with the modified objects")] 135 | ( 136 | self, 137 | group: @group, 138 | new: @group, 139 | ) -> @set { 140 | return self.map( 141 | (o) => o.with(GROUPS, o[GROUPS].map(g => new if g == group else g)) if GROUPS in o else o 142 | ) 143 | }, 144 | } 145 | 146 | 147 | -------------------------------------------------------------------------------- /libraries/std/util.spwn: -------------------------------------------------------------------------------- 1 | // utility macroes 2 | #[no_std, cache_output] 3 | 4 | bool_macro = _ -> @bool 5 | MATCHER = @pattern | bool_macro 6 | 7 | -> return { 8 | create_range_macro: (typ: @type_indicator) -> @macro { 9 | -> return (self, other: typ) { 10 | range = (self as @number)..(other as @number) 11 | let out = [] 12 | for num in range { 13 | out.push(num as typ) 14 | } 15 | -> return out 16 | } 17 | }, 18 | MATCHER, 19 | matches: (v, p: MATCHER) -> @bool => match p { 20 | @pattern: v is p, 21 | @macro: p(v) 22 | }, 23 | prettify_time: (time: @number, max: @number = 3) -> @string { 24 | let microseconds = $.floor(time * 1000000) 25 | let milliseconds = $.floor(time * 1000) 26 | let seconds = $.floor(time) 27 | let minutes = $.floor(seconds / 60) 28 | let hours = $.floor(minutes / 60) 29 | let days = $.floor(hours / 24) 30 | let years = $.floor(days / 365) 31 | 32 | timings = [ 33 | [years, "y"], 34 | [days % 365, "d"], 35 | [hours % 24, "h"], 36 | [minutes % 60, "m"], 37 | [seconds % 60, "s"], 38 | [milliseconds % 1000, "ms"], 39 | [microseconds % 1000, "µs"], 40 | ] 41 | 42 | let out = [] 43 | let skip = false 44 | 45 | for i in 0..timings.length { 46 | if out.length >= max { break } 47 | [ quantity, scale ] = timings[i] 48 | if quantity > 0 || skip == true { 49 | out.push("{}{}".fmt([quantity, scale])) 50 | skip = true 51 | } 52 | } 53 | 54 | return out.join(' ') 55 | }, 56 | cache_macro: (macro: @macro) -> @macro { 57 | let cache = @NULL 58 | return (){ // TODO: add ..args in the future when it will be supported 59 | if cache == @NULL { 60 | cache = macro() // TODO: add ..args in the future 61 | } 62 | return cache 63 | } 64 | }, 65 | } 66 | -------------------------------------------------------------------------------- /optimizer/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "optimizer" 3 | version = "0.0.8" 4 | edition = "2021" 5 | 6 | # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html 7 | 8 | [dependencies] 9 | 10 | compiler = { path = "../compiler" } 11 | parser = { path = "../parser" } 12 | 13 | ahash = "0.7.6" 14 | -------------------------------------------------------------------------------- /optimizer/src/dead_code.rs: -------------------------------------------------------------------------------- 1 | use compiler::{ 2 | builtins::{Group, Id}, 3 | leveldata::ObjParam, 4 | }; 5 | 6 | use crate::{ 7 | obj_props, optimize::is_start_group, ReservedIds, TriggerNetwork, TriggerRole, Triggerlist, 8 | }; 9 | 10 | // performes a DFS from each start group, and removes all triggers that: 11 | // - dont lead to an output trigger 12 | // - are not reachable from a start group 13 | 14 | pub fn dead_code_optimization( 15 | network: &mut TriggerNetwork, 16 | objects: &mut Triggerlist, 17 | //closed_group: &mut u16, 18 | reserved: &ReservedIds, 19 | ) { 20 | for (group, gang) in network.map.clone() { 21 | if is_start_group(group, reserved) { 22 | for (i, _) in gang.triggers.iter().enumerate() { 23 | let mut visited = Vec::new(); 24 | if check_for_dead_code( 25 | network, 26 | objects, 27 | (group, i), 28 | //closed_group, 29 | reserved, 30 | &mut visited, 31 | ) == DeadCodeResult::Keep 32 | { 33 | (*network.map.get_mut(&group).unwrap()).triggers[i].deleted = false; 34 | } 35 | } 36 | } 37 | } 38 | } 39 | 40 | #[derive(PartialEq, Eq)] 41 | enum DeadCodeResult { 42 | Keep, 43 | Delete, 44 | } 45 | 46 | #[must_use] 47 | fn check_for_dead_code<'a>( 48 | network: &'a mut TriggerNetwork, 49 | objects: &mut Triggerlist, 50 | start: (Group, usize), 51 | //closed_group: &mut u16, 52 | reserved: &ReservedIds, 53 | visited_stack: &mut Vec<(Group, usize)>, 54 | ) -> DeadCodeResult { 55 | use DeadCodeResult::*; 56 | //returns whether to keep or delete the trigger 57 | let trigger = network.map[&start.0].triggers[start.1]; 58 | if !trigger.deleted { 59 | return Keep; 60 | } 61 | 62 | if trigger.role == TriggerRole::Output { 63 | if let Some(ObjParam::Group(Group { 64 | id: i @ Id::Arbitrary(_), 65 | })) = objects[trigger.obj].0.params.get(&obj_props::TARGET) 66 | { 67 | // if let Some(ObjParam::Number(id)) = objects[trigger.obj].0.params.get(&1) { 68 | // if matches!(*id as u16, obj_ids::TOGGLE | 1616) // toggle or stop trigger 69 | // && !reserved.object_groups.contains(i) 70 | // && !reserved.trigger_groups.contains(i) 71 | // { 72 | // dbg!(i); 73 | // return false; 74 | // } 75 | // } 76 | 77 | if !reserved.object_groups.contains(i) && !reserved.trigger_groups.contains(i) { 78 | return Delete; 79 | } 80 | } 81 | (*network.map.get_mut(&start.0).unwrap()).triggers[start.1].deleted = false; 82 | return Keep; 83 | } 84 | 85 | if visited_stack.contains(&start) { 86 | return Keep; // keep all loops 87 | } 88 | 89 | // if trigger is an output trigger, keep this branch 90 | 91 | let start_obj = &objects[trigger.obj].0.params; 92 | 93 | //println!("{}", network[&start.0].connections_in); 94 | 95 | let list: Vec<(usize, Group)> = 96 | if let Some(ObjParam::Group(g)) = start_obj.get(&obj_props::TARGET) { 97 | if is_start_group(*g, reserved) { 98 | //(*network.get_mut(&start.0).unwrap()).triggers[start.1].deleted = false; 99 | return Keep; 100 | } else if let Some(gang) = network.map.get(g) { 101 | if gang.triggers.is_empty() { 102 | return Delete; 103 | } 104 | 105 | vec![*g; gang.triggers.len()] 106 | .iter() 107 | .copied() 108 | .enumerate() 109 | .collect() 110 | } else { 111 | //dangling 112 | 113 | return Delete; 114 | } 115 | } else { 116 | //dangling 117 | 118 | return Delete; 119 | }; 120 | 121 | let mut out = Delete; 122 | 123 | visited_stack.push(start); 124 | 125 | for (i, g) in list { 126 | let trigger_ptr = (g, i); 127 | 128 | if check_for_dead_code( 129 | network, 130 | objects, 131 | trigger_ptr, 132 | //closed_group, 133 | reserved, 134 | visited_stack, 135 | ) == Keep 136 | { 137 | (*network.map.get_mut(&trigger_ptr.0).unwrap()).triggers[trigger_ptr.1].deleted = false; 138 | out = Keep; 139 | } 140 | } 141 | 142 | assert_eq!(visited_stack.pop(), Some(start)); 143 | 144 | out 145 | } 146 | -------------------------------------------------------------------------------- /optimizer/src/lib.rs: -------------------------------------------------------------------------------- 1 | use compiler::{ 2 | builtins::{Group, Id}, 3 | compiler_types::{FunctionId, TriggerOrder}, 4 | leveldata::{self, GdObj, ObjParam}, 5 | }; 6 | use ahash::{AHashMap, AHashSet}; 7 | 8 | mod dead_code; 9 | mod group_toggling; 10 | pub mod optimize; 11 | mod spawn_optimisation; 12 | mod trigger_dedup; 13 | 14 | pub type Swaps = AHashMap; 15 | 16 | mod obj_ids { 17 | #![allow(dead_code)] 18 | pub const MOVE: u16 = 901; 19 | pub const ROTATE: u16 = 1346; 20 | pub const ANIMATE: u16 = 1585; 21 | pub const PULSE: u16 = 1006; 22 | pub const COUNT: u16 = 1611; 23 | pub const ALPHA: u16 = 1007; 24 | pub const TOGGLE: u16 = 1049; 25 | pub const FOLLOW: u16 = 1347; 26 | pub const SPAWN: u16 = 1268; 27 | pub const STOP: u16 = 1616; 28 | pub const TOUCH: u16 = 1595; 29 | pub const INSTANT_COUNT: u16 = 1811; 30 | pub const ON_DEATH: u16 = 1812; 31 | pub const FOLLOW_PLAYER_Y: u16 = 1814; 32 | pub const COLLISION: u16 = 1815; 33 | pub const PICKUP: u16 = 1817; 34 | pub const BG_EFFECT_ON: u16 = 1818; 35 | pub const BG_EFFECT_OFF: u16 = 1819; 36 | pub const SHAKE: u16 = 1520; 37 | pub const COLOR: u16 = 899; 38 | pub const ENABLE_TRAIL: u16 = 32; 39 | pub const DISABLE_TRAIL: u16 = 33; 40 | pub const HIDE: u16 = 1612; 41 | pub const SHOW: u16 = 1613; 42 | } 43 | 44 | pub mod obj_props { 45 | pub const TARGET: u16 = 51; 46 | pub const GROUPS: u16 = 57; 47 | pub const ACTIVATE_GROUP: u16 = 56; 48 | } 49 | 50 | #[derive(Debug, Copy, Clone, Eq, PartialEq, Hash, PartialOrd, Ord)] 51 | pub enum TriggerRole { 52 | // Spawn triggers have their own category 53 | // because they can be combined by adding their delays 54 | Spawn, 55 | 56 | // Triggers like move and rotate, which have some output in the level 57 | // and therefore cannot be optimized away 58 | Output, 59 | 60 | // Triggers that send a signal, but don't cause any side effects 61 | Func, 62 | } 63 | 64 | #[derive(Debug)] 65 | pub struct ReservedIds { 66 | pub object_groups: AHashSet, 67 | pub trigger_groups: AHashSet, // only includes the obj_props::GROUPS prop 68 | 69 | pub object_colors: AHashSet, 70 | 71 | pub object_blocks: AHashSet, 72 | 73 | pub object_items: AHashSet, 74 | } 75 | 76 | impl ReservedIds { 77 | pub fn from_objects(objects: &[GdObj], func_ids: &[FunctionId]) -> Self { 78 | let mut reserved = ReservedIds { 79 | object_groups: Default::default(), 80 | trigger_groups: Default::default(), 81 | object_colors: Default::default(), 82 | 83 | object_blocks: Default::default(), 84 | 85 | object_items: Default::default(), 86 | }; 87 | for obj in objects { 88 | for param in obj.params.values() { 89 | match ¶m { 90 | leveldata::ObjParam::Group(g) => { 91 | reserved.object_groups.insert(g.id); 92 | } 93 | leveldata::ObjParam::GroupList(g) => { 94 | reserved.object_groups.extend(g.iter().map(|g| g.id)); 95 | } 96 | 97 | leveldata::ObjParam::Color(g) => { 98 | reserved.object_colors.insert(g.id); 99 | } 100 | 101 | leveldata::ObjParam::Block(g) => { 102 | reserved.object_blocks.insert(g.id); 103 | } 104 | 105 | leveldata::ObjParam::Item(g) => { 106 | reserved.object_items.insert(g.id); 107 | } 108 | _ => (), 109 | } 110 | } 111 | } 112 | 113 | for fn_id in func_ids { 114 | for (trigger, _) in &fn_id.obj_list { 115 | for (prop, param) in trigger.params.iter() { 116 | if *prop == 57 { 117 | match ¶m { 118 | leveldata::ObjParam::Group(g) => { 119 | reserved.trigger_groups.insert(g.id); 120 | } 121 | leveldata::ObjParam::GroupList(g) => { 122 | reserved.trigger_groups.extend(g.iter().map(|g| g.id)); 123 | } 124 | 125 | _ => (), 126 | } 127 | } 128 | } 129 | } 130 | } 131 | reserved 132 | } 133 | } 134 | 135 | #[derive(Debug, Copy, Clone, PartialEq, Eq, Hash, PartialOrd, Ord)] 136 | pub struct ObjPtr(usize, usize); 137 | // triggers connections in 138 | #[derive(Default)] 139 | pub struct TriggerNetwork { 140 | map: AHashMap, 141 | connectors: AHashMap>, 142 | } 143 | 144 | #[derive(Debug, Clone)] 145 | // what do you mean? its a trigger gang! 146 | pub struct TriggerGang { 147 | pub triggers: Vec, 148 | pub connections_in: u32, 149 | // whether any of the connections in are not instant count triggers 150 | pub non_spawn_triggers_in: bool, 151 | } 152 | 153 | impl TriggerGang { 154 | fn new(triggers: Vec) -> Self { 155 | TriggerGang { 156 | triggers, 157 | connections_in: 0, 158 | non_spawn_triggers_in: false, 159 | } 160 | } 161 | } 162 | 163 | #[derive(Debug, Copy, Clone, PartialEq, Eq, Hash, PartialOrd, Ord)] 164 | pub struct Trigger { 165 | pub obj: ObjPtr, 166 | pub role: TriggerRole, 167 | pub deleted: bool, 168 | } 169 | 170 | pub struct Triggerlist<'a> { 171 | list: &'a mut Vec, 172 | } 173 | 174 | impl<'a> std::ops::Index for Triggerlist<'a> { 175 | type Output = (GdObj, TriggerOrder); 176 | 177 | fn index(&self, i: ObjPtr) -> &Self::Output { 178 | &self.list[i.0].obj_list[i.1] 179 | } 180 | } 181 | impl<'a> std::ops::IndexMut for Triggerlist<'a> { 182 | fn index_mut(&mut self, i: ObjPtr) -> &mut Self::Output { 183 | &mut self.list[i.0].obj_list[i.1] 184 | } 185 | } 186 | 187 | pub fn get_role(obj: &GdObj) -> TriggerRole { 188 | if let Some(ObjParam::Number(obj_id)) = obj.params.get(&1) { 189 | let mut hd = false; 190 | if let Some(ObjParam::Bool(hd_val)) = obj.params.get(&103) { 191 | hd = *hd_val; 192 | } 193 | match *obj_id as u16 { 194 | obj_ids::SPAWN => { 195 | if let Some(ObjParam::Group(Group { 196 | id: Id::Specific(_), 197 | })) = obj.params.get(&obj_props::TARGET) 198 | { 199 | TriggerRole::Output 200 | } else if hd { 201 | TriggerRole::Func 202 | } else { 203 | TriggerRole::Spawn 204 | // let togglable = match obj.params.get(&obj_props::GROUPS) { 205 | // Some(ObjParam::GroupList(g)) => { 206 | // g.iter().any(|g| toggle_groups.toggles_off.contains_key(g)) 207 | // } 208 | // Some(ObjParam::Group(g)) => toggle_groups.toggles_off.contains_key(g), 209 | // _ => false, 210 | // }; 211 | 212 | // if togglable { 213 | // TriggerRole::Func 214 | // } else { 215 | // TriggerRole::Spawn 216 | // } 217 | } 218 | } 219 | obj_ids::TOUCH => { 220 | if let Some(ObjParam::Group(g)) = obj.params.get(&obj_props::TARGET) { 221 | if let Id::Specific(_) = g.id { 222 | // might interact with triggers in the editor 223 | TriggerRole::Output 224 | } else { 225 | TriggerRole::Func 226 | } 227 | } else { 228 | // the user didnt provide a target group, so fuck them no optimization for you >:D 229 | TriggerRole::Output 230 | } 231 | } 232 | obj_ids::COUNT | obj_ids::COLLISION | obj_ids::INSTANT_COUNT | obj_ids::ON_DEATH => { 233 | if let Some(ObjParam::Bool(false)) | None = 234 | obj.params.get(&obj_props::ACTIVATE_GROUP) 235 | { 236 | // will toggle a group off 237 | TriggerRole::Output 238 | } else if let Some(ObjParam::Group(g)) = obj.params.get(&obj_props::TARGET) { 239 | if let Id::Specific(_) = g.id { 240 | // might interact with triggers in the editor 241 | TriggerRole::Output 242 | } else { 243 | TriggerRole::Func 244 | } 245 | // else if toggle_groups.contains(g) { 246 | // // might toggle a group on 247 | // TriggerRole::Output 248 | // } 249 | } else { 250 | // the user didnt provide a target group, so fuck them no optimization for you >:D 251 | TriggerRole::Output 252 | } 253 | } 254 | _ => TriggerRole::Output, 255 | } 256 | } else { 257 | TriggerRole::Output 258 | } 259 | } 260 | 261 | pub const NO_GROUP: Group = Group { 262 | id: Id::Specific(0), 263 | }; 264 | -------------------------------------------------------------------------------- /optimizer/src/trigger_dedup.rs: -------------------------------------------------------------------------------- 1 | use crate::optimize::clean_network; 2 | use crate::Swaps; 3 | 4 | use crate::optimize::replace_groups; 5 | 6 | use crate::obj_ids; 7 | 8 | use crate::optimize::is_start_group; 9 | 10 | use compiler::builtins::Block; 11 | use compiler::builtins::Color; 12 | use compiler::builtins::Group; 13 | use compiler::builtins::Item; 14 | use compiler::compiler_types::TriggerOrder; 15 | 16 | use crate::ReservedIds; 17 | 18 | use crate::TriggerNetwork; 19 | 20 | use crate::TriggerGang; 21 | 22 | use crate::obj_props; 23 | 24 | use crate::Triggerlist; 25 | 26 | use crate::Trigger; 27 | 28 | use compiler::builtins::Id; 29 | 30 | use compiler::leveldata::ObjParam; 31 | 32 | pub(crate) fn param_identifier(param: &ObjParam) -> String { 33 | let str = match param { 34 | ObjParam::Group(Group { id }) 35 | | ObjParam::Color(Color { id }) 36 | | ObjParam::Block(Block { id }) 37 | | ObjParam::Item(Item { id }) => match id { 38 | Id::Specific(id) => format!("{}", id), 39 | Id::Arbitrary(id) => format!("?{}", id), 40 | }, 41 | ObjParam::Number(n) => { 42 | if (n.round() - n).abs() < 0.001 { 43 | format!("{}", *n as i32) 44 | } else { 45 | format!("{:.1$}", n, 3) 46 | } 47 | } 48 | ObjParam::Bool(b) => (if *b { "1" } else { "0" }).to_string(), 49 | ObjParam::Text(t) => t.to_string(), 50 | ObjParam::GroupList(list) => { 51 | let mut out = String::new(); 52 | 53 | for g in list { 54 | match g.id { 55 | Id::Specific(id) => out += &format!("{}.", id), 56 | Id::Arbitrary(id) => out += &format!("?{}.", id), 57 | } 58 | } 59 | out.pop(); 60 | out 61 | } 62 | ObjParam::Epsilon => "0.050".to_string(), 63 | }; 64 | str 65 | // use std::collections::hash_map::DefaultHasher; 66 | // use std::hash::{Hash, Hasher}; 67 | 68 | // let mut hasher = DefaultHasher::new(); 69 | 70 | // str.hash(&mut hasher); 71 | // hasher.finish() 72 | } 73 | use std::cmp::Ordering; 74 | use std::collections::BTreeSet; 75 | #[derive(Debug, PartialEq, Eq, Hash)] 76 | pub(crate) struct TriggerParam(u16, String); 77 | 78 | impl PartialOrd for TriggerParam { 79 | fn partial_cmp(&self, other: &Self) -> Option { 80 | let first = self.0.cmp(&other.0); 81 | Some(if first == Ordering::Equal { 82 | self.1.cmp(&other.1) 83 | } else { 84 | first 85 | }) 86 | } 87 | } 88 | 89 | impl Ord for TriggerParam { 90 | fn cmp(&self, other: &Self) -> Ordering { 91 | self.partial_cmp(other).unwrap() 92 | } 93 | } 94 | 95 | #[derive(Debug)] 96 | pub(crate) struct TriggerBehavior(BTreeSet, i64); 97 | 98 | impl PartialEq for TriggerBehavior { 99 | fn eq(&self, other: &Self) -> bool { 100 | self.0 == other.0 101 | } 102 | } 103 | 104 | impl Eq for TriggerBehavior {} 105 | 106 | impl Ord for TriggerBehavior { 107 | fn cmp(&self, other: &Self) -> Ordering { 108 | self.partial_cmp(other).unwrap() 109 | } 110 | } 111 | 112 | // TODO: make this sort by trigger order as well 113 | impl PartialOrd for TriggerBehavior { 114 | fn partial_cmp(&self, other: &Self) -> Option { 115 | let order_cmp = self.1.partial_cmp(&other.1).unwrap(); 116 | if order_cmp != Ordering::Equal { 117 | return Some(order_cmp); 118 | } 119 | 120 | let mut iter1 = self.0.iter(); 121 | let mut iter2 = other.0.iter(); 122 | loop { 123 | if let Some(val1) = iter1.next() { 124 | if let Some(val2) = iter2.next() { 125 | let cmp = val1.cmp(val2); 126 | if cmp != Ordering::Equal { 127 | return Some(cmp); 128 | } 129 | } else { 130 | return Some(Ordering::Greater); 131 | } 132 | } else { 133 | return Some(Ordering::Less); 134 | } 135 | } 136 | } 137 | } 138 | 139 | pub(crate) fn get_trigger_behavior(t: Trigger, objects: &Triggerlist) -> TriggerBehavior { 140 | let mut set = BTreeSet::new(); 141 | for (prop, param) in &objects[t.obj].0.params { 142 | if *prop == obj_props::GROUPS { 143 | // group 144 | continue; 145 | } 146 | set.insert(TriggerParam(*prop, param_identifier(param))); 147 | } 148 | TriggerBehavior(set, (objects[t.obj].1 .0 * 100000.0) as i64) 149 | } 150 | 151 | #[derive(Debug, PartialEq, Eq, PartialOrd, Ord)] 152 | pub(crate) struct TriggerGangBehavior(BTreeSet); 153 | 154 | pub(crate) fn get_triggergang_behavior( 155 | gang: &TriggerGang, 156 | objects: &Triggerlist, 157 | ) -> TriggerGangBehavior { 158 | let mut set = BTreeSet::new(); 159 | 160 | for trigger in &gang.triggers { 161 | set.insert(get_trigger_behavior(*trigger, objects)); 162 | } 163 | 164 | TriggerGangBehavior(set) 165 | } 166 | 167 | pub(crate) fn dedup_triggers( 168 | network: &mut TriggerNetwork, 169 | objects: &mut Triggerlist, 170 | reserved: &ReservedIds, 171 | ) { 172 | loop { 173 | let mut swaps = Swaps::default(); 174 | let mut representative_groups = Vec::<(TriggerGangBehavior, Group, TriggerOrder)>::new(); 175 | 176 | for (group, gang) in network.map.iter_mut() { 177 | if is_start_group(*group, reserved) { 178 | continue; 179 | } 180 | let contains_stackable_trigger = gang.triggers.iter().any(|t| { 181 | let obj = &objects[t.obj].0; 182 | if let Some(ObjParam::Number(n)) = obj.params.get(&1) { 183 | let id = *n as u16; 184 | id == obj_ids::MOVE || id == 1817 185 | } else { 186 | false 187 | } 188 | }); 189 | if contains_stackable_trigger { 190 | continue; 191 | } 192 | let behavior = get_triggergang_behavior(gang, objects); 193 | 194 | let mut found = false; 195 | for (b, repr, order) in representative_groups.iter() { 196 | if b == &behavior { 197 | for trigger in &mut gang.triggers { 198 | (*trigger).deleted = true; 199 | } 200 | //dbg!(behavior, repr, group, &representative_groups); 201 | assert!(swaps.insert(*group, (*repr, *order)).is_none()); 202 | 203 | found = true; 204 | break; 205 | } 206 | } 207 | if !found { 208 | let mut order = TriggerOrder(0.0); 209 | for o in gang.triggers.iter().map(|t| objects[t.obj].1 .0) { 210 | if o > order.0 { 211 | order = TriggerOrder(o); 212 | } 213 | } 214 | representative_groups.push((behavior, *group, order)); 215 | } 216 | } 217 | 218 | //dbg!(&swaps); 219 | 220 | if swaps.is_empty() { 221 | break; 222 | } 223 | replace_groups(swaps, objects); 224 | clean_network(network, objects, false); 225 | } 226 | } 227 | -------------------------------------------------------------------------------- /parser/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "parser" 3 | version = "0.0.8" 4 | edition = "2021" 5 | 6 | # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html 7 | 8 | [dependencies] 9 | 10 | logos = "0.12.0" 11 | internment = "0.5.4" 12 | ariadne = "0.1.3" # errors 13 | ahash = "0.7.6" 14 | base64 = "0.13.0" 15 | 16 | shared = { path = "../shared" } 17 | errors = { path = "../errors" } 18 | -------------------------------------------------------------------------------- /parser/src/lib.rs: -------------------------------------------------------------------------------- 1 | pub mod ast; 2 | pub mod fmt; 3 | pub mod parser; 4 | -------------------------------------------------------------------------------- /pckp/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "pckp" 3 | version = "0.1.0" 4 | edition = "2021" 5 | 6 | # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html 7 | [dependencies] 8 | yaml-rust = "0.4" 9 | fs_extra = "1.2.0" 10 | git2 = "0.13" 11 | reqwest = {version = "0.11.6", features = ["blocking"]} 12 | lazy_static = "1.4.0" 13 | 14 | # only on non wasm 15 | [target.'cfg(not(target_arch = "wasm32"))'.dependencies] 16 | path-absolutize = "3.0" -------------------------------------------------------------------------------- /pckp/src/download.rs: -------------------------------------------------------------------------------- 1 | use crate::package::DependencySource; 2 | use std::collections::HashMap; 3 | 4 | use crate::error::PckpError; 5 | use lazy_static::lazy_static; 6 | use reqwest; 7 | 8 | //TODO: replace with real repo 9 | pub const MAIN_REPO: &str = "https://raw.githubusercontent.com/camila314/ttest/master/index.txt"; 10 | 11 | lazy_static! { 12 | static ref REPO_CACHE: HashMap = { 13 | reqwest::blocking::get(MAIN_REPO) 14 | .unwrap() 15 | .text() 16 | .unwrap() 17 | .replace(' ', "") 18 | .split('\n') 19 | .map(|x| x.split('|')) 20 | .map(|mut x| (x.next().unwrap().to_string(), x.next().unwrap().to_string())) 21 | .collect::>() 22 | }; 23 | } 24 | 25 | fn find_in_repo(name: &str) -> Option<&String> { 26 | REPO_CACHE.get(name) 27 | } 28 | 29 | impl DependencySource { 30 | pub fn to_string(&self, parent_name: String) -> Result { 31 | match self { 32 | DependencySource::Url(a) => Ok(a.to_string()), 33 | DependencySource::Name(b) => match b.split('/').count().cmp(&2) { 34 | std::cmp::Ordering::Greater => { 35 | return Err(PckpError::custom( 36 | format!("Invalid dependency name '{}'", b), 37 | Some(parent_name), 38 | )); 39 | } 40 | std::cmp::Ordering::Equal => Ok("https://github.com/".to_string() + b), 41 | _ => match find_in_repo(b) { 42 | Some(x) => Ok(x.to_string()), 43 | None => Err(PckpError::custom( 44 | format!("Unable to locate dependency '{}' in pckp repo", b), 45 | Some(parent_name), 46 | )), 47 | }, 48 | }, 49 | } 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /pckp/src/error.rs: -------------------------------------------------------------------------------- 1 | use std::path::PathBuf; 2 | 3 | // replace this with spwn's error system 4 | 5 | pub enum PckpError { 6 | CustomError { 7 | message: String, 8 | note: Option, 9 | in_package: Option, 10 | }, 11 | ConfigError { 12 | message: String, 13 | note: Option, 14 | pos: Option<(usize, usize)>, 15 | file: PathBuf, 16 | }, 17 | } 18 | 19 | impl PckpError { 20 | pub fn custom_with_note( 21 | message: String, 22 | in_package: Option, 23 | note: Option, 24 | ) -> PckpError { 25 | PckpError::CustomError { 26 | message, 27 | note, 28 | in_package, 29 | } 30 | } 31 | pub fn custom(message: String, in_package: Option) -> PckpError { 32 | PckpError::CustomError { 33 | message, 34 | note: None, 35 | in_package, 36 | } 37 | } 38 | pub fn config(message: String, file: PathBuf, pos: Option<(usize, usize)>) -> PckpError { 39 | PckpError::ConfigError { 40 | message, 41 | note: None, 42 | pos, 43 | file, 44 | } 45 | } 46 | pub fn config_with_note( 47 | message: String, 48 | file: PathBuf, 49 | pos: Option<(usize, usize)>, 50 | note: Option, 51 | ) -> PckpError { 52 | PckpError::ConfigError { 53 | message, 54 | note, 55 | pos, 56 | file, 57 | } 58 | } 59 | // pub fn to_string(&self) -> String { 60 | // match self { 61 | // PckpError::CustomError { message, note, in_package } => { 62 | // let ipkg = if let Some(pkg) = &in_package { 63 | // format!(" in package '{}'", pkg) 64 | // } else { 65 | // String::new() 66 | // }; 67 | 68 | // let note = if let Some(n) = ¬e { 69 | // format!("\nNote: {}", n) 70 | // } else { 71 | // String::new() 72 | // }; 73 | 74 | // return format!("PckpError{}: {}{}", ipkg, message, note); 75 | // }, 76 | // PckpError::ConfigError {message, note, pos, file} => { 77 | // let note = if let Some(n) = ¬e { 78 | // format!("\nNote: {}", n) 79 | // } else { 80 | // String::new() 81 | // }; 82 | 83 | // let ps = if let Some(p) = &pos { 84 | // format!(" on line {} column {}", p.0, p.1) 85 | // } else { 86 | // String::new() 87 | // }; 88 | 89 | // return format!("PckpConfigError: {}\nError located in file {}{}{}", message, file.clone().into_os_string().into_string().unwrap(), ps, note); 90 | // } 91 | // } 92 | // } 93 | } 94 | 95 | impl std::fmt::Display for PckpError { 96 | fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result { 97 | match self { 98 | PckpError::CustomError { 99 | message, 100 | note, 101 | in_package, 102 | } => { 103 | let ipkg = if let Some(pkg) = &in_package { 104 | format!(" in package '{}'", pkg) 105 | } else { 106 | String::new() 107 | }; 108 | 109 | let note = if let Some(n) = ¬e { 110 | format!("\nNote: {}", n) 111 | } else { 112 | String::new() 113 | }; 114 | 115 | write!(f, "PckpError{}: {}{}", ipkg, message, note) 116 | } 117 | PckpError::ConfigError { 118 | message, 119 | note, 120 | pos, 121 | file, 122 | } => { 123 | let note = if let Some(n) = ¬e { 124 | format!("\nNote: {}", n) 125 | } else { 126 | String::new() 127 | }; 128 | 129 | let ps = if let Some(p) = &pos { 130 | format!(" on line {} column {}", p.0, p.1) 131 | } else { 132 | String::new() 133 | }; 134 | 135 | write!( 136 | f, 137 | "PckpConfigError: {}\nError located in file {}{}{}", 138 | message, 139 | file.clone().into_os_string().into_string().unwrap(), 140 | ps, 141 | note 142 | ) 143 | } 144 | } 145 | } 146 | } 147 | -------------------------------------------------------------------------------- /pckp/src/lib.rs: -------------------------------------------------------------------------------- 1 | pub mod config_file; 2 | pub mod error; 3 | pub mod package; 4 | pub mod version; 5 | pub mod download; -------------------------------------------------------------------------------- /pckp/src/package.rs: -------------------------------------------------------------------------------- 1 | use std::fs; 2 | use std::path::PathBuf; 3 | 4 | use git2::Repository; 5 | 6 | use crate::config_file::{config_to_package, get_config}; 7 | use crate::error::PckpError; 8 | use crate::version::{export_version, get_version_file, import_version}; 9 | 10 | use fs_extra::dir as fs_dir; 11 | 12 | pub const PACKAGE_DIR: &str = "pckp_libraries"; 13 | 14 | #[derive(PartialEq, Clone, Debug)] 15 | pub enum DependencySource { 16 | Name(String), 17 | Url(String), 18 | } 19 | 20 | #[derive(Clone, PartialEq, Debug)] 21 | pub struct Dependency { 22 | pub source: DependencySource, 23 | pub version: String, 24 | } 25 | 26 | #[derive(Clone, PartialEq, Debug)] 27 | pub struct LocalPackage { 28 | pub name: String, 29 | pub version: String, 30 | pub paths: Vec, 31 | pub dependencies: Vec, 32 | } 33 | 34 | #[derive(Clone, PartialEq, Debug)] 35 | pub enum PackageType { 36 | Local(LocalPackage), 37 | External(Dependency), 38 | } 39 | 40 | #[derive(Clone, PartialEq, Debug)] 41 | pub struct Package { 42 | internal: PackageType, 43 | } 44 | 45 | impl Package { 46 | pub fn local( 47 | name: String, 48 | version: String, 49 | paths: Vec, 50 | dependencies: Vec, 51 | ) -> Package { 52 | Package { 53 | internal: PackageType::Local(LocalPackage { 54 | name, 55 | version, 56 | paths, 57 | dependencies, 58 | }), 59 | } 60 | } 61 | 62 | pub fn dependency(dep: Dependency) -> Package { 63 | Package { 64 | internal: PackageType::External(dep), 65 | } 66 | } 67 | 68 | pub fn install_dependencies(&self, path: PathBuf) -> Result<(), PckpError> { 69 | match &self.internal { 70 | PackageType::Local(root) => { 71 | for x in &root.dependencies { 72 | x.install(&root.name, path.clone(), false)?; 73 | } 74 | Ok(()) 75 | } 76 | _ => unreachable!("ensure_local"), 77 | } 78 | } 79 | 80 | #[allow(dead_code)] 81 | fn get_version(&self) -> String { 82 | match &self.internal { 83 | PackageType::Local(p) => p.version.clone(), 84 | PackageType::External(d) => d.version.clone(), 85 | } 86 | } 87 | pub fn install( 88 | &self, 89 | parent_name: &str, 90 | path: PathBuf, 91 | ignore_version: bool, 92 | ) -> Result<(), PckpError> { 93 | match &self.internal { 94 | PackageType::Local(p) => { 95 | /*for folder in &p.paths { 96 | // folder guaranteed to exist from config parser 97 | 98 | }*/ 99 | let mut dest = path.clone(); 100 | dest.push(PACKAGE_DIR); 101 | 102 | if !dest.exists() { 103 | fs::create_dir(&dest).unwrap(); 104 | } 105 | 106 | let version_file = get_version_file(path.clone()); 107 | let mut version_info = import_version(&version_file); 108 | 109 | if !version_info 110 | .iter() 111 | .any(|(n, v)| n == &p.name && v == &p.version) 112 | { 113 | println!("Installing {}", p.name); 114 | 115 | let new_path = if ignore_version { 116 | p.name.to_string() 117 | } else { 118 | format!("{}@{}", p.name, p.version) 119 | }; 120 | dest.push(new_path); 121 | 122 | for folder in &p.paths { 123 | let mut opts = fs_dir::CopyOptions::new(); 124 | opts.content_only = true; 125 | fs_dir::copy(folder, &dest, &opts).unwrap(); 126 | } 127 | 128 | version_info.push((p.name.clone(), p.version.clone())); 129 | //println!("package {:#?}", p); 130 | } 131 | 132 | for dep in &p.dependencies { 133 | dep.install(&p.name, path.clone(), false)?; 134 | } 135 | 136 | export_version(version_info, &version_file); 137 | Ok(()) 138 | } 139 | PackageType::External(d) => { 140 | let source_url = d.source.to_string(parent_name.to_string())?; 141 | 142 | let tmp_path = PathBuf::from(".pckp_tmp"); 143 | 144 | if tmp_path.exists() { 145 | fs::remove_dir_all(&tmp_path).unwrap(); 146 | } 147 | let repo = match Repository::clone(&source_url, &tmp_path) { 148 | Ok(repo) => repo, 149 | Err(e) => { 150 | return Err(PckpError::custom( 151 | format!("Unable to clone package '{}'. Reason: {}", source_url, e), 152 | Some(parent_name.to_string()), 153 | )) 154 | } 155 | }; 156 | 157 | if d.version != "latest" { 158 | let tag_object = 159 | match repo.revparse_single(&("refs/tags/".to_string() + &d.version)) { 160 | Ok(x) => x, 161 | Err(_) => { 162 | return Err(PckpError::custom( 163 | format!( 164 | "Unable to find version {} for package {}", 165 | d.version, source_url 166 | ), 167 | Some(parent_name.to_string()), 168 | )) 169 | } 170 | }; 171 | 172 | repo.checkout_tree(&tag_object, None).unwrap(); 173 | } 174 | 175 | let cfg_dir = get_config(Some(tmp_path)); 176 | let local_package = if cfg_dir.exists() { 177 | config_to_package(cfg_dir)?.unwrap() 178 | } else { 179 | return Err(PckpError::custom( 180 | format!("Package at {} does not have config file", source_url), 181 | Some(parent_name.to_string()), 182 | )); 183 | }; 184 | 185 | local_package.install(parent_name, path, d.version == "latest") 186 | //todo!("download and stuff"); 187 | } 188 | } 189 | } 190 | } 191 | -------------------------------------------------------------------------------- /pckp/src/version.rs: -------------------------------------------------------------------------------- 1 | use crate::package::PACKAGE_DIR; 2 | use std::fs; 3 | use std::path::{Path, PathBuf}; 4 | 5 | pub const VERSION_FILE_NAME: &str = ".vrsn"; 6 | type VersionFile = Vec<(String, String)>; 7 | 8 | pub fn get_version_file(mut pckp_dir: PathBuf) -> PathBuf { 9 | pckp_dir.push(PACKAGE_DIR); 10 | pckp_dir.push(VERSION_FILE_NAME); 11 | pckp_dir 12 | } 13 | 14 | pub fn export_version(version: VersionFile, path: &Path) { 15 | let output = version 16 | .into_iter() 17 | .map(|(n, v)| format!("{}:{}", n, v)) 18 | .collect::>() 19 | .join(","); 20 | fs::write(path, output).unwrap(); 21 | } 22 | 23 | pub fn import_version(path: &Path) -> VersionFile { 24 | if !path.exists() { 25 | export_version(VersionFile::new(), path); 26 | } 27 | let a = fs::read(path).unwrap(); 28 | 29 | if a.is_empty() { 30 | return VersionFile::new(); 31 | } 32 | 33 | let input = std::str::from_utf8(&a).unwrap(); 34 | input 35 | .split(',') 36 | .into_iter() 37 | .map(|x| { 38 | let mut b = x.split(':'); 39 | (b.next().unwrap().to_string(), b.next().unwrap().to_string()) 40 | }) 41 | .collect::() 42 | } 43 | -------------------------------------------------------------------------------- /shared/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "shared" 3 | version = "0.0.8" 4 | edition = "2021" 5 | 6 | # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html 7 | 8 | [dependencies] 9 | internment = "0.5.4" 10 | slotmap = "1.0.6" -------------------------------------------------------------------------------- /shared/src/lib.rs: -------------------------------------------------------------------------------- 1 | use std::path::PathBuf; 2 | 3 | use internment::LocalIntern; 4 | use slotmap::new_key_type; 5 | new_key_type! { 6 | pub struct StoredValue; 7 | } //index to stored value in globals.stored_values 8 | pub type FileRange = (usize, usize); 9 | 10 | #[derive(PartialEq, Eq, Debug, Clone, Hash)] 11 | pub enum ImportType { 12 | Script(PathBuf), 13 | Lib(String), 14 | } 15 | 16 | #[derive(Debug, Clone, Copy, PartialEq)] 17 | pub enum BreakType { 18 | // used for return statements 19 | Macro(Option, bool), 20 | // used for Break statements 21 | Loop, 22 | // used for continue statements 23 | ContinueLoop, 24 | // used for switch cases 25 | Switch(StoredValue), 26 | // used for contexts 27 | } 28 | #[derive(Debug, PartialEq, Eq, Hash, Clone)] 29 | pub enum SpwnSource { 30 | File(PathBuf), 31 | BuiltIn(PathBuf), 32 | String(LocalIntern), 33 | } 34 | -------------------------------------------------------------------------------- /spwn-web/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "spwn-web" 3 | version = "0.1.0" 4 | edition = "2021" 5 | 6 | # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html 7 | 8 | [lib] 9 | crate-type = ["cdylib"] 10 | 11 | [dependencies] 12 | wasm-bindgen = "*" 13 | console_error_panic_hook = "0.1.6" 14 | spwn = { path = "../spwn" } 15 | js-sys = "0.3.55" -------------------------------------------------------------------------------- /spwn-web/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | SPWN Playground 7 | 8 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 21 | 159 | 160 | 161 | 162 | 163 | 164 | 165 | 166 | 167 | 168 | 169 | 170 |
171 | 172 | 173 |

174 | 
175 | 
176 | 
177 | 
178 |   
179 |   
180 |
181 | level string 182 |
183 |
184 | 185 | 186 | 187 | 188 |
189 | 240 | 241 | 242 | 243 | -------------------------------------------------------------------------------- /spwn-web/src/lib.rs: -------------------------------------------------------------------------------- 1 | use std::panic; 2 | 3 | use wasm_bindgen::prelude::*; 4 | 5 | #[wasm_bindgen] 6 | pub fn init_panics() { 7 | panic::set_hook(Box::new(console_error_panic_hook::hook)); 8 | } 9 | 10 | fn js_array(values: Vec) -> JsValue { 11 | JsValue::from( 12 | values 13 | .into_iter() 14 | .map(|x| JsValue::from_str(&x)) 15 | .collect::(), 16 | ) 17 | } 18 | 19 | #[wasm_bindgen] 20 | pub fn run_spwn(code: &str, optimize: bool) -> JsValue { 21 | let output = spwn::run_spwn(code.to_string(), Vec::new(), optimize); 22 | js_array(match output { 23 | Ok(a) => a.to_vec(), 24 | Err(e) => vec![e, String::new()], 25 | }) 26 | } 27 | 28 | // #[wasm_bindgen] 29 | // extern "C" { 30 | // pub fn alert(s: &str); 31 | // } 32 | 33 | // #[wasm_bindgen] 34 | // pub fn greet(name: &str) { 35 | // unsafe { 36 | // alert(&format!("Hello, {}!", name)); 37 | // } 38 | // } 39 | -------------------------------------------------------------------------------- /spwn/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "spwn" 3 | version = "0.0.8" 4 | authors = ["Spu7Nix "] 5 | edition = "2021" 6 | description = "A language for Geometry Dash triggers" 7 | readme = "README.md" 8 | homepage = "https://spu7nix.net/spwn/" 9 | repository = "https://github.com/Spu7Nix/SPWN-language/" 10 | license-file = "LICENSE" 11 | 12 | 13 | # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html 14 | 15 | [[bin]] 16 | name = "spwn" 17 | path = "src/main.rs" 18 | 19 | [target.'cfg(target_arch = "wasm32")'.dependencies] 20 | parking_lot = { version = "0.11", features = ["wasm-bindgen"] } 21 | 22 | [dependencies] 23 | 24 | termcolor = "1.1.2" 25 | clap = "3.0.5" 26 | 27 | internment = "0.5.4" 28 | ariadne = "0.1.3" # errors 29 | 30 | 31 | parser = { path = "../parser" } 32 | shared = { path = "../shared" } 33 | errors = { path = "../errors" } 34 | editorlive = { path = "../editorlive" } 35 | compiler = { path = "../compiler" } 36 | optimizer = { path = "../optimizer" } 37 | docgen = { path = "../docgen" } 38 | 39 | [dev-dependencies] 40 | criterion = "0.3.5" 41 | 42 | [[bench]] 43 | harness = false 44 | name = "time" 45 | 46 | [target.'cfg(not(target_arch = "wasm32"))'.dependencies] 47 | levelstring = { path = "../levelstring" } 48 | pckp = { path = "../pckp" } 49 | 50 | # heat dir libraries -o wix/libraries.wxs -scom -frag -srd -sreg -gg -cg libraries -dr LIB_DIR -suid 51 | -------------------------------------------------------------------------------- /spwn/benches/time.rs: -------------------------------------------------------------------------------- 1 | use criterion::{criterion_group, criterion_main, Criterion, Throughput}; 2 | use internment::{LocalIntern}; 3 | use shared::SpwnSource; 4 | use std::fs; 5 | use spwn::parse_spwn; 6 | 7 | fn test_all(input: &str, src: SpwnSource) { 8 | parse_spwn(input.to_string(), src, &[]).unwrap(); 9 | } 10 | 11 | fn criterion_benchmark(c: &mut Criterion) { 12 | let input = fs::read_to_string("test/test.spwn").unwrap(); 13 | let src = LocalIntern::new("".to_string()); 14 | 15 | let mut bmg = c.benchmark_group("main"); 16 | bmg.throughput(Throughput::Bytes(input.len() as u64)); 17 | bmg.bench_function("test_all", |b| b.iter(|| test_all(&input, SpwnSource::String(src)))); 18 | } 19 | 20 | criterion_group!(benches, criterion_benchmark); 21 | criterion_main!(benches); 22 | -------------------------------------------------------------------------------- /spwn/src/lib.rs: -------------------------------------------------------------------------------- 1 | pub use ::compiler::builtins; 2 | use ::compiler::builtins::BUILTIN_NAMES; 3 | 4 | use ::compiler::builtins::get_lib_file; 5 | pub use ::compiler::compiler; 6 | pub use ::compiler::compiler_types; 7 | pub use ::compiler::context; 8 | pub use ::compiler::globals; 9 | pub use ::compiler::leveldata; 10 | pub use ::compiler::value; 11 | pub use ::compiler::value_storage; 12 | pub use ::docgen::documentation; 13 | pub use ::parser::ast; 14 | use ariadne::Source; 15 | use errors::create_report; 16 | use errors::ErrorReport; 17 | use internment::LocalIntern; 18 | 19 | pub use ::compiler::STD_PATH; 20 | 21 | pub use ::parser::fmt; 22 | pub use ::parser::parser; 23 | pub use ::parser::parser::parse_spwn; 24 | 25 | pub use errors; 26 | pub use errors::compiler_info; 27 | pub use shared; 28 | use shared::SpwnSource; 29 | 30 | use std::collections::hash_map::Entry; 31 | use std::collections::HashMap; 32 | use std::fs; 33 | use std::path::PathBuf; 34 | 35 | #[derive(Default)] 36 | pub struct SpwnCache { 37 | files: HashMap, 38 | } 39 | 40 | impl ariadne::Cache for SpwnCache { 41 | fn fetch(&mut self, source: &SpwnSource) -> Result<&Source, Box> { 42 | Ok(match self.files.entry(source.clone()) { 43 | Entry::Occupied(entry) => entry.into_mut(), 44 | Entry::Vacant(entry) => entry.insert(Source::from(match source { 45 | SpwnSource::File(path) => fs::read_to_string(path).map_err(|e| Box::new(e) as _)?, 46 | SpwnSource::BuiltIn(path) => match get_lib_file(path) { 47 | Some(file) => match file.contents_utf8() { 48 | Some(c) => c.to_string(), 49 | None => return Err(Box::new("Invalid built in file content")), 50 | }, 51 | _ => return Err(Box::new("Could not find built in file")), 52 | }, 53 | SpwnSource::String(a) => a.as_ref().clone(), 54 | })), 55 | }) 56 | } 57 | fn display<'a>(&self, source: &'a SpwnSource) -> Option> { 58 | match source { 59 | SpwnSource::File(path) | SpwnSource::BuiltIn(path) => Some(Box::new(path.display())), 60 | SpwnSource::String(_) => Some(Box::new("source")), 61 | } 62 | } 63 | } 64 | 65 | pub fn run_spwn( 66 | code: String, 67 | included: Vec, 68 | optimize: bool, 69 | ) -> Result<[String; 2], String> { 70 | let source = SpwnSource::String(LocalIntern::new(code.clone())); 71 | let cache = SpwnCache::default(); 72 | let (statements, notes) = match parse_spwn(code, source.clone(), BUILTIN_NAMES) { 73 | Ok(a) => a, 74 | Err(e) => { 75 | let mut out = Vec::::new(); 76 | create_report(ErrorReport::from(e)) 77 | .write(cache, &mut out) 78 | .unwrap(); 79 | return Err(String::from_utf8_lossy(&out).to_string()); 80 | } 81 | }; 82 | 83 | let mut std_out = Vec::::new(); 84 | 85 | let mut compiled = match compiler::compile_spwn( 86 | statements, 87 | source, 88 | included, 89 | notes, 90 | Default::default(), 91 | "".to_string(), 92 | &mut std_out, 93 | ) { 94 | Ok(a) => a, 95 | Err(e) => { 96 | let mut out = Vec::::new(); 97 | create_report(ErrorReport::from(e)) 98 | .write(cache, &mut out) 99 | .unwrap(); 100 | return Err(String::from_utf8_lossy(&out).to_string()); 101 | } 102 | }; 103 | 104 | let has_stuff = compiled.func_ids.iter().any(|x| !x.obj_list.is_empty()); 105 | 106 | let reserved = optimizer::ReservedIds::from_objects(&compiled.objects, &compiled.func_ids); 107 | 108 | if has_stuff && optimize { 109 | compiled.func_ids = 110 | optimizer::optimize::optimize(compiled.func_ids, compiled.closed_groups, reserved); 111 | } 112 | 113 | let mut objects = leveldata::apply_fn_ids(&compiled.func_ids); 114 | 115 | objects.extend(compiled.objects); 116 | 117 | let (new_ls, _) = leveldata::append_objects(objects, &String::new())?; 118 | 119 | Ok([String::from_utf8_lossy(&std_out).to_string(), new_ls]) 120 | } 121 | #[cfg(test)] 122 | mod tests; 123 | 124 | #[test] 125 | pub fn run_all_doc_examples() { 126 | use shared::ImportType; 127 | use std::str::FromStr; 128 | 129 | let mut globals_path = std::env::current_dir().unwrap(); 130 | globals_path.push("temp"); // this folder doesn't actually exist, but it needs to be there because .parent is called in import_module 131 | let mut std_out = Vec::::new(); 132 | 133 | let permissions = builtins::BuiltinPermissions::new(); 134 | 135 | let mut globals = globals::Globals::new( 136 | SpwnSource::File(globals_path), 137 | permissions.clone(), 138 | String::from(""), 139 | &mut std_out, 140 | ); 141 | globals.includes.push(PathBuf::from("./")); 142 | 143 | let mut start_context = context::FullContext::new(&globals); 144 | 145 | let info = compiler_info::CompilerInfo::new(); 146 | 147 | compiler::import_module( 148 | &ImportType::Lib(STD_PATH.to_string()), 149 | &mut start_context, 150 | &mut globals, 151 | info, 152 | false, 153 | ) 154 | .unwrap(); 155 | 156 | let exports = globals.stored_values[start_context.inner().return_value].clone(); 157 | 158 | if let value::Value::Dict(d) = &exports { 159 | for (a, b, c) in d.iter().map(|(k, v)| (*k, *v, -1)) { 160 | start_context.inner().new_redefinable_variable(a, b, c) 161 | } 162 | } else { 163 | panic!("The standard library must return a dictionary"); 164 | } 165 | 166 | let implementations = globals.implementations.clone(); 167 | 168 | let mut all_tests = Vec::new(); 169 | 170 | add_tests("std".to_string(), exports, &globals, &mut all_tests); 171 | 172 | for (typ, dict) in implementations { 173 | let type_name = value::find_key_for_value(&globals.type_ids, typ).unwrap(); 174 | for (name, (val, _)) in dict { 175 | let val = globals.stored_values[val].clone(); 176 | add_tests( 177 | format!("@{}::{}", type_name, name), 178 | val, 179 | &globals, 180 | &mut all_tests, 181 | ); 182 | } 183 | } 184 | 185 | for (name, code) in builtins::BUILTIN_EXAMPLES { 186 | if permissions.is_allowed(builtins::Builtin::from_str(*name).unwrap()) { 187 | all_tests.push((format!("$.{}", name), code.to_string())); 188 | } 189 | } 190 | 191 | let mut all_failed_tests = Vec::new(); 192 | 193 | //let storage = globals.stored_values.clone(); 194 | 195 | for (name, code) in all_tests { 196 | //println!("Running test: `\n{}\n`", code); 197 | 198 | let source = SpwnSource::String(LocalIntern::new(code.clone())); 199 | let cache = SpwnCache::default(); 200 | 201 | let info = compiler_info::CompilerInfo { 202 | ..compiler_info::CompilerInfo::from_area(errors::compiler_info::CodeArea { 203 | file: LocalIntern::new(source.clone()), 204 | pos: (0, 0), 205 | }) 206 | }; 207 | 208 | let (statements, _) = match parse_spwn(code, source.clone(), BUILTIN_NAMES) { 209 | Ok(a) => a, 210 | Err(e) => { 211 | let mut out = Vec::::new(); 212 | create_report(ErrorReport::from(e)) 213 | .write(cache, &mut out) 214 | .unwrap(); 215 | all_failed_tests.push((name, String::from_utf8_lossy(&out).to_string())); 216 | continue; 217 | } 218 | }; 219 | 220 | //globals.stored_values = storage.clone(); 221 | globals.objects.clear(); 222 | 223 | let mut contexts = start_context.clone(); 224 | contexts.inner().root_context_ptr = &mut contexts; 225 | 226 | match compiler::compile_scope(&statements, &mut contexts, &mut globals, info.clone()) { 227 | Ok(_) => (), 228 | Err(e) => { 229 | let mut out = Vec::::new(); 230 | create_report(ErrorReport::from(e)) 231 | .write(cache, &mut out) 232 | .unwrap(); 233 | all_failed_tests.push((name, String::from_utf8_lossy(&out).to_string())); 234 | continue; 235 | } 236 | } 237 | } 238 | 239 | if !all_failed_tests.is_empty() { 240 | eprintln!( 241 | "{} examples from the STD failed to compile:", 242 | all_failed_tests.len() 243 | ); 244 | for (name, err) in all_failed_tests { 245 | eprintln!("{}:\n{}\n", name, err); 246 | } 247 | panic!("Some examples failed to compile"); 248 | } 249 | } 250 | 251 | #[cfg(test)] 252 | fn add_tests( 253 | name: String, 254 | val: value::Value, 255 | globals: &globals::Globals, 256 | all_tests: &mut Vec<(String, String)>, 257 | ) { 258 | match val { 259 | value::Value::Macro(m) => { 260 | if let Some(example) = m.tag.get_example(true) { 261 | all_tests.push((name, example)); 262 | } 263 | } 264 | value::Value::Dict(d) => { 265 | for (prop_name, v) in d.iter() { 266 | add_tests( 267 | format!("{}.{}", name, prop_name), 268 | globals.stored_values[*v].clone(), 269 | globals, 270 | all_tests, 271 | ); 272 | } 273 | } 274 | value::Value::Array(l) => { 275 | for (i, v) in l.iter().enumerate() { 276 | add_tests( 277 | format!("{}[{}]", name, i), 278 | globals.stored_values[*v].clone(), 279 | globals, 280 | all_tests, 281 | ); 282 | } 283 | } 284 | _ => (), 285 | }; 286 | } 287 | -------------------------------------------------------------------------------- /spwn/src/tests.rs: -------------------------------------------------------------------------------- 1 | #![allow(unused_variables)] 2 | 3 | use std::path::PathBuf; 4 | 5 | use crate::run_spwn; 6 | 7 | macro_rules! run_test { 8 | {$([$attr:ident])? NAME: $name:ident CODE: $code:literal $(OUTPUT: $output:literal)?} => { 9 | #[test] 10 | $(#[$attr])? 11 | fn $name() { 12 | let res = match run_spwn($code.to_string(), vec![PathBuf::from("./")], false) { 13 | Ok(a) => a, 14 | Err(e) => { 15 | eprintln!("{}", e); 16 | panic!("test {} failed, see message above", stringify!($name)); 17 | } 18 | }; 19 | $(assert_eq!(res[0].trim(), $output.trim());)? 20 | } 21 | }; 22 | } 23 | 24 | // just basic parsing 25 | run_test! { 26 | NAME: basic_parsing 27 | CODE: r" 28 | #[no_std] 29 | a = 1 30 | b = 2 31 | 32 | m = (a, let b, &c) => a + b + c 33 | m(1, 2, 3) 34 | 35 | () { 36 | m(1, 2, 3) 37 | } () 38 | 39 | m2 = (a, b, c) -> @number { 40 | return a + b + c 41 | } 42 | " 43 | } 44 | 45 | // mutability 46 | run_test! { 47 | [should_panic] 48 | NAME: mutability1 49 | CODE: r" 50 | #[no_std] 51 | a = 1 52 | a += 1 53 | " 54 | } 55 | 56 | run_test! { 57 | [should_panic] 58 | NAME: mutability2 59 | CODE: r" 60 | #[no_std] 61 | a = 1 62 | m = (&b) { 63 | b += 1 64 | } 65 | m(a) 66 | " 67 | } 68 | 69 | // non-std things 70 | run_test! { 71 | NAME: print_basic 72 | CODE: r" 73 | #[no_std] 74 | $.print('Hello') 75 | $.print(r'Hello\nWorld') 76 | " 77 | OUTPUT: r" 78 | Hello 79 | Hello\nWorld 80 | " 81 | } 82 | 83 | run_test! { 84 | NAME: math 85 | CODE: r"#[no_std] $.print(10 * 2 + 2 * 3^2)" 86 | OUTPUT: r"38" 87 | } 88 | 89 | // std things 90 | 91 | // strings 92 | run_test! { 93 | NAME: string_fmt 94 | CODE: r" 95 | $.print('Hello {}!'.fmt('world')) 96 | " 97 | OUTPUT: r" 98 | Hello world! 99 | " 100 | } 101 | 102 | // arrays 103 | run_test! { 104 | NAME: arr 105 | CODE: r" 106 | arr = [1, 2, 3, 4] 107 | $.print(arr.filter(>2)) 108 | $.print(arr.filter(>=2)) 109 | $.print(arr.filter(a => a > 2)) 110 | $.print(arr.all(a => a > 0)) 111 | $.print(arr.all(>0)) 112 | " 113 | OUTPUT: r" 114 | [3, 4] 115 | [2, 3, 4] 116 | [3, 4] 117 | true 118 | true 119 | " 120 | } 121 | -------------------------------------------------------------------------------- /spwn/wix/commands.sh: -------------------------------------------------------------------------------- 1 | python spwn/wix/generate_wix_lib_file.py 2 | rustup override unset 3 | 4 | rustup default nightly-i686-pc-windows-msvc 5 | cargo install cargo-wix 6 | cargo wix spwn/Cargo.toml --nocapture --package spwn 7 | 8 | rustup default nightly-x86_64-pc-windows-msvc 9 | cargo install cargo-wix 10 | cargo wix spwn/Cargo.toml --nocapture --package spwn 11 | 12 | rustup override set nightly-x86_64-pc-windows-msvc 13 | -------------------------------------------------------------------------------- /spwn/wix/generate_wix_lib_file.py: -------------------------------------------------------------------------------- 1 | 2 | import os 3 | import uuid 4 | 5 | str = """ 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | """ 21 | 22 | for lib in os.listdir("./libraries"): 23 | str += f"\t\n" 24 | 25 | str += """ 26 | 27 | 28 | 29 | 30 | """ 31 | i = 0 32 | 33 | for lib in os.listdir("./libraries"): 34 | for file in os.listdir(f"./libraries/{lib}"): 35 | str += fr""" 36 | 37 | 38 | 39 | """ 40 | i += 1 41 | 42 | str += """ 43 | 44 | 45 | 46 | """ 47 | print(str) 48 | new_file = open("spwn/wix/libraries.wxs", mode="w") 49 | new_file.write(str) 50 | new_file.close() 51 | -------------------------------------------------------------------------------- /spwn/wix/libraries.wxs: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | 90 | 91 | 92 | 93 | 94 | 95 | 96 | 97 | 98 | 99 | 100 | 101 | 102 | 103 | 104 | 105 | 106 | 107 | 108 | 109 | 110 | 111 | 112 | 113 | 114 | 115 | 116 | 117 | 118 | 119 | 120 | 121 | 122 | 123 | 124 | 125 | 126 | 127 | 128 | 129 | 130 | 131 | -------------------------------------------------------------------------------- /spwn/wix/main.wxs: -------------------------------------------------------------------------------- 1 | 2 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 23 | 24 | 26 | 27 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 55 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | 90 | 91 | 92 | 93 | 95 | 96 | 100 | 101 | 102 | 103 | 104 | 107 | 108 | 109 | 110 | 111 | 112 | 113 | 114 | 115 | 116 | 117 | 118 | 119 | 120 | 121 | 122 | 123 | 129 | 130 | 131 | 132 | 133 | 142 | 143 | 144 | 145 | 146 | 147 | 157 | 1 158 | 1 159 | 160 | 161 | 162 | 163 | 168 | 169 | 170 | 171 | 179 | 180 | 181 | 182 | 190 | 191 | 192 | 193 | 194 | 195 | 196 | 197 | -------------------------------------------------------------------------------- /test/bf/bfold.spwn: -------------------------------------------------------------------------------- 1 | RIGHT = 1b 2 | LEFT = 2b 3 | INCR = 3b 4 | DECR = 4b 5 | DOT = 5b 6 | OPEN = 6b //opening bracket 7 | CLOSE = 7b //closing bracket 8 | 9 | spacing = 10 10 | 11 | insert_bf_script = (str: @string, offset: [@number]) { 12 | let index = 0 13 | for ch in str { 14 | blockid = match ch { 15 | ==">": RIGHT,//increment the data pointer (to point to the next cell to the right). 16 | =="<": LEFT, //decrement the data pointer (to point to the next cell to the left). 17 | =="+": INCR, //increment (increase by one) the byte at the data pointer. 18 | =="-": DECR, //decrement (decrease by one) the byte at the data pointer. 19 | ==".": DOT //output the byte at the data pointer. 20 | //==",": //accept one byte of input, storing its value in the byte at the data pointer. 21 | =="[": OPEN, 22 | =="]": CLOSE, 23 | else: null 24 | } 25 | if blockid != null { 26 | extract obj_props 27 | $.add(obj { 28 | OBJ_ID: 1816, 29 | X: offset[0] + index * spacing * 3, 30 | Y: offset[1], 31 | BLOCK_A: blockid, 32 | SCALING: 0.2, 33 | }) 34 | $.add(obj { 35 | OBJ_ID: 914, 36 | X: offset[0] + index * spacing * 3, 37 | Y: offset[1] - 30, 38 | TEXT: $.b64encode(ch), 39 | COLOR: 1c, 40 | }) 41 | index++ 42 | } 43 | } 44 | } 45 | 46 | type @bytedisplay 47 | 48 | impl @bytedisplay { 49 | new: (offset: [@number]) { 50 | 1c.set(255, 255, 255) 51 | //order = "!\"#$%&'()*+,-./0123456789:;<=>?@ABCDEFGHIJKLMNOPQRSTUVWXYZ[ ]^_`abcdefghijklmnopqrstuvwxyz{|}~" 52 | order = "ABCDEFGHIJKLMNOPQRSTUVWXYZ" 53 | let list = [] 54 | for ch in order { 55 | color = ?c 56 | extract obj_props 57 | $.add(obj { 58 | OBJ_ID: 914, 59 | X: offset[0], 60 | Y: offset[1], 61 | TEXT: $.b64encode(ch), 62 | COLOR: color, 63 | }) 64 | color.set(0,0,0, blending = true) 65 | list.push(color) 66 | } 67 | return @bytedisplay::{list: list, chars: order.length} 68 | }, 69 | display: (self, index: @number) { 70 | for i in ..self.list.length { 71 | if i == index { 72 | -> self.list[index].pulse(0,255,0, fade_out = 0.5) 73 | -> self.list[index].set(255,255,255, 0.5) 74 | } else { 75 | self.list[i].set(0,0,0, blending = true) 76 | } 77 | } 78 | } 79 | } 80 | 81 | type @bfreader 82 | layers = counter(0) 83 | layers.display(150, 300) 84 | 85 | impl @bfreader { 86 | new: (script_offset: [@number], cell_count: @number) { 87 | 88 | extract obj_props 89 | block = ?b 90 | group = ?g 91 | $.add(obj { 92 | OBJ_ID: 1816, 93 | X: script_offset[0] - spacing * 6, 94 | Y: script_offset[1], 95 | GROUPS: group, 96 | BLOCK_A: block, 97 | SCALING: 0.5, 98 | DYNAMIC_BLOCK: true, 99 | }) 100 | for b in [RIGHT,LEFT,INCR,DECR,DOT,OPEN,CLOSE] { 101 | $.add(obj { 102 | OBJ_ID: 1816, 103 | X: script_offset[0] - spacing * 3, 104 | Y: script_offset[1], 105 | SCALING: 0.5, 106 | BLOCK_A: b, 107 | }) 108 | } 109 | 110 | 111 | // CREATE CELLS 112 | let cells = [] 113 | for i in ..cell_count { 114 | c = counter() 115 | cells.push(c) 116 | c.display(script_offset[0] + i * 45, script_offset[1] + 90) 117 | } 118 | ptr = counter(0) 119 | ptr.display(script_offset[0], script_offset[1] + 60) 120 | 121 | 122 | std_out = @bytedisplay::new([script_offset[0], script_offset[1] + 120]) 123 | out = @bfreader::{ 124 | group: group, 125 | right: counter(block.create_tracker_item(RIGHT)), 126 | left: counter(block.create_tracker_item(LEFT)), 127 | incr: counter(block.create_tracker_item(INCR)), 128 | dot: counter(block.create_tracker_item(DOT)), 129 | decr: counter(block.create_tracker_item(DECR)), 130 | open: counter(block.create_tracker_item(OPEN)), 131 | close: counter(block.create_tracker_item(CLOSE)), 132 | std_out: std_out, 133 | ptr: ptr, 134 | cells: cells, 135 | } 136 | group.move(spacing * 2, 0, 0.3) 137 | 138 | return out 139 | }, 140 | currently_on: (self) { 141 | if self.right == 1 { 142 | return RIGHT 143 | } else if self.left == 1 { 144 | return LEFT 145 | } else if self.incr == 1 { 146 | return INCR 147 | } else if self.decr == 1 { 148 | return DECR 149 | } else if self.dot == 1 { 150 | return DOT 151 | } else if self.open == 1 { 152 | return OPEN 153 | } else if self.close == 1 { 154 | return CLOSE 155 | } 156 | }, 157 | current_cell: (self) => self.cells[self.ptr.to_const(..self.cells.length)], 158 | interpret: (self) { 159 | 160 | ret = !{ 161 | self.group.move(spacing, 0, 0) 162 | -> return 163 | } 164 | current = self.currently_on() 165 | 166 | if current == RIGHT { 167 | self.ptr += 1 168 | ret! 169 | } 170 | else if current == LEFT { 171 | self.ptr -= 1 172 | ret! 173 | } 174 | else if current == INCR { 175 | -> self.current_cell() += 1 176 | ret! 177 | } 178 | else if current == DECR { 179 | -> self.current_cell() -= 1 180 | ret! 181 | } 182 | else if current == DOT { 183 | std_out = counter(0) 184 | () { 185 | std_out.reset() 186 | cell = self.current_cell() 187 | wait() 188 | cell.copy_to([std_out]) 189 | } () 190 | 191 | self.std_out.display(std_out.to_const(..self.std_out.chars)) 192 | ret! 193 | } 194 | else if current == OPEN { 195 | move_to = !{ 196 | condition = () => !(layers == 0 && self.close == 1) 197 | 198 | while_loop(condition, delay = 0.05, (){ 199 | 200 | 201 | -> if self.open == 1{ 202 | layers += 1 203 | } 204 | 205 | -> if self.close == 1 { 206 | layers -= 1 207 | } 208 | 209 | self.group.move(spacing, 0) 210 | }) 211 | ret! 212 | } 213 | if self.current_cell() == 0 { 214 | layers -= 1 215 | call_with_delay(0.05, move_to) 216 | } else { 217 | ret! 218 | } 219 | } 220 | else if current == CLOSE { 221 | move_back = !{ 222 | condition = ()=> !(layers == 0 && self.open == 1) 223 | while_loop(condition, delay = 0.05, (){ 224 | 225 | -> if self.close == 1 { 226 | layers += 1 227 | } 228 | 229 | -> if self.open == 1 { 230 | layers -= 1 231 | } 232 | 233 | self.group.move(-spacing, 0) 234 | 235 | }) 236 | 237 | ret! 238 | } 239 | if self.current_cell() != 0 { 240 | layers -= 1 241 | call_with_delay(0.05, move_back) 242 | } else { 243 | ret! 244 | } 245 | } 246 | 247 | 248 | } 249 | } 250 | 251 | offset = [300, 300] 252 | insert_bf_script(">>++++>+++[-<-[<]>>]<.", offset) 253 | 254 | reader = @bfreader::new(offset, 100) 255 | while_loop(()=>true, (){ 256 | reader.interpret() 257 | }) -------------------------------------------------------------------------------- /test/bf/brainfugd_tutorial.txt: -------------------------------------------------------------------------------- 1 | INTRODUCTION: 2 | 3 | Your goal in this game is to create programs that fulfill certain tasks. 4 | 5 | Your programs have access to a memory containing six numbers, each in a box called a cell. 6 | The cell with a line under it is the currently selected cell. This underline is called the "cell selector". 7 | 8 | A program is a list of commands, which will be done one by one in order when the program is running. 9 | There are 8 different commands you can use: 10 | 11 | + Adds 1 to the number in the currently selected cell 12 | - Subtracts 1 from the number in the currently selected cell 13 | > Moves the cell selector one cell to the right 14 | < Moves the cell selector one cell to the left 15 | . Outputs the number in the current cell, and also the corresponding letter in the alphabet 16 | , Takes one number as input from the player and puts that number in the currently selected cell 17 | 18 | 19 | BRACKETS: 20 | 21 | The last two commands you can use are [ and ]. All the commands between a [ and a ] will be repeated over and over until the selected cell's number is 0. This can be done by having a number larger than 0 in the selected cell, and then decreasing it with - until it reaches 0. If it is 0 to begin with, the commands inside get skipped, and if it never reaches 0, the commands will repeat forever. 22 | 23 | EXAMPLES: 24 | 25 | +++. 26 | This program adds 3 to the first cell and then outputs the 3 (and also the letter C). 27 | 28 | +>+>+ 29 | This program sets the 3 first cells in the memory to 1. 30 | 31 | +++[-] 32 | This program sets the current cell to 3 and then subtracts 1 again and again until the cell is 0. 33 | 34 | +++[->++<] 35 | This program adds 3 to the first cell, and then subtracts 1 from the first cell and adds 2 to the second cell until the first cell is 0, and the second cell is 6. Notice how this essentially multiplies 3 by 2. 36 | 37 | 38 | 39 | Welcome to Brainfugd## 40 | 41 | A programming language for your GMD system. 42 | ### 43 | Spu7Nix systems INC## [c] 2021 44 | ~Memory 45 | ## 46 | Your goal in this game is to create 47 | programs that fulfill certain tasks. 48 | ## 49 | Your programs have access to a memory containing 50 | six numbers,# each in a box called a cell.## 51 | The cell with a line under it is the currently 52 | selected cell.## This underline is called the 53 | "cell selector". 54 | ~Commands 55 | ## 56 | A program is a list of commands,# which will be 57 | done one by one in order when the program 58 | is running. 59 | ## 60 | There are 8 different commands you can use: 61 | ## 62 | +## Adds 1 to the number in the current cell 63 | ## 64 | -## Subtracts 1 from the number in the current cell 65 | ## 66 | [more on the next page] 67 | ~>## Moves the cell selector one cell to the right 68 | # 69 | <## Moves the cell selector one cell to the left 70 | # 71 | .## Outputs the number in the current cell,# 72 | and also the corresponding letter in the alphabet 73 | # 74 | ,## Takes one number as input from the player and 75 | puts that number in the currently selected cell 76 | ## 77 | [more on the next page] 78 | ~Brackets 79 | ## 80 | The last two commands you can use are [ and ].## 81 | All the commands between a [ and a ] will be 82 | repeated over and over until the selected cell's 83 | number is 0. ##This can be done by having a number 84 | larger than 0 in the selected cell, #and then 85 | decreasing it with - until it reaches 0. #If it is 86 | 0 to begin with,# the commands inside get skipped,# 87 | and if it never reaches 0,# the commands will 88 | repeat forever. 89 | ~Examples 90 | ## 91 | +++.## 92 | This program adds 3 to the first cell and then 93 | outputs the 3 (and also the letter C). 94 | ## 95 | +>+>+## 96 | This program sets the 3 first cells in the 97 | memory to 1. 98 | ## 99 | [more on the next page] 100 | ~+++[-]## 101 | This program sets the current cell to 3# and 102 | then subtracts 1 again and again until 103 | the cell is 0. 104 | ## 105 | +++[->++<]## 106 | This program adds 3 to the first cell,# 107 | and then subtracts 1 from the first cell# and 108 | adds 2 to the second cell# until the first cell 109 | is 0,# and the second cell is 6.## Notice how this 110 | essentially multiplies 3 by 2. 111 | ~Practical 112 | ## 113 | Complete challenge 5 to unlock level completion## 114 | Complete the coin challenges for coin 1 and 2## 115 | Complete challenge 7 for coin 3## 116 | ## 117 | In challenges that depend on inputs the input 118 | will be provided automatically when the program 119 | is running# to avoid cheating.## You need 3 successful 120 | runs in a row to complete such a challenge. 121 | -------------------------------------------------------------------------------- /test/bf/brainfugd_video_example.spwn: -------------------------------------------------------------------------------- 1 | bob = { group: 42g, center: 3g, arrow: 4g, block: 3b } 2 | dir = counter(0) // counter storing bob's current direction 3 | gs = import gamescene 4 | // button_a: change direction 5 | on(gs.button_a(), !{ 6 | bob.arrow.rotate(bob.center, 90) // rotate arrow 7 | if dir == 3 { // increment direction counter 8 | wait() 9 | dir -= 3 10 | } else { 11 | wait() 12 | dir += 1 13 | } 14 | }) 15 | // create a list of trigger functions for each move direction 16 | let move_funcs = [] 17 | dirs = [[0, 1], [1, 0], [0, -1], [-1, 0]] 18 | for d in dirs { 19 | move_funcs.push(!{ 20 | bob.group.move(d[0] * 300, d[1] * 300, 10) 21 | }) 22 | } 23 | // stops all movement 24 | stop_moving = !{ 25 | for f in move_funcs { 26 | f.start_group.stop() 27 | } 28 | } 29 | // button_b: movement 30 | on(gs.button_b(), !{ 31 | move_funcs[dir.to_const(..4)]! 32 | }) 33 | // stop movement on end of button press 34 | on(gs.button_b_end(), stop_moving) 35 | // wall collision 36 | walls = 4b 37 | on(collision(bob.block, walls), !{ 38 | stop_moving! 39 | d = dirs[dir.to_const(..4)] 40 | bob.group.move(d[0] * -5, d[1] * -5, 0.1) // move back by 5 units 41 | }) 42 | // coins 43 | coins = [[13g, 13b], [14g, 14b], [15g, 15b], [16g, 16b], [17g, 17b], [18g, 18b]] 44 | balance = counter(10i) 45 | for coin in coins { 46 | on(collision(bob.block, coin[1]), !{ 47 | coin[0].toggle_off() 48 | balance += 1 49 | }) 50 | } 51 | // lemonade 52 | lemonade_stand = 5b 53 | lemonades = counter(11i) 54 | on(collision(bob.block, lemonade_stand), !{ 55 | if balance >= 3 { 56 | balance -= 3 57 | lemonades += 1 58 | } 59 | }) 60 | -------------------------------------------------------------------------------- /test/bf/text_display.spwn: -------------------------------------------------------------------------------- 1 | type @textdisplay 2 | spacing = 0.6 * 30 3 | y_spacing = 20 4 | impl @textdisplay { 5 | new: (offset: [@number], letters: @number) { 6 | extract obj_props 7 | order = "ABCDEFGHIJKLMNOPQRSTUVWXYZ" 8 | writer = ?b 9 | writer_group = ?g 10 | let list = [] 11 | // I N C O 12 | wincon = [?b, ?b, ?b, ?b] 13 | let checkers = [] 14 | //WINCO 15 | for i in ..letters { 16 | 17 | group = ?g 18 | block = ?b 19 | 20 | default_pos = ?g 21 | group.alpha(0) 22 | default_pos.alpha(0) 23 | follow_group = (500g..510g)[i] 24 | 25 | // space 26 | // $.add(obj { 27 | // OBJ_ID: 1816, 28 | // X: offset[0] + i * spacing, 29 | // Y: offset[1], 30 | // GROUPS: follow_group, 31 | // BLOCK_A: wincon[6], 32 | // SCALING: 0.7, 33 | // }) 34 | 35 | 36 | check_block = ?b 37 | checkers.push(check_block) 38 | 39 | $.add(obj { 40 | OBJ_ID: 1816, 41 | X: offset[0] + i * spacing - 12, 42 | Y: offset[1], 43 | //GROUPS: follow_group, 44 | BLOCK_A: check_block, 45 | DYNAMIC_BLOCK: true, 46 | SCALING: 0.2, 47 | }) 48 | 49 | for ch in ..order.length { 50 | $.add(obj { 51 | OBJ_ID: 914, 52 | X: offset[0] + i * spacing + (ch + 1) * spacing * letters, 53 | Y: offset[1], 54 | TEXT: $.b64encode(order[ch]), 55 | GROUPS: follow_group, 56 | SCALING: 0.7, 57 | }) 58 | 59 | cblock = match order[ch] { 60 | =="I": wincon[0], 61 | =="N": wincon[1], 62 | =="C": wincon[2], 63 | =="O": wincon[3], 64 | else: null 65 | } 66 | //$.print(order[ch], cblock) 67 | if cblock != null { 68 | 69 | 70 | $.add(obj { 71 | OBJ_ID: 1816, 72 | X: offset[0] + i * spacing + (ch + 1) * spacing * letters, 73 | Y: offset[1], 74 | GROUPS: follow_group, 75 | BLOCK_A: cblock, 76 | SCALING: 0.3, 77 | }) 78 | } 79 | } 80 | $.add(obj { 81 | OBJ_ID: 1816, 82 | X: offset[0], 83 | Y: offset[1] + i * y_spacing * 3 + 30 + y_spacing * 3, 84 | BLOCK_A: block, 85 | SCALING: 0.6, 86 | GROUPS: follow_group, 87 | }) 88 | 89 | lock = !{ 90 | group.follow(writer_group) 91 | follow_group.follow(writer_group) 92 | -> follow_group.pulse(0,0,0, hold = 0.05, fade_out = 0.15) 93 | } 94 | 95 | collision(block, writer).on(lock) 96 | 97 | $.add(obj { 98 | OBJ_ID: 1765, 99 | X: offset[0] + i * spacing, 100 | Y: offset[1], 101 | GROUPS: group, 102 | }) 103 | $.add(obj { 104 | OBJ_ID: 1765, 105 | X: offset[0] + i * spacing, 106 | Y: offset[1], 107 | GROUPS: default_pos, 108 | }) 109 | 110 | -> follow_group.follow(group) 111 | 112 | list.push({ 113 | group, 114 | default_pos, 115 | lock, 116 | follow_group, 117 | }) 118 | 119 | } 120 | 121 | //writer 122 | $.add(obj { 123 | OBJ_ID: 1816, 124 | X: offset[0] + 30, 125 | Y: offset[1] + 30, 126 | BLOCK_A: writer, 127 | GROUPS: writer_group, 128 | DYNAMIC_BLOCK: true, 129 | }) 130 | 131 | writer_default = ?g 132 | $.add(obj { 133 | OBJ_ID: 1765, 134 | X: offset[0] + 30, 135 | Y: offset[1] + 30, 136 | GROUPS: writer_default, 137 | }) 138 | let coin = [] 139 | let coin_coin = [] 140 | for i in 0..9 { 141 | if i < 4 { 142 | c1 = counter(wincon[[2, 3, 0, 1][i]].create_tracker_item(checkers[i]), bits = 1) 143 | coin.push(c1) 144 | } else { 145 | if i > 4 { 146 | coin_coin.push(counter(wincon[[2, 3, 0, 1][i - 5]].create_tracker_item(checkers[i]), bits = 1)) 147 | } 148 | } 149 | } 150 | 151 | return @textdisplay::{list, writer: writer_group, writer_default, letters, coin, coin_coin} 152 | }, 153 | write: (self, letter: @counter, speed = 3) { 154 | // temp = counter() 155 | // read_counter = () { 156 | // read = !{ 157 | // mini_read = (num){ 158 | // letter.item.if_is(LARGER_THAN, num - 1, !{ 159 | // letter.add(-num) 160 | // temp.add(num) 161 | // self.writer.move(-(spacing / 3) * self.letters * num, 0) 162 | // call_with_delay(@epsilon::{}, read) 163 | // }) 164 | // } 165 | 166 | // for i in 0..speed { 167 | // mini_read(3^i) 168 | // } 169 | // letter.item.if_is(EQUAL_TO, 0, !{ 170 | // wait() 171 | // if speed > 1 { suppress_signal(0.1) } 172 | // -> return 173 | // }) 174 | // } 175 | // read! 176 | // } 177 | 178 | self.writer.move(0, y_spacing, 0) 179 | -> if letter < 27 { 180 | -> self.writer.move(-5, 0, 0.2, easing = EXPONENTIAL_OUT) 181 | wait(0.05) 182 | // read_counter() 183 | // temp.add_to([letter], speed=speed) 184 | self.writer.move(-(spacing / 3) * self.letters * letter.to_const(..27), 0) 185 | wait(0.2) 186 | for l in self.list { 187 | l.lock.start_group.stop() 188 | } 189 | wait(0.05) 190 | self.writer.move_to(self.writer_default, x_only = true) 191 | } 192 | 193 | }, 194 | 195 | reset: (self) { 196 | 197 | for letter in self.list { 198 | -> letter.follow_group.pulse(0,0,0, fade_in = 0.3, hold = 0.5) 199 | } 200 | wait(0.3) 201 | 202 | self.writer.move_to(self.writer_default) 203 | for letter in self.list { 204 | letter.group.move_to(letter.default_pos) 205 | } 206 | } 207 | } 208 | -------------------------------------------------------------------------------- /test/bf/text_display_pro.spwn: -------------------------------------------------------------------------------- 1 | type @textdisplay 2 | spacing = 0.6 * 30 3 | y_spacing = 20 4 | impl @textdisplay { 5 | new: (offset: [@number], letters: @number) { 6 | extract obj_props 7 | order = "ABCDEFGHIJKLMNOPQRSTUVWXYZ" 8 | writer = ?b 9 | writer_group = ?g 10 | let list = [] 11 | // I N C O 12 | 13 | 14 | //WINCO 15 | for i in ..letters { 16 | 17 | group = ?g 18 | block = ?b 19 | 20 | default_pos = ?g 21 | group.alpha(0) 22 | default_pos.alpha(0) 23 | follow_group = (500g..515g)[i] 24 | 25 | // space 26 | // $.add(obj { 27 | // OBJ_ID: 1816, 28 | // X: offset[0] + i * spacing, 29 | // Y: offset[1], 30 | // GROUPS: follow_group, 31 | // BLOCK_A: wincon[6], 32 | // SCALING: 0.7, 33 | // }) 34 | 35 | for ch in ..order.length { 36 | $.add(obj { 37 | OBJ_ID: 914, 38 | X: offset[0] + i * spacing + (ch + 1) * spacing * letters, 39 | Y: offset[1], 40 | TEXT: $.b64encode(order[ch]), 41 | GROUPS: follow_group, 42 | SCALING: 0.7, 43 | }) 44 | } 45 | $.add(obj { 46 | OBJ_ID: 1816, 47 | X: offset[0], 48 | Y: offset[1] + i * y_spacing * 3 + 30 + y_spacing * 3, 49 | BLOCK_A: block, 50 | SCALING: 0.6, 51 | GROUPS: follow_group, 52 | }) 53 | 54 | lock = !{ 55 | group.follow(writer_group) 56 | follow_group.follow(writer_group) 57 | -> follow_group.pulse(0,0,0, hold = 0.05, fade_out = 0.15) 58 | } 59 | 60 | collision(block, writer).on(lock) 61 | 62 | $.add(obj { 63 | OBJ_ID: 1765, 64 | X: offset[0] + i * spacing, 65 | Y: offset[1], 66 | GROUPS: group, 67 | }) 68 | $.add(obj { 69 | OBJ_ID: 1765, 70 | X: offset[0] + i * spacing, 71 | Y: offset[1], 72 | GROUPS: default_pos, 73 | }) 74 | 75 | -> follow_group.follow(group) 76 | 77 | list.push({ 78 | group, 79 | default_pos, 80 | lock, 81 | follow_group, 82 | }) 83 | 84 | } 85 | 86 | //writer 87 | $.add(obj { 88 | OBJ_ID: 1816, 89 | X: offset[0] + 30, 90 | Y: offset[1] + 30, 91 | BLOCK_A: writer, 92 | GROUPS: writer_group, 93 | DYNAMIC_BLOCK: true, 94 | }) 95 | 96 | writer_default = ?g 97 | $.add(obj { 98 | OBJ_ID: 1765, 99 | X: offset[0] + 30, 100 | Y: offset[1] + 30, 101 | GROUPS: writer_default, 102 | }) 103 | 104 | 105 | return @textdisplay::{list, writer: writer_group, writer_default, letters} 106 | }, 107 | write: (self, letter: @counter, speed = 3) { 108 | // temp = counter() 109 | // read_counter = () { 110 | // read = !{ 111 | // mini_read = (num){ 112 | // letter.item.if_is(LARGER_THAN, num - 1, !{ 113 | // letter.add(-num) 114 | // temp.add(num) 115 | // self.writer.move(-(spacing / 3) * self.letters * num, 0) 116 | // call_with_delay(@epsilon::{}, read) 117 | // }) 118 | // } 119 | 120 | // for i in 0..speed { 121 | // mini_read(3^i) 122 | // } 123 | // letter.item.if_is(EQUAL_TO, 0, !{ 124 | // wait() 125 | // if speed > 1 { suppress_signal(0.1) } 126 | // -> return 127 | // }) 128 | // } 129 | // read! 130 | // } 131 | 132 | self.writer.move(0, y_spacing, 0) 133 | -> if letter < 27 { 134 | -> self.writer.move(-5, 0, 0.2, easing = EXPONENTIAL_OUT) 135 | wait(0.05) 136 | // read_counter() 137 | // temp.add_to([letter], speed=speed) 138 | self.writer.move(-(spacing / 3) * self.letters * letter.to_const(..27), 0) 139 | wait(0.2) 140 | for l in self.list { 141 | l.lock.start_group.stop() 142 | } 143 | wait(0.05) 144 | self.writer.move_to(self.writer_default, x_only = true) 145 | } 146 | 147 | }, 148 | 149 | reset: (self) { 150 | 151 | for letter in self.list { 152 | -> letter.follow_group.pulse(0,0,0, fade_in = 0.3, hold = 0.5) 153 | } 154 | wait(0.3) 155 | 156 | self.writer.move_to(self.writer_default) 157 | for letter in self.list { 158 | letter.group.move_to(letter.default_pos) 159 | } 160 | } 161 | } 162 | -------------------------------------------------------------------------------- /test/binaryCalculator.spwn: -------------------------------------------------------------------------------- 1 | extract obj_props; 2 | 3 | type @binaryCalculator; 4 | 5 | impl @binaryCalculator { 6 | 7 | new: (bits: @number, x: @number, y: @number) { 8 | let bitCounters = []; 9 | 10 | for i in 0..bits { 11 | item = ?i; 12 | 13 | $.add(obj { 14 | OBJ_ID: 1615, 15 | X: x + i * 30, 16 | Y: y, 17 | ITEM: item, 18 | GROUPS: 999g 19 | }); 20 | 21 | bitCounters.push(counter(item)); 22 | } 23 | 24 | return @binaryCalculator::{ 25 | bitCounters: bitCounters, 26 | } 27 | }, 28 | binaryToDecimal: (self, target: @counter) { 29 | target = 0; 30 | wait(1); 31 | 32 | for i in 0..self.bitCounters.length { 33 | if self.bitCounters[self.bitCounters.length - 1 - i] as @bool { 34 | target += 2 ^ i; 35 | } 36 | } 37 | }, 38 | decimalToBinary: (self, source: @counter, calcSpeed: @number = 5) { 39 | let tempcounters = []; 40 | 41 | for i in 0..self.bitCounters.length { 42 | tempcounters.push(counter()); 43 | } 44 | 45 | source.copy_to(tempcounters); 46 | 47 | for i in 0..self.bitCounters.length { 48 | -> () { 49 | tempcounters[i].divide(2 ^ i); 50 | tempcounters[i].divide(2, remainder = self.bitCounters[self.bitCounters.length - 1 - i]); 51 | }(); 52 | } 53 | }, 54 | prettyDecimalToBinary: (self, source: @counter, calcSpeed: @number = 10) { 55 | for i in 0..self.bitCounters.length { 56 | source.divide(2, remainder = self.bitCounters[self.bitCounters.length - 1 - i]); 57 | } 58 | 59 | self.binaryToDecimal(source); 60 | } 61 | } 62 | 63 | 64 | c = @binaryCalculator::new(8, 300, 300) 65 | 66 | num = counter(100) 67 | 68 | wait(1) 69 | 70 | c.decimalToBinary(num) 71 | 72 | 73 | -------------------------------------------------------------------------------- /test/bugtest.spwn: -------------------------------------------------------------------------------- 1 | let PI = 314159.2653589793; 2 | let HALF_PI = 157079.6326794897; 3 | let DOUBLE_PI = 628318.5307179586; 4 | let SIN_CURVE_A = 004158.96; 5 | let SIN_CURVE_B = 000129.810625032; 6 | 7 | let x = counter(?i) 8 | x.display(100,200) 9 | let r = counter(?i) 10 | r.display(100,200) 11 | let cos1 = !{ 12 | if x < 0 { 13 | let q = x*-1 / DOUBLE_PI; 14 | q += 1; 15 | let y = q * DOUBLE_PI; 16 | x = (x - y)*-1; 17 | } 18 | if x >= DOUBLE_PI { 19 | let q = x / DOUBLE_PI; 20 | let y = q * DOUBLE_PI; 21 | x = x - y; 22 | } 23 | let s = counter(1); 24 | if x >= PI { 25 | s = -1; 26 | x -= PI; 27 | } 28 | if x > HALF_PI { 29 | x = counter(PI) - x; 30 | s = s*-1; 31 | } 32 | let z = x * x; 33 | r = z * (z * (counter(SIN_CURVE_A) - z*SIN_CURVE_B) - 0.5) + 1.0; 34 | if r > 1.0 { 35 | r = r - 2.0; 36 | } 37 | if s > 0 { 38 | r = r*-1; 39 | } 40 | } 41 | 42 | x = 40 43 | cos1! -------------------------------------------------------------------------------- /test/collection.spwn: -------------------------------------------------------------------------------- 1 | extract obj_props 2 | 3 | type @collection 4 | 5 | 6 | let _collection_ind = 0; 7 | 8 | impl @collection { 9 | new: (count: @number) { 10 | _collection_ind++; 11 | 12 | let slf = @collection::{ 13 | indexer: ?g, 14 | reset_target: ?g, 15 | output_id: counter(?i), 16 | input_id: counter(?i), 17 | done_indicator: counter(?i), 18 | set_indicator: counter(20i), 19 | indexer_block: ?b, 20 | blocks: [?b for $ in 0..count], 21 | slots: [counter(?i) for $ in 0..count] 22 | }; 23 | 24 | let slf._trigfuncs = { 25 | reset: !{ 26 | slf.indexer.move_to(slf.reset_target, y_only=true, duration=0); 27 | wait() 28 | slf.indexer.move_to(slf.reset_target, x_only=true, duration=0); 29 | } 30 | } 31 | slf._trigfuncs.index = !{ 32 | let ind1 = !{ 33 | slf.indexer.move(20, 0, duration=0); 34 | wait() 35 | slf.input_id--; 36 | 37 | if slf.input_id > 0 { 38 | ind1! 39 | } else { 40 | slf.indexer.move(0, -10, duration=0); 41 | wait() 42 | slf._trigfuncs.reset!; 43 | } 44 | }; 45 | ind1!; 46 | } 47 | 48 | let base_x = _collection_ind * 255; 49 | let base_y = 315; 50 | 51 | $.add(obj { 52 | OBJ_ID: 279, 53 | GROUPS: [slf.reset_target], 54 | X: base_x, 55 | Y: base_y 56 | }); 57 | 58 | $.add(obj { 59 | OBJ_ID: 1816, 60 | ITEM: slf.indexer_block, 61 | GROUPS: [slf.indexer], 62 | X: base_x, 63 | Y: base_y, 64 | DYNAMIC_BLOCK: true 65 | }); 66 | 67 | let i = 0; 68 | for b in slf.blocks { 69 | i++; 70 | $.add(obj { 71 | OBJ_ID: 1816, 72 | ITEM: b, 73 | X: base_x + (60*i), 74 | Y: base_y - 34 75 | }); 76 | 77 | [[on(collision_exit(slf.indexer_block, b))]] !{ 78 | if slf.set_indicator == 0 { 79 | slf.output_id = slf.slots[i-1]; 80 | slf.done_indicator = 1; 81 | } else { 82 | slf.slots[i-1] = slf.output_id; 83 | slf.done_indicator = 1; 84 | wait(0.1) 85 | } 86 | } 87 | } 88 | 89 | return slf; 90 | }, 91 | 92 | index: (self, deco: @macro, input: @number | @counter) { 93 | self.input_id = input; 94 | self.input_id++; 95 | self._trigfuncs.index!; 96 | self.set_indicator = 0; 97 | 98 | [[on(count(self.done_indicator.item, 1))]] !{ 99 | self.done_indicator=0; 100 | wait(0.1) 101 | deco(self.output_id); 102 | } 103 | }, 104 | 105 | set: (self, input: @number | @counter, value: @number | @counter) { 106 | self.input_id = input; 107 | self.input_id++; 108 | self.output_id = value; 109 | self.set_indicator++; 110 | //wait(0.1); 111 | self._trigfuncs.index!; 112 | 113 | while_loop(_=>self.done_indicator==0, (){wait()}); 114 | } 115 | } -------------------------------------------------------------------------------- /test/inclrange.spwn: -------------------------------------------------------------------------------- 1 | range = 0..=10 2 | for i in range { $.print(i) } 3 | $.print(range) 4 | 5 | range_step = 0..2..=10 6 | for i in range_step { $.print(i) } 7 | $.print(range_step) 8 | -------------------------------------------------------------------------------- /test/jsontest.json: -------------------------------------------------------------------------------- 1 | { 2 | "qwe": [ 3 | {"a": 1, "b": "Hello"}, 4 | {"a": 2, "b": ","}, 5 | {"a": 3, "b": "World"}, 6 | {"a": 4, "b": "!"} 7 | ] 8 | } -------------------------------------------------------------------------------- /test/level_reading/draw.spwn: -------------------------------------------------------------------------------- 1 | extract obj_props 2 | 3 | line_objs = [ 4 | {id: 211, ratio: 1/1, scale: 1}, 5 | {id: 1767, ratio: 8/3, scale: 15/4}, 6 | {id: 579, ratio: 3/1, scale: 1}, 7 | //{id: 1191, ratio: 4/1, scale: 1}, doesn't rotate about center 8 | {id: 1757, ratio: 15/1, scale: 2}, 9 | //{id: 507, ratio: 20/1, scale: 1}, doesn't rotate about center 10 | {id: 1753, ratio: 30/1, scale: 1}, 11 | ] 12 | 13 | draw_line = (x1: @number, y1: @number, x2: @number, y2: @number, width: @number = 0.25, runtime_allowed: @bool = false) { 14 | 15 | line_length = $.sqrt((x1-x2)^2 + (y1-y2)^2) 16 | if line_length == 0 || width == 0 { 17 | return 18 | } 19 | 20 | line_ratio = line_length / (30*width) 21 | 22 | obj_rot = $.atan2(y2-y1,x2-x1) 23 | 24 | if line_ratio < 1 { 25 | mid_x = (x1+x2)/2 26 | mid_y = (y1+y2)/2 27 | draw_line( 28 | //sin and cos are matched here to result in a 90 degree rotation (technically transposition) 29 | mid_x + $.sin(obj_rot)*15*width, 30 | mid_y - $.cos(obj_rot)*15*width, 31 | mid_x - $.sin(obj_rot)*15*width, 32 | mid_y + $.cos(obj_rot)*15*width, 33 | line_length / 30, 34 | runtime_allowed 35 | ) 36 | return 37 | } 38 | 39 | filtered_objs = line_objs.filter(el => el.ratio <= line_ratio) 40 | line_obj = filtered_objs[filtered_objs.length - 1] 41 | obj_scale = width / (1 / line_obj.scale / line_obj.ratio) 42 | 43 | obj_length = 30 / line_obj.scale * obj_scale 44 | 45 | amount = line_length / obj_length 46 | 47 | for i in 0..$.floor(amount) { 48 | pos = obj_length / 2 + i * obj_length 49 | $.add( obj{ 50 | OBJ_ID: line_obj.id, 51 | ROTATION: -obj_rot * 180 / 3.141592, 52 | X: x1 + $.cos(obj_rot) * pos, 53 | Y: y1 + $.sin(obj_rot) * pos, 54 | SCALING: obj_scale, 55 | }, runtime_allowed) 56 | } 57 | 58 | if !(amount == $.floor(amount)) { 59 | $.add( obj{ 60 | OBJ_ID: line_obj.id, 61 | ROTATION: -obj_rot * 180 / 3.141592, 62 | X: x2 - $.cos(obj_rot) * obj_length / 2, 63 | Y: y2 - $.sin(obj_rot) * obj_length / 2, 64 | SCALING: obj_scale, 65 | }, runtime_allowed) 66 | } 67 | } 68 | 69 | return { 70 | draw_line: draw_line, 71 | } -------------------------------------------------------------------------------- /test/level_reading/main.spwn: -------------------------------------------------------------------------------- 1 | 2 | extract obj_props 3 | extract import "draw.spwn" 4 | 5 | objs = get_objects(GROUPS, ==[1g]).sorted(comp = (a, b) => a[X] < b[X]) 6 | 7 | for i in 0..(objs.length - 1) { 8 | draw_line(objs[i][X], objs[i][Y], objs[i+1][X], objs[i+1][Y], 0.1) 9 | } 10 | 11 | 12 | -------------------------------------------------------------------------------- /test/number_literals_max.spwn: -------------------------------------------------------------------------------- 1 | x = 0x7FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF 2 | y = 0o1777777777777777777777777777777777777777777 3 | z = 0b1111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111 4 | 5 | $.print(x) 6 | $.print(y) 7 | $.print(z) 8 | -------------------------------------------------------------------------------- /test/orthographic_rotation/full_cube.spwn: -------------------------------------------------------------------------------- 1 | m = s => 1 if s == '#' else 0 2 | 3 | // material_pat = [ 4 | // "### # # #", 5 | // " # # # # ", 6 | // "# # ### #", 7 | // " ### # # ", 8 | // "# # ### #", 9 | // " # ### ", 10 | // "# # # ##", 11 | // " ### # # ", 12 | // "# # ### #", 13 | // ] 14 | 15 | material_pat = [ 16 | "# # # # #", 17 | " # # # # ", 18 | "# # # # #", 19 | " # # # # ", 20 | "# # # # #", 21 | " # # # # ", 22 | "# # # # #", 23 | " # # # # ", 24 | "# # # # #", 25 | ] 26 | 27 | 28 | HVS_to_string = (HVSArray) { 29 | HVSstr = HVSArray[0] as @string + 'a' + HVSArray[1] as @string + 'a' + HVSArray[2] as @string + 'a0a0' 30 | return HVSstr 31 | } 32 | 33 | type @fullcube 34 | 35 | impl @fullcube { 36 | new: ( 37 | s, 38 | scaling, 39 | vert, 40 | dir, 41 | ref_grid, 42 | ref_grid_main, ref_grid_center, 43 | follow_frame, 44 | offset, 45 | side1, 46 | side2, 47 | move_group, 48 | ) { 49 | 50 | box = @voxel::new([[?c, ?c], [?c, ?c]], [?g, ?g, ?g], ?g, dir, vert, scaling) 51 | 52 | make_letter_cube = (side1, side2, vert) { 53 | let cube = [] 54 | for x in ..s { 55 | let layer = [] 56 | for y in ..s { 57 | let strip = [] 58 | for z in ..s { 59 | if vert { 60 | strip.push( 61 | side1[s-y-1][x] == '#' && side2[(s-z-1) if dir == -1 else z][x] == '#' 62 | ) 63 | } else { 64 | strip.push( 65 | side1[s-y-1][x] == '#' && side2[s-y-1][(s-z-1) if dir == 1 else z] == '#' 66 | ) 67 | } 68 | } 69 | layer.push(strip) 70 | } 71 | cube.push(layer) 72 | } 73 | return cube 74 | } 75 | 76 | 77 | 78 | cube = make_letter_cube(side1, side2, vert) 79 | 80 | 81 | for x in ..s { 82 | for y in ..s { 83 | for z in ..s { 84 | if cube[x][y][z] { 85 | box.make( 86 | [offset[0] + x * 30 * scaling, offset[1] + y * 30 * scaling], 87 | z + (y * dir if vert else x * dir), 88 | [ 89 | follow_frame[y][z] if vert else follow_frame[x][z], move_group 90 | ], 91 | m(material_pat[x][y]), 92 | m(material_pat[x][z] if vert else material_pat[z][y]), 93 | ) 94 | } 95 | } 96 | } 97 | } 98 | 99 | return @fullcube::{ 100 | s, 101 | dir, 102 | ref_grid, 103 | ref_grid_main, ref_grid_center, 104 | follow_frame, 105 | vert, 106 | box, 107 | } 108 | }, 109 | 110 | animate: (self, t) { 111 | s = self.s 112 | 113 | 114 | 115 | for x in ..s { 116 | for y in ..s { 117 | g = self.follow_frame[(s-x-1) if self.dir == -1 else x][y] 118 | -> g.pulse(0, 1, 0.4 + 0.6*((y + 1) / s), 0.1 * t * 2, 0, 1.1 * t * 2, hsv = true) 119 | -> g.pulse(0, 1, 0.4 + 0.6*((x + 1) / s), 1.1 * t * 2, 0, 0.1 * t * 2, hsv = true) 120 | 121 | if self.vert { 122 | g.follow(self.ref_grid[y][x], x_mod = 0, y_mod = 1 * self.dir, duration = t * 2 + 0.2) 123 | } else { 124 | g.follow(self.ref_grid[x][y], x_mod = 1 * self.dir, y_mod = 0, duration = t * 2 + 0.2) 125 | } 126 | } 127 | } 128 | wait(0.1) 129 | 130 | -> self.ref_grid_main.rotate(self.ref_grid_center, 90 if self.vert else -90, t * 2, EASE_IN_OUT) 131 | -> self.box.animate(t) 132 | wait(t * 2 + 0.15) 133 | -> self.ref_grid_main.rotate(self.ref_grid_center, -90 if self.vert else 90) 134 | } 135 | } 136 | -------------------------------------------------------------------------------- /test/orthographic_rotation/index.spwn: -------------------------------------------------------------------------------- 1 | import "voxel.spwn" 2 | extract obj_props 3 | 4 | 5 | message = $.readfile("message.txt") 6 | let sides = [] 7 | for letter in message.split("-") { 8 | let side = [] 9 | for row in letter.trim().split("\n") { 10 | 11 | if row.length > 1 { 12 | side.push(row) 13 | } 14 | } 15 | sides.push(side) 16 | } 17 | 18 | 19 | 20 | 21 | s = 9 22 | scaling = 2/3 23 | 24 | move_group = 100g 25 | 26 | 27 | ref = import "reference_grid.spwn" 28 | 29 | 30 | extract ref.make_ref_grid(s, scaling) 31 | 32 | import "full_cube.spwn" 33 | 34 | let cubes = [] 35 | 36 | for i in ..(sides.length - 1) { 37 | cube = @fullcube::new( 38 | s, 39 | scaling, 40 | i % 2 == 0, 41 | -1 if (i % 4 < 2) else 1, 42 | 43 | ref_grid, ref_grid_main, ref_grid_center, 44 | ref.make_follow_frame(s), 45 | [300 + 600 * i, 300], 46 | sides[i], sides[i + 1], 47 | move_group, 48 | ) 49 | cubes.push(cube) 50 | } 51 | 52 | 53 | wait(3) 54 | 55 | for i in ..cubes.length { 56 | cube = cubes[i] 57 | -> cube.animate(0.7) 58 | if i != cubes.length - 1 { 59 | wait(2) 60 | move_group.move(-200, 0) 61 | wait(0.1) 62 | } 63 | } 64 | 65 | -------------------------------------------------------------------------------- /test/orthographic_rotation/message.txt: -------------------------------------------------------------------------------- 1 | _________ 2 | _________ 3 | _________ 4 | _________ 5 | ######### 6 | _________ 7 | _________ 8 | _________ 9 | _________ 10 | - 11 | __#####__ 12 | _##___##_ 13 | ##_____## 14 | _##______ 15 | __#####__ 16 | ______##_ 17 | ##_____## 18 | _##___##_ 19 | __#####__ 20 | - 21 | ##_____## 22 | ##_____## 23 | ##_____## 24 | ##_____## 25 | ##_____## 26 | ##_____## 27 | ##_____## 28 | _##___##_ 29 | __#####__ 30 | - 31 | #######__ 32 | ##____##_ 33 | ##_____## 34 | ##____##_ 35 | #######__ 36 | ##____##_ 37 | ##_____## 38 | ##_____## 39 | ########_ 40 | - 41 | __#####__ 42 | _##___##_ 43 | ##_____## 44 | _##______ 45 | __#####__ 46 | ______##_ 47 | ##_____## 48 | _##___##_ 49 | __#####__ 50 | - 51 | ___#####_ 52 | __##___## 53 | _##______ 54 | ##_______ 55 | ##_______ 56 | ##_______ 57 | _##______ 58 | __##___## 59 | ___#####_ 60 | - 61 | #######__ 62 | ##____##_ 63 | ##_____## 64 | ##____##_ 65 | #######__ 66 | ##____##_ 67 | ##_____## 68 | ##_____## 69 | ##_____## 70 | - 71 | ######### 72 | ___###___ 73 | ___###___ 74 | ___###___ 75 | ___###___ 76 | ___###___ 77 | ___###___ 78 | ___###___ 79 | ######### 80 | - 81 | #######__ 82 | ##____##_ 83 | ##_____## 84 | ##____##_ 85 | #######__ 86 | ##____##_ 87 | ##_____## 88 | ##_____## 89 | ########_ 90 | - 91 | ######### 92 | ##_______ 93 | ##_______ 94 | ##_______ 95 | #######__ 96 | ##_______ 97 | ##_______ 98 | ##_______ 99 | ######### 100 | - 101 | ____#____ 102 | ____#____ 103 | ____#____ 104 | ____#____ 105 | ____#____ 106 | ____#____ 107 | ____#____ 108 | ____#____ 109 | ____#____ -------------------------------------------------------------------------------- /test/orthographic_rotation/reference_grid.spwn: -------------------------------------------------------------------------------- 1 | extract obj_props 2 | return { 3 | make_ref_grid: (s, scaling) { 4 | ref_grid_off = [300, 900] 5 | ref_grid_main = ?g 6 | ref_grid_center = ?g 7 | 8 | $.add(obj { 9 | OBJ_ID: 211, 10 | SCALING: 0.3, 11 | X: ref_grid_off[0] + ((s - 1) / 2) * 30 * scaling, 12 | Y: ref_grid_off[1] + ((s - 1) / 2) * 30 * scaling, 13 | GROUPS: ref_grid_center, 14 | }) 15 | 16 | let ref_grid = [] 17 | 18 | 19 | for x in ..s { 20 | let column = [] 21 | 22 | for y in ..s { 23 | group = ?g 24 | $.add(obj { 25 | OBJ_ID: 211, 26 | SCALING: 0.3, 27 | X: ref_grid_off[0] + x * 30 * scaling, 28 | Y: ref_grid_off[1] + y * 30 * scaling, 29 | GROUPS: [group, ref_grid_main], 30 | }) 31 | column.push(group) 32 | 33 | } 34 | ref_grid.push(column) 35 | } 36 | return {ref_grid, ref_grid_main, ref_grid_center} 37 | }, 38 | 39 | make_follow_frame: (s) { 40 | let follow_frame = [] 41 | 42 | for x in ..s { 43 | let follow_column = [] 44 | for y in ..s { 45 | follow_column.push(?g) 46 | } 47 | follow_frame.push(follow_column) 48 | } 49 | return follow_frame 50 | } 51 | } -------------------------------------------------------------------------------- /test/path_test.spwn: -------------------------------------------------------------------------------- 1 | // cargo run build test/path_test.spwn -l --allow filekind --allow readdir --allow fileexists --allow readfile --allow writefile --allow metadata --allow deletefile --allow mkdir --allow rmdir 2 | 3 | cwd = path($.cwd()) 4 | dirname = path($.dirname()) 5 | test = dirname.join('readfiletest.txt') 6 | 7 | uwu = "hello uwu" 8 | owo = "\nhi owo" 9 | 10 | $.assert(cwd.kind() == "dir") 11 | $.assert(cwd.is_absolute() == true) 12 | $.assert(cwd.readdir().contains("README.md")) 13 | $.assert(dirname.is_relative() == true) 14 | $.assert(dirname.format() == "test") 15 | $.assert(dirname.readdir().length > 10) 16 | $.assert(test.kind() == "file") 17 | $.assert(test.exists() == true) 18 | $.assert(test.readonly() == false) // hopefully 19 | if test.read() != uwu { test.write(uwu) } 20 | $.assert(test.read() == uwu) 21 | test.append(owo) 22 | $.assert(test.read() == uwu + owo) 23 | test.write(uwu) 24 | $.assert(test.read() == uwu) 25 | test.delete() 26 | $.assert(test.exists() == false) 27 | test.mkdir() 28 | $.assert(test.exists() == true) 29 | $.assert(test.kind() == "dir") 30 | test.rmdir() 31 | $.assert(test.exists() == false) 32 | test.write(uwu) 33 | 34 | $.print("everything seems correct! 😎") 35 | -------------------------------------------------------------------------------- /test/pckp/main.spwn: -------------------------------------------------------------------------------- 1 | 2 | complex = import "pckp_libraries/SPWN-complex@1.0.0" 3 | 4 | let a = @complex::new(2,4) 5 | let b = @complex::new(-1,5) 6 | 7 | $.print(a * b) 8 | -------------------------------------------------------------------------------- /test/pckp/pckp.yaml: -------------------------------------------------------------------------------- 1 | %YAML 1.2 2 | --- 3 | 4 | name: shit_fuck 5 | version: 1.0.0 6 | folders: 7 | - src 8 | 9 | dependencies: 10 | - url: https://github.com/FlowVix/SPWN-complex -------------------------------------------------------------------------------- /test/pckup.spwn: -------------------------------------------------------------------------------- 1 | $.print("test") 2 | open("ok.spwn") -------------------------------------------------------------------------------- /test/physics.spwn: -------------------------------------------------------------------------------- 1 | body = { 2 | g: 10g, 3 | b: 3b, 4 | } 5 | 6 | ground = 4b 7 | side = 6b 8 | 9 | lava = 5b 10 | spawn_point = 119g 11 | 12 | gs = import gamescene 13 | 14 | move_body = (m: [@counter], fac = 1) { 15 | m[1].clone().reset(for_each = x => body.g.move(0, x * fac)) 16 | m[0].clone().reset(for_each = x => body.g.move(x * fac, 0)) 17 | } 18 | 19 | pos = [counter(100), counter(0)] 20 | vel = [counter(0), counter(0)] 21 | 22 | 23 | button_a = counter(false) 24 | button_b = counter(false) 25 | 26 | on(gs.button_a(), !{ 27 | button_a = true 28 | }) 29 | on(gs.button_a_end(), !{ 30 | button_a = false 31 | }) 32 | 33 | on(gs.button_b(), !{ 34 | button_b = true 35 | }) 36 | on(gs.button_b_end(), !{ 37 | button_b = false 38 | }) 39 | 40 | on(collision(body.b, lava), !{ 41 | body.g.move_to(spawn_point) 42 | }) 43 | 44 | is_in_ground = counter(body.b.create_tracker_item(ground), bits = 1) 45 | is_in_side_wall = counter(side.create_tracker_item(body.b), bits = 1) 46 | 47 | x_vel_max = 5 48 | max_fall_speed = 10 49 | 50 | -> while_loop(() => true, () { 51 | if @bool(is_in_ground) { 52 | vel[1] *= -1 53 | if vel[1] < max_fall_speed { 54 | vel[1] = max_fall_speed 55 | } 56 | //move_body(pos, -1) 57 | } else { 58 | if vel[1] > -max_fall_speed { 59 | vel[1] -= 1 60 | } 61 | } 62 | 63 | 64 | if @bool(is_in_side_wall) { 65 | vel[0] *= -1 66 | } else { 67 | if @bool(button_b) && vel[0] < x_vel_max { 68 | vel[0] += 1 69 | } else if vel[0] > 0 { 70 | vel[0] -= 1 71 | } 72 | 73 | if @bool(button_a) && vel[0] > -x_vel_max { 74 | vel[0] -= 1 75 | } else if vel[0] < 0 { 76 | vel[0] += 1 77 | } 78 | 79 | } 80 | 81 | pos[1] += vel[1] 82 | pos[0] += vel[0] 83 | 84 | move_body(vel) 85 | }) -------------------------------------------------------------------------------- /test/readfiletest.spwn: -------------------------------------------------------------------------------- 1 | json = $.readfile("jsontest.json", "json") 2 | $.print("JSON format:") 3 | $.print(json) 4 | 5 | toml = $.readfile("tomltest.toml", "toml") 6 | $.print("TOML format:") 7 | $.print(toml) 8 | 9 | yaml = $.readfile("yamltest.yaml", "yaml") 10 | $.print("YAML format:") 11 | $.print(yaml) 12 | -------------------------------------------------------------------------------- /test/readfiletest.txt: -------------------------------------------------------------------------------- 1 | hello uwu -------------------------------------------------------------------------------- /test/set/graphs.spwn: -------------------------------------------------------------------------------- 1 | 2 | extract obj_props 3 | 4 | center = get_objects(GROUPS, g => 2g in g)[0] 5 | thingy = @set::from(get_objects(GROUPS, g => 1g in g || 2g in g), center[X], center[Y]) 6 | 7 | d = (f, x) => (f(x+0.000001) - f(x)) / 0.000001 8 | a = (f, x) => - $.atan( d(f, x) ) * 180 / PI 9 | 10 | f1 = x => 2*$.sin(x/2) 11 | f2 = x => 2*(x/2)^2/10 12 | f3 = x => 2*$.fract(x/2) 13 | 14 | let groups = [] 15 | let f1_results = [] 16 | let f2_results = [] 17 | let f3_results = [] 18 | 19 | origin = [15 + 30*30, 15 + 30*30] 20 | 21 | let x = -18 22 | while x <= 18 { 23 | g1 = ?g; g2 = ?g 24 | groups.push([g1, g2]) 25 | o_x = origin[0] + x*30 26 | thingy.replace_group(1g, g1).replace_group(2g, g2).place(o_x, origin[1]) 27 | f1_results.push([ 28 | f1(x) * 30, 29 | a(f1, x), 30 | ]) 31 | f2_results.push([ 32 | f2(x) * 30, 33 | a(f2, x), 34 | ]) 35 | f3_results.push([ 36 | f3(x) * 30, 37 | a(f3, x), 38 | ]) 39 | x += 0.7 40 | } 41 | for i in 0..groups.length { 42 | -> groups[i][0].follow(groups[i][1]) 43 | } 44 | wait(3) 45 | for i in 0..groups.length { 46 | -> groups[i][1].move_to_xy(y = origin[1] + f1_results[i][0], duration = 1, easing = EASE_IN_OUT) 47 | -> groups[i][0].rotate(groups[i][1], f1_results[i][1], 1, easing = EASE_IN_OUT) 48 | } 49 | 50 | wait(2) 51 | for i in 0..groups.length { 52 | -> groups[i][1].move_to_xy(y = origin[1] + f2_results[i][0], duration = 1, easing = EASE_IN_OUT) 53 | -> groups[i][0].rotate(groups[i][1], f2_results[i][1] - f1_results[i][1], 1, easing = EASE_IN_OUT) 54 | } 55 | wait(2) 56 | for i in 0..groups.length { 57 | -> groups[i][1].move_to_xy(y = origin[1] + f3_results[i][0], duration = 1, easing = EASE_IN_OUT) 58 | -> groups[i][0].rotate(groups[i][1], f3_results[i][1] - f2_results[i][1], 1, easing = EASE_IN_OUT) 59 | } 60 | 61 | 62 | 63 | -------------------------------------------------------------------------------- /test/set/tile.spwn: -------------------------------------------------------------------------------- 1 | 2 | extract obj_props 3 | 4 | tiles = [ 5 | get_objects(GROUPS, ==[2g]), 6 | get_objects(GROUPS, ==[3g]), 7 | get_objects(GROUPS, ==[4g]) 8 | ].map(objs => @set::from(objs)) 9 | 10 | places = get_objects(GROUPS, ==[1g]) 11 | 12 | for i in places { 13 | tiles[$.random(@array(0..3))].place(i[X], i[Y]) 14 | } 15 | 16 | 17 | -------------------------------------------------------------------------------- /test/sync_group_idea.spwn: -------------------------------------------------------------------------------- 1 | 2 | sync a //defines a "sync group" 3 | 4 | // any two parts of a sync group can not run at the same time 5 | n = counter() 6 | 7 | part1 = ?g 8 | part2 = ?g 9 | 10 | 11 | -> if n > 10 { 12 | a[part1] { 13 | // ... part [1] of sync group a 14 | } 15 | } else { 16 | a[part2] { 17 | // ... part [2] of sync group a 18 | } 19 | } 20 | 21 | if n == 12 { 22 | // since this could happen at the 23 | // same time as a[1] (bacause of the ->) 24 | // this must also be a[1] 25 | a[part1] { 26 | // ... 27 | } 28 | } 29 | 30 | 31 | // if a sync group has only one part it will be optimized away 32 | 33 | // in a situation like this 34 | a[1] { 35 | b[1] { 36 | //... 37 | } 38 | } 39 | 40 | a[1] { 41 | b[2] { 42 | //... 43 | } 44 | } 45 | 46 | b[3] { 47 | //... 48 | } 49 | 50 | // sync group a can be optimized away 51 | 52 | 53 | // FURTHER STUFF: 54 | 55 | 56 | // a synchronous macro is defined like this 57 | 58 | m = sync (arguments) { 59 | // ... 60 | } 61 | 62 | // a synchronous macro is assumed to be done when it returns, 63 | // so its triggers can be toggled off 64 | // if it is called with a -> it is not assumed to be sync anymore 65 | 66 | // if it should still be sync with a ->, 67 | // the tag #[force_sync] can be applied 68 | -------------------------------------------------------------------------------- /test/test.spwn: -------------------------------------------------------------------------------- 1 | #[no_level] 2 | 3 | 4 | 5 | $.print([0, 1, 2].reduce($._plus_)) 6 | 7 | -------------------------------------------------------------------------------- /test/test2.spwn: -------------------------------------------------------------------------------- 1 | sqrt = (n: @counter, result: @counter) { 2 | result = n 3 | y = counter(1, bits = 10) 4 | 5 | 6 | while_loop(() => result > y, () { 7 | result = (result + y) / 2 8 | wait(0.05) 9 | y = n / result 10 | }) 11 | } 12 | 13 | buttons = (1g..12g) + (13g..20g) 14 | $.print(buttons.length) 15 | selected = counter(0) 16 | selector = 12g 17 | 18 | gamescene = import gamescene 19 | 20 | main = counter(1i, bits = 17) 21 | sec = counter(2i, bits = 17) 22 | 23 | 24 | on(gamescene.button_b(), !{ 25 | selected += 1 26 | if selected == buttons.length { 27 | selected = 0 28 | } 29 | selector.move_to(buttons[selected.to_const(..buttons.length)]) 30 | }) 31 | 32 | current_op = counter() 33 | res = counter(3i) 34 | 35 | main10 = counter(bits = 12) 36 | sec10 = counter(bits = 12) 37 | 38 | on(gamescene.button_a(), !{ 39 | if selected <= 9 { 40 | //number 41 | main = main * 10 + selected 42 | } else if selected == 10 { 43 | main = 0 44 | } else if selected == 11 { 45 | //solve 46 | if current_op == 0 { 47 | //plus 48 | main += sec 49 | } else if current_op == 1 { 50 | //minus 51 | sec -= main 52 | main = sec 53 | } else if current_op == 2 { 54 | //mult 55 | main *= sec 56 | } else if current_op == 3 { 57 | //div 58 | sec /= main 59 | main = sec 60 | } else if current_op == 4 { 61 | res = 1 62 | while_loop(() => main > 0, () { 63 | res *= sec 64 | main-- 65 | }) 66 | wait(0.1) 67 | main = res 68 | } 69 | sec = 0 70 | } else if selected == 17 { 71 | //-> BG.pulse(255, 0, 0, fade_out = 0.5) 72 | main10 = main 73 | sec10 = sec 74 | sqrt(main10, sec10) 75 | main = sec10 76 | } else { 77 | //operator 78 | button = selected.to_const(12..18) 79 | -> buttons[button].pulse(0, 255, 0, fade_out = 3) 80 | current_op = button - 12 81 | sec = main 82 | main = 0 83 | } 84 | }) -------------------------------------------------------------------------------- /test/tomltest.toml: -------------------------------------------------------------------------------- 1 | [hello] 2 | text="Hello world" 3 | number=123 4 | pi=3.141 5 | 6 | [world] 7 | aa=true 8 | bb=false 9 | cc=1970-01-01T01:01:01-01:00 -------------------------------------------------------------------------------- /test/unsafe/main.spwn: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | a = counter(1i) 6 | b = counter(2i) 7 | c = counter(0) 8 | 9 | a.display(100,100) 10 | b.display(150,100) 11 | c.display(250,100) 12 | 13 | wait(1) 14 | 15 | c = a / b 16 | 17 | -------------------------------------------------------------------------------- /test/vector/main.spwn: -------------------------------------------------------------------------------- 1 | 2 | 3 | let a = @vector::new([4,1,-2,6]) 4 | $.print(a.z) 5 | -------------------------------------------------------------------------------- /test/yamltest.yaml: -------------------------------------------------------------------------------- 1 | b: [true, false] 2 | x: 0.12345 3 | y: 12313 4 | z: false 5 | 6 | qweqwe: 7 | - 'hello' 8 | - 'world' 9 | - 'asdasdasdasdasd' 10 | 11 | asd: 12 | dsa: 13 | - qwe 14 | - rty 15 | - qweqwe: iop 16 | --------------------------------------------------------------------------------