├── .github └── workflows │ ├── deploy.yml │ └── release.yml ├── .gitignore ├── LICENSE ├── README.md ├── build.zig ├── build.zig.zon ├── examples ├── asteroid.wren ├── basic.wren ├── breakout │ ├── ball.wren │ ├── brick.wren │ ├── game.wren │ ├── main.wren │ ├── paddle.wren │ ├── particle.wren │ ├── res │ │ ├── brick.png │ │ ├── paddle.png │ │ └── tennis.png │ ├── score.wren │ └── utils.wren ├── build-exe │ ├── .gitignore │ ├── Dockerfile │ ├── docker-compose.yml │ ├── game.wren │ └── main.wren ├── build-wasm │ ├── .gitignore │ ├── Dockerfile │ ├── docker-compose.yml │ ├── game.wren │ ├── main.wren │ └── shell.html ├── camera.wren ├── extend │ ├── add.c │ ├── add.dll │ ├── build.zig │ ├── build.zig.zon │ ├── game.wren │ └── main.wren ├── main.wren ├── playground │ ├── abc.zig │ ├── build.zig │ ├── build.zig.zon │ ├── flake.lock │ ├── flake.nix │ ├── game.wren │ ├── index.html │ ├── main.wren │ └── samples │ │ ├── .nojekyll │ │ ├── asteroid.wren │ │ ├── basic.wren │ │ └── camera.wren └── shapes │ └── main.wren ├── flake.lock ├── flake.nix ├── index.wren └── src ├── bindings ├── builtin.wren ├── builtin.zig ├── embed.wren ├── generate │ ├── README.md │ ├── generate.zig │ └── rcore.zig ├── index.zig ├── math.wren ├── math.zig ├── raylib.wren └── raylib.zig ├── cli.zig ├── common.zig ├── main.c ├── main.zig ├── resolve_module.zig ├── root-wasm.zig ├── root.zig ├── templates ├── exe │ ├── Dockerfile │ └── docker-compose.yml └── wasm │ ├── Dockerfile │ ├── docker-compose.yml │ └── shell.html └── watcher ├── LinuxWatcher.zig ├── MacosWatcher.zig ├── README.md ├── WindowsWatcher.zig ├── channel.zig ├── hot.zig └── websocket.zig /.github/workflows/deploy.yml: -------------------------------------------------------------------------------- 1 | on: 2 | workflow_dispatch: 3 | 4 | name: Deploy 5 | 6 | jobs: 7 | release: 8 | permissions: 9 | contents: write 10 | runs-on: ubuntu-latest 11 | 12 | steps: 13 | - name: Checkout 14 | uses: actions/checkout@v4 15 | 16 | - name: Install Nix 17 | uses: cachix/install-nix-action@v31 18 | with: 19 | nix_path: nixpkgs=channel:nixos-unstable 20 | 21 | - run: nix develop -L --verbose 22 | 23 | - name: build-emscripten 24 | run: cd examples/playground && nix develop -c zig build -Dtarget=wasm32-emscripten -Doptimize=ReleaseSmall 25 | 26 | - name: Deploy to Github Pages 27 | uses: peaceiris/actions-gh-pages@v3 28 | with: 29 | github_token: ${{ secrets.GITHUB_TOKEN }} 30 | publish_dir: ./examples/playground/zig-out/www/ 31 | publish_branch: gh-pages 32 | destination_dir: . 33 | -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | on: 2 | push: 3 | tags: 4 | - "*" 5 | 6 | name: Release 7 | 8 | jobs: 9 | release: 10 | permissions: 11 | contents: write 12 | runs-on: ubuntu-latest 13 | 14 | steps: 15 | - name: Checkout 16 | uses: actions/checkout@v4 17 | 18 | - name: Install Nix 19 | uses: cachix/install-nix-action@v31 20 | with: 21 | nix_path: nixpkgs=channel:nixos-unstable 22 | 23 | - run: nix develop -L --verbose 24 | 25 | - name: release-linux 26 | run: nix develop -c zig build -Doptimize=ReleaseSmall 27 | 28 | - name: release-windows 29 | run: nix develop -c zig build -Dtarget=x86_64-windows -Doptimize=ReleaseSmall 30 | 31 | - name: Upload Releases 32 | uses: softprops/action-gh-release@v1 33 | with: 34 | files: | 35 | ./zig-out/bin/talon.exe 36 | ./zig-out/bin/talon 37 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .git 2 | .zig-cache 3 | zig-out 4 | .emscripten_cache-4.0.8 5 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2025 Jossephus 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |

Talon

2 |

Write Raylib programs in Wren

3 |

4 | 5 | ## Talon 6 | 7 | **Talon** is a (wannabe) 2D Game framwork that uses [Raylib](https://www.raylib.com/) as its rendering engine and [Wren](https://wren.io) as its scripting language. I recently started playing with Raylib to build a couple of toy games and I was really impressed with it. Wren is also one of my favorite lightweight languages. This project is a love letter to both of these projects. 8 | 9 | ## Status 10 | 11 | The goal of this project is to eventually become stable enough to satisfy my needs in creating any 2D games. The inspiration for this was [love2d](love2d) and my plan is to make Talon a good alternative to love2d. At this time, it only implements basic Raylib functionalities, but in the future We will add Audio, Physics, Full Math, ... supports. 12 | 13 | ### Features 14 | 15 | - [Raylib Functions](#raylib-functions) 16 | - [Hot Reload on File Change](#hot-reload-on-file-change) 17 | - [Build Executable files for Linux/Windows](#build-executable-files-for-linuxwindows) 18 | - [Build Wasm using emscripten for ur talon games](#build-html-files) 19 | - [Use dynamic libraries (.so, .dll) to add functionalities](#use-dynamic-libraries-sodll-to-to-add-functionalities) 20 | - [Playground](#playground) 21 | - ... and many others 22 | 23 | ## Demo 24 | 25 | Wren is a very simple language. You can master it in less than 30 mins by just skimming the documentation on its [official website](wren.io). 26 | 27 | Here is how you create a very basic window using Talon. 28 | 29 | ```wren 30 | import "raylib" for Color, Raylib, Rectangle, Vector2, Camera2D, KeyCode, Texture2D 31 | 32 | var width = 800 33 | var height = 450 34 | var title = "Sample" 35 | 36 | Raylib.initWindow(width, height, title) 37 | Raylib.setTargetFPS(60) 38 | 39 | var camera = Camera2D.new( 40 | Vector2.new(2.0, 0.0), 41 | Vector2.new(0.0, 0.0), 42 | 0.0, 43 | 1.0 44 | ) 45 | 46 | var target = Raylib.loadRenderTexture(width, height) 47 | 48 | while (!Raylib.windowShouldClose()) { 49 | Raylib.beginDrawing() 50 | 51 | Raylib.clearBackground(Color.Red) 52 | 53 | Raylib.drawText("Congrats! You created your first window!", 190, 200, 20, Color.Green) 54 | 55 | Raylib.getScreenWidth() 56 | Raylib.getScreenHeight() 57 | 58 | if (Raylib.isKeyDown(KeyCode.KEY_SPACE)) { 59 | System.print("Space is Pressed") 60 | } 61 | 62 | Raylib.drawTexturePro(target.texture, 63 | Rectangle.new(0.0, 0.0, target.texture.width, target.texture.height), 64 | Rectangle.new(0.0, 0.0, target.texture.width, target.texture.height), 65 | Vector2.new(0.0, 0.0), 66 | 0.0, 67 | Color.new(255, 255, 255, 255 ) 68 | ) 69 | 70 | Raylib.endDrawing() 71 | } 72 | 73 | Raylib.clearBackground(Color.Gray) 74 | Raylib.unloadRenderTexture(target) 75 | Raylib.closeWindow() 76 | ``` 77 | 78 | ## Getting Started 79 | 80 | We provide releases for linux/windows (other platforms coming soon). You can download from releases and try it out. You can save the above demo program in index.wren file and you can use 81 | 82 | ```sh 83 | $ talon index.wren 84 | ``` 85 | 86 | to run it. You should see a simple window at this point. 87 | 88 | ## Features 89 | 90 | #### Raylib Functions 91 | 92 | Almost all Raylib functions are exposed to users of talon. We change each function of to be exposed from the Raylib class and uses Snake Case for Talon instead. For example for the raylib function 'InitWindow' it would be 'Raylib.initWindow' in Talon. 93 | 94 | Raylib structs like Camera2D, Vector2 and ... are exposed as class in Talon with the same name. You can checkout the examples to see how to utilize them. 95 | 96 | #### Hot Reload on File Change 97 | 98 | running talon with --hot option runs it in full file watcher mode and reloads when there is any change in ur wren files. 99 | 100 | ```sh 101 | talon --hot index.wren 102 | 103 | ``` 104 | 105 | TODO: At its current status we are rerunning the whole program but it will need some work so we can reload the parts that get changed. 106 | 107 | #### Build Executable files for Linux/Windows 108 | 109 | We use docker to provide linux/windows builds for talon. When you are ready to distribute your game for windows/linux users you can run 110 | 111 | ```sh 112 | $ talon init-exe index.wren 113 | ``` 114 | 115 | Two files with name Dockerfile and docker-compose.yml will be created for you and then You can use 'docker compose up --build -d' which will create windows/linux builds in ur current dist/ folder. 116 | 117 | #### Build HTML files 118 | 119 | We use emscripten to give u a working implementation of ur raylib games. This also depends on docker and you can run 120 | 121 | ```sh 122 | $ talon init-wasm index.wren 123 | $ docker compose up --build -d 124 | ``` 125 | 126 | You can then see ur wasm builds in dist/ folder 127 | 128 | #### Use dynamic libraries (.so, .dll) to to add functionalities 129 | 130 | Check examples/extend to see how this works. TODO:// add details 131 | 132 | #### Playground 133 | 134 | Checkout [Playground](jossephus.github.io/talon/) 135 | -------------------------------------------------------------------------------- /build.zig: -------------------------------------------------------------------------------- 1 | const std = @import("std"); 2 | 3 | fn addAssets(b: *std.Build, exe: *std.Build.Step.Compile) void { 4 | const assets = [_][]const u8{}; 5 | 6 | for (assets) |asset| { 7 | exe.root_module.addAnonymousImport(asset, .{ .root_source_file = b.path(asset) }); 8 | } 9 | } 10 | 11 | fn addWrenDep(b: *std.Build, target: std.Build.ResolvedTarget, optimize: std.builtin.Mode) *std.Build.Step.Compile { 12 | const upstream = b.dependency("wren", .{}); 13 | 14 | const wren_lib = b.addStaticLibrary(.{ 15 | .name = "wren", 16 | .target = target, 17 | .optimize = optimize, 18 | }); 19 | wren_lib.linkLibC(); 20 | wren_lib.addIncludePath(upstream.path("src/include")); 21 | wren_lib.addIncludePath(upstream.path("src/vm")); 22 | wren_lib.addIncludePath(upstream.path("src/optional")); 23 | 24 | wren_lib.addCSourceFiles(.{ .root = upstream.path("src/vm"), .files = &.{ 25 | "wren_compiler.c", 26 | "wren_core.c", 27 | "wren_debug.c", 28 | "wren_primitive.c", 29 | "wren_utils.c", 30 | "wren_value.c", 31 | "wren_vm.c", 32 | } }); 33 | 34 | wren_lib.addCSourceFiles(.{ .root = upstream.path("src/optional"), .files = &.{ 35 | "wren_opt_meta.c", 36 | "wren_opt_random.c", 37 | } }); 38 | 39 | wren_lib.installHeadersDirectory(upstream.path("src/include"), "", .{ 40 | .include_extensions = &.{ 41 | "wren.h", 42 | }, 43 | }); 44 | 45 | if (target.result.cpu.arch.isWasm()) wren_lib.addIncludePath(.{ .cwd_relative = ".emscripten_cache-4.0.8/sysroot/include" }); 46 | 47 | b.installArtifact(wren_lib); 48 | return wren_lib; 49 | } 50 | 51 | pub fn build(b: *std.Build) void { 52 | const target = b.standardTargetOptions(.{}); 53 | const optimize = b.standardOptimizeOption(.{}); 54 | 55 | const wren_lib = addWrenDep(b, target, optimize); 56 | 57 | const raylib_dep = if (target.result.cpu.arch.isWasm()) b.dependency("raylib", .{ 58 | .target = target, 59 | .optimize = optimize, 60 | .rmodels = false, 61 | }) else b.dependency("raylib", .{ 62 | .target = target, 63 | .optimize = optimize, 64 | .linux_display_backend = .X11, 65 | }); 66 | 67 | const raylib_lib = raylib_dep.artifact("raylib"); 68 | 69 | //exe_mod.linkLibrary(wren_lib); 70 | 71 | const exe = b.addExecutable(.{ 72 | .name = "talon", 73 | .root_source_file = b.path("src/main.zig"), 74 | .target = target, 75 | .optimize = optimize, 76 | .link_libc = true, 77 | }); 78 | 79 | exe.linkLibrary(wren_lib); 80 | exe.linkLibrary(raylib_lib); 81 | 82 | const lib = b.addModule("tolan", .{ 83 | .root_source_file = b.path("src/root.zig"), 84 | }); 85 | 86 | lib.linkLibrary(wren_lib); 87 | lib.linkLibrary(raylib_lib); 88 | if (target.result.cpu.arch.isWasm()) lib.addIncludePath(.{ .cwd_relative = ".emscripten_cache-4.0.8/sysroot/include" }); 89 | 90 | const libWasm = b.addModule("tolan-wasm", .{ 91 | .root_source_file = b.path("src/root-wasm.zig"), 92 | }); 93 | 94 | libWasm.linkLibrary(wren_lib); 95 | libWasm.linkLibrary(raylib_lib); 96 | if (target.result.cpu.arch.isWasm()) libWasm.addIncludePath(.{ .cwd_relative = ".emscripten_cache-4.0.8/sysroot/include" }); 97 | 98 | const c_exe_mod = b.createModule(.{ 99 | .target = target, 100 | .optimize = optimize, 101 | }); 102 | const c_exe = b.addExecutable(.{ 103 | .name = "sample_c", 104 | .root_module = c_exe_mod, 105 | }); 106 | c_exe.addCSourceFiles(.{ 107 | .root = b.path("src/"), 108 | .files = &.{"main.c"}, 109 | }); 110 | c_exe.linkLibrary(wren_lib); 111 | c_exe.linkLibrary(raylib_lib); 112 | const c_sample_step = b.step("sample", "Generate raylib bindings"); 113 | const sample_run = b.addRunArtifact(c_exe); 114 | c_sample_step.dependOn(&sample_run.step); 115 | 116 | //const install_step = b.addInstallDirectory(.{ 117 | //.source_dir = b.path("res"), 118 | //.install_dir = std.Build.InstallDir{ .custom = "res" }, 119 | //.install_subdir = "res", 120 | //}); 121 | //exe.step.dependOn(&install_step.step); 122 | //addAssets(b, exe); 123 | 124 | b.installArtifact(exe); 125 | b.installArtifact(raylib_lib); 126 | 127 | const run_cmd = b.addRunArtifact(exe); 128 | 129 | run_cmd.step.dependOn(b.getInstallStep()); 130 | 131 | if (b.args) |args| { 132 | run_cmd.addArgs(args); 133 | } 134 | const run_step = b.step("run", "Run the app"); 135 | run_step.dependOn(&run_cmd.step); 136 | 137 | const run_abc_step = b.step("breakout", "Run the executable with abc.txt"); 138 | 139 | const run_abc = b.addRunArtifact(exe); 140 | run_abc.setCwd(b.path("examples/breakout")); 141 | run_abc.addArg("./main.wren"); 142 | 143 | run_abc_step.dependOn(&run_abc.step); 144 | 145 | const generator = b.addExecutable(.{ 146 | .name = "generator", 147 | .root_source_file = b.path("src/bindings/generate/generate.zig"), 148 | .target = target, 149 | .optimize = optimize, 150 | .link_libc = true, 151 | }); 152 | 153 | const generator_step = b.step("generator", "Generate raylib bindings"); 154 | 155 | const generator_run = b.addRunArtifact(generator); 156 | //generator_run.setCwd(b.path("examples/breakout")); 157 | //run_abc.addArg("./main.wren"); 158 | 159 | generator_step.dependOn(&generator_run.step); 160 | } 161 | -------------------------------------------------------------------------------- /build.zig.zon: -------------------------------------------------------------------------------- 1 | .{ 2 | // This is the default name used by packages depending on this one. For 3 | // example, when a user runs `zig fetch --save `, this field is used 4 | // as the key in the `dependencies` table. Although the user can choose a 5 | // different name, most users will stick with this provided value. 6 | // 7 | // It is redundant to include "zig" in this name because it is already 8 | // within the Zig package namespace. 9 | .name = .zig_wren, 10 | 11 | // This is a [Semantic Version](https://semver.org/). 12 | // In a future version of Zig it will be used for package deduplication. 13 | .version = "0.0.0", 14 | 15 | // Together with name, this represents a globally unique package 16 | // identifier. This field is generated by the Zig toolchain when the 17 | // package is first created, and then *never changes*. This allows 18 | // unambiguous detection of one package being an updated version of 19 | // another. 20 | // 21 | // When forking a Zig project, this id should be regenerated (delete the 22 | // field and run `zig build`) if the upstream project is still maintained. 23 | // Otherwise, the fork is *hostile*, attempting to take control over the 24 | // original project's identity. Thus it is recommended to leave the comment 25 | // on the following line intact, so that it shows up in code reviews that 26 | // modify the field. 27 | .fingerprint = 0xfa33cb598878ccfc, // Changing this has security and trust implications. 28 | 29 | // Tracks the earliest Zig version that the package considers to be a 30 | // supported use case. 31 | .minimum_zig_version = "0.14.0", 32 | 33 | // This field is optional. 34 | // Each dependency must either provide a `url` and `hash`, or a `path`. 35 | // `zig build --fetch` can be used to fetch all dependencies of a package, recursively. 36 | // Once all dependencies are fetched, `zig build` no longer requires 37 | // internet connectivity. 38 | .dependencies = .{ 39 | .wren = .{ 40 | .url = "https://github.com/wren-lang/wren/archive/refs/heads/main.zip", 41 | .hash = "N-V-__8AAPbpYgAZgdr-sM49A18GSKr5MVR56MwpfI65FmAZ", 42 | }, 43 | .raylib = .{ 44 | .url = "git+https://github.com/raysan5/raylib#27a4fe885164b315a90b67682f981a1e03d6079c", 45 | .hash = "raylib-5.5.0-whq8uFqtNARw6t1tTakBDSVYgUjlBqnDppOyNfE_yfCa", 46 | }, 47 | }, 48 | .paths = .{ 49 | "build.zig", 50 | "build.zig.zon", 51 | "src", 52 | // For example... 53 | //"LICENSE", 54 | //"README.md", 55 | }, 56 | } 57 | -------------------------------------------------------------------------------- /examples/asteroid.wren: -------------------------------------------------------------------------------- 1 | import "raylib" for Color, Raylib, Rectangle, Vector2, Camera2D, KeyCode, Texture2D, Vector3 2 | import "math" for Math 3 | 4 | 5 | var PLAYER_BASE_SIZE = 20 6 | var PLAYER_SPEED = 6.0 7 | var PLAYER_MAX_SHOOTS = 10 8 | var DEG2RAD = 0.017453 9 | 10 | var METEORS_SPEED = 2 11 | var MAX_MEDIUM_METEORS = 8 12 | var MAX_SMALL_METEORS = 16 13 | 14 | var ScreenWidth = 800 15 | var ScreenHeight = 450 16 | 17 | class Meteor { 18 | construct new(position, speed, radius, active, color) { 19 | _position = position 20 | _speed = speed 21 | _radius = radius 22 | _active = active 23 | _color = color 24 | } 25 | 26 | position { _position } 27 | speed { _speed } 28 | active { _active } 29 | color { _color } 30 | radius { _radius } 31 | 32 | position=(value) { _position = value } 33 | speed=(value) { _speed = value } 34 | active=(value) { _active = value } 35 | color=(value) { _color = value } 36 | radius=(value) { _radius = value } 37 | } 38 | 39 | class Player { 40 | construct new(position, speed, acceleration, rotation, collider, color) { 41 | _position = position 42 | _speed = speed 43 | _acceleration = acceleration 44 | _rotation = rotation 45 | _collider = collider 46 | _color = color 47 | } 48 | position { _position } 49 | speed { _speed } 50 | acceleration { _acceleration } 51 | rotation { _rotation } 52 | collider { _collider } 53 | color { _color } 54 | 55 | position=(value) { _position = value } 56 | speed=(value) { _speed = value } 57 | acceleration=(value) { _acceleration = value } 58 | rotation=(value) { _rotation = value } 59 | collider=(value) { _collider = value } 60 | color=(value) { _color = value } 61 | } 62 | 63 | 64 | class Game { 65 | initGame() { 66 | var posx = 0 67 | var posy = 0 68 | var velx = 0 69 | var vely = 0 70 | var correctRange = false 71 | 72 | _pause = false 73 | _gameOver = false 74 | _framesCounter = 0 75 | _shipHeight = (PLAYER_BASE_SIZE / 2) / Math.tan(20 * DEG2RAD) 76 | 77 | _mediumMeteor = [] 78 | _smallMeteor = [] 79 | 80 | var x = ScreenWidth / 2 81 | var y = ScreenHeight / 2 - _shipHeight / 2 82 | 83 | _player = Player.new( 84 | Vector2.new(x, y), 85 | Vector2.new(0, 0), 86 | 0, 87 | 0, 88 | Vector3.new( 89 | x + Math.sin(0 * DEG2RAD) * (_shipHeight / 2.5), 90 | y - Math.cos(0 * DEG2RAD) * (_shipHeight / 2.5), 91 | 12), 92 | Color.LightGray 93 | ) 94 | 95 | for (i in 0...MAX_MEDIUM_METEORS) { 96 | 97 | posx = Raylib.getRandomValue(0, ScreenWidth) 98 | 99 | while (!correctRange) { 100 | if (posx > ScreenWidth/2 - 150 && posx < ScreenWidth/2 + 150) { 101 | posx = Raylib.getRandomValue(0, ScreenWidth) 102 | } else { 103 | correctRange = true 104 | } 105 | } 106 | 107 | correctRange = false 108 | 109 | posy = Raylib.getRandomValue(0, ScreenHeight) 110 | 111 | while (!correctRange) { 112 | if (posy > ScreenHeight/2 - 150 && posy < ScreenHeight/2 + 150) { 113 | posy = Raylib.getRandomValue(0, ScreenHeight) 114 | } else { 115 | correctRange = true 116 | } 117 | } 118 | 119 | correctRange = false 120 | velx = Raylib.getRandomValue(-METEORS_SPEED, METEORS_SPEED) 121 | vely = Raylib.getRandomValue(-METEORS_SPEED, METEORS_SPEED) 122 | 123 | while (!correctRange) { 124 | if (velx == 0 && vely == 0) { 125 | velx = Raylib.getRandomValue(-METEORS_SPEED, METEORS_SPEED) 126 | vely = Raylib.getRandomValue(-METEORS_SPEED, METEORS_SPEED) 127 | } else { 128 | correctRange = true 129 | } 130 | } 131 | _mediumMeteor.add( 132 | Meteor.new(Vector2.new(posx, posy), Vector2.new(velx, vely), 20, true, Color.Green) 133 | ) 134 | } 135 | 136 | for (j in 0...MAX_SMALL_METEORS) { 137 | posx = Raylib.getRandomValue(0, ScreenWidth) 138 | 139 | while (!correctRange) { 140 | if (posx > ScreenWidth/2 - 150 && posx < ScreenWidth/2 + 150) { 141 | posx = Raylib.getRandomValue(0, ScreenWidth) 142 | } else { 143 | correctRange = true 144 | } 145 | } 146 | 147 | correctRange = false 148 | 149 | posy = Raylib.getRandomValue(0, ScreenHeight) 150 | 151 | while (!correctRange) { 152 | if (posy > ScreenHeight/2 - 150 && posy < ScreenHeight/2 + 150) { 153 | posy = Raylib.getRandomValue(0, ScreenHeight) 154 | } else { 155 | correctRange = true 156 | } 157 | } 158 | 159 | correctRange = false 160 | velx = Raylib.getRandomValue(-METEORS_SPEED, METEORS_SPEED) 161 | vely = Raylib.getRandomValue(-METEORS_SPEED, METEORS_SPEED) 162 | 163 | while (!correctRange) { 164 | if (velx == 0 && vely == 0) { 165 | velx = Raylib.getRandomValue(-METEORS_SPEED, METEORS_SPEED) 166 | vely = Raylib.getRandomValue(-METEORS_SPEED, METEORS_SPEED) 167 | } else { 168 | correctRange = true 169 | } 170 | } 171 | _smallMeteor.add( 172 | Meteor.new(Vector2.new(posx, posy), Vector2.new(velx, vely), 20, true, Color.Yellow) 173 | ) 174 | } 175 | 176 | 177 | } 178 | 179 | construct new() { 180 | initGame() 181 | } 182 | 183 | player { _player } 184 | mediumMeteor { _mediumMeteor } 185 | smallMeteor { _smallMeteor } 186 | 187 | update() { 188 | if (!_gameOver) { 189 | if (Raylib.isKeyDown(KeyCode.KEY_P)) { 190 | _pause = !_pause 191 | } 192 | 193 | if (!_pause) { 194 | _framesCounter = _framesCounter + 1 195 | 196 | // Rotation 197 | if (Raylib.isKeyDown(KeyCode.KEY_LEFT)) { 198 | _player.rotation = player.rotation - 5 199 | } 200 | if (Raylib.isKeyDown(KeyCode.KEY_RIGHT)) { 201 | System.print("Right is clicked") 202 | _player.rotation = _player.rotation + 5 203 | } 204 | 205 | // Speed 206 | _player.speed.x = Math.sin(_player.rotation*DEG2RAD)*PLAYER_SPEED 207 | _player.speed.y = Math.cos(_player.rotation*DEG2RAD)*PLAYER_SPEED 208 | 209 | // Controller 210 | if (Raylib.isKeyDown(KeyCode.KEY_UP)) { 211 | System.print("UP IS CLICKING") 212 | if (_player.acceleration < 1) _player.acceleration = _player.acceleration + 0.04 213 | } else { 214 | if (_player.acceleration > 0) { 215 | _player.acceleration = _player.acceleration - 0.02 216 | } else if (player.acceleration < 0) { 217 | _player.acceleration = 0 218 | } 219 | } 220 | if (Raylib.isKeyDown(KeyCode.KEY_DOWN)) { 221 | if (_player.acceleration > 0) { 222 | _player.acceleration = _player.acceleration - 0.04 223 | } else if (_player.acceleration < 0) { 224 | _player.acceleration = 0 225 | } 226 | } 227 | 228 | // Movement 229 | _player.position.x = _player.position.x + (_player.speed.x*_player.acceleration) 230 | _player.position.y = _player.position.y - (player.speed.y * player.acceleration) 231 | 232 | // Wall behaviour for player 233 | if (_player.position.x > ScreenWidth + _shipHeight) { 234 | _player.position.x = -(_shipHeight) 235 | } else if (_player.position.x < -(_shipHeight)) { 236 | player.position.x = ScreenWidth + _shipHeight 237 | } 238 | if (player.position.y > (ScreenHeight + _shipHeight)) { 239 | player.position.y = -(_shipHeight) 240 | } else if (_player.position.y < -(_shipHeight)) { 241 | _player.position.y = ScreenHeight + _shipHeight 242 | } 243 | 244 | // Collision Player to meteors 245 | _player.collider = Vector3.new(player.position.x + Math.sin(player.rotation*DEG2RAD)*(_shipHeight/2.5), player.position.y - Math.cos(player.rotation*DEG2RAD)*(_shipHeight/2.5), 12) 246 | 247 | 248 | for (i in 0...MAX_MEDIUM_METEORS) { 249 | if (Raylib.checkCollisionCircles(Vector2.new(player.collider.x, player.collider.y), player.collider.z, _mediumMeteor[i].position, _mediumMeteor[i].radius) && _mediumMeteor[i].active) { 250 | _gameOver = true 251 | } 252 | } 253 | 254 | for (a in 0...MAX_SMALL_METEORS) { 255 | if (Raylib.checkCollisionCircles(Vector2.new(player.collider.x, player.collider.y), player.collider.z, _smallMeteor[a].position, _smallMeteor[a].radius) && _smallMeteor[a].active) { 256 | _gameOver = true 257 | } 258 | } 259 | 260 | // Meteor logic 261 | 262 | for (i in 0...MAX_MEDIUM_METEORS) { 263 | if (mediumMeteor[i].active) { 264 | // movement 265 | mediumMeteor[i].position.x = mediumMeteor[i].position.x + mediumMeteor[i].speed.x 266 | mediumMeteor[i].position.y = mediumMeteor[i].position.y + mediumMeteor[i].speed.y 267 | 268 | if (mediumMeteor[i].position.x > ScreenWidth + mediumMeteor[i].radius) mediumMeteor[i].position.x = -(mediumMeteor[i].radius) else if (mediumMeteor[i].position.x < 0 - mediumMeteor[i].radius) { 269 | mediumMeteor[i].position.x = ScreenWidth + mediumMeteor[i].radius 270 | } 271 | if (mediumMeteor[i].position.y > ScreenHeight + mediumMeteor[i].radius) mediumMeteor[i].position.y = -(mediumMeteor[i].radius) else if (mediumMeteor[i].position.y < 0 - mediumMeteor[i].radius) { 272 | mediumMeteor[i].position.y = ScreenHeight + mediumMeteor[i].radius 273 | } 274 | } 275 | } 276 | for (i in 0...MAX_SMALL_METEORS) { 277 | if (smallMeteor[i].active) { 278 | // movement 279 | smallMeteor[i].position.x = smallMeteor[i].position.x + smallMeteor[i].speed.x 280 | smallMeteor[i].position.y = smallMeteor[i].position.y + smallMeteor[i].speed.y 281 | 282 | if (smallMeteor[i].position.x > ScreenWidth + smallMeteor[i].radius) smallMeteor[i].position.x = -(smallMeteor[i].radius) else if (smallMeteor[i].position.x < 0 - smallMeteor[i].radius) { 283 | smallMeteor[i].position.x = ScreenWidth + smallMeteor[i].radius 284 | } 285 | if (smallMeteor[i].position.y > ScreenHeight + smallMeteor[i].radius) smallMeteor[i].position.y = -(smallMeteor[i].radius) else if (smallMeteor[i].position.y < 0 - smallMeteor[i].radius) { 286 | smallMeteor[i].position.y = ScreenHeight + smallMeteor[i].radius 287 | } 288 | } 289 | } 290 | } 291 | } else { 292 | if (Raylib.isKeyDown(KeyCode.KEY_ENTER)) { 293 | _gameOver = false 294 | initGame() 295 | } 296 | } 297 | } 298 | 299 | draw() { 300 | Raylib.beginDrawing() 301 | 302 | Raylib.clearBackground(Color.RayWhite) 303 | 304 | if (!_gameOver) { 305 | // Draw spaceship 306 | var v1 = Vector2.new(player.position.x + Math.sin(player.rotation*DEG2RAD)*(_shipHeight), player.position.y - Math.cos(player.rotation*DEG2RAD)*(_shipHeight)) 307 | var v2 = Vector2.new( 308 | player.position.x - Math.cos(player.rotation * DEG2RAD) * (PLAYER_BASE_SIZE / 2), 309 | player.position.y - Math.sin(player.rotation * DEG2RAD) * (PLAYER_BASE_SIZE / 2) 310 | ) 311 | var v3 = Vector2.new( 312 | player.position.x + Math.cos(player.rotation * DEG2RAD) * (PLAYER_BASE_SIZE / 2), 313 | player.position.y + Math.sin(player.rotation * DEG2RAD) * (PLAYER_BASE_SIZE / 2) 314 | ) 315 | 316 | Raylib.drawTriangle(v1, v2, v3, Color.Maroon) 317 | 318 | // Draw meteor 319 | for (i in 0...MAX_MEDIUM_METEORS) { 320 | if (mediumMeteor[i].active) Raylib.drawCircleV(mediumMeteor[i].position, mediumMeteor[i].radius, Color.Gray) else Raylib.drawCircleV(mediumMeteor[i].position, mediumMeteor[i].radius, Color.LightGray) 321 | } 322 | 323 | for (i in 0...MAX_SMALL_METEORS) { 324 | if (smallMeteor[i].active) Raylib.drawCircleV(smallMeteor[i].position, smallMeteor[i].radius, Color.DarkGray) else Raylib.drawCircleV(smallMeteor[i].position, smallMeteor[i].radius, Color.LightGray) 325 | } 326 | 327 | Raylib.drawText("TIME: %(_framesCounter / 60)", 10, 10, 20, Color.Black) 328 | 329 | if (_pause) Raylib.drawText("GAME PAUSED", 20, 40/2, ScreenHeight/2 - 40, 40, Color.Gray) 330 | } else { 331 | Raylib.drawText("PRESS [ENTER] TO PLAY AGAIN", 20, Raylib.getScreenHeight()/2 - 50, 20, Color.Gray) 332 | } 333 | 334 | Raylib.endDrawing() 335 | 336 | 337 | } 338 | } 339 | 340 | Raylib.initWindow(ScreenWidth, ScreenHeight, "classic game: asteroids survival") 341 | 342 | var game = Game.new() 343 | 344 | Raylib.setTargetFPS(60) 345 | 346 | while (!Raylib.windowShouldClose()) { 347 | game.update() 348 | game.draw() 349 | } 350 | 351 | Raylib.closeWindow() 352 | -------------------------------------------------------------------------------- /examples/basic.wren: -------------------------------------------------------------------------------- 1 | import "raylib" for Color, Raylib, Rectangle, Vector2, Camera2D, KeyCode, Texture2D 2 | import "builtin" for Build 3 | 4 | // Automatic reloads are very crucial for ui buildings 5 | // Here is talon running in hot reload mode 6 | 7 | var title = "Title Screen Changed 2" 8 | var color = Color.Yellow 9 | 10 | class GameScreen { 11 | static Logo { 0 } 12 | static Title { 1 } 13 | static GamePlay { 2 } 14 | static Ending { 3 } 15 | } 16 | 17 | var screenWidth = 1000 18 | var screenHeight = 1000 19 | 20 | Raylib.initWindow(screenWidth, screenHeight, "raylib [core] example - basic screen manager") 21 | 22 | var currentScreen = GameScreen.Logo 23 | 24 | var framesCounter = 0 25 | 26 | Raylib.setTargetFPS(60) 27 | 28 | while (!Raylib.windowShouldClose() && !Build.shouldStop()) { 29 | if (currentScreen == GameScreen.Logo) { 30 | framesCounter = framesCounter + 1 31 | // if (framesCounter > 120) { 32 | // currentScreen = GameScreen.Title 33 | // } 34 | } 35 | 36 | if (currentScreen == GameScreen.Title) { 37 | if (Raylib.isKeyPressed(KeyCode.KEY_ENTER)) { 38 | currentScreen = GameScreen.GamePlay 39 | } 40 | } 41 | 42 | if (currentScreen == GameScreen.GamePlay) { 43 | if (Raylib.isKeyPressed(KeyCode.KEY_ENTER)) { 44 | currentScreen = GameScreen.Ending 45 | } 46 | } 47 | 48 | Raylib.beginDrawing() 49 | 50 | Raylib.clearBackground(Color.RayWhite) 51 | 52 | if (currentScreen == GameScreen.Logo) { 53 | Raylib.drawText(title, 20, 20, 60, color) 54 | Raylib.drawText("Wait for 3 seconds...", 290, 220, 20, Color.Gray) 55 | } 56 | 57 | if (currentScreen == GameScreen.Title) { 58 | Raylib.drawRectangle(0, 0, screenWidth, screenHeight, Color.Green) 59 | Raylib.drawText("TITLE SCREEN 4", 20, 20, 40, Color.Blue) 60 | Raylib.drawText("PRESS ENTER or TAP to JUMP to GAMEPLAY SCREEN", 120, 220, 20, Color.DarkGreen) 61 | } 62 | 63 | if (currentScreen == GameScreen.GamePlay) { 64 | Raylib.drawText("GAMEPLAY SCREEN", 20, 20, 40, Color.Purple) 65 | Raylib.drawText("PRESS ENTER or TAP to JUMP to ENDING SCREEN", 130, 220, 20, Color.Maroon) 66 | } 67 | 68 | if (currentScreen == GameScreen.Ending) { 69 | Raylib.drawText("GAMEPLAY SCREEN", 20, 20, 40, Color.DarkBlue) 70 | Raylib.drawText("PRESS ENTER or TAP to RETURN to TITLE SCREEN", 130, 220, 20, Color.Maroon) 71 | } 72 | 73 | Raylib.endDrawing() 74 | } 75 | 76 | Raylib.closeWindow() 77 | 78 | 79 | 80 | -------------------------------------------------------------------------------- /examples/breakout/ball.wren: -------------------------------------------------------------------------------- 1 | import "raylib" for Raylib, Rectangle, Vector2, Color 2 | import "./paddle" for PADDLE_H 3 | import "./utils" for SCREEN_HEIGHT, SCREEN_WIDTH 4 | 5 | var BALL_W = 16.0 6 | var BALL_H = 16.0 7 | var BALL_SPEED = 300.0 8 | 9 | 10 | var BALL_TEXTURE_DIMS = Rectangle.new(0.0, 0.0, 16.0, 16.0) 11 | 12 | class Ball { 13 | construct new() { 14 | _rec = Rectangle.new( 15 | SCREEN_WIDTH / 2.0 - BALL_W / 2.0, 16 | SCREEN_HEIGHT - BALL_H - PADDLE_H - 40.0, 17 | BALL_W, 18 | BALL_H 19 | ) 20 | _vel = Vector2.new( 21 | BALL_SPEED, 22 | -BALL_SPEED 23 | ) 24 | } 25 | 26 | rec { _rec } 27 | 28 | vel { _vel } 29 | 30 | draw(texture) { 31 | Raylib.drawTexturePro( 32 | texture, 33 | BALL_TEXTURE_DIMS, 34 | _rec, 35 | Vector2.new(0.0, 0.0), 36 | 0.0, 37 | Color.new(255, 255, 255, 255) 38 | ) 39 | } 40 | 41 | apply_collission(rec) { 42 | var prev_x = _rec.x - _vel.x 43 | var prev_y = _rec.y - _vel.y 44 | 45 | if (prev_y + _rec.height <= _rec.y) { 46 | _vel.y = _vel.y * -1.0 47 | } else if (prev_y >= _rec.y + _rec.height) { 48 | _vel.y = _vel.y * -1.0 49 | } else if (prev_x + _rec.width <= _rec.x) { 50 | _vel.x = _vel.x * -1.0 51 | } else if (prev_x >= _rec.x + _rec.width) { 52 | _vel.x = _vel.x * -1.0 53 | } else { 54 | _vel.y = _vel.y * -1.0 55 | } 56 | } 57 | } 58 | 59 | -------------------------------------------------------------------------------- /examples/breakout/brick.wren: -------------------------------------------------------------------------------- 1 | import "raylib" for Raylib, Rectangle, Vector2, Color 2 | 3 | var BRICK_W = 48.0 4 | var BRICK_H = 16.0 5 | 6 | var BRICK_GAP = 22.0 7 | var BRICK_MARGIN = 32.0 8 | 9 | var BRICK_TEXTURE_DIMS = Rectangle.new(0.0, 0.0, 48.0, 16.0) 10 | 11 | class Brick { 12 | construct new(x, y) { 13 | _rec = Rectangle.new(x, y, BRICK_W, BRICK_H ) 14 | } 15 | 16 | rec { _rec } 17 | 18 | draw(texture) { 19 | Raylib.drawTexturePro( 20 | texture, 21 | BRICK_TEXTURE_DIMS, 22 | _rec, 23 | Vector2.new(0.0, 0.0), 24 | 0.0, 25 | Color.new(255, 255, 255, 255) 26 | ) 27 | } 28 | 29 | static new_bricks(rows, cols) { 30 | var list = [] 31 | 32 | var i = 0 33 | 34 | while (i < rows) { 35 | var j = 0 36 | while (j < cols) { 37 | var bx = j * BRICK_W + j + 1.0 * BRICK_GAP + BRICK_MARGIN 38 | var by = i * BRICK_H + i + 1.0 * BRICK_GAP + BRICK_MARGIN 39 | list.add(Brick.new(bx, by)) 40 | j = j + 1 41 | } 42 | i = i + 1 43 | } 44 | return list 45 | } 46 | } 47 | 48 | 49 | -------------------------------------------------------------------------------- /examples/breakout/game.wren: -------------------------------------------------------------------------------- 1 | import "raylib" for Color, Raylib, Rectangle, Vector2, Camera2D, KeyCode, Texture2D 2 | import "math" for Math 3 | import "./brick" for Brick, BRICK_W 4 | import "./ball" for Ball, BALL_W, BALL_H 5 | import "./paddle" for Paddle, PADDLE_SPEED, PADDLE_W 6 | import "./score" for ScoreIndicator, SCORE_INC_Y, SCORE_PROG_INC_Y 7 | import "./utils" for SCREEN_HEIGHT, SCREEN_WIDTH 8 | 9 | var width = 600 10 | var height = 800 11 | 12 | var CAMERA_SHAKE_DEC = 0.8 13 | var CAMERA_SHAKE_TTL = 10.0 14 | 15 | 16 | class GameStatus { 17 | static Start { "Start" } 18 | static Playing { "Playing" } 19 | static Won { "Won" } 20 | static Over { "Over" } 21 | } 22 | 23 | class Game { 24 | construct new() { 25 | _brick_texture = Texture2D.loadTexture("res/brick.png") 26 | _ball_texture = Texture2D.loadTexture("res/tennis.png") 27 | _paddle_texture = Texture2D.loadTexture("res/paddle.png") 28 | _status = GameStatus.Start 29 | _bricks = Brick.new_bricks(5, 10) 30 | _ball = Ball.new() 31 | _paddle = Paddle.new() 32 | _camera_shake_ttl = 0.0 33 | _particle_instances = [] 34 | _score = 0.0 35 | _camera = Camera2D.new( 36 | Vector2.new(0.0, 0.0), 37 | Vector2.new(0.0, 0.0), 38 | 0.0, 39 | 1.0 40 | ) 41 | _score_indicators = [] 42 | _score_multiplier = 1 43 | } 44 | 45 | restart() { 46 | _brick_texture = Texture2D.loadTexture("res/brick.png") 47 | _ball_texture = Texture2D.loadTexture("res/tennis.png") 48 | _paddle_texture = Texture2D.loadTexture("res/paddle.png") 49 | _status = GameStatus.Start 50 | _bricks = Brick.new_bricks(5, 10) 51 | _ball = Ball.new() 52 | _paddle = Paddle.new() 53 | _camera_shake_ttl = 0.0 54 | _particle_instances = [] 55 | _score = 0.0 56 | _camera = Camera2D.new( 57 | Vector2.new(0.0, 0.0), 58 | Vector2.new(0.0, 0.0), 59 | 0.0, 60 | 1.0 61 | ) 62 | _score_indicators = [] 63 | _score_multiplier = 1 64 | } 65 | 66 | 67 | 68 | draw() { 69 | if (_status == GameStatus.Start) { 70 | draw_start() 71 | } else if (_status == GameStatus.Playing) { 72 | draw_playing() 73 | } else if (_status == GameStatus.Won) { 74 | draw_won() 75 | } else if (_status == GameStatus.Over) { 76 | draw_over() 77 | } 78 | } 79 | 80 | update() { 81 | if (_status == GameStatus.Start) { 82 | update_start() 83 | } else if (_status == GameStatus.Playing) { 84 | update_playing() 85 | } else if (_status == GameStatus.Won) { 86 | update_won() 87 | } else if (_status == GameStatus.Over) { 88 | update_over() 89 | } 90 | } 91 | 92 | 93 | draw_start() { 94 | Raylib.clearBackground(Color.new(0, 0, 0, 255)) 95 | var tx = 30 96 | var font_size = 22 97 | var text = "use left and right arrow keys to move" 98 | Raylib.drawText(text, tx, 800 / 2, font_size, Color.new(255, 255, 255, 255)) 99 | 100 | var text2 = "press to start" 101 | Raylib.drawText(text2, tx, 800 / 2 + font_size, font_size, Color.new(255, 255, 255, 255)) 102 | } 103 | 104 | update_start() { 105 | if (Raylib.isKeyDown(KeyCode.KEY_SPACE)) { 106 | _status = GameStatus.Playing 107 | } 108 | } 109 | 110 | update_playing() { 111 | var dt = Raylib.getFrameTime() 112 | 113 | var i = 0 114 | 115 | while (i < _bricks.count) { 116 | var b = _bricks[i] 117 | var collided = Raylib.checkCollisionRecs(_ball.rec, b.rec) 118 | 119 | if (!collided) { 120 | i = i + 1 121 | continue 122 | } 123 | 124 | _ball.apply_collission(b.rec) 125 | 126 | _camera_shake_ttl = CAMERA_SHAKE_TTL 127 | 128 | _score_indicators.add(ScoreIndicator.new(_score_multiplier, b.rec.x + BRICK_W / 2.0, b.rec.y)) 129 | 130 | _score = _score + 1 * _score_multiplier 131 | _score_multiplier = _score_multiplier + 1 132 | 133 | _bricks.removeAt(i) 134 | 135 | if (_bricks.count == 0) { 136 | _status = GameStatus.Won 137 | return 138 | } 139 | } 140 | 141 | if (Raylib.checkCollisionRecs(_ball.rec, _paddle.rec)) { 142 | _score_multiplier = 1 143 | 144 | if (_ball.rec.x + BALL_W / 2.0 < _paddle.rec.x + PADDLE_W / 2.0) { 145 | _ball.vel.x = -PADDLE_SPEED 146 | } else { 147 | _ball.vel.x = PADDLE_SPEED 148 | } 149 | _ball.vel.y = _ball.vel.y * -1.0 150 | _ball.rec.y = _ball.rec.y - BALL_H 151 | } 152 | 153 | _ball.rec.x = _ball.rec.x + _ball.vel.x * dt 154 | _ball.rec.y = _ball.rec.y + _ball.vel.y * dt 155 | 156 | var c_left = _ball.rec.x < 0.0 157 | var c_up = _ball.rec.y < 0.0 158 | var c_right = _ball.rec.x + _ball.rec.width > SCREEN_WIDTH 159 | var c_down = _ball.rec.y + _ball.rec.height > SCREEN_HEIGHT 160 | 161 | if (c_left || c_right) { 162 | _ball.vel.x = _ball.vel.x * -1.0 163 | } 164 | if (c_up) { 165 | self.ball.vel.y = _ball.vel.y * -1.0 166 | } 167 | if (c_down) { 168 | _status = GameStatus.Over 169 | return 170 | } 171 | 172 | if (Raylib.isKeyDown(KeyCode.KEY_LEFT)) { 173 | _paddle.rec.x = _paddle.rec.x - PADDLE_SPEED * dt 174 | } 175 | 176 | if (Raylib.isKeyDown(KeyCode.KEY_RIGHT)) { 177 | _paddle.rec.x = _paddle.rec.x + PADDLE_SPEED * dt 178 | } 179 | 180 | var j = 0 181 | 182 | while (j < _score_indicators.count) { 183 | var s = _score_indicators[j] 184 | var new_p = 1.0 - Math.pow(1.0 - s.progress, 5.0) 185 | s.progress = s.progress + SCORE_PROG_INC_Y 186 | s.pos.y = s.start_y - SCORE_INC_Y * new_p 187 | if (s.progress > 1.0) { 188 | _score_indicators.removeAt(j) 189 | } else { 190 | j = j + 1 191 | } 192 | } 193 | 194 | } 195 | 196 | draw_playing() { 197 | _camera_shake_ttl = Math.max(0.0, _camera_shake_ttl - CAMERA_SHAKE_DEC) 198 | // _camera.offset.y = Math.sin(_camera_shake_ttl * 2.0) * 2.0 * _camera_shake_ttl / CAMERA_SHAKE_TTL 199 | // _camera.target.x = Math.sin(_camera_shake_ttl * 4.0) * 2.0 * _camera_shake_ttl / CAMERA_SHAKE_TTL 200 | 201 | _camera.beginMode2D() 202 | 203 | Raylib.clearBackground(Color.new(0, 0, 0, 255)) 204 | 205 | for (brick in _bricks) { 206 | brick.draw(_brick_texture) 207 | } 208 | 209 | for (particle in _particle_instances) { 210 | particle.draw() 211 | } 212 | 213 | _paddle.draw(_paddle_texture) 214 | _ball.draw(_ball_texture) 215 | 216 | _camera.endMode2D() 217 | 218 | // for (self.score_indicators.items) |s| { 219 | // s.draw() 220 | // } 221 | 222 | var fps_text = "Score %(_score)" 223 | 224 | Raylib.drawText(fps_text, 12, 12, 24, Color.new(255, 255, 255, 255 )) 225 | } 226 | 227 | draw_over() { 228 | Raylib.clearBackground(Color.new(0, 0, 0 , 255)) 229 | 230 | var tx = 30 231 | var font_size = 22 232 | var text = "You lose" 233 | Raylib.drawText(text, tx, SCREEN_HEIGHT / 2, font_size, Color.new(255, 255, 255, 255)) 234 | 235 | var text_2 = "press to start" 236 | Raylib.drawText(text_2, tx, 800 / 2 + font_size * 2, font_size, Color.new(255, 255, 255, 255)) 237 | 238 | } 239 | 240 | update_over() { 241 | if (Raylib.isKeyDown(KeyCode.KEY_SPACE)) { 242 | restart() 243 | } 244 | } 245 | } 246 | -------------------------------------------------------------------------------- /examples/breakout/main.wren: -------------------------------------------------------------------------------- 1 | import "raylib" for Color, Raylib, Rectangle, Vector2, Camera2D, KeyCode, Texture2D 2 | import "math" for Math 3 | import "./game" for Game 4 | 5 | var width = 600 6 | var height = 800 7 | var title = "Sample" 8 | 9 | Raylib.initWindow(width, height, title) 10 | 11 | var min = Fn.new { |a, b| 12 | if (a < b) return a 13 | return b 14 | } 15 | 16 | var game = Game.new() 17 | 18 | Raylib.setTargetFPS(60) 19 | 20 | var target = Raylib.loadRenderTexture(width, height) 21 | System.print(target) 22 | 23 | 24 | while (!Raylib.windowShouldClose()) { 25 | game.update() 26 | 27 | var width_scale = Raylib.getScreenWidth() / width 28 | var height_scale = Raylib.getScreenHeight() / height 29 | var scale = min.call(width_scale, height_scale) 30 | 31 | Raylib.beginTextureMode(target) 32 | game.draw() 33 | Raylib.endTextureMode(target) 34 | 35 | 36 | Raylib.beginDrawing() 37 | Raylib.clearBackground(Color.new(20, 20, 20, 255)) 38 | 39 | Raylib.drawTexturePro( 40 | target.texture, 41 | Rectangle.new( 42 | 0.0, 43 | 0.0, 44 | target.texture.width, 45 | target.texture.height), 46 | Rectangle.new( 47 | 0.0, 48 | 0.0, 49 | target.texture.width, 50 | target.texture.height), 51 | Vector2.new(0.0, 0.0), 52 | 0.0, 53 | Color.new(255, 255, 255, 255 ) 54 | ) 55 | 56 | Raylib.endDrawing() 57 | } 58 | 59 | Raylib.clearBackground(Color.Gray) 60 | Raylib.unloadRenderTexture(target) 61 | Raylib.closeWindow() 62 | 63 | -------------------------------------------------------------------------------- /examples/breakout/paddle.wren: -------------------------------------------------------------------------------- 1 | import "raylib" for Raylib, Rectangle, Vector2, Color 2 | 3 | var PADDLE_W = 128.0 4 | var PADDLE_H = 16.0 5 | var PADDLE_SPEED = 500.0 6 | 7 | var PADDLE_TEXTURE_DIMS = Rectangle.new(0.0, 0.0, 64.0, 16.0) 8 | 9 | var SCREEN_WIDTH = 600 10 | var SCREEN_HEIGHT = 800 11 | 12 | class Paddle { 13 | construct new() { 14 | _rec = Rectangle.new( 15 | SCREEN_WIDTH / 2.0 - PADDLE_W / 2.0, 16 | SCREEN_HEIGHT - PADDLE_H - 20.0, 17 | PADDLE_W, 18 | PADDLE_H 19 | ) 20 | } 21 | rec { _rec } 22 | 23 | draw(texture) { 24 | Raylib.drawTexturePro( 25 | texture, 26 | PADDLE_TEXTURE_DIMS, 27 | _rec, 28 | Vector2.new(0.0, 0.0), 29 | 0.0, 30 | Color.new(255, 255, 255, 255) 31 | ) 32 | } 33 | } 34 | 35 | -------------------------------------------------------------------------------- /examples/breakout/particle.wren: -------------------------------------------------------------------------------- 1 | import "raylib" for Raylib, Rectangle, Vector2, Color 2 | 3 | var PARTICLE_AY = 2400.0 4 | var PARTICLE_AX = 200.0 5 | var PARTICLE_VY= -600.0 6 | var PARTICLE_VX = 1200.0 7 | var PARTICLE_W = 4.0 8 | var PARTICLE_H = 4.0 9 | var PARTICLE_TTL = 60.0 10 | 11 | 12 | class Particle { 13 | construct new(x, y, vx, vy, ax) { 14 | _vel = Vector2.new(vx, vy) 15 | _acc = Vector2.new(ax, PARTICLE_AY) 16 | _rec = Rectangle.new(x, y, PARTICLE_W, PARTICLE_H) 17 | _ttl = PARTICLE_TTL 18 | } 19 | 20 | } 21 | 22 | class Particles { 23 | construct new() {} 24 | } 25 | -------------------------------------------------------------------------------- /examples/breakout/res/brick.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jossephus/talon/9b95d00db9afb86452ba14eb02fce9e89477b86e/examples/breakout/res/brick.png -------------------------------------------------------------------------------- /examples/breakout/res/paddle.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jossephus/talon/9b95d00db9afb86452ba14eb02fce9e89477b86e/examples/breakout/res/paddle.png -------------------------------------------------------------------------------- /examples/breakout/res/tennis.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jossephus/talon/9b95d00db9afb86452ba14eb02fce9e89477b86e/examples/breakout/res/tennis.png -------------------------------------------------------------------------------- /examples/breakout/score.wren: -------------------------------------------------------------------------------- 1 | import "raylib" for Raylib, Rectangle, Vector2, Color 2 | 3 | var SCORE_FONT_SIZE = 16 4 | var SCORE_INC_Y = 0.0 5 | var SCORE_PROG_INC_Y = 0.025 6 | 7 | class ScoreIndicator { 8 | construct new(value, x, y) { 9 | _value = value 10 | _pos = Vector2.new(x, y) 11 | _progress = 0.0 12 | _start_y = y 13 | _x = x 14 | } 15 | 16 | progress { _progress } 17 | progress=(value) { _progress = value } 18 | pos { _pos } 19 | start_y { _start_y } 20 | 21 | draw() { 22 | var text = "+%(_value)" 23 | 24 | Raylib.drawText( 25 | text, 26 | _x, 27 | _y, 28 | SCORE_FONT_SIZE, 29 | Color.new(255, 255, 255, _progress * 255.0) 30 | ) 31 | } 32 | 33 | } 34 | -------------------------------------------------------------------------------- /examples/breakout/utils.wren: -------------------------------------------------------------------------------- 1 | 2 | var SCREEN_WIDTH = 600 3 | var SCREEN_HEIGHT = 800 4 | -------------------------------------------------------------------------------- /examples/build-exe/.gitignore: -------------------------------------------------------------------------------- 1 | /dist 2 | -------------------------------------------------------------------------------- /examples/build-exe/Dockerfile: -------------------------------------------------------------------------------- 1 | # took inspiration from https://github.com/QSmally/Zig-Build/blob/master/Dockerfile 2 | # 3 | FROM alpine AS compiler 4 | 5 | ARG VERSION=0.14.0 6 | 7 | RUN apk update && apk add curl tar xz 8 | 9 | RUN curl https://ziglang.org/download/$VERSION/zig-linux-$(uname -m)-$VERSION.tar.xz -O && \ 10 | tar -xf *.tar.xz && \ 11 | mv zig-linux-$(uname -m)-$VERSION /compiler 12 | 13 | WORKDIR /build 14 | COPY . /build 15 | 16 | RUN echo '.{' \ 17 | ' .name = .build_exe,' \ 18 | ' .version = "0.0.0",' \ 19 | ' .fingerprint = 0x7aca45f40a314ba8,' \ 20 | ' .minimum_zig_version = "0.14.0",' \ 21 | ' .dependencies = .{' \ 22 | ' .wren = .{' \ 23 | ' .url = "https://github.com/wren-lang/wren/archive/refs/heads/main.zip",' \ 24 | ' .hash = "N-V-__8AAPbpYgAZgdr-sM49A18GSKr5MVR56MwpfI65FmAZ",' \ 25 | ' },' \ 26 | ' .raylib = .{' \ 27 | ' .url = "git+https://github.com/raysan5/raylib#27a4fe885164b315a90b67682f981a1e03d6079c",' \ 28 | ' .hash = "raylib-5.5.0-whq8uFqtNARw6t1tTakBDSVYgUjlBqnDppOyNfE_yfCa",' \ 29 | ' },' \ 30 | ' .tolan = .{' \ 31 | ' .url = "git+https://github.com/jossephus/talon#ec9dc6910fa96c414406b5ed539e441943e4aadd",' \ 32 | ' .hash = "zig_wren-0.0.0-_Mx4iLclCACui2mzMF9z9j7iRplB0vS_jZjf-k9EQNLC",' \ 33 | ' },' \ 34 | ' },' \ 35 | ' .paths = .{' \ 36 | ' "build.zig",' \ 37 | ' "build.zig.zon",' \ 38 | ' "src",' \ 39 | ' },' \ 40 | '}' > build.zig.zon 41 | 42 | RUN cat <<'EOF' > build.zig 43 | const std = @import("std"); 44 | 45 | const mainFile = "main.wren"; 46 | 47 | pub fn build(b: *std.Build) !void { 48 | const target = b.standardTargetOptions(.{}); 49 | const optimize = b.standardOptimizeOption(.{}); 50 | 51 | const tolan_lib = b.dependency("tolan", .{ 52 | .target = target, 53 | .optimize = optimize, 54 | }); 55 | const tolan_mod = tolan_lib.module("tolan"); 56 | 57 | const exe_mod = b.createModule(.{ 58 | .root_source_file = b.path("build.zig"), 59 | .target = target, 60 | .optimize = optimize, 61 | }); 62 | 63 | exe_mod.addImport("tolan", tolan_mod); 64 | 65 | const tolan_exe = b.addExecutable(.{ 66 | .name = "camera", 67 | .root_module = exe_mod, 68 | }); 69 | 70 | try addAssetsOption(b, tolan_exe, target, optimize, b.getInstallStep()); 71 | 72 | b.installArtifact(tolan_exe); 73 | 74 | const run_cmd = b.addRunArtifact(tolan_exe); 75 | 76 | run_cmd.step.dependOn(b.getInstallStep()); 77 | 78 | if (b.args) |args| { 79 | run_cmd.addArgs(args); 80 | } 81 | 82 | const run_step = b.step("run", "Run the app"); 83 | run_step.dependOn(&run_cmd.step); 84 | } 85 | 86 | pub fn addAssetsOption(b: *std.Build, exe: anytype, target: anytype, optimize: anytype, step: *std.Build.Step) !void { 87 | var options = b.addOptions(); 88 | 89 | var files = std.ArrayList([]const u8).init(b.allocator); 90 | defer files.deinit(); 91 | 92 | try checkWrenFiles(b.allocator, &files, b, ".", ".", step); 93 | 94 | options.addOption([]const []const u8, "files", files.items); 95 | exe.step.dependOn(&options.step); 96 | 97 | const assets_mod = b.addModule("assets", .{ 98 | .root_source_file = options.getOutput(), 99 | .target = target, 100 | .optimize = optimize, 101 | }); 102 | 103 | exe.root_module.addImport("assets", assets_mod); 104 | } 105 | 106 | fn checkWrenFiles( 107 | allocator: std.mem.Allocator, 108 | files: *std.ArrayList([]const u8), 109 | b: *std.Build, 110 | base_path: []const u8, 111 | rel_path: []const u8, 112 | step: *std.Build.Step, 113 | ) !void { 114 | var dir = try std.fs.cwd().openDir(rel_path, .{ .iterate = true }); 115 | var it = dir.iterate(); 116 | while (try it.next()) |entry| { 117 | if (std.mem.eql(u8, entry.name, ".zig-cache") or std.mem.eql(u8, entry.name, "zig-out")) { 118 | continue; 119 | } 120 | const rel_to_base = try std.fs.path.join(allocator, &.{ rel_path, entry.name }); 121 | defer allocator.free(rel_to_base); 122 | 123 | switch (entry.kind) { 124 | .file => { 125 | if (std.mem.endsWith(u8, entry.name, ".wren")) { 126 | const rel = try std.fs.path.relative(allocator, base_path, rel_to_base); 127 | defer allocator.free(rel); 128 | 129 | try files.append(b.dupe(rel)); 130 | } 131 | }, 132 | .directory => { 133 | try checkWrenFiles(allocator, files, b, base_path, rel_to_base, step); 134 | }, 135 | else => {}, 136 | } 137 | } 138 | } 139 | 140 | const assets = @import("assets"); 141 | 142 | const embeddedFilesMap = std.StaticStringMap([]const u8).initComptime(genMap()); 143 | const EmbeddedAsset = struct { 144 | []const u8, 145 | []const u8, 146 | }; 147 | 148 | fn genMap() [assets.files.len]EmbeddedAsset { 149 | var embassets: [assets.files.len]EmbeddedAsset = undefined; 150 | comptime var i = 0; 151 | inline for (assets.files) |file| { 152 | embassets[i][0] = file; 153 | embassets[i][1] = @embedFile(file); 154 | i += 1; 155 | } 156 | return embassets; 157 | } 158 | 159 | pub fn main() !void { 160 | try tolan.run(mainFile, embeddedFilesMap); 161 | } 162 | 163 | const tolan = @import("tolan"); 164 | EOF 165 | 166 | # RUN /compiler/zig build -Doptimize=ReleaseSafe -Dtarget=x86_64-windows 167 | #RUN /compiler/zig build -Dtarget=x86_64-windows 168 | -------------------------------------------------------------------------------- /examples/build-exe/docker-compose.yml: -------------------------------------------------------------------------------- 1 | services: 2 | camera: 3 | build: 4 | context: . 5 | dockerfile: Dockerfile 6 | volumes: 7 | - ./dist:/build/zig-out/bin 8 | working_dir: /build 9 | command: /compiler/zig build -Dtarget=x86_64-windows 10 | -------------------------------------------------------------------------------- /examples/build-exe/game.wren: -------------------------------------------------------------------------------- 1 | var Game = "Game" 2 | -------------------------------------------------------------------------------- /examples/build-exe/main.wren: -------------------------------------------------------------------------------- 1 | import "raylib" for Color, Raylib, Rectangle, Vector2, Camera2D, KeyCode, Texture2D 2 | import "math" for Math 3 | 4 | var MAX_BUILDINGS = 100 5 | 6 | var screenWidth = 800 7 | var screenHeight = 450 8 | 9 | Raylib.initWindow(screenWidth, screenHeight, "raylib [core] example - 2d camera") 10 | 11 | var player = Rectangle.new(400, 280, 40, 40) 12 | var buildings = [] 13 | var buildColors = [] 14 | 15 | var spacing = 0 16 | 17 | var i = 0 18 | 19 | while (i < MAX_BUILDINGS) { 20 | var height = Raylib.getRandomValue(100, 800) 21 | var random = Raylib.getRandomValue(50, 200) 22 | 23 | var building = Rectangle.new( 24 | -6000 + spacing, 25 | screenHeight - 130.0 - height, 26 | random, 27 | height 28 | ) 29 | 30 | buildings.add(building) 31 | 32 | spacing = spacing + building.width 33 | 34 | buildColors.add(Color.new( 35 | Raylib.getRandomValue(200, 240), 36 | Raylib.getRandomValue(200, 240), 37 | Raylib.getRandomValue(200, 250), 38 | 255 39 | )) 40 | 41 | i = i + 1 42 | } 43 | 44 | var camera = Camera2D.new( 45 | Vector2.new(screenWidth / 2.0, screenHeight / 2.0), 46 | Vector2.new(player.x + 20.0, player.y + 20.0), 47 | 1.0, 48 | 0.0 49 | ) 50 | 51 | Raylib.setTargetFPS(60) // Set our game to run at 60 frames-per-secon 52 | 53 | while (!Raylib.windowShouldClose()) { 54 | 55 | if (Raylib.isKeyDown(KeyCode.KEY_RIGHT)) { 56 | player.x = player.x + 2 57 | } else if (Raylib.isKeyDown(KeyCode.KEY_LEFT)) { player.x = player.x - 2 } 58 | 59 | camera.target = Vector2.new(player.x + 20, player.y + 20) 60 | 61 | if (Raylib.isKeyDown(KeyCode.KEY_A)) { 62 | camera.rotation = camera.rotation - 1 63 | } else if (Raylib.isKeyDown(KeyCode.KEY_S)) { 64 | camera.rotation = camera.rotation + 1 65 | } 66 | 67 | if (camera.rotation > 40) { 68 | camera.rotation = 40 69 | } else if (camera.rotation < -40) { camera.rotation = -40 } 70 | 71 | camera.zoom = Math.exp(Math.log(camera.zoom) + (Raylib.getMouseWheelMove() * 0.1)) 72 | 73 | if (camera.zoom > 3.0) { 74 | camera.zoom = 3.0 75 | } else if (camera.zoom < 0.1) { 76 | camera.zoom = 0.1 77 | } 78 | 79 | if (Raylib.isKeyDown(KeyCode.KEY_R)) { 80 | camera.zoom = 1.0 81 | camera.rotation = 0.0 82 | } 83 | Raylib.beginDrawing() 84 | 85 | Raylib.clearBackground(Color.RayWhite) 86 | 87 | Raylib.beginMode2D(camera) 88 | 89 | Raylib.drawRectangle(-6000, 320, 13000, 8000, Color.DarkGray) 90 | 91 | var i = 0 92 | while (i < MAX_BUILDINGS) { 93 | Raylib.drawRectangleRec(buildings[i], buildColors[i]) 94 | i = i + 1 95 | } 96 | 97 | Raylib.drawRectangleRec(player, Color.Red) 98 | 99 | Raylib.drawLine(camera.target.x, -screenHeight*10, camera.target.x, screenHeight*10, Color.Green) 100 | Raylib.drawLine(-screenWidth*10, camera.target.y, screenWidth*10, camera.target.y, Color.Green) 101 | 102 | Raylib.endMode2D() 103 | 104 | Raylib.drawText("SCREEN AREA", 640, 10, 20, Color.Red) 105 | 106 | Raylib.drawRectangle(0, 0, screenWidth, 5, Color.Red) 107 | Raylib.drawRectangle(0, 5, 5, screenHeight - 10, Color.Red) 108 | Raylib.drawRectangle(screenWidth - 5, 5, 5, screenHeight - 10, Color.Red) 109 | Raylib.drawRectangle(0, screenHeight - 5, screenWidth, 5, Color.Red) 110 | 111 | Raylib.drawRectangle( 10, 10, 250, 113, Color.SkyBlue ) 112 | Raylib.drawRectangleLines( 10, 10, 250, 113, Color.Blue) 113 | 114 | Raylib.drawText("Free 2d camera controls:", 20, 20, 10, Color.Black) 115 | Raylib.drawText("- Right/Left to move Offset", 40, 40, 10, Color.DarkGray) 116 | Raylib.drawText("- Mouse Wheel to Zoom in-out", 40, 60, 10, Color.DarkGray) 117 | Raylib.drawText("- A / S to Rotate", 40, 80, 10, Color.DarkGray) 118 | Raylib.drawText("- R to reset Zoom and Rotation", 40, 100, 10, Color.DarkGray) 119 | 120 | Raylib.endDrawing() 121 | } 122 | 123 | Raylib.closeWindow() 124 | -------------------------------------------------------------------------------- /examples/build-wasm/.gitignore: -------------------------------------------------------------------------------- 1 | dist/ 2 | -------------------------------------------------------------------------------- /examples/build-wasm/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM nixos/nix 2 | 3 | ARG VERSION=0.14.0 4 | 5 | RUN echo "experimental-features = nix-command flakes" >> /etc/nix/nix.conf 6 | 7 | WORKDIR /build 8 | COPY . /build 9 | 10 | 11 | RUN cat <<'EOF' > flake.nix 12 | { 13 | inputs = { 14 | nixpkgs.url = "nixpkgs/nixos-unstable"; 15 | flake-utils.url = "github:numtide/flake-utils"; 16 | zig.url = "github:mitchellh/zig-overlay"; 17 | }; 18 | 19 | outputs = { 20 | nixpkgs, 21 | flake-utils, 22 | zig, 23 | ... 24 | }: 25 | flake-utils.lib.eachDefaultSystem ( 26 | system: let 27 | overlays = [ 28 | (final: prev: { 29 | zigpkgs = zig.packages.${system}; 30 | }) 31 | ]; 32 | pkgs = import nixpkgs { 33 | inherit overlays system; 34 | }; 35 | packages = with pkgs; [ 36 | glfw 37 | libGL 38 | libxkbcommon 39 | python3 40 | pkg-config 41 | xorg.libxcb 42 | xorg.libXft 43 | xorg.libX11 44 | xorg.libX11.dev 45 | xorg.libXrandr 46 | xorg.libXinerama 47 | xorg.libXcursor 48 | xorg.libXi 49 | glfw-wayland 50 | zigpkgs."0.14.0" 51 | emscripten 52 | ]; 53 | in { 54 | devShell = pkgs.mkShell { 55 | buildInputs = packages; 56 | nativeBuildInputs = with pkgs; [cmake pkg-config ncurses fontconfig freetype]; 57 | shellHook = '' 58 | export SHELL=/usr/bin/bash 59 | if [ ! -d $(pwd)/.emscripten_cache-${pkgs.emscripten.version} ]; then 60 | cp -R ${pkgs.emscripten}/share/emscripten/cache/ $(pwd)/.emscripten_cache-${pkgs.emscripten.version} 61 | chmod u+rwX -R $(pwd)/.emscripten_cache-${pkgs.emscripten.version} 62 | fi 63 | export EM_CACHE=$(pwd)/.emscripten_cache-${pkgs.emscripten.version} 64 | echo emscripten cache dir: $EM_CACHE 65 | ''; 66 | }; 67 | } 68 | ); 69 | } 70 | 71 | EOF 72 | 73 | RUN cat <<'EOF' > build.zig.zon 74 | .{ 75 | .name = .build_exe, 76 | 77 | .version = "0.0.0", 78 | 79 | .fingerprint = 0x7aca45f40a314ba8, // Changing this has security and trust implications. 80 | 81 | .minimum_zig_version = "0.14.0", 82 | 83 | .dependencies = .{ 84 | .tolan = .{ 85 | .url = "git+https://github.com/jossephus/talon#ec9dc6910fa96c414406b5ed539e441943e4aadd", 86 | .hash = "zig_wren-0.0.0-_Mx4iLclCACui2mzMF9z9j7iRplB0vS_jZjf-k9EQNLC", 87 | }, 88 | .wren = .{ 89 | .url = "https://github.com/wren-lang/wren/archive/refs/heads/main.zip", 90 | .hash = "N-V-__8AAPbpYgAZgdr-sM49A18GSKr5MVR56MwpfI65FmAZ", 91 | }, 92 | .raylib = .{ 93 | .url = "git+https://github.com/raysan5/raylib#27a4fe885164b315a90b67682f981a1e03d6079c", 94 | .hash = "raylib-5.5.0-whq8uFqtNARw6t1tTakBDSVYgUjlBqnDppOyNfE_yfCa", 95 | }, 96 | }, 97 | 98 | .paths = .{ 99 | "build.zig", 100 | "build.zig.zon", 101 | "src", 102 | }, 103 | } 104 | 105 | EOF 106 | 107 | RUN cat <<'EOF' > build.zig 108 | const std = @import("std"); 109 | 110 | const mainFile = "main.wren"; 111 | 112 | pub fn build(b: *std.Build) !void { 113 | const target = b.standardTargetOptions(.{}); 114 | const optimize = b.standardOptimizeOption(.{}); 115 | 116 | if (target.result.cpu.arch.isWasm()) { 117 | const wasm_target = b.resolveTargetQuery(.{ 118 | .cpu_arch = .wasm32, 119 | .cpu_model = .{ .explicit = &std.Target.wasm.cpu.mvp }, 120 | .cpu_features_add = std.Target.wasm.featureSet(&.{ 121 | .atomics, 122 | .bulk_memory, 123 | }), 124 | .os_tag = .emscripten, 125 | }); 126 | 127 | const tolan_lib = b.dependency("tolan", .{ 128 | .target = wasm_target, 129 | .optimize = optimize, 130 | }); 131 | const tolan_mod = tolan_lib.module("tolan"); 132 | 133 | const wren_lib = tolan_lib.artifact("wren"); 134 | const raylib_lib = tolan_lib.artifact("raylib"); 135 | 136 | const app_lib = b.addLibrary(.{ 137 | .linkage = .static, 138 | .name = "camera", 139 | .root_module = b.createModule(.{ 140 | .root_source_file = b.path("build.zig"), 141 | .target = wasm_target, 142 | .optimize = optimize, 143 | .imports = &.{ 144 | .{ .name = "tolan", .module = tolan_mod }, 145 | }, 146 | }), 147 | }); 148 | app_lib.linkLibC(); 149 | app_lib.shared_memory = true; 150 | app_lib.linkLibrary(wren_lib); 151 | app_lib.linkLibrary(raylib_lib); 152 | app_lib.addIncludePath(.{ .cwd_relative = ".emscripten_cache-4.0.8/sysroot/include" }); 153 | 154 | try addAssetsOption(b, app_lib, target, optimize, b.getInstallStep()); 155 | 156 | const emcc = b.addSystemCommand(&.{"emcc"}); 157 | 158 | for (app_lib.getCompileDependencies(false)) |lib| { 159 | if (lib.isStaticLibrary()) { 160 | emcc.addArtifactArg(lib); 161 | } 162 | } 163 | 164 | for (wren_lib.getCompileDependencies(false)) |lib| { 165 | if (lib.isStaticLibrary()) { 166 | emcc.addArtifactArg(lib); 167 | } 168 | } 169 | 170 | for (raylib_lib.getCompileDependencies(false)) |lib| { 171 | if (lib.isStaticLibrary()) { 172 | emcc.addArtifactArg(lib); 173 | } 174 | } 175 | 176 | emcc.addArgs(&.{ 177 | "-sUSE_GLFW=3", 178 | "-sUSE_OFFSET_CONVERTER", 179 | 180 | //"-sAUDIO_WORKLET=1", 181 | //"-sWASM_WORKERS=1", 182 | "-sSHARED_MEMORY=1", 183 | "-sALLOW_MEMORY_GROWTH=1", 184 | 185 | "-sASYNCIFY", 186 | "-sundefs", 187 | "-sERROR_ON_UNDEFINED_SYMBOLS=0", 188 | "--shell-file", 189 | b.path("shell.html").getPath(b), 190 | }); 191 | 192 | const link_items: []const *std.Build.Step.Compile = &.{ 193 | wren_lib, 194 | raylib_lib, 195 | app_lib, 196 | }; 197 | 198 | for (link_items) |item| { 199 | emcc.addFileArg(item.getEmittedBin()); 200 | emcc.step.dependOn(&item.step); 201 | } 202 | 203 | //emcc.addArg("--pre-js"); 204 | emcc.addArg("-o"); 205 | 206 | const app_html = emcc.addOutputFileArg("index.html"); 207 | b.getInstallStep().dependOn(&b.addInstallDirectory(.{ 208 | .source_dir = app_html.dirname(), 209 | .install_dir = .{ .custom = "www" }, 210 | .install_subdir = "", 211 | }).step); 212 | } else { 213 | const failed = b.addFail("Non wasm target chosen"); 214 | _ = failed; 215 | } 216 | } 217 | 218 | // varied version of https://github.com/ringtailsoftware/zig-embeddir/blob/main/build.zig to include wren files 219 | pub fn addAssetsOption(b: *std.Build, exe: anytype, target: anytype, optimize: anytype, step: *std.Build.Step) !void { 220 | var options = b.addOptions(); 221 | 222 | var files = std.ArrayList([]const u8).init(b.allocator); 223 | defer files.deinit(); 224 | 225 | try checkWrenFiles(b.allocator, &files, b, ".", ".", step); 226 | 227 | options.addOption([]const []const u8, "files", files.items); 228 | exe.step.dependOn(&options.step); 229 | 230 | const assets_mod = b.addModule("assets", .{ 231 | .root_source_file = options.getOutput(), 232 | .target = target, 233 | .optimize = optimize, 234 | }); 235 | 236 | exe.root_module.addImport("assets", assets_mod); 237 | } 238 | 239 | fn checkWrenFiles( 240 | allocator: std.mem.Allocator, 241 | files: *std.ArrayList([]const u8), 242 | b: *std.Build, 243 | base_path: []const u8, 244 | rel_path: []const u8, 245 | step: *std.Build.Step, 246 | ) !void { 247 | var dir = try std.fs.cwd().openDir(rel_path, .{ .iterate = true }); 248 | var it = dir.iterate(); 249 | while (try it.next()) |entry| { 250 | if (std.mem.eql(u8, entry.name, ".zig-cache") or std.mem.eql(u8, entry.name, "zig-out")) { 251 | continue; 252 | } 253 | const rel_to_base = try std.fs.path.join(allocator, &.{ rel_path, entry.name }); 254 | defer allocator.free(rel_to_base); 255 | 256 | switch (entry.kind) { 257 | .file => { 258 | if (std.mem.endsWith(u8, entry.name, ".wren")) { 259 | const rel = try std.fs.path.relative(allocator, base_path, rel_to_base); 260 | defer allocator.free(rel); 261 | 262 | try files.append(b.dupe(rel)); 263 | } 264 | }, 265 | .directory => { 266 | try checkWrenFiles(allocator, files, b, base_path, rel_to_base, step); 267 | }, 268 | else => {}, 269 | } 270 | } 271 | } 272 | 273 | const assets = @import("assets"); 274 | 275 | const embeddedFilesMap = std.StaticStringMap([]const u8).initComptime(genMap()); 276 | const EmbeddedAsset = struct { 277 | []const u8, 278 | []const u8, 279 | }; 280 | 281 | fn genMap() [assets.files.len]EmbeddedAsset { 282 | var embassets: [assets.files.len]EmbeddedAsset = undefined; 283 | comptime var i = 0; 284 | inline for (assets.files) |file| { 285 | embassets[i][0] = file; 286 | embassets[i][1] = @embedFile(file); 287 | i += 1; 288 | } 289 | return embassets; 290 | } 291 | 292 | pub fn main() !void { 293 | try tolan.run(mainFile, embeddedFilesMap); 294 | } 295 | 296 | export fn add(a: i32, b: i32) i32 { 297 | return a + b; 298 | } 299 | 300 | export fn sub(a: i32, b: i32) i32 { 301 | return a - b; 302 | } 303 | 304 | const tolan = @import("tolan"); 305 | 306 | EOF 307 | 308 | 309 | RUN nix develop -L --verbose 310 | 311 | # RUN /compiler/zig build -Doptimize=ReleaseSafe -Dtarget=x86_64-windows 312 | #RUN /compiler/zig build -Dtarget=x86_64-windows 313 | -------------------------------------------------------------------------------- /examples/build-wasm/docker-compose.yml: -------------------------------------------------------------------------------- 1 | services: 2 | wasm: 3 | build: 4 | context: . 5 | dockerfile: Dockerfile 6 | volumes: 7 | - ./dist:/build/zig-out/www 8 | working_dir: /build 9 | #command: nix develop -L --verbose && zig build -Dtarget=wasm32-emscripten -Doptimize=ReleaseSmall && tail -f /dev/null 10 | command: tail -f /dev/null 11 | -------------------------------------------------------------------------------- /examples/build-wasm/game.wren: -------------------------------------------------------------------------------- 1 | var Game = "Game" 2 | -------------------------------------------------------------------------------- /examples/build-wasm/main.wren: -------------------------------------------------------------------------------- 1 | import "raylib" for Color, Raylib, Rectangle, Vector2, Camera2D, KeyCode, Texture2D, Vector3 2 | import "math" for Math 3 | 4 | 5 | var PLAYER_BASE_SIZE = 20 6 | var PLAYER_SPEED = 6.0 7 | var PLAYER_MAX_SHOOTS = 10 8 | var DEG2RAD = 0.017453 9 | 10 | var METEORS_SPEED = 2 11 | var MAX_MEDIUM_METEORS = 8 12 | var MAX_SMALL_METEORS = 16 13 | 14 | var ScreenWidth = 800 15 | var ScreenHeight = 450 16 | 17 | class Meteor { 18 | construct new(position, speed, radius, active, color) { 19 | _position = position 20 | _speed = speed 21 | _radius = radius 22 | _active = active 23 | _color = color 24 | } 25 | 26 | position { _position } 27 | speed { _speed } 28 | active { _active } 29 | color { _color } 30 | radius { _radius } 31 | 32 | position=(value) { _position = value } 33 | speed=(value) { _speed = value } 34 | active=(value) { _active = value } 35 | color=(value) { _color = value } 36 | radius=(value) { _radius = value } 37 | } 38 | 39 | class Player { 40 | construct new(position, speed, acceleration, rotation, collider, color) { 41 | _position = position 42 | _speed = speed 43 | _acceleration = acceleration 44 | _rotation = rotation 45 | _collider = collider 46 | _color = color 47 | } 48 | position { _position } 49 | speed { _speed } 50 | acceleration { _acceleration } 51 | rotation { _rotation } 52 | collider { _collider } 53 | color { _color } 54 | 55 | position=(value) { _position = value } 56 | speed=(value) { _speed = value } 57 | acceleration=(value) { _acceleration = value } 58 | rotation=(value) { _rotation = value } 59 | collider=(value) { _collider = value } 60 | color=(value) { _color = value } 61 | } 62 | 63 | 64 | class Game { 65 | initGame() { 66 | var posx = 0 67 | var posy = 0 68 | var velx = 0 69 | var vely = 0 70 | var correctRange = false 71 | 72 | _pause = false 73 | _gameOver = false 74 | _framesCounter = 0 75 | _shipHeight = (PLAYER_BASE_SIZE / 2) / Math.tan(20 * DEG2RAD) 76 | 77 | _mediumMeteor = [] 78 | _smallMeteor = [] 79 | 80 | var x = ScreenWidth / 2 81 | var y = ScreenHeight / 2 - _shipHeight / 2 82 | 83 | _player = Player.new( 84 | Vector2.new(x, y), 85 | Vector2.new(0, 0), 86 | 0, 87 | 0, 88 | Vector3.new( 89 | x + Math.sin(0 * DEG2RAD) * (_shipHeight / 2.5), 90 | y - Math.cos(0 * DEG2RAD) * (_shipHeight / 2.5), 91 | 12), 92 | Color.LightGray 93 | ) 94 | 95 | for (i in 0...MAX_MEDIUM_METEORS) { 96 | 97 | posx = Raylib.getRandomValue(0, ScreenWidth) 98 | 99 | while (!correctRange) { 100 | if (posx > ScreenWidth/2 - 150 && posx < ScreenWidth/2 + 150) { 101 | posx = Raylib.getRandomValue(0, ScreenWidth) 102 | } else { 103 | correctRange = true 104 | } 105 | } 106 | 107 | correctRange = false 108 | 109 | posy = Raylib.getRandomValue(0, ScreenHeight) 110 | 111 | while (!correctRange) { 112 | if (posy > ScreenHeight/2 - 150 && posy < ScreenHeight/2 + 150) { 113 | posy = Raylib.getRandomValue(0, ScreenHeight) 114 | } else { 115 | correctRange = true 116 | } 117 | } 118 | 119 | correctRange = false 120 | velx = Raylib.getRandomValue(-METEORS_SPEED, METEORS_SPEED) 121 | vely = Raylib.getRandomValue(-METEORS_SPEED, METEORS_SPEED) 122 | 123 | while (!correctRange) { 124 | if (velx == 0 && vely == 0) { 125 | velx = Raylib.getRandomValue(-METEORS_SPEED, METEORS_SPEED) 126 | vely = Raylib.getRandomValue(-METEORS_SPEED, METEORS_SPEED) 127 | } else { 128 | correctRange = true 129 | } 130 | } 131 | _mediumMeteor.add( 132 | Meteor.new(Vector2.new(posx, posy), Vector2.new(velx, vely), 20, true, Color.Green) 133 | ) 134 | } 135 | 136 | for (j in 0...MAX_SMALL_METEORS) { 137 | posx = Raylib.getRandomValue(0, ScreenWidth) 138 | 139 | while (!correctRange) { 140 | if (posx > ScreenWidth/2 - 150 && posx < ScreenWidth/2 + 150) { 141 | posx = Raylib.getRandomValue(0, ScreenWidth) 142 | } else { 143 | correctRange = true 144 | } 145 | } 146 | 147 | correctRange = false 148 | 149 | posy = Raylib.getRandomValue(0, ScreenHeight) 150 | 151 | while (!correctRange) { 152 | if (posy > ScreenHeight/2 - 150 && posy < ScreenHeight/2 + 150) { 153 | posy = Raylib.getRandomValue(0, ScreenHeight) 154 | } else { 155 | correctRange = true 156 | } 157 | } 158 | 159 | correctRange = false 160 | velx = Raylib.getRandomValue(-METEORS_SPEED, METEORS_SPEED) 161 | vely = Raylib.getRandomValue(-METEORS_SPEED, METEORS_SPEED) 162 | 163 | while (!correctRange) { 164 | if (velx == 0 && vely == 0) { 165 | velx = Raylib.getRandomValue(-METEORS_SPEED, METEORS_SPEED) 166 | vely = Raylib.getRandomValue(-METEORS_SPEED, METEORS_SPEED) 167 | } else { 168 | correctRange = true 169 | } 170 | } 171 | _smallMeteor.add( 172 | Meteor.new(Vector2.new(posx, posy), Vector2.new(velx, vely), 20, true, Color.Yellow) 173 | ) 174 | } 175 | 176 | 177 | } 178 | 179 | construct new() { 180 | initGame() 181 | } 182 | 183 | player { _player } 184 | mediumMeteor { _mediumMeteor } 185 | smallMeteor { _smallMeteor } 186 | 187 | update() { 188 | if (!_gameOver) { 189 | if (Raylib.isKeyDown(KeyCode.KEY_P)) { 190 | _pause = !_pause 191 | } 192 | 193 | if (!_pause) { 194 | _framesCounter = _framesCounter + 1 195 | 196 | // Rotation 197 | if (Raylib.isKeyDown(KeyCode.KEY_LEFT)) { 198 | _player.rotation = player.rotation - 5 199 | } 200 | if (Raylib.isKeyDown(KeyCode.KEY_RIGHT)) { 201 | System.print("Right is clicked") 202 | _player.rotation = _player.rotation + 5 203 | } 204 | 205 | // Speed 206 | _player.speed.x = Math.sin(_player.rotation*DEG2RAD)*PLAYER_SPEED 207 | _player.speed.y = Math.cos(_player.rotation*DEG2RAD)*PLAYER_SPEED 208 | 209 | // Controller 210 | if (Raylib.isKeyDown(KeyCode.KEY_UP)) { 211 | System.print("UP IS CLICKING") 212 | if (_player.acceleration < 1) _player.acceleration = _player.acceleration + 0.04 213 | } else { 214 | if (_player.acceleration > 0) { 215 | _player.acceleration = _player.acceleration - 0.02 216 | } else if (player.acceleration < 0) { 217 | _player.acceleration = 0 218 | } 219 | } 220 | if (Raylib.isKeyDown(KeyCode.KEY_DOWN)) { 221 | if (_player.acceleration > 0) { 222 | _player.acceleration = _player.acceleration - 0.04 223 | } else if (_player.acceleration < 0) { 224 | _player.acceleration = 0 225 | } 226 | } 227 | 228 | // Movement 229 | _player.position.x = _player.position.x + (_player.speed.x*_player.acceleration) 230 | _player.position.y = _player.position.y - (player.speed.y * player.acceleration) 231 | 232 | // Wall behaviour for player 233 | if (_player.position.x > ScreenWidth + _shipHeight) { 234 | _player.position.x = -(_shipHeight) 235 | } else if (_player.position.x < -(_shipHeight)) { 236 | player.position.x = ScreenWidth + _shipHeight 237 | } 238 | if (player.position.y > (ScreenHeight + _shipHeight)) { 239 | player.position.y = -(_shipHeight) 240 | } else if (_player.position.y < -(_shipHeight)) { 241 | _player.position.y = ScreenHeight + _shipHeight 242 | } 243 | 244 | // Collision Player to meteors 245 | _player.collider = Vector3.new(player.position.x + Math.sin(player.rotation*DEG2RAD)*(_shipHeight/2.5), player.position.y - Math.cos(player.rotation*DEG2RAD)*(_shipHeight/2.5), 12) 246 | 247 | 248 | for (i in 0...MAX_MEDIUM_METEORS) { 249 | if (Raylib.checkCollisionCircles(Vector2.new(player.collider.x, player.collider.y), player.collider.z, _mediumMeteor[i].position, _mediumMeteor[i].radius) && _mediumMeteor[i].active) { 250 | _gameOver = true 251 | } 252 | } 253 | 254 | for (a in 0...MAX_SMALL_METEORS) { 255 | if (Raylib.checkCollisionCircles(Vector2.new(player.collider.x, player.collider.y), player.collider.z, _smallMeteor[a].position, _smallMeteor[a].radius) && _smallMeteor[a].active) { 256 | _gameOver = true 257 | } 258 | } 259 | 260 | // Meteor logic 261 | 262 | for (i in 0...MAX_MEDIUM_METEORS) { 263 | if (mediumMeteor[i].active) { 264 | // movement 265 | mediumMeteor[i].position.x = mediumMeteor[i].position.x + mediumMeteor[i].speed.x 266 | mediumMeteor[i].position.y = mediumMeteor[i].position.y + mediumMeteor[i].speed.y 267 | 268 | if (mediumMeteor[i].position.x > ScreenWidth + mediumMeteor[i].radius) mediumMeteor[i].position.x = -(mediumMeteor[i].radius) else if (mediumMeteor[i].position.x < 0 - mediumMeteor[i].radius) { 269 | mediumMeteor[i].position.x = ScreenWidth + mediumMeteor[i].radius 270 | } 271 | if (mediumMeteor[i].position.y > ScreenHeight + mediumMeteor[i].radius) mediumMeteor[i].position.y = -(mediumMeteor[i].radius) else if (mediumMeteor[i].position.y < 0 - mediumMeteor[i].radius) { 272 | mediumMeteor[i].position.y = ScreenHeight + mediumMeteor[i].radius 273 | } 274 | } 275 | } 276 | for (i in 0...MAX_SMALL_METEORS) { 277 | if (smallMeteor[i].active) { 278 | // movement 279 | smallMeteor[i].position.x = smallMeteor[i].position.x + smallMeteor[i].speed.x 280 | smallMeteor[i].position.y = smallMeteor[i].position.y + smallMeteor[i].speed.y 281 | 282 | if (smallMeteor[i].position.x > ScreenWidth + smallMeteor[i].radius) smallMeteor[i].position.x = -(smallMeteor[i].radius) else if (smallMeteor[i].position.x < 0 - smallMeteor[i].radius) { 283 | smallMeteor[i].position.x = ScreenWidth + smallMeteor[i].radius 284 | } 285 | if (smallMeteor[i].position.y > ScreenHeight + smallMeteor[i].radius) smallMeteor[i].position.y = -(smallMeteor[i].radius) else if (smallMeteor[i].position.y < 0 - smallMeteor[i].radius) { 286 | smallMeteor[i].position.y = ScreenHeight + smallMeteor[i].radius 287 | } 288 | } 289 | } 290 | } 291 | } else { 292 | if (Raylib.isKeyDown(KeyCode.KEY_ENTER)) { 293 | _gameOver = false 294 | initGame() 295 | } 296 | } 297 | } 298 | 299 | draw() { 300 | Raylib.beginDrawing() 301 | 302 | Raylib.clearBackground(Color.RayWhite) 303 | 304 | if (!_gameOver) { 305 | // Draw spaceship 306 | var v1 = Vector2.new(player.position.x + Math.sin(player.rotation*DEG2RAD)*(_shipHeight), player.position.y - Math.cos(player.rotation*DEG2RAD)*(_shipHeight)) 307 | var v2 = Vector2.new( 308 | player.position.x - Math.cos(player.rotation * DEG2RAD) * (PLAYER_BASE_SIZE / 2), 309 | player.position.y - Math.sin(player.rotation * DEG2RAD) * (PLAYER_BASE_SIZE / 2) 310 | ) 311 | var v3 = Vector2.new( 312 | player.position.x + Math.cos(player.rotation * DEG2RAD) * (PLAYER_BASE_SIZE / 2), 313 | player.position.y + Math.sin(player.rotation * DEG2RAD) * (PLAYER_BASE_SIZE / 2) 314 | ) 315 | 316 | Raylib.drawTriangle(v1, v2, v3, Color.Maroon) 317 | 318 | // Draw meteor 319 | for (i in 0...MAX_MEDIUM_METEORS) { 320 | if (mediumMeteor[i].active) Raylib.drawCircleV(mediumMeteor[i].position, mediumMeteor[i].radius, Color.Gray) else Raylib.drawCircleV(mediumMeteor[i].position, mediumMeteor[i].radius, Color.LightGray) 321 | } 322 | 323 | for (i in 0...MAX_SMALL_METEORS) { 324 | if (smallMeteor[i].active) Raylib.drawCircleV(smallMeteor[i].position, smallMeteor[i].radius, Color.DarkGray) else Raylib.drawCircleV(smallMeteor[i].position, smallMeteor[i].radius, Color.LightGray) 325 | } 326 | 327 | Raylib.drawText("TIME: %(_framesCounter / 60)", 10, 10, 20, Color.Black) 328 | 329 | if (_pause) Raylib.drawText("GAME PAUSED", 20, 40/2, ScreenHeight/2 - 40, 40, Color.Gray) 330 | } else { 331 | Raylib.drawText("PRESS [ENTER] TO PLAY AGAIN", 20, Raylib.getScreenHeight()/2 - 50, 20, Color.Gray) 332 | } 333 | 334 | Raylib.endDrawing() 335 | 336 | 337 | } 338 | } 339 | 340 | Raylib.initWindow(ScreenWidth, ScreenHeight, "classic game: asteroids survival") 341 | 342 | var game = Game.new() 343 | 344 | Raylib.setTargetFPS(60) 345 | 346 | while (!Raylib.windowShouldClose()) { 347 | game.update() 348 | game.draw() 349 | } 350 | 351 | Raylib.closeWindow() 352 | -------------------------------------------------------------------------------- /examples/camera.wren: -------------------------------------------------------------------------------- 1 | import "raylib" for Color, Raylib, Rectangle, Vector2, Camera2D, KeyCode, Texture2D 2 | import "math" for Math 3 | 4 | var MAX_BUILDINGS = 100 5 | 6 | var screenWidth = 800 7 | var screenHeight = 450 8 | 9 | Raylib.initWindow(screenWidth, screenHeight, "raylib [core] example - 2d camera") 10 | 11 | var player = Rectangle.new(400, 280, 40, 40) 12 | var buildings = [] 13 | var buildColors = [] 14 | 15 | var spacing = 0 16 | 17 | var i = 0 18 | 19 | while (i < MAX_BUILDINGS) { 20 | var height = Raylib.getRandomValue(100, 800) 21 | var random = Raylib.getRandomValue(50, 200) 22 | 23 | var building = Rectangle.new( 24 | -6000 + spacing, 25 | screenHeight - 130.0 - height, 26 | random, 27 | height 28 | ) 29 | 30 | buildings.add(building) 31 | 32 | spacing = spacing + building.width 33 | 34 | buildColors.add(Color.new( 35 | Raylib.getRandomValue(200, 240), 36 | Raylib.getRandomValue(200, 240), 37 | Raylib.getRandomValue(200, 250), 38 | 255 39 | )) 40 | 41 | i = i + 1 42 | } 43 | 44 | var camera = Camera2D.new( 45 | Vector2.new(screenWidth / 2.0, screenHeight / 2.0), 46 | Vector2.new(player.x + 20.0, player.y + 20.0), 47 | 1.0, 48 | 0.0 49 | ) 50 | 51 | Raylib.setTargetFPS(60) // Set our game to run at 60 frames-per-secon 52 | 53 | while (!Raylib.windowShouldClose()) { 54 | 55 | if (Raylib.isKeyDown(KeyCode.KEY_RIGHT)) { 56 | player.x = player.x + 2 57 | } else if (Raylib.isKeyDown(KeyCode.KEY_LEFT)) { player.x = player.x - 2 } 58 | 59 | camera.target = Vector2.new(player.x + 20, player.y + 20) 60 | 61 | if (Raylib.isKeyDown(KeyCode.KEY_A)) { 62 | camera.rotation = camera.rotation - 1 63 | } else if (Raylib.isKeyDown(KeyCode.KEY_S)) { 64 | camera.rotation = camera.rotation + 1 65 | } 66 | 67 | if (camera.rotation > 40) { 68 | camera.rotation = 40 69 | } else if (camera.rotation < -40) { camera.rotation = -40 } 70 | 71 | camera.zoom = Math.exp(Math.log(camera.zoom) + (Raylib.getMouseWheelMove() * 0.1)) 72 | 73 | if (camera.zoom > 3.0) { 74 | camera.zoom = 3.0 75 | } else if (camera.zoom < 0.1) { 76 | camera.zoom = 0.1 77 | } 78 | 79 | if (Raylib.isKeyDown(KeyCode.KEY_R)) { 80 | camera.zoom = 1.0 81 | camera.rotation = 0.0 82 | } 83 | Raylib.beginDrawing() 84 | 85 | Raylib.clearBackground(Color.RayWhite) 86 | 87 | Raylib.beginMode2D(camera) 88 | 89 | Raylib.drawRectangle(-6000, 320, 13000, 8000, Color.DarkGray) 90 | 91 | var i = 0 92 | while (i < MAX_BUILDINGS) { 93 | Raylib.drawRectangleRec(buildings[i], buildColors[i]) 94 | i = i + 1 95 | } 96 | 97 | Raylib.drawRectangleRec(player, Color.Red) 98 | 99 | Raylib.drawLine(camera.target.x, -screenHeight*10, camera.target.x, screenHeight*10, Color.Green) 100 | Raylib.drawLine(-screenWidth*10, camera.target.y, screenWidth*10, camera.target.y, Color.Green) 101 | 102 | Raylib.endMode2D() 103 | 104 | Raylib.drawText("SCREEN AREA", 640, 10, 20, Color.Red) 105 | 106 | Raylib.drawRectangle(0, 0, screenWidth, 5, Color.Red) 107 | Raylib.drawRectangle(0, 5, 5, screenHeight - 10, Color.Red) 108 | Raylib.drawRectangle(screenWidth - 5, 5, 5, screenHeight - 10, Color.Red) 109 | Raylib.drawRectangle(0, screenHeight - 5, screenWidth, 5, Color.Red) 110 | 111 | Raylib.drawRectangle( 10, 10, 250, 113, Color.SkyBlue ) 112 | Raylib.drawRectangleLines( 10, 10, 250, 113, Color.Blue) 113 | 114 | Raylib.drawText("Free 2d camera controls:", 20, 20, 10, Color.Black) 115 | Raylib.drawText("- Right/Left to move Offset", 40, 40, 10, Color.DarkGray) 116 | Raylib.drawText("- Mouse Wheel to Zoom in-out", 40, 60, 10, Color.DarkGray) 117 | Raylib.drawText("- A / S to Rotate", 40, 80, 10, Color.DarkGray) 118 | Raylib.drawText("- R to reset Zoom and Rotation", 40, 100, 10, Color.DarkGray) 119 | 120 | Raylib.endDrawing() 121 | } 122 | 123 | Raylib.closeWindow() 124 | -------------------------------------------------------------------------------- /examples/extend/add.c: -------------------------------------------------------------------------------- 1 | #include "wren.h" 2 | #include 3 | 4 | void wren_c_embed_add(WrenVM* vm) { 5 | double a = wrenGetSlotDouble(vm, 1); 6 | double b = wrenGetSlotDouble(vm, 2); 7 | wrenSetSlotDouble(vm, 0, (double)(a + b)); 8 | } 9 | -------------------------------------------------------------------------------- /examples/extend/add.dll: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jossephus/talon/9b95d00db9afb86452ba14eb02fce9e89477b86e/examples/extend/add.dll -------------------------------------------------------------------------------- /examples/extend/build.zig: -------------------------------------------------------------------------------- 1 | const std = @import("std"); 2 | 3 | const mainFile = "main.wren"; 4 | 5 | pub fn build(b: *std.Build) !void { 6 | const target = b.standardTargetOptions(.{}); 7 | const optimize = b.standardOptimizeOption(.{}); 8 | 9 | const tolan_lib = b.dependency("tolan", .{ 10 | .target = target, 11 | .optimize = optimize, 12 | }); 13 | const tolan_mod = tolan_lib.module("tolan"); 14 | 15 | const exe_mod = b.createModule(.{ 16 | .root_source_file = b.path("build.zig"), 17 | .target = target, 18 | .optimize = optimize, 19 | }); 20 | 21 | exe_mod.addImport("tolan", tolan_mod); 22 | 23 | const add = b.addSharedLibrary(.{ 24 | .name = "add", 25 | .target = target, 26 | .optimize = optimize, 27 | .link_libc = true, 28 | }); 29 | 30 | add.addCSourceFiles(.{ .root = b.path("."), .files = &.{ 31 | "add.c", 32 | } }); 33 | b.installArtifact(add); 34 | 35 | const wren_lib = tolan_lib.artifact("wren"); 36 | add.linkLibrary(wren_lib); 37 | exe_mod.linkLibrary(add); 38 | 39 | const tolan_exe = b.addExecutable(.{ 40 | .name = "camera", 41 | .root_module = exe_mod, 42 | }); 43 | 44 | try addAssetsOption(b, tolan_exe, target, optimize, b.getInstallStep()); 45 | 46 | //const tolan_exe = try tolan.createExecutable(b, "sample-exe", target, optimize); 47 | b.installArtifact(tolan_exe); 48 | 49 | const run_cmd = b.addRunArtifact(tolan_exe); 50 | 51 | run_cmd.step.dependOn(b.getInstallStep()); 52 | 53 | if (b.args) |args| { 54 | run_cmd.addArgs(args); 55 | } 56 | 57 | const run_step = b.step("run", "Run the app"); 58 | run_step.dependOn(&run_cmd.step); 59 | } 60 | 61 | // varied version of https://github.com/ringtailsoftware/zig-embeddir/blob/main/build.zig to include wren files 62 | pub fn addAssetsOption(b: *std.Build, exe: anytype, target: anytype, optimize: anytype, step: *std.Build.Step) !void { 63 | var options = b.addOptions(); 64 | 65 | var files = std.ArrayList([]const u8).init(b.allocator); 66 | defer files.deinit(); 67 | 68 | try checkWrenFiles(b.allocator, &files, b, ".", ".", step); 69 | 70 | options.addOption([]const []const u8, "files", files.items); 71 | exe.step.dependOn(&options.step); 72 | 73 | const assets_mod = b.addModule("assets", .{ 74 | .root_source_file = options.getOutput(), 75 | .target = target, 76 | .optimize = optimize, 77 | }); 78 | 79 | exe.root_module.addImport("assets", assets_mod); 80 | } 81 | 82 | fn checkWrenFiles( 83 | allocator: std.mem.Allocator, 84 | files: *std.ArrayList([]const u8), 85 | b: *std.Build, 86 | base_path: []const u8, 87 | rel_path: []const u8, 88 | step: *std.Build.Step, 89 | ) !void { 90 | var dir = try std.fs.cwd().openDir(rel_path, .{ .iterate = true }); 91 | var it = dir.iterate(); 92 | while (try it.next()) |entry| { 93 | if (std.mem.eql(u8, entry.name, ".zig-cache") or std.mem.eql(u8, entry.name, "zig-out")) { 94 | continue; 95 | } 96 | const rel_to_base = try std.fs.path.join(allocator, &.{ rel_path, entry.name }); 97 | defer allocator.free(rel_to_base); 98 | 99 | switch (entry.kind) { 100 | .file => { 101 | if (std.mem.endsWith(u8, entry.name, ".wren")) { 102 | const rel = try std.fs.path.relative(allocator, base_path, rel_to_base); 103 | defer allocator.free(rel); 104 | 105 | try files.append(b.dupe(rel)); 106 | } 107 | }, 108 | .directory => { 109 | try checkWrenFiles(allocator, files, b, base_path, rel_to_base, step); 110 | }, 111 | else => {}, 112 | } 113 | } 114 | } 115 | 116 | const assets = @import("assets"); 117 | 118 | const embeddedFilesMap = std.StaticStringMap([]const u8).initComptime(genMap()); 119 | const EmbeddedAsset = struct { 120 | []const u8, 121 | []const u8, 122 | }; 123 | 124 | fn genMap() [assets.files.len]EmbeddedAsset { 125 | var embassets: [assets.files.len]EmbeddedAsset = undefined; 126 | comptime var i = 0; 127 | inline for (assets.files) |file| { 128 | embassets[i][0] = file; 129 | embassets[i][1] = @embedFile(file); 130 | i += 1; 131 | } 132 | return embassets; 133 | } 134 | 135 | pub fn main() !void { 136 | try tolan.run(mainFile, embeddedFilesMap); 137 | } 138 | 139 | const tolan = @import("tolan"); 140 | -------------------------------------------------------------------------------- /examples/extend/build.zig.zon: -------------------------------------------------------------------------------- 1 | .{ 2 | .name = .build_exe, 3 | 4 | .version = "0.0.0", 5 | 6 | .fingerprint = 0x7aca45f40a314ba8, // Changing this has security and trust implications. 7 | 8 | .minimum_zig_version = "0.14.0", 9 | 10 | .dependencies = .{ 11 | .tolan = .{ 12 | .path = "../../" 13 | }, 14 | .wren = .{ 15 | .url = "https://github.com/wren-lang/wren/archive/refs/heads/main.zip", 16 | .hash = "N-V-__8AAPbpYgAZgdr-sM49A18GSKr5MVR56MwpfI65FmAZ", 17 | }, 18 | .raylib = .{ 19 | .url = "git+https://github.com/raysan5/raylib#27a4fe885164b315a90b67682f981a1e03d6079c", 20 | .hash = "raylib-5.5.0-whq8uFqtNARw6t1tTakBDSVYgUjlBqnDppOyNfE_yfCa", 21 | }, 22 | }, 23 | 24 | .paths = .{ 25 | "build.zig", 26 | "build.zig.zon", 27 | "src", 28 | }, 29 | } 30 | -------------------------------------------------------------------------------- /examples/extend/game.wren: -------------------------------------------------------------------------------- 1 | import "embed" for Load 2 | var Game = "Game" 3 | 4 | Load.foreignFunction("C.add(_,_)", "add.dll", "wren_c_embed_add") 5 | class C { 6 | foreign static add(a, b) 7 | } 8 | 9 | -------------------------------------------------------------------------------- /examples/extend/main.wren: -------------------------------------------------------------------------------- 1 | import "./game" for C 2 | 3 | System.print(C.add(3, 2)) 4 | -------------------------------------------------------------------------------- /examples/main.wren: -------------------------------------------------------------------------------- 1 | import "raylib" for Color, Raylib, Rectangle, Vector2, Camera2D, KeyCode, Texture2D 2 | 3 | var width = 800 4 | var height = 450 5 | var title = "Sample" 6 | 7 | Raylib.initWindow(width, height, title) 8 | Raylib.setTargetFPS(60) 9 | 10 | System.print(Raylib.windowShouldClose()) 11 | 12 | var target = Raylib.loadRenderTexture(width, height) 13 | System.print(target.texture) 14 | 15 | var camera = Camera2D.new( 16 | Vector2.new(2.0, 0.0), 17 | Vector2.new(0.0, 0.0), 18 | 0.0, 19 | 1.0 20 | ) 21 | 22 | while (!Raylib.windowShouldClose()) { 23 | Raylib.beginDrawing() 24 | 25 | Raylib.clearBackground(Color.Red) 26 | 27 | Raylib.drawText("Congrats! You created your first window!", 190, 200, 20, Color.Green) 28 | 29 | Raylib.getScreenWidth() 30 | Raylib.getScreenHeight() 31 | 32 | if (Raylib.isKeyDown(KeyCode.KEY_SPACE)) { 33 | } 34 | 35 | Raylib.drawTexturePro( 36 | target.texture, 37 | Rectangle.new(0.0,0.0,target.texture.width, target.texture.height), 38 | Rectangle.new( 0.0, 0.0, target.texture.width, target.texture.height), 39 | Vector2.new(0.0, 0.0), 40 | 0.0, 41 | Color.new(255, 255, 255, 255 ) 42 | ) 43 | 44 | 45 | 46 | Raylib.endDrawing() 47 | } 48 | 49 | Raylib.closeWindow() 50 | 51 | 52 | 53 | System.print(Color.Gray) 54 | 55 | Raylib.clearBackground(Color.Gray) 56 | Raylib.unloadRenderTexture(target) 57 | 58 | -------------------------------------------------------------------------------- /examples/playground/abc.zig: -------------------------------------------------------------------------------- 1 | const std = @import("std"); 2 | const tolan = @import("tolan"); 3 | const c = @cImport({ 4 | @cInclude("stdio.h"); 5 | }); 6 | 7 | export fn run(code: [*]const u8, len: usize) void { 8 | tolan.runWasm(code[0..len]) catch @panic("Failed to run wasm"); 9 | } 10 | 11 | pub fn main() void {} 12 | 13 | export fn add(a: i32, b: i32) i32 { 14 | return a + b; 15 | } 16 | 17 | export fn sub(a: i32, b: i32) i32 { 18 | return a - b; 19 | } 20 | 21 | extern fn emscripten_cancel_main_loop() void; 22 | 23 | export fn stop_game() void { 24 | emscripten_cancel_main_loop(); 25 | } 26 | -------------------------------------------------------------------------------- /examples/playground/build.zig: -------------------------------------------------------------------------------- 1 | const std = @import("std"); 2 | 3 | pub fn build(b: *std.Build) !void { 4 | const target = b.standardTargetOptions(.{}); 5 | const optimize = b.standardOptimizeOption(.{}); 6 | 7 | if (target.result.cpu.arch.isWasm()) { 8 | const wasm_target = b.resolveTargetQuery(.{ 9 | .cpu_arch = .wasm32, 10 | .cpu_model = .{ .explicit = &std.Target.wasm.cpu.mvp }, 11 | .cpu_features_add = std.Target.wasm.featureSet(&.{ 12 | .atomics, 13 | .bulk_memory, 14 | }), 15 | .os_tag = .emscripten, 16 | }); 17 | 18 | const tolan_lib = b.dependency("tolan", .{ 19 | .target = wasm_target, 20 | .optimize = optimize, 21 | }); 22 | const tolan_mod = tolan_lib.module("tolan-wasm"); 23 | 24 | const wren_lib = tolan_lib.artifact("wren"); 25 | const raylib_lib = tolan_lib.artifact("raylib"); 26 | 27 | const app_lib = b.addLibrary(.{ 28 | .linkage = .static, 29 | .name = "camera", 30 | .root_module = b.createModule(.{ 31 | .root_source_file = b.path("abc.zig"), 32 | .target = wasm_target, 33 | .optimize = optimize, 34 | .imports = &.{ 35 | .{ .name = "tolan", .module = tolan_mod }, 36 | }, 37 | }), 38 | }); 39 | app_lib.linkLibC(); 40 | app_lib.shared_memory = true; 41 | app_lib.linkLibrary(wren_lib); 42 | app_lib.linkLibrary(raylib_lib); 43 | app_lib.addIncludePath(.{ .cwd_relative = ".emscripten_cache-4.0.8/sysroot/include" }); 44 | 45 | try addAssetsOption(b, app_lib, target, optimize, b.getInstallStep()); 46 | 47 | const emcc = b.addSystemCommand(&.{"emcc"}); 48 | 49 | for (app_lib.getCompileDependencies(false)) |lib| { 50 | if (lib.isStaticLibrary()) { 51 | emcc.addArtifactArg(lib); 52 | } 53 | } 54 | 55 | for (wren_lib.getCompileDependencies(false)) |lib| { 56 | if (lib.isStaticLibrary()) { 57 | emcc.addArtifactArg(lib); 58 | } 59 | } 60 | 61 | for (raylib_lib.getCompileDependencies(false)) |lib| { 62 | if (lib.isStaticLibrary()) { 63 | emcc.addArtifactArg(lib); 64 | } 65 | } 66 | 67 | emcc.addArgs(&.{ 68 | "-sUSE_GLFW=3", 69 | "-sUSE_OFFSET_CONVERTER", 70 | 71 | //"-sAUDIO_WORKLET=1", 72 | //"-sWASM_WORKERS=1", 73 | "-sSHARED_MEMORY=1", 74 | "-sALLOW_MEMORY_GROWTH=1", 75 | 76 | "-sASYNCIFY", 77 | "-sundefs", 78 | "-sEXPORTED_FUNCTIONS=['_main', '_run', '_stop_game']", 79 | "-sEXPORTED_RUNTIME_METHODS=['cwrap', 'ccall']", 80 | "-sERROR_ON_UNDEFINED_SYMBOLS=0", 81 | //"--shell-file", 82 | //b.path("shell.html").getPath(b), 83 | }); 84 | 85 | const link_items: []const *std.Build.Step.Compile = &.{ 86 | wren_lib, 87 | raylib_lib, 88 | app_lib, 89 | }; 90 | 91 | for (link_items) |item| { 92 | emcc.addFileArg(item.getEmittedBin()); 93 | emcc.step.dependOn(&item.step); 94 | } 95 | 96 | //emcc.addArg("--pre-js"); 97 | emcc.addArg("-o"); 98 | 99 | const app_html = emcc.addOutputFileArg("shell.html"); 100 | 101 | b.getInstallStep().dependOn(&b.addInstallFile(b.path("index.html"), "www/index.html").step); 102 | b.getInstallStep().dependOn(&b.addInstallDirectory(.{ 103 | .source_dir = b.path("samples"), 104 | .install_dir = .{ .custom = "www" }, 105 | .install_subdir = "samples", 106 | }).step); 107 | 108 | b.getInstallStep().dependOn(&b.addInstallDirectory(.{ 109 | .source_dir = app_html.dirname(), 110 | .install_dir = .{ .custom = "www" }, 111 | .install_subdir = "", 112 | }).step); 113 | } else { 114 | const failed = b.addFail("Non wasm target chosen"); 115 | _ = failed; 116 | } 117 | } 118 | 119 | // varied version of https://github.com/ringtailsoftware/zig-embeddir/blob/main/build.zig to include wren files 120 | pub fn addAssetsOption(b: *std.Build, exe: anytype, target: anytype, optimize: anytype, step: *std.Build.Step) !void { 121 | var options = b.addOptions(); 122 | 123 | var files = std.ArrayList([]const u8).init(b.allocator); 124 | defer files.deinit(); 125 | 126 | try checkWrenFiles(b.allocator, &files, b, ".", ".", step); 127 | 128 | options.addOption([]const []const u8, "files", files.items); 129 | exe.step.dependOn(&options.step); 130 | 131 | const assets_mod = b.addModule("assets", .{ 132 | .root_source_file = options.getOutput(), 133 | .target = target, 134 | .optimize = optimize, 135 | }); 136 | 137 | exe.root_module.addImport("assets", assets_mod); 138 | } 139 | 140 | fn checkWrenFiles( 141 | allocator: std.mem.Allocator, 142 | files: *std.ArrayList([]const u8), 143 | b: *std.Build, 144 | base_path: []const u8, 145 | rel_path: []const u8, 146 | step: *std.Build.Step, 147 | ) !void { 148 | var dir = try std.fs.cwd().openDir(rel_path, .{ .iterate = true }); 149 | var it = dir.iterate(); 150 | while (try it.next()) |entry| { 151 | if (std.mem.eql(u8, entry.name, ".zig-cache") or std.mem.eql(u8, entry.name, "zig-out")) { 152 | continue; 153 | } 154 | const rel_to_base = try std.fs.path.join(allocator, &.{ rel_path, entry.name }); 155 | defer allocator.free(rel_to_base); 156 | 157 | switch (entry.kind) { 158 | .file => { 159 | if (std.mem.endsWith(u8, entry.name, ".wren")) { 160 | const rel = try std.fs.path.relative(allocator, base_path, rel_to_base); 161 | defer allocator.free(rel); 162 | 163 | try files.append(b.dupe(rel)); 164 | } 165 | }, 166 | .directory => { 167 | try checkWrenFiles(allocator, files, b, base_path, rel_to_base, step); 168 | }, 169 | else => {}, 170 | } 171 | } 172 | } 173 | -------------------------------------------------------------------------------- /examples/playground/build.zig.zon: -------------------------------------------------------------------------------- 1 | .{ 2 | .name = .build_exe, 3 | 4 | .version = "0.0.0", 5 | 6 | .fingerprint = 0x7aca45f40a314ba8, // Changing this has security and trust implications. 7 | 8 | .minimum_zig_version = "0.14.0", 9 | 10 | .dependencies = .{ 11 | .tolan = .{ 12 | .path = "../../" 13 | }, 14 | .wren = .{ 15 | .url = "https://github.com/wren-lang/wren/archive/refs/heads/main.zip", 16 | .hash = "N-V-__8AAPbpYgAZgdr-sM49A18GSKr5MVR56MwpfI65FmAZ", 17 | }, 18 | .raylib = .{ 19 | .url = "git+https://github.com/raysan5/raylib#27a4fe885164b315a90b67682f981a1e03d6079c", 20 | .hash = "raylib-5.5.0-whq8uFqtNARw6t1tTakBDSVYgUjlBqnDppOyNfE_yfCa", 21 | }, 22 | }, 23 | 24 | .paths = .{ 25 | "build.zig", 26 | "build.zig.zon", 27 | "src", 28 | "shell.html" 29 | }, 30 | } 31 | -------------------------------------------------------------------------------- /examples/playground/flake.lock: -------------------------------------------------------------------------------- 1 | { 2 | "nodes": { 3 | "flake-compat": { 4 | "flake": false, 5 | "locked": { 6 | "lastModified": 1696426674, 7 | "narHash": "sha256-kvjfFW7WAETZlt09AgDn1MrtKzP7t90Vf7vypd3OL1U=", 8 | "owner": "edolstra", 9 | "repo": "flake-compat", 10 | "rev": "0f9255e01c2351cc7d116c072cb317785dd33b33", 11 | "type": "github" 12 | }, 13 | "original": { 14 | "owner": "edolstra", 15 | "repo": "flake-compat", 16 | "type": "github" 17 | } 18 | }, 19 | "flake-utils": { 20 | "inputs": { 21 | "systems": "systems" 22 | }, 23 | "locked": { 24 | "lastModified": 1731533236, 25 | "narHash": "sha256-l0KFg5HjrsfsO/JpG+r7fRrqm12kzFHyUHqHCVpMMbI=", 26 | "owner": "numtide", 27 | "repo": "flake-utils", 28 | "rev": "11707dc2f618dd54ca8739b309ec4fc024de578b", 29 | "type": "github" 30 | }, 31 | "original": { 32 | "owner": "numtide", 33 | "repo": "flake-utils", 34 | "type": "github" 35 | } 36 | }, 37 | "flake-utils_2": { 38 | "inputs": { 39 | "systems": "systems_2" 40 | }, 41 | "locked": { 42 | "lastModified": 1705309234, 43 | "narHash": "sha256-uNRRNRKmJyCRC/8y1RqBkqWBLM034y4qN7EprSdmgyA=", 44 | "owner": "numtide", 45 | "repo": "flake-utils", 46 | "rev": "1ef2e671c3b0c19053962c07dbda38332dcebf26", 47 | "type": "github" 48 | }, 49 | "original": { 50 | "owner": "numtide", 51 | "repo": "flake-utils", 52 | "type": "github" 53 | } 54 | }, 55 | "nixpkgs": { 56 | "locked": { 57 | "lastModified": 1748460289, 58 | "narHash": "sha256-7doLyJBzCllvqX4gszYtmZUToxKvMUrg45EUWaUYmBg=", 59 | "owner": "NixOS", 60 | "repo": "nixpkgs", 61 | "rev": "96ec055edbe5ee227f28cdbc3f1ddf1df5965102", 62 | "type": "github" 63 | }, 64 | "original": { 65 | "id": "nixpkgs", 66 | "ref": "nixos-unstable", 67 | "type": "indirect" 68 | } 69 | }, 70 | "nixpkgs_2": { 71 | "locked": { 72 | "lastModified": 1708161998, 73 | "narHash": "sha256-6KnemmUorCvlcAvGziFosAVkrlWZGIc6UNT9GUYr0jQ=", 74 | "owner": "NixOS", 75 | "repo": "nixpkgs", 76 | "rev": "84d981bae8b5e783b3b548de505b22880559515f", 77 | "type": "github" 78 | }, 79 | "original": { 80 | "owner": "NixOS", 81 | "ref": "nixos-23.11", 82 | "repo": "nixpkgs", 83 | "type": "github" 84 | } 85 | }, 86 | "root": { 87 | "inputs": { 88 | "flake-utils": "flake-utils", 89 | "nixpkgs": "nixpkgs", 90 | "zig": "zig" 91 | } 92 | }, 93 | "systems": { 94 | "locked": { 95 | "lastModified": 1681028828, 96 | "narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=", 97 | "owner": "nix-systems", 98 | "repo": "default", 99 | "rev": "da67096a3b9bf56a91d16901293e51ba5b49a27e", 100 | "type": "github" 101 | }, 102 | "original": { 103 | "owner": "nix-systems", 104 | "repo": "default", 105 | "type": "github" 106 | } 107 | }, 108 | "systems_2": { 109 | "locked": { 110 | "lastModified": 1681028828, 111 | "narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=", 112 | "owner": "nix-systems", 113 | "repo": "default", 114 | "rev": "da67096a3b9bf56a91d16901293e51ba5b49a27e", 115 | "type": "github" 116 | }, 117 | "original": { 118 | "owner": "nix-systems", 119 | "repo": "default", 120 | "type": "github" 121 | } 122 | }, 123 | "zig": { 124 | "inputs": { 125 | "flake-compat": "flake-compat", 126 | "flake-utils": "flake-utils_2", 127 | "nixpkgs": "nixpkgs_2" 128 | }, 129 | "locked": { 130 | "lastModified": 1748693530, 131 | "narHash": "sha256-uOBNjRdKFV33NcagUCoxr5fGFj25AIeAygEcKp9/TwY=", 132 | "owner": "mitchellh", 133 | "repo": "zig-overlay", 134 | "rev": "e0615554b93df776f3a6f7b5b5d2009c8364391c", 135 | "type": "github" 136 | }, 137 | "original": { 138 | "owner": "mitchellh", 139 | "repo": "zig-overlay", 140 | "type": "github" 141 | } 142 | } 143 | }, 144 | "root": "root", 145 | "version": 7 146 | } 147 | -------------------------------------------------------------------------------- /examples/playground/flake.nix: -------------------------------------------------------------------------------- 1 | { 2 | inputs = { 3 | nixpkgs.url = "nixpkgs/nixos-unstable"; 4 | flake-utils.url = "github:numtide/flake-utils"; 5 | zig.url = "github:mitchellh/zig-overlay"; 6 | }; 7 | 8 | outputs = { 9 | nixpkgs, 10 | flake-utils, 11 | zig, 12 | ... 13 | }: 14 | flake-utils.lib.eachDefaultSystem ( 15 | system: let 16 | overlays = [ 17 | (final: prev: { 18 | zigpkgs = zig.packages.${system}; 19 | }) 20 | ]; 21 | pkgs = import nixpkgs { 22 | inherit overlays system; 23 | }; 24 | packages = with pkgs; [ 25 | glfw 26 | libGL 27 | libxkbcommon 28 | pkg-config 29 | xorg.libxcb 30 | xorg.libXft 31 | xorg.libX11 32 | xorg.libX11.dev 33 | xorg.libXrandr 34 | xorg.libXinerama 35 | xorg.libXcursor 36 | xorg.libXi 37 | glfw-wayland 38 | zigpkgs."0.14.0" 39 | emscripten 40 | ]; 41 | in { 42 | devShell = pkgs.mkShell { 43 | buildInputs = packages; 44 | nativeBuildInputs = with pkgs; [cmake pkg-config ncurses fontconfig freetype]; 45 | shellHook = '' 46 | export SHELL=/usr/bin/bash 47 | if [ ! -d $(pwd)/.emscripten_cache-${pkgs.emscripten.version} ]; then 48 | cp -R ${pkgs.emscripten}/share/emscripten/cache/ $(pwd)/.emscripten_cache-${pkgs.emscripten.version} 49 | chmod u+rwX -R $(pwd)/.emscripten_cache-${pkgs.emscripten.version} 50 | fi 51 | export EM_CACHE=$(pwd)/.emscripten_cache-${pkgs.emscripten.version} 52 | echo emscripten cache dir: $EM_CACHE 53 | ''; 54 | }; 55 | } 56 | ); 57 | } 58 | -------------------------------------------------------------------------------- /examples/playground/game.wren: -------------------------------------------------------------------------------- 1 | var Game = "Game" 2 | -------------------------------------------------------------------------------- /examples/playground/main.wren: -------------------------------------------------------------------------------- 1 | import "raylib" for Color, Raylib, Rectangle, Vector2, Camera2D, KeyCode, Texture2D, Vector3 2 | import "math" for Math 3 | 4 | 5 | var PLAYER_BASE_SIZE = 20 6 | var PLAYER_SPEED = 6.0 7 | var PLAYER_MAX_SHOOTS = 10 8 | var DEG2RAD = 0.017453 9 | 10 | var METEORS_SPEED = 2 11 | var MAX_MEDIUM_METEORS = 8 12 | var MAX_SMALL_METEORS = 16 13 | 14 | var ScreenWidth = 800 15 | var ScreenHeight = 450 16 | 17 | class Meteor { 18 | construct new(position, speed, radius, active, color) { 19 | _position = position 20 | _speed = speed 21 | _radius = radius 22 | _active = active 23 | _color = color 24 | } 25 | 26 | position { _position } 27 | speed { _speed } 28 | active { _active } 29 | color { _color } 30 | radius { _radius } 31 | 32 | position=(value) { _position = value } 33 | speed=(value) { _speed = value } 34 | active=(value) { _active = value } 35 | color=(value) { _color = value } 36 | radius=(value) { _radius = value } 37 | } 38 | 39 | class Player { 40 | construct new(position, speed, acceleration, rotation, collider, color) { 41 | _position = position 42 | _speed = speed 43 | _acceleration = acceleration 44 | _rotation = rotation 45 | _collider = collider 46 | _color = color 47 | } 48 | position { _position } 49 | speed { _speed } 50 | acceleration { _acceleration } 51 | rotation { _rotation } 52 | collider { _collider } 53 | color { _color } 54 | 55 | position=(value) { _position = value } 56 | speed=(value) { _speed = value } 57 | acceleration=(value) { _acceleration = value } 58 | rotation=(value) { _rotation = value } 59 | collider=(value) { _collider = value } 60 | color=(value) { _color = value } 61 | } 62 | 63 | 64 | class Game { 65 | initGame() { 66 | var posx = 0 67 | var posy = 0 68 | var velx = 0 69 | var vely = 0 70 | var correctRange = false 71 | 72 | _pause = false 73 | _gameOver = false 74 | _framesCounter = 0 75 | _shipHeight = (PLAYER_BASE_SIZE / 2) / Math.tan(20 * DEG2RAD) 76 | 77 | _mediumMeteor = [] 78 | _smallMeteor = [] 79 | 80 | var x = ScreenWidth / 2 81 | var y = ScreenHeight / 2 - _shipHeight / 2 82 | 83 | _player = Player.new( 84 | Vector2.new(x, y), 85 | Vector2.new(0, 0), 86 | 0, 87 | 0, 88 | Vector3.new( 89 | x + Math.sin(0 * DEG2RAD) * (_shipHeight / 2.5), 90 | y - Math.cos(0 * DEG2RAD) * (_shipHeight / 2.5), 91 | 12), 92 | Color.LightGray 93 | ) 94 | 95 | for (i in 0...MAX_MEDIUM_METEORS) { 96 | 97 | posx = Raylib.getRandomValue(0, ScreenWidth) 98 | 99 | while (!correctRange) { 100 | if (posx > ScreenWidth/2 - 150 && posx < ScreenWidth/2 + 150) { 101 | posx = Raylib.getRandomValue(0, ScreenWidth) 102 | } else { 103 | correctRange = true 104 | } 105 | } 106 | 107 | correctRange = false 108 | 109 | posy = Raylib.getRandomValue(0, ScreenHeight) 110 | 111 | while (!correctRange) { 112 | if (posy > ScreenHeight/2 - 150 && posy < ScreenHeight/2 + 150) { 113 | posy = Raylib.getRandomValue(0, ScreenHeight) 114 | } else { 115 | correctRange = true 116 | } 117 | } 118 | 119 | correctRange = false 120 | velx = Raylib.getRandomValue(-METEORS_SPEED, METEORS_SPEED) 121 | vely = Raylib.getRandomValue(-METEORS_SPEED, METEORS_SPEED) 122 | 123 | while (!correctRange) { 124 | if (velx == 0 && vely == 0) { 125 | velx = Raylib.getRandomValue(-METEORS_SPEED, METEORS_SPEED) 126 | vely = Raylib.getRandomValue(-METEORS_SPEED, METEORS_SPEED) 127 | } else { 128 | correctRange = true 129 | } 130 | } 131 | _mediumMeteor.add( 132 | Meteor.new(Vector2.new(posx, posy), Vector2.new(velx, vely), 20, true, Color.Green) 133 | ) 134 | } 135 | 136 | for (j in 0...MAX_SMALL_METEORS) { 137 | posx = Raylib.getRandomValue(0, ScreenWidth) 138 | 139 | while (!correctRange) { 140 | if (posx > ScreenWidth/2 - 150 && posx < ScreenWidth/2 + 150) { 141 | posx = Raylib.getRandomValue(0, ScreenWidth) 142 | } else { 143 | correctRange = true 144 | } 145 | } 146 | 147 | correctRange = false 148 | 149 | posy = Raylib.getRandomValue(0, ScreenHeight) 150 | 151 | while (!correctRange) { 152 | if (posy > ScreenHeight/2 - 150 && posy < ScreenHeight/2 + 150) { 153 | posy = Raylib.getRandomValue(0, ScreenHeight) 154 | } else { 155 | correctRange = true 156 | } 157 | } 158 | 159 | correctRange = false 160 | velx = Raylib.getRandomValue(-METEORS_SPEED, METEORS_SPEED) 161 | vely = Raylib.getRandomValue(-METEORS_SPEED, METEORS_SPEED) 162 | 163 | while (!correctRange) { 164 | if (velx == 0 && vely == 0) { 165 | velx = Raylib.getRandomValue(-METEORS_SPEED, METEORS_SPEED) 166 | vely = Raylib.getRandomValue(-METEORS_SPEED, METEORS_SPEED) 167 | } else { 168 | correctRange = true 169 | } 170 | } 171 | _smallMeteor.add( 172 | Meteor.new(Vector2.new(posx, posy), Vector2.new(velx, vely), 20, true, Color.Yellow) 173 | ) 174 | } 175 | 176 | 177 | } 178 | 179 | construct new() { 180 | initGame() 181 | } 182 | 183 | player { _player } 184 | mediumMeteor { _mediumMeteor } 185 | smallMeteor { _smallMeteor } 186 | 187 | update() { 188 | if (!_gameOver) { 189 | if (Raylib.isKeyDown(KeyCode.KEY_P)) { 190 | _pause = !_pause 191 | } 192 | 193 | if (!_pause) { 194 | _framesCounter = _framesCounter + 1 195 | 196 | // Rotation 197 | if (Raylib.isKeyDown(KeyCode.KEY_LEFT)) { 198 | _player.rotation = player.rotation - 5 199 | } 200 | if (Raylib.isKeyDown(KeyCode.KEY_RIGHT)) { 201 | System.print("Right is clicked") 202 | _player.rotation = _player.rotation + 5 203 | } 204 | 205 | // Speed 206 | _player.speed.x = Math.sin(_player.rotation*DEG2RAD)*PLAYER_SPEED 207 | _player.speed.y = Math.cos(_player.rotation*DEG2RAD)*PLAYER_SPEED 208 | 209 | // Controller 210 | if (Raylib.isKeyDown(KeyCode.KEY_UP)) { 211 | System.print("UP IS CLICKING") 212 | if (_player.acceleration < 1) _player.acceleration = _player.acceleration + 0.04 213 | } else { 214 | if (_player.acceleration > 0) { 215 | _player.acceleration = _player.acceleration - 0.02 216 | } else if (player.acceleration < 0) { 217 | _player.acceleration = 0 218 | } 219 | } 220 | if (Raylib.isKeyDown(KeyCode.KEY_DOWN)) { 221 | if (_player.acceleration > 0) { 222 | _player.acceleration = _player.acceleration - 0.04 223 | } else if (_player.acceleration < 0) { 224 | _player.acceleration = 0 225 | } 226 | } 227 | 228 | // Movement 229 | _player.position.x = _player.position.x + (_player.speed.x*_player.acceleration) 230 | _player.position.y = _player.position.y - (player.speed.y * player.acceleration) 231 | 232 | // Wall behaviour for player 233 | if (_player.position.x > ScreenWidth + _shipHeight) { 234 | _player.position.x = -(_shipHeight) 235 | } else if (_player.position.x < -(_shipHeight)) { 236 | player.position.x = ScreenWidth + _shipHeight 237 | } 238 | if (player.position.y > (ScreenHeight + _shipHeight)) { 239 | player.position.y = -(_shipHeight) 240 | } else if (_player.position.y < -(_shipHeight)) { 241 | _player.position.y = ScreenHeight + _shipHeight 242 | } 243 | 244 | // Collision Player to meteors 245 | _player.collider = Vector3.new(player.position.x + Math.sin(player.rotation*DEG2RAD)*(_shipHeight/2.5), player.position.y - Math.cos(player.rotation*DEG2RAD)*(_shipHeight/2.5), 12) 246 | 247 | 248 | for (i in 0...MAX_MEDIUM_METEORS) { 249 | if (Raylib.checkCollisionCircles(Vector2.new(player.collider.x, player.collider.y), player.collider.z, _mediumMeteor[i].position, _mediumMeteor[i].radius) && _mediumMeteor[i].active) { 250 | _gameOver = true 251 | } 252 | } 253 | 254 | for (a in 0...MAX_SMALL_METEORS) { 255 | if (Raylib.checkCollisionCircles(Vector2.new(player.collider.x, player.collider.y), player.collider.z, _smallMeteor[a].position, _smallMeteor[a].radius) && _smallMeteor[a].active) { 256 | _gameOver = true 257 | } 258 | } 259 | 260 | // Meteor logic 261 | 262 | for (i in 0...MAX_MEDIUM_METEORS) { 263 | if (mediumMeteor[i].active) { 264 | // movement 265 | mediumMeteor[i].position.x = mediumMeteor[i].position.x + mediumMeteor[i].speed.x 266 | mediumMeteor[i].position.y = mediumMeteor[i].position.y + mediumMeteor[i].speed.y 267 | 268 | if (mediumMeteor[i].position.x > ScreenWidth + mediumMeteor[i].radius) mediumMeteor[i].position.x = -(mediumMeteor[i].radius) else if (mediumMeteor[i].position.x < 0 - mediumMeteor[i].radius) { 269 | mediumMeteor[i].position.x = ScreenWidth + mediumMeteor[i].radius 270 | } 271 | if (mediumMeteor[i].position.y > ScreenHeight + mediumMeteor[i].radius) mediumMeteor[i].position.y = -(mediumMeteor[i].radius) else if (mediumMeteor[i].position.y < 0 - mediumMeteor[i].radius) { 272 | mediumMeteor[i].position.y = ScreenHeight + mediumMeteor[i].radius 273 | } 274 | } 275 | } 276 | for (i in 0...MAX_SMALL_METEORS) { 277 | if (smallMeteor[i].active) { 278 | // movement 279 | smallMeteor[i].position.x = smallMeteor[i].position.x + smallMeteor[i].speed.x 280 | smallMeteor[i].position.y = smallMeteor[i].position.y + smallMeteor[i].speed.y 281 | 282 | if (smallMeteor[i].position.x > ScreenWidth + smallMeteor[i].radius) smallMeteor[i].position.x = -(smallMeteor[i].radius) else if (smallMeteor[i].position.x < 0 - smallMeteor[i].radius) { 283 | smallMeteor[i].position.x = ScreenWidth + smallMeteor[i].radius 284 | } 285 | if (smallMeteor[i].position.y > ScreenHeight + smallMeteor[i].radius) smallMeteor[i].position.y = -(smallMeteor[i].radius) else if (smallMeteor[i].position.y < 0 - smallMeteor[i].radius) { 286 | smallMeteor[i].position.y = ScreenHeight + smallMeteor[i].radius 287 | } 288 | } 289 | } 290 | } 291 | } else { 292 | if (Raylib.isKeyDown(KeyCode.KEY_ENTER)) { 293 | _gameOver = false 294 | initGame() 295 | } 296 | } 297 | } 298 | 299 | draw() { 300 | Raylib.beginDrawing() 301 | 302 | Raylib.clearBackground(Color.RayWhite) 303 | 304 | if (!_gameOver) { 305 | // Draw spaceship 306 | var v1 = Vector2.new(player.position.x + Math.sin(player.rotation*DEG2RAD)*(_shipHeight), player.position.y - Math.cos(player.rotation*DEG2RAD)*(_shipHeight)) 307 | var v2 = Vector2.new( 308 | player.position.x - Math.cos(player.rotation * DEG2RAD) * (PLAYER_BASE_SIZE / 2), 309 | player.position.y - Math.sin(player.rotation * DEG2RAD) * (PLAYER_BASE_SIZE / 2) 310 | ) 311 | var v3 = Vector2.new( 312 | player.position.x + Math.cos(player.rotation * DEG2RAD) * (PLAYER_BASE_SIZE / 2), 313 | player.position.y + Math.sin(player.rotation * DEG2RAD) * (PLAYER_BASE_SIZE / 2) 314 | ) 315 | 316 | Raylib.drawTriangle(v1, v2, v3, Color.Maroon) 317 | 318 | // Draw meteor 319 | for (i in 0...MAX_MEDIUM_METEORS) { 320 | if (mediumMeteor[i].active) Raylib.drawCircleV(mediumMeteor[i].position, mediumMeteor[i].radius, Color.Gray) else Raylib.drawCircleV(mediumMeteor[i].position, mediumMeteor[i].radius, Color.LightGray) 321 | } 322 | 323 | for (i in 0...MAX_SMALL_METEORS) { 324 | if (smallMeteor[i].active) Raylib.drawCircleV(smallMeteor[i].position, smallMeteor[i].radius, Color.DarkGray) else Raylib.drawCircleV(smallMeteor[i].position, smallMeteor[i].radius, Color.LightGray) 325 | } 326 | 327 | Raylib.drawText("TIME: %(_framesCounter / 60)", 10, 10, 20, Color.Black) 328 | 329 | if (_pause) Raylib.drawText("GAME PAUSED", 20, 40/2, ScreenHeight/2 - 40, 40, Color.Gray) 330 | } else { 331 | Raylib.drawText("PRESS [ENTER] TO PLAY AGAIN", 20, Raylib.getScreenHeight()/2 - 50, 20, Color.Gray) 332 | } 333 | 334 | Raylib.endDrawing() 335 | 336 | 337 | } 338 | } 339 | 340 | Raylib.initWindow(ScreenWidth, ScreenHeight, "classic game: asteroids survival") 341 | 342 | var game = Game.new() 343 | 344 | Raylib.setTargetFPS(60) 345 | 346 | while (!Raylib.windowShouldClose()) { 347 | game.update() 348 | game.draw() 349 | } 350 | 351 | Raylib.closeWindow() 352 | -------------------------------------------------------------------------------- /examples/playground/samples/.nojekyll: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jossephus/talon/9b95d00db9afb86452ba14eb02fce9e89477b86e/examples/playground/samples/.nojekyll -------------------------------------------------------------------------------- /examples/playground/samples/basic.wren: -------------------------------------------------------------------------------- 1 | import "raylib" for Color, Raylib, Rectangle, Vector2, Camera2D, KeyCode, Texture2D 2 | import "builtin" for Build 3 | 4 | // Automatic reloads are very crucial for ui buildings 5 | // Here is talon running in hot reload mode 6 | 7 | var title = "Title Screen Changed 2" 8 | var color = Color.Yellow 9 | 10 | class GameScreen { 11 | static Logo { 0 } 12 | static Title { 1 } 13 | static GamePlay { 2 } 14 | static Ending { 3 } 15 | } 16 | 17 | var screenWidth = 1000 18 | var screenHeight = 1000 19 | 20 | Raylib.initWindow(screenWidth, screenHeight, "raylib [core] example - basic screen manager") 21 | 22 | var currentScreen = GameScreen.Logo 23 | 24 | var framesCounter = 0 25 | 26 | Raylib.setTargetFPS(60) 27 | 28 | while (!Raylib.windowShouldClose() && !Build.shouldStop()) { 29 | if (currentScreen == GameScreen.Logo) { 30 | framesCounter = framesCounter + 1 31 | // if (framesCounter > 120) { 32 | // currentScreen = GameScreen.Title 33 | // } 34 | } 35 | 36 | if (currentScreen == GameScreen.Title) { 37 | if (Raylib.isKeyPressed(KeyCode.KEY_ENTER)) { 38 | currentScreen = GameScreen.GamePlay 39 | } 40 | } 41 | 42 | if (currentScreen == GameScreen.GamePlay) { 43 | if (Raylib.isKeyPressed(KeyCode.KEY_ENTER)) { 44 | currentScreen = GameScreen.Ending 45 | } 46 | } 47 | 48 | Raylib.beginDrawing() 49 | 50 | Raylib.clearBackground(Color.RayWhite) 51 | 52 | if (currentScreen == GameScreen.Logo) { 53 | Raylib.drawText(title, 20, 20, 60, color) 54 | Raylib.drawText("Wait for 3 seconds...", 290, 220, 20, Color.Gray) 55 | } 56 | 57 | if (currentScreen == GameScreen.Title) { 58 | Raylib.drawRectangle(0, 0, screenWidth, screenHeight, Color.Green) 59 | Raylib.drawText("TITLE SCREEN 4", 20, 20, 40, Color.Blue) 60 | Raylib.drawText("PRESS ENTER or TAP to JUMP to GAMEPLAY SCREEN", 120, 220, 20, Color.DarkGreen) 61 | } 62 | 63 | if (currentScreen == GameScreen.GamePlay) { 64 | Raylib.drawText("GAMEPLAY SCREEN", 20, 20, 40, Color.Purple) 65 | Raylib.drawText("PRESS ENTER or TAP to JUMP to ENDING SCREEN", 130, 220, 20, Color.Maroon) 66 | } 67 | 68 | if (currentScreen == GameScreen.Ending) { 69 | Raylib.drawText("GAMEPLAY SCREEN", 20, 20, 40, Color.DarkBlue) 70 | Raylib.drawText("PRESS ENTER or TAP to RETURN to TITLE SCREEN", 130, 220, 20, Color.Maroon) 71 | } 72 | 73 | Raylib.endDrawing() 74 | } 75 | 76 | Raylib.closeWindow() 77 | 78 | 79 | 80 | -------------------------------------------------------------------------------- /examples/playground/samples/camera.wren: -------------------------------------------------------------------------------- 1 | import "raylib" for Color, Raylib, Rectangle, Vector2, Camera2D, KeyCode, Texture2D 2 | import "math" for Math 3 | 4 | var MAX_BUILDINGS = 100 5 | 6 | var screenWidth = 800 7 | var screenHeight = 450 8 | 9 | Raylib.initWindow(screenWidth, screenHeight, "raylib [core] example - 2d camera") 10 | 11 | var player = Rectangle.new(400, 280, 40, 40) 12 | var buildings = [] 13 | var buildColors = [] 14 | 15 | var spacing = 0 16 | 17 | var i = 0 18 | 19 | while (i < MAX_BUILDINGS) { 20 | var height = Raylib.getRandomValue(100, 800) 21 | var random = Raylib.getRandomValue(50, 200) 22 | 23 | var building = Rectangle.new( 24 | -6000 + spacing, 25 | screenHeight - 130.0 - height, 26 | random, 27 | height 28 | ) 29 | 30 | buildings.add(building) 31 | 32 | spacing = spacing + building.width 33 | 34 | buildColors.add(Color.new( 35 | Raylib.getRandomValue(200, 240), 36 | Raylib.getRandomValue(200, 240), 37 | Raylib.getRandomValue(200, 250), 38 | 255 39 | )) 40 | 41 | i = i + 1 42 | } 43 | 44 | var camera = Camera2D.new( 45 | Vector2.new(screenWidth / 2.0, screenHeight / 2.0), 46 | Vector2.new(player.x + 20.0, player.y + 20.0), 47 | 1.0, 48 | 0.0 49 | ) 50 | 51 | Raylib.setTargetFPS(60) // Set our game to run at 60 frames-per-secon 52 | 53 | while (!Raylib.windowShouldClose()) { 54 | 55 | if (Raylib.isKeyDown(KeyCode.KEY_RIGHT)) { 56 | player.x = player.x + 2 57 | } else if (Raylib.isKeyDown(KeyCode.KEY_LEFT)) { player.x = player.x - 2 } 58 | 59 | camera.target = Vector2.new(player.x + 20, player.y + 20) 60 | 61 | if (Raylib.isKeyDown(KeyCode.KEY_A)) { 62 | camera.rotation = camera.rotation - 1 63 | } else if (Raylib.isKeyDown(KeyCode.KEY_S)) { 64 | camera.rotation = camera.rotation + 1 65 | } 66 | 67 | if (camera.rotation > 40) { 68 | camera.rotation = 40 69 | } else if (camera.rotation < -40) { camera.rotation = -40 } 70 | 71 | camera.zoom = Math.exp(Math.log(camera.zoom) + (Raylib.getMouseWheelMove() * 0.1)) 72 | 73 | if (camera.zoom > 3.0) { 74 | camera.zoom = 3.0 75 | } else if (camera.zoom < 0.1) { 76 | camera.zoom = 0.1 77 | } 78 | 79 | if (Raylib.isKeyDown(KeyCode.KEY_R)) { 80 | camera.zoom = 1.0 81 | camera.rotation = 0.0 82 | } 83 | Raylib.beginDrawing() 84 | 85 | Raylib.clearBackground(Color.RayWhite) 86 | 87 | Raylib.beginMode2D(camera) 88 | 89 | Raylib.drawRectangle(-6000, 320, 13000, 8000, Color.DarkGray) 90 | 91 | var i = 0 92 | while (i < MAX_BUILDINGS) { 93 | Raylib.drawRectangleRec(buildings[i], buildColors[i]) 94 | i = i + 1 95 | } 96 | 97 | Raylib.drawRectangleRec(player, Color.Red) 98 | 99 | Raylib.drawLine(camera.target.x, -screenHeight*10, camera.target.x, screenHeight*10, Color.Green) 100 | Raylib.drawLine(-screenWidth*10, camera.target.y, screenWidth*10, camera.target.y, Color.Green) 101 | 102 | Raylib.endMode2D() 103 | 104 | Raylib.drawText("SCREEN AREA", 640, 10, 20, Color.Red) 105 | 106 | Raylib.drawRectangle(0, 0, screenWidth, 5, Color.Red) 107 | Raylib.drawRectangle(0, 5, 5, screenHeight - 10, Color.Red) 108 | Raylib.drawRectangle(screenWidth - 5, 5, 5, screenHeight - 10, Color.Red) 109 | Raylib.drawRectangle(0, screenHeight - 5, screenWidth, 5, Color.Red) 110 | 111 | Raylib.drawRectangle( 10, 10, 250, 113, Color.SkyBlue ) 112 | Raylib.drawRectangleLines( 10, 10, 250, 113, Color.Blue) 113 | 114 | Raylib.drawText("Free 2d camera controls:", 20, 20, 10, Color.Black) 115 | Raylib.drawText("- Right/Left to move Offset", 40, 40, 10, Color.DarkGray) 116 | Raylib.drawText("- Mouse Wheel to Zoom in-out", 40, 60, 10, Color.DarkGray) 117 | Raylib.drawText("- A / S to Rotate", 40, 80, 10, Color.DarkGray) 118 | Raylib.drawText("- R to reset Zoom and Rotation", 40, 100, 10, Color.DarkGray) 119 | 120 | Raylib.endDrawing() 121 | } 122 | 123 | Raylib.closeWindow() 124 | -------------------------------------------------------------------------------- /examples/shapes/main.wren: -------------------------------------------------------------------------------- 1 | import "raylib" for Raylib, Rectangle, Vector2, Color 2 | 3 | var SCREEN_WIDTH = 800 4 | var SCREEN_HEIGHT = 900 5 | 6 | Raylib.initWindow(SCREEN_WIDTH, SCREEN_HEIGHT, "Shapes") 7 | 8 | var rotation = 0.0 9 | 10 | while (!Raylib.windowShouldClose()) { 11 | rotation = rotation + 0.2 12 | 13 | Raylib.beginDrawing() 14 | 15 | Raylib.clearBackground(Color.RayWhite) 16 | 17 | Raylib.drawText("some basic shapes available on raylib", 20, 20, 20, Color.DarkGray) 18 | 19 | Raylib.endDrawing() 20 | } 21 | 22 | /* 23 | 24 | 25 | DrawText("some basic shapes available on raylib", 20, 20, 20, DARKGRAY); 26 | 27 | // Circle shapes and lines 28 | DrawCircle(screenWidth/5, 120, 35, DARKBLUE); 29 | DrawCircleGradient(screenWidth/5, 220, 60, GREEN, SKYBLUE); 30 | DrawCircleLines(screenWidth/5, 340, 80, DARKBLUE); 31 | 32 | // Rectangle shapes and lines 33 | DrawRectangle(screenWidth/4*2 - 60, 100, 120, 60, RED); 34 | DrawRectangleGradientH(screenWidth/4*2 - 90, 170, 180, 130, MAROON, GOLD); 35 | DrawRectangleLines(screenWidth/4*2 - 40, 320, 80, 60, ORANGE); // NOTE: Uses QUADS internally, not lines 36 | 37 | // Triangle shapes and lines 38 | DrawTriangle((Vector2){ screenWidth/4.0f *3.0f, 80.0f }, 39 | (Vector2){ screenWidth/4.0f *3.0f - 60.0f, 150.0f }, 40 | (Vector2){ screenWidth/4.0f *3.0f + 60.0f, 150.0f }, VIOLET); 41 | 42 | DrawTriangleLines((Vector2){ screenWidth/4.0f*3.0f, 160.0f }, 43 | (Vector2){ screenWidth/4.0f*3.0f - 20.0f, 230.0f }, 44 | (Vector2){ screenWidth/4.0f*3.0f + 20.0f, 230.0f }, DARKBLUE); 45 | 46 | // Polygon shapes and lines 47 | DrawPoly((Vector2){ screenWidth/4.0f*3, 330 }, 6, 80, rotation, BROWN); 48 | DrawPolyLines((Vector2){ screenWidth/4.0f*3, 330 }, 6, 90, rotation, BROWN); 49 | DrawPolyLinesEx((Vector2){ screenWidth/4.0f*3, 330 }, 6, 85, rotation, 6, BEIGE); 50 | 51 | // NOTE: We draw all LINES based shapes together to optimize internal drawing, 52 | // this way, all LINES are rendered in a single draw pass 53 | DrawLine(18, 42, screenWidth - 18, 42, BLACK); 54 | */ 55 | -------------------------------------------------------------------------------- /flake.lock: -------------------------------------------------------------------------------- 1 | { 2 | "nodes": { 3 | "flake-compat": { 4 | "flake": false, 5 | "locked": { 6 | "lastModified": 1696426674, 7 | "narHash": "sha256-kvjfFW7WAETZlt09AgDn1MrtKzP7t90Vf7vypd3OL1U=", 8 | "owner": "edolstra", 9 | "repo": "flake-compat", 10 | "rev": "0f9255e01c2351cc7d116c072cb317785dd33b33", 11 | "type": "github" 12 | }, 13 | "original": { 14 | "owner": "edolstra", 15 | "repo": "flake-compat", 16 | "type": "github" 17 | } 18 | }, 19 | "flake-utils": { 20 | "inputs": { 21 | "systems": "systems" 22 | }, 23 | "locked": { 24 | "lastModified": 1731533236, 25 | "narHash": "sha256-l0KFg5HjrsfsO/JpG+r7fRrqm12kzFHyUHqHCVpMMbI=", 26 | "owner": "numtide", 27 | "repo": "flake-utils", 28 | "rev": "11707dc2f618dd54ca8739b309ec4fc024de578b", 29 | "type": "github" 30 | }, 31 | "original": { 32 | "owner": "numtide", 33 | "repo": "flake-utils", 34 | "type": "github" 35 | } 36 | }, 37 | "flake-utils_2": { 38 | "inputs": { 39 | "systems": "systems_2" 40 | }, 41 | "locked": { 42 | "lastModified": 1705309234, 43 | "narHash": "sha256-uNRRNRKmJyCRC/8y1RqBkqWBLM034y4qN7EprSdmgyA=", 44 | "owner": "numtide", 45 | "repo": "flake-utils", 46 | "rev": "1ef2e671c3b0c19053962c07dbda38332dcebf26", 47 | "type": "github" 48 | }, 49 | "original": { 50 | "owner": "numtide", 51 | "repo": "flake-utils", 52 | "type": "github" 53 | } 54 | }, 55 | "nixpkgs": { 56 | "locked": { 57 | "lastModified": 1747327360, 58 | "narHash": "sha256-LSmTbiq/nqZR9B2t4MRnWG7cb0KVNU70dB7RT4+wYK4=", 59 | "owner": "NixOS", 60 | "repo": "nixpkgs", 61 | "rev": "e06158e58f3adee28b139e9c2bcfcc41f8625b46", 62 | "type": "github" 63 | }, 64 | "original": { 65 | "id": "nixpkgs", 66 | "ref": "nixos-unstable", 67 | "type": "indirect" 68 | } 69 | }, 70 | "nixpkgs_2": { 71 | "locked": { 72 | "lastModified": 1708161998, 73 | "narHash": "sha256-6KnemmUorCvlcAvGziFosAVkrlWZGIc6UNT9GUYr0jQ=", 74 | "owner": "NixOS", 75 | "repo": "nixpkgs", 76 | "rev": "84d981bae8b5e783b3b548de505b22880559515f", 77 | "type": "github" 78 | }, 79 | "original": { 80 | "owner": "NixOS", 81 | "ref": "nixos-23.11", 82 | "repo": "nixpkgs", 83 | "type": "github" 84 | } 85 | }, 86 | "root": { 87 | "inputs": { 88 | "flake-utils": "flake-utils", 89 | "nixpkgs": "nixpkgs", 90 | "zig": "zig" 91 | } 92 | }, 93 | "systems": { 94 | "locked": { 95 | "lastModified": 1681028828, 96 | "narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=", 97 | "owner": "nix-systems", 98 | "repo": "default", 99 | "rev": "da67096a3b9bf56a91d16901293e51ba5b49a27e", 100 | "type": "github" 101 | }, 102 | "original": { 103 | "owner": "nix-systems", 104 | "repo": "default", 105 | "type": "github" 106 | } 107 | }, 108 | "systems_2": { 109 | "locked": { 110 | "lastModified": 1681028828, 111 | "narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=", 112 | "owner": "nix-systems", 113 | "repo": "default", 114 | "rev": "da67096a3b9bf56a91d16901293e51ba5b49a27e", 115 | "type": "github" 116 | }, 117 | "original": { 118 | "owner": "nix-systems", 119 | "repo": "default", 120 | "type": "github" 121 | } 122 | }, 123 | "zig": { 124 | "inputs": { 125 | "flake-compat": "flake-compat", 126 | "flake-utils": "flake-utils_2", 127 | "nixpkgs": "nixpkgs_2" 128 | }, 129 | "locked": { 130 | "lastModified": 1747311192, 131 | "narHash": "sha256-93UmnwzJn/pRK6aawww3w6vcahHZQEFVPkBly88ZQPo=", 132 | "owner": "mitchellh", 133 | "repo": "zig-overlay", 134 | "rev": "a61dae82ac04d21a3ff26d29e3dc86d1c3564b67", 135 | "type": "github" 136 | }, 137 | "original": { 138 | "owner": "mitchellh", 139 | "repo": "zig-overlay", 140 | "type": "github" 141 | } 142 | } 143 | }, 144 | "root": "root", 145 | "version": 7 146 | } 147 | -------------------------------------------------------------------------------- /flake.nix: -------------------------------------------------------------------------------- 1 | { 2 | inputs = { 3 | nixpkgs.url = "nixpkgs/nixos-unstable"; 4 | flake-utils.url = "github:numtide/flake-utils"; 5 | zig.url = "github:mitchellh/zig-overlay"; 6 | }; 7 | 8 | outputs = { 9 | nixpkgs, 10 | flake-utils, 11 | zig, 12 | ... 13 | }: 14 | flake-utils.lib.eachDefaultSystem ( 15 | system: let 16 | overlays = [ 17 | (final: prev: { 18 | zigpkgs = zig.packages.${system}; 19 | }) 20 | ]; 21 | pkgs = import nixpkgs { 22 | inherit overlays system; 23 | }; 24 | packages = with pkgs; [ 25 | glfw 26 | libGL 27 | libxkbcommon 28 | pkg-config 29 | xorg.libxcb 30 | xorg.libXft 31 | xorg.libX11 32 | xorg.libX11.dev 33 | xorg.libXrandr 34 | xorg.libXinerama 35 | xorg.libXcursor 36 | xorg.libXi 37 | glfw-wayland 38 | zigpkgs."0.14.0" 39 | emscripten 40 | ]; 41 | in { 42 | devShell = pkgs.mkShell { 43 | buildInputs = packages; 44 | nativeBuildInputs = with pkgs; [cmake pkg-config ncurses fontconfig freetype]; 45 | shellHook = '' 46 | export SHELL=/usr/bin/bash 47 | if [ ! -d $(pwd)/.emscripten_cache-${pkgs.emscripten.version} ]; then 48 | cp -R ${pkgs.emscripten}/share/emscripten/cache/ $(pwd)/.emscripten_cache-${pkgs.emscripten.version} 49 | chmod u+rwX -R $(pwd)/.emscripten_cache-${pkgs.emscripten.version} 50 | fi 51 | export EM_CACHE=$(pwd)/.emscripten_cache-${pkgs.emscripten.version} 52 | echo emscripten cache dir: $EM_CACHE 53 | ''; 54 | }; 55 | } 56 | ); 57 | } 58 | 59 | -------------------------------------------------------------------------------- /index.wren: -------------------------------------------------------------------------------- 1 | System.print("Hello World") 2 | System.print("Hi") 3 | System.print("Hi 2") 4 | 5 | var i = 0 6 | 7 | while (i < 10) { 8 | System.print(i) 9 | i = i + 1 10 | } 11 | -------------------------------------------------------------------------------- /src/bindings/builtin.wren: -------------------------------------------------------------------------------- 1 | 2 | class Build { 3 | foreign static shouldStop() 4 | } 5 | -------------------------------------------------------------------------------- /src/bindings/builtin.zig: -------------------------------------------------------------------------------- 1 | const std = @import("std"); 2 | const wren = @cImport({ 3 | @cInclude("wren.h"); 4 | @cInclude("stdio.h"); 5 | }); 6 | const VmContext = @import("../watcher/hot.zig").VmContext; 7 | 8 | pub const BuiltinBindings = @This(); 9 | 10 | pub fn shouldStop(vm: ?*wren.WrenVM) callconv(.C) void { 11 | const context: *VmContext = @ptrCast(@alignCast(wren.wrenGetUserData(vm))); 12 | const should_stop = context.stop_signal.load(.acquire); 13 | wren.wrenSetSlotBool(vm, 0, should_stop); 14 | } 15 | -------------------------------------------------------------------------------- /src/bindings/embed.wren: -------------------------------------------------------------------------------- 1 | 2 | class Load { 3 | foreign static foreignFunction(name, signature, symbolName) 4 | } 5 | -------------------------------------------------------------------------------- /src/bindings/generate/README.md: -------------------------------------------------------------------------------- 1 | Raylib Bindings Generator 2 | 3 | When i started the project i was manually porting each Raylib methods but it started to get repetitive and hectic. generate.zig simplifies the process of this creation although it was written poorly as i couldnt get the time to write a simple c-parser. 4 | -------------------------------------------------------------------------------- /src/bindings/math.wren: -------------------------------------------------------------------------------- 1 | 2 | class Math { 3 | foreign static min(a, b) 4 | foreign static max(a, b) 5 | foreign static sin(a) 6 | foreign static cos(a) 7 | foreign static pow(a, b) 8 | foreign static exp(a) 9 | foreign static exp2(a) 10 | foreign static log(a) 11 | foreign static log10(a) 12 | foreign static log2(a) 13 | foreign static sqrt(a) 14 | foreign static floor(a) 15 | foreign static ceil(a) 16 | foreign static round(a) 17 | foreign static tan(a) 18 | } 19 | -------------------------------------------------------------------------------- /src/bindings/math.zig: -------------------------------------------------------------------------------- 1 | const std = @import("std"); 2 | const wren = @cImport({ 3 | @cInclude("wren.h"); 4 | @cInclude("stdio.h"); 5 | }); 6 | 7 | pub const MathBindings = @This(); 8 | 9 | pub fn min(vm: ?*wren.WrenVM) callconv(.C) void { 10 | const a = wren.wrenGetSlotDouble(vm, 1); 11 | const b = wren.wrenGetSlotDouble(vm, 2); 12 | wren.wrenSetSlotDouble(vm, 0, @as(f32, @floatCast(@min(a, b)))); 13 | } 14 | 15 | pub fn max(vm: ?*wren.WrenVM) callconv(.C) void { 16 | const a = wren.wrenGetSlotDouble(vm, 1); 17 | const b = wren.wrenGetSlotDouble(vm, 2); 18 | wren.wrenSetSlotDouble(vm, 0, @as(f32, @floatCast(@max(a, b)))); 19 | } 20 | 21 | pub fn sin(vm: ?*wren.WrenVM) callconv(.C) void { 22 | const a = wren.wrenGetSlotDouble(vm, 1); 23 | wren.wrenSetSlotDouble(vm, 0, @as(f32, @floatCast(@sin(a)))); 24 | } 25 | 26 | pub fn cos(vm: ?*wren.WrenVM) callconv(.C) void { 27 | const a = wren.wrenGetSlotDouble(vm, 1); 28 | wren.wrenSetSlotDouble(vm, 0, @as(f32, @floatCast(@cos(a)))); 29 | } 30 | 31 | pub fn pow(vm: ?*wren.WrenVM) callconv(.C) void { 32 | const a = wren.wrenGetSlotDouble(vm, 1); 33 | const b = wren.wrenGetSlotDouble(vm, 2); 34 | wren.wrenSetSlotDouble(vm, 0, @as(f32, @floatCast(std.math.pow(f64, a, b)))); 35 | } 36 | 37 | pub fn log(vm: ?*wren.WrenVM) callconv(.C) void { 38 | const a = wren.wrenGetSlotDouble(vm, 1); 39 | wren.wrenSetSlotDouble(vm, 0, @as(f32, @floatCast(@log(a)))); 40 | } 41 | 42 | pub fn log2(vm: ?*wren.WrenVM) callconv(.C) void { 43 | const a = wren.wrenGetSlotDouble(vm, 1); 44 | wren.wrenSetSlotDouble(vm, 0, @as(f32, @floatCast(@log2(a)))); 45 | } 46 | 47 | pub fn log10(vm: ?*wren.WrenVM) callconv(.C) void { 48 | const a = wren.wrenGetSlotDouble(vm, 1); 49 | wren.wrenSetSlotDouble(vm, 0, @as(f32, @floatCast(@log10(a)))); 50 | } 51 | 52 | pub fn abs(vm: ?*wren.WrenVM) callconv(.C) void { 53 | const a = wren.wrenGetSlotDouble(vm, 1); 54 | wren.wrenSetSlotDouble(vm, 0, @as(f32, @floatCast(@abs(a)))); 55 | } 56 | 57 | pub fn exp(vm: ?*wren.WrenVM) callconv(.C) void { 58 | const a = wren.wrenGetSlotDouble(vm, 1); 59 | wren.wrenSetSlotDouble(vm, 0, @as(f32, @floatCast(@exp(a)))); 60 | } 61 | 62 | pub fn exp2(vm: ?*wren.WrenVM) callconv(.C) void { 63 | const a = wren.wrenGetSlotDouble(vm, 1); 64 | wren.wrenSetSlotDouble(vm, 0, @as(f32, @floatCast(@exp2(a)))); 65 | } 66 | 67 | pub fn tan(vm: ?*wren.WrenVM) callconv(.C) void { 68 | const a = wren.wrenGetSlotDouble(vm, 1); 69 | wren.wrenSetSlotDouble(vm, 0, @as(f32, @floatCast(@tan(a)))); 70 | } 71 | 72 | pub fn sqrt(vm: ?*wren.WrenVM) callconv(.C) void { 73 | const a = wren.wrenGetSlotDouble(vm, 1); 74 | wren.wrenSetSlotDouble(vm, 0, @as(f32, @floatCast(@sqrt(a)))); 75 | } 76 | 77 | pub fn floor(vm: ?*wren.WrenVM) callconv(.C) void { 78 | const a = wren.wrenGetSlotDouble(vm, 1); 79 | wren.wrenSetSlotDouble(vm, 0, @as(f32, @floatCast(@floor(a)))); 80 | } 81 | 82 | pub fn ceil(vm: ?*wren.WrenVM) callconv(.C) void { 83 | const a = wren.wrenGetSlotDouble(vm, 1); 84 | wren.wrenSetSlotDouble(vm, 0, @as(f32, @floatCast(@ceil(a)))); 85 | } 86 | 87 | pub fn trunc(vm: ?*wren.WrenVM) callconv(.C) void { 88 | const a = wren.wrenGetSlotDouble(vm, 1); 89 | wren.wrenSetSlotDouble(vm, 0, @as(f32, @floatCast(@trunc(a)))); 90 | } 91 | 92 | pub fn round(vm: ?*wren.WrenVM) callconv(.C) void { 93 | const a = wren.wrenGetSlotDouble(vm, 1); 94 | wren.wrenSetSlotDouble(vm, 0, @as(f32, @floatCast(@round(a)))); 95 | } 96 | -------------------------------------------------------------------------------- /src/cli.zig: -------------------------------------------------------------------------------- 1 | const std = @import("std"); 2 | 3 | pub fn initWasm(gpa: std.mem.Allocator, path: []const u8) !void { 4 | const dockerFile = @embedFile("templates/wasm/Dockerfile"); 5 | const dockerComposeFile = @embedFile("templates/wasm/docker-compose.yml"); 6 | const shellHtmlFile = @embedFile("templates/wasm/shell.html"); 7 | 8 | const cwd = try std.fs.cwd().openDir(".", .{}); 9 | const dir_name = try cwd.realpathAlloc(gpa, "."); 10 | defer gpa.free(dir_name); 11 | const project_name = std.fs.path.basename(dir_name); 12 | 13 | const dockerfile_content = try gpa.dupe(u8, dockerFile); 14 | defer gpa.free(dockerfile_content); 15 | 16 | const dockerfile_replaced = try std.mem.replaceOwned( 17 | u8, 18 | gpa, 19 | dockerfile_content, 20 | "{{{ PROJECT_NAME }}}", 21 | project_name, 22 | ); 23 | defer gpa.free(dockerfile_replaced); 24 | 25 | const dockerfile_final = try std.mem.replaceOwned( 26 | u8, 27 | gpa, 28 | dockerfile_replaced, 29 | "{{{ MAIN_FILE }}}", 30 | path, 31 | ); 32 | defer gpa.free(dockerfile_final); 33 | 34 | const compose_content = try gpa.dupe(u8, dockerComposeFile); 35 | defer gpa.free(compose_content); 36 | 37 | const compose_replaced = try std.mem.replaceOwned( 38 | u8, 39 | gpa, 40 | compose_content, 41 | "{{{ PROJECT_NAME }}}", 42 | project_name, 43 | ); 44 | defer gpa.free(compose_replaced); 45 | 46 | var dockerfile_out = try cwd.createFile("Dockerfile", .{ .read = false }); 47 | defer dockerfile_out.close(); 48 | try dockerfile_out.writeAll(dockerfile_final); 49 | 50 | var compose_out = try cwd.createFile("docker-compose.yml", .{ .read = false }); 51 | defer compose_out.close(); 52 | try compose_out.writeAll(compose_replaced); 53 | 54 | var shell_file_out = try cwd.createFile("shell.html", .{ .read = false }); 55 | defer shell_file_out.close(); 56 | try shell_file_out.writeAll(shellHtmlFile); 57 | } 58 | 59 | pub fn initExe(gpa: std.mem.Allocator, path: []const u8) !void { 60 | const dockerFile = @embedFile("templates/exe/Dockerfile"); 61 | const dockerComposeFile = @embedFile("templates/exe/docker-compose.yml"); 62 | 63 | const cwd = try std.fs.cwd().openDir(".", .{}); 64 | const dir_name = try cwd.realpathAlloc(gpa, "."); 65 | defer gpa.free(dir_name); 66 | const project_name = std.fs.path.basename(dir_name); 67 | 68 | const dockerfile_content = try gpa.dupe(u8, dockerFile); 69 | defer gpa.free(dockerfile_content); 70 | 71 | const dockerfile_replaced = try std.mem.replaceOwned( 72 | u8, 73 | gpa, 74 | dockerfile_content, 75 | "{{{ PROJECT_NAME }}}", 76 | project_name, 77 | ); 78 | defer gpa.free(dockerfile_replaced); 79 | 80 | const dockerfile_final = try std.mem.replaceOwned( 81 | u8, 82 | gpa, 83 | dockerfile_replaced, 84 | "{{{ MAIN_FILE }}}", 85 | path, 86 | ); 87 | defer gpa.free(dockerfile_final); 88 | 89 | const compose_content = try gpa.dupe(u8, dockerComposeFile); 90 | defer gpa.free(compose_content); 91 | 92 | const compose_replaced = try std.mem.replaceOwned( 93 | u8, 94 | gpa, 95 | compose_content, 96 | "{{{ PROJECT_NAME }}}", 97 | project_name, 98 | ); 99 | defer gpa.free(compose_replaced); 100 | 101 | var dockerfile_out = try cwd.createFile("Dockerfile", .{ .read = false }); 102 | defer dockerfile_out.close(); 103 | try dockerfile_out.writeAll(dockerfile_final); 104 | 105 | var compose_out = try cwd.createFile("docker-compose.yml", .{ .read = false }); 106 | defer compose_out.close(); 107 | try compose_out.writeAll(compose_replaced); 108 | } 109 | -------------------------------------------------------------------------------- /src/common.zig: -------------------------------------------------------------------------------- 1 | const std = @import("std"); 2 | const builtin = @import("builtin"); 3 | const wren = @cImport({ 4 | @cInclude("wren.h"); 5 | @cInclude("stdio.h"); 6 | }); 7 | const c = if (builtin.os.tag == .windows) @cImport({ 8 | @cInclude("windows.h"); 9 | }) else @cImport({ 10 | @cInclude("dlfcn.h"); 11 | }); 12 | 13 | pub const r = @cImport({ 14 | @cInclude("raylib.h"); 15 | @cInclude("raymath.h"); 16 | @cInclude("rlgl.h"); 17 | }); 18 | 19 | const Bindings = @import("bindings/index.zig"); 20 | 21 | const raylib = @embedFile("bindings/raylib.wren"); 22 | const builtinWren = @embedFile("bindings/builtin.wren"); 23 | const math = @embedFile("bindings/math.wren"); 24 | const embed = @embedFile("bindings/embed.wren"); 25 | 26 | //var debug_allocator: std.heap.DebugAllocator(.{}) = .init; 27 | //pub const allocator = debug_allocator.allocator(); 28 | 29 | var debug_allocator: std.heap.DebugAllocator(.{}) = .init; 30 | //pub const allocator = debug_allocator.allocator(); 31 | pub const allocator = if (builtin.target.cpu.arch.isWasm()) std.heap.wasm_allocator else debug_allocator.allocator(); 32 | 33 | pub const WrenForeignMethodFn = fn (?*wren.WrenVM) callconv(.c) void; 34 | //var map = std.StringHashMap(*const fn (?*wren.WrenVM) callconv(.c) void).init( 35 | //allocator, 36 | //); 37 | 38 | const ForeignFunctionMap = struct { 39 | name: []const u8, 40 | bind_name: []const u8, 41 | }; 42 | var map = std.StringHashMap(ForeignFunctionMap).init( 43 | allocator, 44 | ); 45 | 46 | const Handle = if (builtin.os.tag == .windows) c.HMODULE else ?*anyopaque; 47 | var main_program_handle: Handle = null; 48 | 49 | pub fn initDynamicSymbolHandling() void { 50 | if (builtin.os.tag == .windows) { 51 | // On Windows, get a handle to the .exe instance. 52 | // The 'A' stands for ANSI (char*), which matches our []const u8 strings. 53 | main_program_handle = c.GetModuleHandleA(null); 54 | } else { 55 | // On POSIX, a null path gives a handle to the global symbol namespace. 56 | main_program_handle = c.dlopen(null, c.RTLD_LAZY); 57 | } 58 | 59 | if (main_program_handle == null) { 60 | std.log.err("DynamicSymbols: Could not get a handle to the main program.", .{}); 61 | } 62 | } 63 | 64 | pub fn bindForiegnClass(vm: ?*wren.WrenVM, module: [*c]const u8, className: [*c]const u8) callconv(.c) wren.WrenForeignClassMethods { 65 | _ = .{ vm, module, className }; 66 | var methods: wren.WrenForeignClassMethods = .{ 67 | .allocate = null, 68 | .finalize = null, 69 | }; 70 | 71 | const mod = std.mem.span(module); 72 | const class = std.mem.span(className); 73 | 74 | const name = std.mem.concat(allocator, u8, &[_][]const u8{ 75 | mod, ".", class, 76 | }) catch unreachable; 77 | 78 | const foreign_class_binding = Bindings.ForeignClassBindings.get(name) orelse null; 79 | 80 | if (foreign_class_binding) |binding| { 81 | methods.allocate = binding.allocate; 82 | methods.finalize = finalize_binding_foreign_classes; 83 | } 84 | 85 | return methods; 86 | } 87 | 88 | pub fn bindForeignMethod(vm: ?*wren.WrenVM, module: [*c]const u8, className: [*c]const u8, isStatic: bool, signature: [*c]const u8) callconv(.c) ?*const Bindings.WrenForeignMethodFn { 89 | _ = .{ vm, module, className, isStatic, signature }; 90 | 91 | const mod = std.mem.span(module); 92 | const class = std.mem.span(className); 93 | const sign = std.mem.span(signature); 94 | 95 | const name = std.mem.concat(allocator, u8, &[_][]const u8{ 96 | mod, ".", class, ".", sign, 97 | }) catch unreachable; 98 | 99 | defer allocator.free(name); 100 | 101 | const binding_method = Bindings.ForeignMethodBindings.get(name) orelse null; 102 | 103 | if (binding_method) |method| { 104 | return method; 105 | } 106 | 107 | const foreignFunctionName = std.mem.concat(allocator, u8, &[_][]const u8{ 108 | class, ".", sign, 109 | }) catch unreachable; 110 | 111 | defer allocator.free(foreignFunctionName); 112 | 113 | // Lets check if it is loaded dynamically 114 | const load_method = map.get(foreignFunctionName) orelse null; 115 | 116 | if (load_method) |method| { 117 | if (builtin.os.tag == .windows) { 118 | const libname = method.name; 119 | 120 | const handle = c.LoadLibraryA(libname.ptr); 121 | if (handle == null) { 122 | std.debug.print("Failed to load DLL: {s}\n", .{libname}); 123 | return null; 124 | } 125 | 126 | const symbol_name = method.bind_name; 127 | const add_fn_ptr = c.GetProcAddress(handle, symbol_name.ptr); 128 | 129 | if (add_fn_ptr == null) { 130 | std.debug.print("Failed to find symbol: {s}\n", .{symbol_name}); 131 | return null; 132 | } 133 | 134 | return @ptrFromInt(@intFromPtr(add_fn_ptr)); 135 | 136 | //std.debug.print("Load method: {s} \n", .{method}); 137 | //const proc = c.GetProcAddress(main_program_handle, method.ptr); 138 | //std.debug.print("Proc is {any}", .{proc}); 139 | //return @ptrFromInt(@intFromPtr(proc)); 140 | } else { 141 | const proc = c.dlsym(main_program_handle, @ptrCast(method.bind_name)); 142 | std.debug.print("Proc is {any}\n", .{proc}); 143 | return @ptrFromInt(@intFromPtr(c.dlsym(main_program_handle, @ptrCast(method.bind_name)))); 144 | } 145 | 146 | //return method; 147 | } 148 | 149 | std.debug.print("Module {s}\n", .{mod}); 150 | std.log.err("Could not find binding for .{s}", .{name}); 151 | 152 | return null; 153 | } 154 | 155 | pub fn writeFn(vm: ?*wren.WrenVM, text: [*c]const u8) callconv(.C) void { 156 | _ = vm; 157 | std.debug.print("{s}", .{text}); 158 | } 159 | 160 | pub fn errorFn( 161 | vm: ?*wren.WrenVM, 162 | errorType: wren.WrenErrorType, 163 | module: [*c]const u8, 164 | line: c_int, 165 | msg: [*c]const u8, 166 | ) callconv(.C) void { 167 | _ = vm; 168 | switch (errorType) { 169 | wren.WREN_ERROR_COMPILE => { 170 | std.debug.print("[{s} line {d}] [Error] {s}\n", .{ module, line, msg }); 171 | }, 172 | wren.WREN_ERROR_STACK_TRACE => { 173 | std.debug.print("[{s} line {d}] in {s}\n", .{ module, line, msg }); 174 | }, 175 | wren.WREN_ERROR_RUNTIME => { 176 | std.debug.print("[Runtime Error] {s}\n", .{msg}); 177 | }, 178 | else => {}, 179 | } 180 | } 181 | 182 | pub fn loadModuleComplete(vm: ?*wren.WrenVM, name: [*c]const u8, result: wren.WrenLoadModuleResult) callconv(.c) void { 183 | _ = .{ vm, name }; 184 | if (result.source == null) return; 185 | } 186 | 187 | pub fn loadBuiltinModule(name: [*c]const u8) callconv(.c) wren.WrenLoadModuleResult { 188 | if (std.mem.eql(u8, "raylib", std.mem.span(name))) { 189 | return .{ 190 | .source = raylib, 191 | .onComplete = loadModuleComplete, 192 | }; 193 | } 194 | 195 | if (std.mem.eql(u8, "builtin", std.mem.span(name))) { 196 | return .{ 197 | .source = builtinWren, 198 | .onComplete = loadModuleComplete, 199 | }; 200 | } 201 | 202 | if (std.mem.eql(u8, "math", std.mem.span(name))) { 203 | return .{ 204 | .source = math, 205 | .onComplete = loadModuleComplete, 206 | }; 207 | } 208 | 209 | if (std.mem.eql(u8, "embed", std.mem.span(name))) { 210 | return .{ 211 | .source = embed, 212 | .onComplete = loadModuleComplete, 213 | }; 214 | } 215 | 216 | return .{ 217 | .source = null, 218 | .onComplete = null, 219 | }; 220 | } 221 | 222 | pub fn loadModule(vm: ?*wren.WrenVM, name: [*c]const u8) callconv(.c) wren.WrenLoadModuleResult { 223 | _ = .{ vm, name }; 224 | const mod = std.mem.span(name); 225 | 226 | if (!std.fs.path.isAbsolute(mod) and !isRelative(mod)) { 227 | return loadBuiltinModule(name); 228 | } 229 | 230 | const path = std.mem.concat(allocator, u8, &[_][]const u8{ std.mem.span(name), ".wren" }) catch return .{ 231 | .source = null, 232 | .onComplete = null, 233 | }; 234 | 235 | defer allocator.free(path); 236 | 237 | // TODO: There is a lot of noise here 238 | var file: std.fs.File = undefined; 239 | if (isRelative(mod)) { 240 | file = std.fs.cwd().openFile(path, .{}) catch |err| { 241 | std.log.err("Failed to open file: {s} {s}\n", .{ @errorName(err), path }); 242 | return .{ 243 | .source = null, 244 | .onComplete = null, 245 | }; 246 | }; 247 | } else { 248 | file = std.fs.openFileAbsolute(path, .{ .mode = .read_write }) catch |err| { 249 | std.log.err("Failed to open file: {s} {s}\n", .{ path, @errorName(err) }); 250 | return .{ 251 | .source = null, 252 | .onComplete = null, 253 | }; 254 | }; 255 | } 256 | 257 | defer file.close(); 258 | 259 | var buf_reader = std.io.bufferedReader(file.reader()); 260 | var in_stream = buf_reader.reader(); 261 | var script: [8192]u8 = undefined; 262 | 263 | const read = in_stream.readAll(&script) catch { 264 | return .{ 265 | .source = null, 266 | .onComplete = null, 267 | }; 268 | }; 269 | script[read] = 0; 270 | 271 | const source = allocator.dupeZ(u8, &script) catch return .{ 272 | .source = null, 273 | .onComplete = null, 274 | }; 275 | 276 | return .{ 277 | .source = source, 278 | .onComplete = loadModuleComplete, 279 | }; 280 | } 281 | 282 | pub fn isRelative(path: []const u8) bool { 283 | if (path.len < 2) { 284 | return false; 285 | } 286 | 287 | if (path[0] == '.' and std.fs.path.isSep(path[1])) { 288 | return true; 289 | } 290 | 291 | if (path[0] == '.' and path[1] == '.' and std.fs.path.isSep(path[1])) { 292 | return true; 293 | } 294 | 295 | return false; 296 | } 297 | 298 | pub fn resolveModule(vm: ?*wren.WrenVM, importer: [*c]const u8, module: [*c]const u8) callconv(.C) ?[*:0]const u8 { 299 | _ = .{ importer, module, vm }; 300 | const imp = std.mem.span(importer); 301 | const mod = std.mem.span(module); 302 | 303 | if (!std.fs.path.isAbsolute(mod) and !isRelative(mod)) { 304 | return module; 305 | } 306 | 307 | // TODO: There is a lot of noise here 308 | if (isRelative(mod)) { 309 | const resolved = 310 | std.fs.path.resolvePosix(allocator, &[_][]const u8{ 311 | std.fs.path.dirname(imp).?, 312 | mod, 313 | }) catch return null; 314 | 315 | const final = if (std.mem.startsWith(u8, resolved, "." ++ std.fs.path.sep_str)) resolved else std.mem.concat(allocator, u8, &[_][]const u8{ ".", "/", resolved }) catch |err| { 316 | std.log.err("Failed to open file: {s} \n", .{@errorName(err)}); 317 | return null; 318 | }; 319 | 320 | std.debug.print("Importer {s}\n", .{final}); 321 | const path = allocator.dupeZ(u8, resolved) catch |err| { 322 | std.log.err("Failed to open file: {s}\n", .{@errorName(err)}); 323 | return null; 324 | }; 325 | _ = path; 326 | return module; 327 | } 328 | return module; 329 | } 330 | 331 | pub fn finalize_binding_foreign_classes(data: ?*anyopaque) callconv(.c) void { 332 | // TODO: we are using the same finlizer for everyone, this will be changed if i ever get any usecase 333 | // to use finalizer for 334 | _ = .{data}; 335 | } 336 | 337 | pub fn wren_load_foreign_function(vm: ?*wren.WrenVM) callconv(.C) void { 338 | _ = .{vm}; 339 | 340 | const @"1" = std.mem.span(wren.wrenGetSlotString(vm, 1)); // Shared Object File 341 | const @"2" = std.mem.span(wren.wrenGetSlotString(vm, 2)); // C function names 342 | const @"3" = std.mem.span(wren.wrenGetSlotString(vm, 3)); // C function names 343 | 344 | map.put(@"1", .{ 345 | .name = @"2", 346 | .bind_name = @"3", 347 | }) catch unreachable; 348 | } 349 | -------------------------------------------------------------------------------- /src/main.c: -------------------------------------------------------------------------------- 1 | #include 2 | #include "wren.h" 3 | 4 | static void writeFn(WrenVM* vm, const char* text) 5 | { 6 | printf("%s", text); 7 | } 8 | 9 | void errorFn(WrenVM* vm, WrenErrorType errorType, 10 | const char* module, const int line, 11 | const char* msg) 12 | { 13 | switch (errorType) 14 | { 15 | case WREN_ERROR_COMPILE: 16 | { 17 | printf("[%s line %d] [Error] %s\n", module, line, msg); 18 | } break; 19 | case WREN_ERROR_STACK_TRACE: 20 | { 21 | printf("[%s line %d] in %s\n", module, line, msg); 22 | } break; 23 | case WREN_ERROR_RUNTIME: 24 | { 25 | printf("[Runtime Error] %s\n", msg); 26 | } break; 27 | } 28 | } 29 | 30 | int main() 31 | { 32 | 33 | WrenConfiguration config; 34 | wrenInitConfiguration(&config); 35 | config.writeFn = &writeFn; 36 | config.errorFn = &errorFn; 37 | WrenVM* vm = wrenNewVM(&config); 38 | 39 | const char* module = "main"; 40 | const char* script = "import \"random\" for Random\n" 41 | "var random = Random.new(12345)\n" 42 | "System.print(random.float())"; 43 | 44 | WrenInterpretResult result = wrenInterpret(vm, module, script); 45 | 46 | switch (result) { 47 | case WREN_RESULT_COMPILE_ERROR: 48 | { printf("Compile Error!\n"); } break; 49 | case WREN_RESULT_RUNTIME_ERROR: 50 | { printf("Runtime Error!\n"); } break; 51 | case WREN_RESULT_SUCCESS: 52 | { printf("Success!\n"); } break; 53 | } 54 | 55 | wrenFreeVM(vm); 56 | 57 | } 58 | -------------------------------------------------------------------------------- /src/main.zig: -------------------------------------------------------------------------------- 1 | const std = @import("std"); 2 | const builtin = @import("builtin"); 3 | const common = @import("common.zig"); 4 | const hot = @import("watcher/hot.zig"); 5 | const cli = @import("cli.zig"); 6 | const wren = @cImport({ 7 | @cInclude("wren.h"); 8 | @cInclude("stdio.h"); 9 | }); 10 | 11 | pub const r = @cImport({ 12 | @cInclude("raylib.h"); 13 | @cInclude("raymath.h"); 14 | @cInclude("rlgl.h"); 15 | }); 16 | 17 | const Bindings = @import("bindings/index.zig"); 18 | 19 | var produce_executable = true; 20 | 21 | var debug_allocator: std.heap.DebugAllocator(.{}) = .init; 22 | pub const allocator = if (builtin.target.cpu.arch.isWasm()) std.heap.wasm_allocator else debug_allocator.allocator(); 23 | 24 | const Command = enum { 25 | @"--hot", 26 | @"init-exe", 27 | @"init-wasm", 28 | }; 29 | 30 | pub fn main() !void { 31 | const args = std.process.argsAlloc(allocator) catch unreachable; 32 | defer std.process.argsFree(allocator, args); 33 | 34 | if (args.len < 2) { 35 | @panic("We require a wren script to run"); 36 | } else if (args.len == 2) { 37 | try runProgram(args[1]); 38 | return; 39 | } 40 | 41 | const command = std.meta.stringToEnum(Command, args[1]) orelse { 42 | std.log.err("Invalid command: {s}\n", .{args[1]}); 43 | return; 44 | }; 45 | 46 | switch (command) { 47 | .@"--hot" => { 48 | try hot.hot(allocator, args[2]); 49 | }, 50 | .@"init-exe" => { 51 | try cli.initExe(allocator, args[2]); 52 | }, 53 | 54 | .@"init-wasm" => { 55 | try cli.initWasm(allocator, args[2]); 56 | }, 57 | } 58 | } 59 | 60 | pub fn runProgram(path: []const u8) !void { 61 | const module: [*:0]const u8 = @ptrCast(path); 62 | 63 | //var buf: [std.fs.max_path_bytes]u8 = undefined; 64 | //const rel_path = try std.fs.cwd().realpath(".", &buf); 65 | 66 | //const child_rel_path = try std.fs.path.join(allocator, &.{rel_path}); 67 | //std.debug.print("Absolute path: {s}\n", .{child_rel_path}); 68 | 69 | const file = std.fs.cwd().openFile(path, .{}) catch |err| { 70 | std.log.err("Failed to open file: {s} {s}\n", .{ @errorName(err), path }); 71 | return; 72 | }; 73 | defer file.close(); 74 | 75 | var buf_reader = std.io.bufferedReader(file.reader()); 76 | var in_stream = buf_reader.reader(); 77 | var script: [std.fs.max_path_bytes]u8 = undefined; 78 | 79 | const read = try in_stream.readAll(&script); 80 | script[read] = 0; 81 | 82 | var config: wren.WrenConfiguration = undefined; 83 | wren.wrenInitConfiguration(&config); 84 | config.writeFn = common.writeFn; 85 | config.errorFn = common.errorFn; 86 | 87 | config.bindForeignClassFn = common.bindForiegnClass; 88 | config.bindForeignMethodFn = common.bindForeignMethod; 89 | 90 | config.resolveModuleFn = if (produce_executable) @import("./resolve_module.zig").resolveModule else common.resolveModule; 91 | config.loadModuleFn = common.loadModule; 92 | 93 | const vm = wren.wrenNewVM(&config); 94 | defer wren.wrenFreeVM(vm); 95 | 96 | const source_code: [*c]const u8 = @ptrCast(&script); 97 | 98 | const result = wren.wrenInterpret(vm, module, source_code); 99 | 100 | switch (result) { 101 | wren.WREN_RESULT_COMPILE_ERROR => { 102 | std.debug.print("Compile Error!\n", .{}); 103 | }, 104 | wren.WREN_RESULT_RUNTIME_ERROR => { 105 | std.debug.print("Runtime Error!\n", .{}); 106 | }, 107 | wren.WREN_RESULT_SUCCESS => { 108 | std.debug.print("Success!\n", .{}); 109 | }, 110 | else => {}, 111 | } 112 | } 113 | 114 | pub fn runProgramHot(path: []const u8, stop_signal: *std.atomic.Value(bool)) !void { 115 | if (stop_signal.load(.acquire)) { 116 | std.log.info("Stop signal received, exiting program.", .{}); 117 | return; // Gracefully exit the function, and thus the thread. 118 | } 119 | 120 | const module: [*:0]const u8 = @ptrCast(path); 121 | 122 | //var buf: [std.fs.max_path_bytes]u8 = undefined; 123 | //const rel_path = try std.fs.cwd().realpath(".", &buf); 124 | 125 | //const child_rel_path = try std.fs.path.join(allocator, &.{rel_path}); 126 | //std.debug.print("Absolute path: {s}\n", .{child_rel_path}); 127 | 128 | const file = std.fs.cwd().openFile(path, .{}) catch |err| { 129 | std.log.err("Failed to open file: {s} {s}\n", .{ @errorName(err), path }); 130 | return; 131 | }; 132 | defer file.close(); 133 | 134 | var buf_reader = std.io.bufferedReader(file.reader()); 135 | var in_stream = buf_reader.reader(); 136 | var script: [std.fs.max_path_bytes]u8 = undefined; 137 | 138 | const read = try in_stream.readAll(&script); 139 | script[read] = 0; 140 | 141 | var config: wren.WrenConfiguration = undefined; 142 | wren.wrenInitConfiguration(&config); 143 | config.writeFn = common.writeFn; 144 | config.errorFn = common.errorFn; 145 | 146 | config.bindForeignClassFn = common.bindForiegnClass; 147 | config.bindForeignMethodFn = common.bindForeignMethod; 148 | 149 | var context = hot.VmContext{ 150 | .stop_signal = stop_signal, 151 | }; 152 | 153 | config.resolveModuleFn = if (produce_executable) @import("./resolve_module.zig").resolveModule else common.resolveModule; 154 | config.loadModuleFn = common.loadModule; 155 | config.userData = @as(*anyopaque, &context); 156 | 157 | const vm = wren.wrenNewVM(&config); 158 | defer wren.wrenFreeVM(vm); 159 | 160 | const source_code: [*c]const u8 = @ptrCast(&script); 161 | 162 | const result = wren.wrenInterpret(vm, module, source_code); 163 | 164 | switch (result) { 165 | wren.WREN_RESULT_COMPILE_ERROR => { 166 | std.debug.print("Compile Error!\n", .{}); 167 | }, 168 | wren.WREN_RESULT_RUNTIME_ERROR => { 169 | std.debug.print("Runtime Error!\n", .{}); 170 | }, 171 | wren.WREN_RESULT_SUCCESS => { 172 | //std.debug.print("Success!\n", .{}); 173 | }, 174 | else => {}, 175 | } 176 | } 177 | -------------------------------------------------------------------------------- /src/resolve_module.zig: -------------------------------------------------------------------------------- 1 | const wren = @cImport({ 2 | @cInclude("wren.h"); 3 | @cInclude("stdio.h"); 4 | }); 5 | const std = @import("std"); 6 | const allocator = @import("main.zig").allocator; 7 | 8 | fn isRelative(path: []const u8) bool { 9 | if (path.len < 2) { 10 | return false; 11 | } 12 | 13 | if (path[0] == '.' and std.fs.path.isSep(path[1])) { 14 | return true; 15 | } 16 | 17 | if (path[0] == '.' and path[1] == '.' and std.fs.path.isSep(path[1])) { 18 | return true; 19 | } 20 | 21 | return false; 22 | } 23 | 24 | pub fn resolveModule(vm: ?*wren.WrenVM, importer: [*c]const u8, module: [*c]const u8) callconv(.C) ?[*:0]const u8 { 25 | _ = .{ importer, module, vm }; 26 | const imp = std.mem.span(importer); 27 | const mod = std.mem.span(module); 28 | //std.debug.print("Importer: {s}\n", .{mod}); 29 | 30 | if (!std.fs.path.isAbsolute(mod) and !isRelative(mod)) { 31 | return module; 32 | } 33 | 34 | // TODO: There is a lot of noise here 35 | if (isRelative(mod)) { 36 | const resolved = 37 | std.fs.path.resolvePosix(allocator, &[_][]const u8{ 38 | std.fs.path.dirname(imp).?, 39 | mod, 40 | }) catch return null; 41 | 42 | const final = if (std.mem.startsWith(u8, resolved, "." ++ std.fs.path.sep_str)) resolved else std.mem.concat(allocator, u8, &[_][]const u8{ ".", "/", resolved }) catch |err| { 43 | std.log.err("Failed to open file: {s} \n", .{@errorName(err)}); 44 | return null; 45 | }; 46 | 47 | std.debug.print("Importer {s}\n", .{final}); 48 | const path = allocator.dupeZ(u8, resolved) catch |err| { 49 | std.log.err("Failed to open file: {s}\n", .{@errorName(err)}); 50 | return null; 51 | }; 52 | _ = path; 53 | return module; 54 | } 55 | return module; 56 | } 57 | -------------------------------------------------------------------------------- /src/root-wasm.zig: -------------------------------------------------------------------------------- 1 | const std = @import("std"); 2 | const builtin = @import("builtin"); 3 | const common = @import("common.zig"); 4 | const wren = @cImport({ 5 | @cInclude("wren.h"); 6 | @cInclude("stdio.h"); 7 | }); 8 | 9 | pub const r = @cImport({ 10 | @cInclude("raylib.h"); 11 | @cInclude("raymath.h"); 12 | @cInclude("rlgl.h"); 13 | }); 14 | 15 | const Bindings = @import("bindings/index.zig"); 16 | 17 | var produce_executable = true; 18 | 19 | var debug_allocator: std.heap.DebugAllocator(.{}) = .init; 20 | //pub const allocator = debug_allocator.allocator(); 21 | pub const allocator = std.heap.wasm_allocator; 22 | 23 | pub fn runWasm(script: []const u8) !void { 24 | var config: wren.WrenConfiguration = undefined; 25 | wren.wrenInitConfiguration(&config); 26 | config.writeFn = common.writeFn; 27 | config.errorFn = common.errorFn; 28 | 29 | config.bindForeignClassFn = common.bindForiegnClass; 30 | config.bindForeignMethodFn = common.bindForeignMethod; 31 | 32 | config.resolveModuleFn = if (produce_executable) @import("./resolve_module.zig").resolveModule else common.resolveModule; 33 | config.loadModuleFn = common.loadModule; 34 | 35 | const vm = wren.wrenNewVM(&config); 36 | defer wren.wrenFreeVM(vm); 37 | 38 | const result = wren.wrenInterpret(vm, "main", script.ptr); 39 | 40 | switch (result) { 41 | wren.WREN_RESULT_COMPILE_ERROR => { 42 | std.debug.print("Compile Error!\n", .{}); 43 | }, 44 | wren.WREN_RESULT_RUNTIME_ERROR => { 45 | std.debug.print("Runtime Error!\n", .{}); 46 | }, 47 | wren.WREN_RESULT_SUCCESS => { 48 | //std.debug.print("Success!\n", .{}); 49 | }, 50 | else => {}, 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /src/root.zig: -------------------------------------------------------------------------------- 1 | const std = @import("std"); 2 | const builtin = @import("builtin"); 3 | const common = @import("common.zig"); 4 | const wren = @cImport({ 5 | @cInclude("wren.h"); 6 | @cInclude("stdio.h"); 7 | }); 8 | 9 | pub const r = @cImport({ 10 | @cInclude("raylib.h"); 11 | @cInclude("raymath.h"); 12 | @cInclude("rlgl.h"); 13 | }); 14 | 15 | const Bindings = @import("bindings/index.zig"); 16 | 17 | var wrenFilesMap: std.StaticStringMap([]const u8) = undefined; 18 | 19 | var debug_allocator: std.heap.DebugAllocator(.{}) = .init; 20 | pub const allocator = if (builtin.target.cpu.arch.isWasm()) std.heap.wasm_allocator else debug_allocator.allocator(); 21 | 22 | pub fn run(mainFile: []const u8, embeddedFilesMap: std.StaticStringMap([]const u8)) !void { 23 | common.initDynamicSymbolHandling(); 24 | 25 | const args = std.process.argsAlloc(allocator) catch unreachable; 26 | defer std.process.argsFree(allocator, args); 27 | 28 | wrenFilesMap = embeddedFilesMap; 29 | 30 | const module: [*:0]const u8 = @ptrCast(mainFile); 31 | const formalized = try formalizeImport(std.mem.span(module)); 32 | 33 | var config: wren.WrenConfiguration = undefined; 34 | wren.wrenInitConfiguration(&config); 35 | config.writeFn = common.writeFn; 36 | config.errorFn = common.errorFn; 37 | 38 | config.bindForeignClassFn = common.bindForiegnClass; 39 | config.bindForeignMethodFn = common.bindForeignMethod; 40 | 41 | config.resolveModuleFn = resolveModule; 42 | config.loadModuleFn = loadModule; 43 | 44 | const source_code: [*c]const u8 = @ptrCast(wrenFilesMap.get(formalized).?); 45 | 46 | const vm = wren.wrenNewVM(&config); 47 | defer wren.wrenFreeVM(vm); 48 | 49 | const result = wren.wrenInterpret(vm, "main", source_code); 50 | 51 | switch (result) { 52 | wren.WREN_RESULT_COMPILE_ERROR => { 53 | std.debug.print("Compile Error!\n", .{}); 54 | }, 55 | wren.WREN_RESULT_RUNTIME_ERROR => { 56 | std.debug.print("Runtime Error!\n", .{}); 57 | }, 58 | wren.WREN_RESULT_SUCCESS => { 59 | //std.debug.print("Success!\n", .{}); 60 | }, 61 | else => {}, 62 | } 63 | } 64 | 65 | fn formalizeImport(module: []const u8) ![]const u8 { 66 | const abc = try std.fmt.allocPrint(allocator, "{s}", .{module}); 67 | return abc; 68 | } 69 | 70 | pub fn loadModule(vm: ?*wren.WrenVM, name: [*c]const u8) callconv(.c) wren.WrenLoadModuleResult { 71 | _ = .{ vm, name }; 72 | const mod = std.mem.span(name); 73 | 74 | if (!std.fs.path.isAbsolute(mod) and !common.isRelative(mod)) { 75 | return common.loadBuiltinModule(name); 76 | } 77 | 78 | const path = std.mem.concat(allocator, u8, &[_][]const u8{ std.mem.span(name), ".wren" }) catch return .{ 79 | .source = null, 80 | .onComplete = null, 81 | }; 82 | 83 | defer allocator.free(path); 84 | 85 | // TODO: There is a lot of noise here 86 | var file: std.fs.File = undefined; 87 | if (common.isRelative(mod)) { 88 | file = std.fs.cwd().openFile(path, .{}) catch |err| { 89 | std.log.err("Failed to open file: {s} {s}\n", .{ @errorName(err), path }); 90 | return .{ 91 | .source = null, 92 | .onComplete = null, 93 | }; 94 | }; 95 | } else { 96 | file = std.fs.openFileAbsolute(path, .{ .mode = .read_write }) catch |err| { 97 | std.log.err("Failed to open file: {s} {s}\n", .{ path, @errorName(err) }); 98 | return .{ 99 | .source = null, 100 | .onComplete = null, 101 | }; 102 | }; 103 | } 104 | 105 | defer file.close(); 106 | 107 | var buf_reader = std.io.bufferedReader(file.reader()); 108 | var in_stream = buf_reader.reader(); 109 | var script: [8192]u8 = undefined; 110 | 111 | const read = in_stream.readAll(&script) catch { 112 | return .{ 113 | .source = null, 114 | .onComplete = null, 115 | }; 116 | }; 117 | script[read] = 0; 118 | 119 | const source = allocator.dupeZ(u8, &script) catch return .{ 120 | .source = null, 121 | .onComplete = null, 122 | }; 123 | 124 | return .{ 125 | .source = source, 126 | .onComplete = common.loadModuleComplete, 127 | }; 128 | } 129 | 130 | pub fn resolveModule(vm: ?*wren.WrenVM, importer: [*c]const u8, module: [*c]const u8) callconv(.C) ?[*:0]const u8 { 131 | _ = .{ importer, module, vm }; 132 | const imp = std.mem.span(importer); 133 | const mod = std.mem.span(module); 134 | 135 | if (!std.fs.path.isAbsolute(mod) and !common.isRelative(mod)) { 136 | return module; 137 | } 138 | _ = .{ imp, mod }; 139 | 140 | return module; 141 | } 142 | -------------------------------------------------------------------------------- /src/templates/exe/Dockerfile: -------------------------------------------------------------------------------- 1 | # took inspiration from https://github.com/QSmally/Zig-Build/blob/master/Dockerfile 2 | # 3 | FROM alpine AS compiler 4 | 5 | ARG VERSION=0.14.0 6 | 7 | RUN apk update && apk add curl tar xz 8 | 9 | RUN curl https://ziglang.org/download/$VERSION/zig-linux-$(uname -m)-$VERSION.tar.xz -O && \ 10 | tar -xf *.tar.xz && \ 11 | mv zig-linux-$(uname -m)-$VERSION /compiler 12 | 13 | WORKDIR /build 14 | COPY . /build 15 | 16 | RUN echo '.{' \ 17 | ' .name = .{{{ PROJECT_NAME }}},' \ 18 | ' .version = "0.0.0",' \ 19 | ' .fingerprint = 0x7aca45f40a314ba8,' \ 20 | ' .minimum_zig_version = "0.14.0",' \ 21 | ' .dependencies = .{' \ 22 | ' .wren = .{' \ 23 | ' .url = "https://github.com/wren-lang/wren/archive/refs/heads/main.zip",' \ 24 | ' .hash = "N-V-__8AAPbpYgAZgdr-sM49A18GSKr5MVR56MwpfI65FmAZ",' \ 25 | ' },' \ 26 | ' .raylib = .{' \ 27 | ' .url = "git+https://github.com/raysan5/raylib#27a4fe885164b315a90b67682f981a1e03d6079c",' \ 28 | ' .hash = "raylib-5.5.0-whq8uFqtNARw6t1tTakBDSVYgUjlBqnDppOyNfE_yfCa",' \ 29 | ' },' \ 30 | ' .tolan = .{' \ 31 | ' .url = "git+https://github.com/jossephus/talon#ec9dc6910fa96c414406b5ed539e441943e4aadd",' \ 32 | ' .hash = "zig_wren-0.0.0-_Mx4iLclCACui2mzMF9z9j7iRplB0vS_jZjf-k9EQNLC",' \ 33 | ' },' \ 34 | ' },' \ 35 | ' .paths = .{' \ 36 | ' "build.zig",' \ 37 | ' "build.zig.zon",' \ 38 | ' "src",' \ 39 | ' },' \ 40 | '}' > build.zig.zon 41 | 42 | RUN cat <<'EOF' > build.zig 43 | const std = @import("std"); 44 | 45 | const mainFile = "{{{ MAIN_FILE }}}"; 46 | 47 | pub fn build(b: *std.Build) !void { 48 | const target = b.standardTargetOptions(.{}); 49 | const optimize = b.standardOptimizeOption(.{}); 50 | 51 | const tolan_lib = b.dependency("tolan", .{ 52 | .target = target, 53 | .optimize = optimize, 54 | }); 55 | const tolan_mod = tolan_lib.module("tolan"); 56 | 57 | const exe_mod = b.createModule(.{ 58 | .root_source_file = b.path("build.zig"), 59 | .target = target, 60 | .optimize = optimize, 61 | }); 62 | 63 | exe_mod.addImport("tolan", tolan_mod); 64 | 65 | const tolan_exe = b.addExecutable(.{ 66 | .name = "{{{ PROJECT_NAME }}}", 67 | .root_module = exe_mod, 68 | }); 69 | 70 | try addAssetsOption(b, tolan_exe, target, optimize, b.getInstallStep()); 71 | 72 | b.installArtifact(tolan_exe); 73 | 74 | const run_cmd = b.addRunArtifact(tolan_exe); 75 | 76 | run_cmd.step.dependOn(b.getInstallStep()); 77 | 78 | if (b.args) |args| { 79 | run_cmd.addArgs(args); 80 | } 81 | 82 | const run_step = b.step("run", "Run the app"); 83 | run_step.dependOn(&run_cmd.step); 84 | } 85 | 86 | pub fn addAssetsOption(b: *std.Build, exe: anytype, target: anytype, optimize: anytype, step: *std.Build.Step) !void { 87 | var options = b.addOptions(); 88 | 89 | var files = std.ArrayList([]const u8).init(b.allocator); 90 | defer files.deinit(); 91 | 92 | try checkWrenFiles(b.allocator, &files, b, ".", ".", step); 93 | 94 | options.addOption([]const []const u8, "files", files.items); 95 | exe.step.dependOn(&options.step); 96 | 97 | const assets_mod = b.addModule("assets", .{ 98 | .root_source_file = options.getOutput(), 99 | .target = target, 100 | .optimize = optimize, 101 | }); 102 | 103 | exe.root_module.addImport("assets", assets_mod); 104 | } 105 | 106 | fn checkWrenFiles( 107 | allocator: std.mem.Allocator, 108 | files: *std.ArrayList([]const u8), 109 | b: *std.Build, 110 | base_path: []const u8, 111 | rel_path: []const u8, 112 | step: *std.Build.Step, 113 | ) !void { 114 | var dir = try std.fs.cwd().openDir(rel_path, .{ .iterate = true }); 115 | var it = dir.iterate(); 116 | while (try it.next()) |entry| { 117 | if (std.mem.eql(u8, entry.name, ".zig-cache") or std.mem.eql(u8, entry.name, "zig-out")) { 118 | continue; 119 | } 120 | const rel_to_base = try std.fs.path.join(allocator, &.{ rel_path, entry.name }); 121 | defer allocator.free(rel_to_base); 122 | 123 | switch (entry.kind) { 124 | .file => { 125 | if (std.mem.endsWith(u8, entry.name, ".wren")) { 126 | const rel = try std.fs.path.relative(allocator, base_path, rel_to_base); 127 | defer allocator.free(rel); 128 | 129 | try files.append(b.dupe(rel)); 130 | } 131 | }, 132 | .directory => { 133 | try checkWrenFiles(allocator, files, b, base_path, rel_to_base, step); 134 | }, 135 | else => {}, 136 | } 137 | } 138 | } 139 | 140 | const assets = @import("assets"); 141 | 142 | const embeddedFilesMap = std.StaticStringMap([]const u8).initComptime(genMap()); 143 | const EmbeddedAsset = struct { 144 | []const u8, 145 | []const u8, 146 | }; 147 | 148 | fn genMap() [assets.files.len]EmbeddedAsset { 149 | var embassets: [assets.files.len]EmbeddedAsset = undefined; 150 | comptime var i = 0; 151 | inline for (assets.files) |file| { 152 | embassets[i][0] = file; 153 | embassets[i][1] = @embedFile(file); 154 | i += 1; 155 | } 156 | return embassets; 157 | } 158 | 159 | pub fn main() !void { 160 | try tolan.run(mainFile, embeddedFilesMap); 161 | } 162 | 163 | const tolan = @import("tolan"); 164 | EOF 165 | 166 | # RUN /compiler/zig build -Doptimize=ReleaseSafe -Dtarget=x86_64-windows 167 | #RUN /compiler/zig build -Dtarget=x86_64-windows 168 | -------------------------------------------------------------------------------- /src/templates/exe/docker-compose.yml: -------------------------------------------------------------------------------- 1 | services: 2 | camera: 3 | build: 4 | context: . 5 | dockerfile: Dockerfile 6 | volumes: 7 | - ./dist:/build/zig-out/bin 8 | working_dir: /build 9 | command: /compiler/zig build -Dtarget=x86_64-windows 10 | -------------------------------------------------------------------------------- /src/templates/wasm/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM nixos/nix 2 | 3 | ARG VERSION=0.14.0 4 | 5 | RUN echo "experimental-features = nix-command flakes" >> /etc/nix/nix.conf 6 | 7 | WORKDIR /build 8 | COPY . /build 9 | 10 | 11 | RUN cat <<'EOF' > flake.nix 12 | { 13 | inputs = { 14 | nixpkgs.url = "nixpkgs/nixos-unstable"; 15 | flake-utils.url = "github:numtide/flake-utils"; 16 | zig.url = "github:mitchellh/zig-overlay"; 17 | }; 18 | 19 | outputs = { 20 | nixpkgs, 21 | flake-utils, 22 | zig, 23 | ... 24 | }: 25 | flake-utils.lib.eachDefaultSystem ( 26 | system: let 27 | overlays = [ 28 | (final: prev: { 29 | zigpkgs = zig.packages.${system}; 30 | }) 31 | ]; 32 | pkgs = import nixpkgs { 33 | inherit overlays system; 34 | }; 35 | packages = with pkgs; [ 36 | glfw 37 | libGL 38 | libxkbcommon 39 | pkg-config 40 | xorg.libxcb 41 | xorg.libXft 42 | xorg.libX11 43 | xorg.libX11.dev 44 | xorg.libXrandr 45 | xorg.libXinerama 46 | xorg.libXcursor 47 | xorg.libXi 48 | glfw-wayland 49 | zigpkgs."0.14.0" 50 | emscripten 51 | ]; 52 | in { 53 | devShell = pkgs.mkShell { 54 | buildInputs = packages; 55 | nativeBuildInputs = with pkgs; [cmake pkg-config ncurses fontconfig freetype]; 56 | shellHook = '' 57 | export SHELL=/usr/bin/bash 58 | if [ ! -d $(pwd)/.emscripten_cache-${pkgs.emscripten.version} ]; then 59 | cp -R ${pkgs.emscripten}/share/emscripten/cache/ $(pwd)/.emscripten_cache-${pkgs.emscripten.version} 60 | chmod u+rwX -R $(pwd)/.emscripten_cache-${pkgs.emscripten.version} 61 | fi 62 | export EM_CACHE=$(pwd)/.emscripten_cache-${pkgs.emscripten.version} 63 | echo emscripten cache dir: $EM_CACHE 64 | ''; 65 | }; 66 | } 67 | ); 68 | } 69 | 70 | EOF 71 | 72 | RUN cat <<'EOF' > build.zig.zon 73 | .{ 74 | .name = .{{{ PROJECT_NAME }}}, 75 | 76 | .version = "0.0.0", 77 | 78 | .fingerprint = 0x7aca45f40a314ba8, // Changing this has security and trust implications. 79 | 80 | .minimum_zig_version = "0.14.0", 81 | 82 | .dependencies = .{ 83 | .tolan = .{ 84 | .url = "git+https://github.com/jossephus/talon#ec9dc6910fa96c414406b5ed539e441943e4aadd", 85 | .hash = "zig_wren-0.0.0-_Mx4iLclCACui2mzMF9z9j7iRplB0vS_jZjf-k9EQNLC", 86 | }, 87 | .wren = .{ 88 | .url = "https://github.com/wren-lang/wren/archive/refs/heads/main.zip", 89 | .hash = "N-V-__8AAPbpYgAZgdr-sM49A18GSKr5MVR56MwpfI65FmAZ", 90 | }, 91 | .raylib = .{ 92 | .url = "git+https://github.com/raysan5/raylib#27a4fe885164b315a90b67682f981a1e03d6079c", 93 | .hash = "raylib-5.5.0-whq8uFqtNARw6t1tTakBDSVYgUjlBqnDppOyNfE_yfCa", 94 | }, 95 | }, 96 | 97 | .paths = .{ 98 | "build.zig", 99 | "build.zig.zon", 100 | "src", 101 | }, 102 | } 103 | 104 | EOF 105 | 106 | RUN cat <<'EOF' > build.zig 107 | const std = @import("std"); 108 | 109 | const mainFile = "{{{ MAIN_FILE }}}"; 110 | 111 | pub fn build(b: *std.Build) !void { 112 | const target = b.standardTargetOptions(.{}); 113 | const optimize = b.standardOptimizeOption(.{}); 114 | 115 | if (target.result.cpu.arch.isWasm()) { 116 | const wasm_target = b.resolveTargetQuery(.{ 117 | .cpu_arch = .wasm32, 118 | .cpu_model = .{ .explicit = &std.Target.wasm.cpu.mvp }, 119 | .cpu_features_add = std.Target.wasm.featureSet(&.{ 120 | .atomics, 121 | .bulk_memory, 122 | }), 123 | .os_tag = .emscripten, 124 | }); 125 | 126 | const tolan_lib = b.dependency("tolan", .{ 127 | .target = wasm_target, 128 | .optimize = optimize, 129 | }); 130 | const tolan_mod = tolan_lib.module("tolan"); 131 | 132 | const wren_lib = tolan_lib.artifact("wren"); 133 | const raylib_lib = tolan_lib.artifact("raylib"); 134 | 135 | const app_lib = b.addLibrary(.{ 136 | .linkage = .static, 137 | .name = "camera", 138 | .root_module = b.createModule(.{ 139 | .root_source_file = b.path("build.zig"), 140 | .target = wasm_target, 141 | .optimize = optimize, 142 | .imports = &.{ 143 | .{ .name = "tolan", .module = tolan_mod }, 144 | }, 145 | }), 146 | }); 147 | app_lib.linkLibC(); 148 | app_lib.shared_memory = true; 149 | app_lib.linkLibrary(wren_lib); 150 | app_lib.linkLibrary(raylib_lib); 151 | app_lib.addIncludePath(.{ .cwd_relative = ".emscripten_cache-4.0.8/sysroot/include" }); 152 | 153 | try addAssetsOption(b, app_lib, target, optimize, b.getInstallStep()); 154 | 155 | const emcc = b.addSystemCommand(&.{"emcc"}); 156 | 157 | for (app_lib.getCompileDependencies(false)) |lib| { 158 | if (lib.isStaticLibrary()) { 159 | emcc.addArtifactArg(lib); 160 | } 161 | } 162 | 163 | for (wren_lib.getCompileDependencies(false)) |lib| { 164 | if (lib.isStaticLibrary()) { 165 | emcc.addArtifactArg(lib); 166 | } 167 | } 168 | 169 | for (raylib_lib.getCompileDependencies(false)) |lib| { 170 | if (lib.isStaticLibrary()) { 171 | emcc.addArtifactArg(lib); 172 | } 173 | } 174 | 175 | emcc.addArgs(&.{ 176 | "-sUSE_GLFW=3", 177 | "-sUSE_OFFSET_CONVERTER", 178 | 179 | //"-sAUDIO_WORKLET=1", 180 | //"-sWASM_WORKERS=1", 181 | "-sSHARED_MEMORY=1", 182 | "-sALLOW_MEMORY_GROWTH=1", 183 | 184 | "-sASYNCIFY", 185 | "-sundefs", 186 | "-sERROR_ON_UNDEFINED_SYMBOLS=0", 187 | "--shell-file", 188 | b.path("shell.html").getPath(b), 189 | }); 190 | 191 | const link_items: []const *std.Build.Step.Compile = &.{ 192 | wren_lib, 193 | raylib_lib, 194 | app_lib, 195 | }; 196 | 197 | for (link_items) |item| { 198 | emcc.addFileArg(item.getEmittedBin()); 199 | emcc.step.dependOn(&item.step); 200 | } 201 | 202 | //emcc.addArg("--pre-js"); 203 | emcc.addArg("-o"); 204 | 205 | const app_html = emcc.addOutputFileArg("index.html"); 206 | b.getInstallStep().dependOn(&b.addInstallDirectory(.{ 207 | .source_dir = app_html.dirname(), 208 | .install_dir = .{ .custom = "www" }, 209 | .install_subdir = "", 210 | }).step); 211 | } else { 212 | const failed = b.addFail("Non wasm target chosen"); 213 | _ = failed; 214 | } 215 | } 216 | 217 | // varied version of https://github.com/ringtailsoftware/zig-embeddir/blob/main/build.zig to include wren files 218 | pub fn addAssetsOption(b: *std.Build, exe: anytype, target: anytype, optimize: anytype, step: *std.Build.Step) !void { 219 | var options = b.addOptions(); 220 | 221 | var files = std.ArrayList([]const u8).init(b.allocator); 222 | defer files.deinit(); 223 | 224 | try checkWrenFiles(b.allocator, &files, b, ".", ".", step); 225 | 226 | options.addOption([]const []const u8, "files", files.items); 227 | exe.step.dependOn(&options.step); 228 | 229 | const assets_mod = b.addModule("assets", .{ 230 | .root_source_file = options.getOutput(), 231 | .target = target, 232 | .optimize = optimize, 233 | }); 234 | 235 | exe.root_module.addImport("assets", assets_mod); 236 | } 237 | 238 | fn checkWrenFiles( 239 | allocator: std.mem.Allocator, 240 | files: *std.ArrayList([]const u8), 241 | b: *std.Build, 242 | base_path: []const u8, 243 | rel_path: []const u8, 244 | step: *std.Build.Step, 245 | ) !void { 246 | var dir = try std.fs.cwd().openDir(rel_path, .{ .iterate = true }); 247 | var it = dir.iterate(); 248 | while (try it.next()) |entry| { 249 | if (std.mem.eql(u8, entry.name, ".zig-cache") or std.mem.eql(u8, entry.name, "zig-out")) { 250 | continue; 251 | } 252 | const rel_to_base = try std.fs.path.join(allocator, &.{ rel_path, entry.name }); 253 | defer allocator.free(rel_to_base); 254 | 255 | switch (entry.kind) { 256 | .file => { 257 | if (std.mem.endsWith(u8, entry.name, ".wren")) { 258 | const rel = try std.fs.path.relative(allocator, base_path, rel_to_base); 259 | defer allocator.free(rel); 260 | 261 | try files.append(b.dupe(rel)); 262 | } 263 | }, 264 | .directory => { 265 | try checkWrenFiles(allocator, files, b, base_path, rel_to_base, step); 266 | }, 267 | else => {}, 268 | } 269 | } 270 | } 271 | 272 | const assets = @import("assets"); 273 | 274 | const embeddedFilesMap = std.StaticStringMap([]const u8).initComptime(genMap()); 275 | const EmbeddedAsset = struct { 276 | []const u8, 277 | []const u8, 278 | }; 279 | 280 | fn genMap() [assets.files.len]EmbeddedAsset { 281 | var embassets: [assets.files.len]EmbeddedAsset = undefined; 282 | comptime var i = 0; 283 | inline for (assets.files) |file| { 284 | embassets[i][0] = file; 285 | embassets[i][1] = @embedFile(file); 286 | i += 1; 287 | } 288 | return embassets; 289 | } 290 | 291 | pub fn main() !void { 292 | try tolan.run(mainFile, embeddedFilesMap); 293 | } 294 | 295 | export fn add(a: i32, b: i32) i32 { 296 | return a + b; 297 | } 298 | 299 | export fn sub(a: i32, b: i32) i32 { 300 | return a - b; 301 | } 302 | 303 | const tolan = @import("tolan"); 304 | 305 | EOF 306 | 307 | 308 | RUN nix develop -L --verbose 309 | 310 | # RUN /compiler/zig build -Doptimize=ReleaseSafe -Dtarget=x86_64-windows 311 | #RUN /compiler/zig build -Dtarget=x86_64-windows 312 | -------------------------------------------------------------------------------- /src/templates/wasm/docker-compose.yml: -------------------------------------------------------------------------------- 1 | services: 2 | wasm: 3 | build: 4 | context: . 5 | dockerfile: Dockerfile 6 | volumes: 7 | - ./dist:/build/zig-out/www 8 | working_dir: /build 9 | command: nix develop -L --verbose && zig build -Dtarget=wasm32-emscripten -Doptimize=ReleaseSmall && tail -f /dev/null 10 | #command: tail -f /dev/null 11 | -------------------------------------------------------------------------------- /src/watcher/MacosWatcher.zig: -------------------------------------------------------------------------------- 1 | const MacosWatcher = @This(); 2 | 3 | const std = @import("std"); 4 | const Debouncer = @import("hot.zig").Debouncer; 5 | 6 | const c = @cImport({ 7 | @cInclude("CoreServices/CoreServices.h"); 8 | }); 9 | 10 | const log = std.log.scoped(.watcher); 11 | 12 | gpa: std.mem.Allocator, 13 | debouncer: *Debouncer, 14 | dir_paths: []const []const u8, 15 | 16 | pub fn init( 17 | gpa: std.mem.Allocator, 18 | debouncer: *Debouncer, 19 | dir_paths: []const []const u8, 20 | ) MacosWatcher { 21 | return .{ 22 | .gpa = gpa, 23 | .debouncer = debouncer, 24 | .dir_paths = dir_paths, 25 | }; 26 | } 27 | 28 | pub fn start(watcher: *MacosWatcher) !void { 29 | const t = try std.Thread.spawn(.{}, MacosWatcher.listen, .{watcher}); 30 | t.detach(); 31 | } 32 | 33 | pub fn listen(watcher: *MacosWatcher) void { 34 | errdefer |err| switch (err) { 35 | error.OutOfMemory => { 36 | std.log.err("Out of memory", .{}); 37 | std.os.exit(1); 38 | }, 39 | }; 40 | 41 | const macos_paths = try watcher.gpa.alloc( 42 | c.CFStringRef, 43 | watcher.dir_paths.len, 44 | ); 45 | defer watcher.gpa.free(macos_paths); 46 | 47 | for (watcher.dir_paths, macos_paths) |str, *ref| { 48 | ref.* = c.CFStringCreateWithCString( 49 | null, 50 | str.ptr, 51 | c.kCFStringEncodingUTF8, 52 | ); 53 | } 54 | 55 | const paths_to_watch: c.CFArrayRef = c.CFArrayCreate( 56 | null, 57 | @ptrCast(macos_paths.ptr), 58 | @intCast(macos_paths.len), 59 | null, 60 | ); 61 | 62 | var stream_context: c.FSEventStreamContext = .{ .info = watcher }; 63 | const stream: c.FSEventStreamRef = c.FSEventStreamCreate( 64 | null, 65 | &macosCallback, 66 | &stream_context, 67 | paths_to_watch, 68 | c.kFSEventStreamEventIdSinceNow, 69 | 0.05, 70 | c.kFSEventStreamCreateFlagFileEvents, 71 | ); 72 | 73 | c.FSEventStreamScheduleWithRunLoop( 74 | stream, 75 | c.CFRunLoopGetCurrent(), 76 | c.kCFRunLoopDefaultMode, 77 | ); 78 | 79 | if (c.FSEventStreamStart(stream) == 0) { 80 | std.log.err("error: macos watcher FSEventStreamStart failed", .{}); 81 | } 82 | 83 | c.CFRunLoopRun(); 84 | 85 | c.FSEventStreamStop(stream); 86 | c.FSEventStreamInvalidate(stream); 87 | c.FSEventStreamRelease(stream); 88 | 89 | c.CFRelease(paths_to_watch); 90 | } 91 | 92 | pub fn macosCallback( 93 | streamRef: c.ConstFSEventStreamRef, 94 | clientCallBackInfo: ?*anyopaque, 95 | numEvents: usize, 96 | eventPaths: ?*anyopaque, 97 | eventFlags: ?[*]const c.FSEventStreamEventFlags, 98 | eventIds: ?[*]const c.FSEventStreamEventId, 99 | ) callconv(.C) void { 100 | _ = eventIds; 101 | _ = eventFlags; 102 | _ = streamRef; 103 | const watcher: *MacosWatcher = @alignCast(@ptrCast(clientCallBackInfo)); 104 | 105 | const paths: [*][*:0]u8 = @alignCast(@ptrCast(eventPaths)); 106 | for (paths[0..numEvents]) |p| { 107 | const path = std.mem.span(p); 108 | log.debug("Changed: {s}\n", .{path}); 109 | 110 | // const basename = std.fs.path.basename(path); 111 | // var base_path = path[0 .. path.len - basename.len]; 112 | // if (std.mem.endsWith(u8, base_path, "/")) 113 | // base_path = base_path[0 .. base_path.len - 1]; 114 | watcher.debouncer.newEvent(); 115 | } 116 | } 117 | -------------------------------------------------------------------------------- /src/watcher/README.md: -------------------------------------------------------------------------------- 1 | File Watcher functionalitiy borrowed from Zine's codebase. 2 | -------------------------------------------------------------------------------- /src/watcher/WindowsWatcher.zig: -------------------------------------------------------------------------------- 1 | const WindowsWatcher = @This(); 2 | 3 | const std = @import("std"); 4 | const windows = std.os.windows; 5 | const Debouncer = @import("hot.zig").Debouncer; 6 | 7 | const log = std.log.scoped(.watcher); 8 | 9 | const notify_filter = windows.FileNotifyChangeFilter{ 10 | .file_name = true, 11 | .dir_name = true, 12 | .attributes = false, 13 | .size = false, 14 | .last_write = true, 15 | .last_access = false, 16 | .creation = false, 17 | .security = false, 18 | }; 19 | 20 | const CompletionKey = usize; 21 | /// Values should be a multiple of `ReadBufferEntrySize` 22 | const ReadBufferIndex = u32; 23 | const ReadBufferEntrySize = 1024; 24 | 25 | const WatchEntry = struct { 26 | dir_path: [:0]const u8, 27 | dir_handle: windows.HANDLE, 28 | 29 | overlap: windows.OVERLAPPED = std.mem.zeroes(windows.OVERLAPPED), 30 | buf_idx: ReadBufferIndex, 31 | }; 32 | 33 | debouncer: *Debouncer, 34 | iocp_port: windows.HANDLE, 35 | entries: std.AutoHashMap(CompletionKey, WatchEntry), 36 | read_buffer: []u8, 37 | 38 | pub fn init( 39 | gpa: std.mem.Allocator, 40 | debouncer: *Debouncer, 41 | dir_paths: []const []const u8, 42 | ) !WindowsWatcher { 43 | errdefer |err| std.log.err("error: unable to start the file watcher: {s}", .{ 44 | @errorName(err), 45 | }); 46 | 47 | var watcher = WindowsWatcher{ 48 | .debouncer = debouncer, 49 | .iocp_port = windows.INVALID_HANDLE_VALUE, 50 | .entries = std.AutoHashMap(CompletionKey, WatchEntry).init(gpa), 51 | .read_buffer = undefined, 52 | }; 53 | errdefer { 54 | var iter = watcher.entries.valueIterator(); 55 | while (iter.next()) |entry| { 56 | windows.CloseHandle(entry.dir_handle); 57 | gpa.free(entry.dir_path); 58 | } 59 | watcher.entries.deinit(); 60 | } 61 | 62 | // Doubles as the number of WatchEntries 63 | var comp_key: CompletionKey = 0; 64 | 65 | for (dir_paths) |path| { 66 | const in_path = try gpa.dupeZ(u8, path); 67 | try watcher.entries.putNoClobber( 68 | comp_key, 69 | try addPath(in_path, comp_key, &watcher.iocp_port), 70 | ); 71 | comp_key += 1; 72 | } 73 | 74 | watcher.read_buffer = try gpa.alloc(u8, ReadBufferEntrySize * comp_key); 75 | 76 | // Here we need pointers to both the read_buffer and entry overlapped structs, 77 | // which we can only do after setting up everything else. 78 | watcher.entries.lockPointers(); 79 | for (0..comp_key) |key| { 80 | const entry = watcher.entries.getPtr(key).?; 81 | if (windows.kernel32.ReadDirectoryChangesW( 82 | entry.dir_handle, 83 | @ptrCast(@alignCast(&watcher.read_buffer[entry.buf_idx])), 84 | ReadBufferEntrySize, 85 | @intFromBool(true), 86 | notify_filter, 87 | null, 88 | &entry.overlap, 89 | null, 90 | ) == 0) { 91 | log.err("ReadDirectoryChanges error: {s}", .{ 92 | @tagName(windows.kernel32.GetLastError()), 93 | }); 94 | return error.QueueFailed; 95 | } 96 | } 97 | return watcher; 98 | } 99 | 100 | fn addPath( 101 | path: [:0]const u8, 102 | /// Assumed to increment by 1 after each invocation, starting at 0. 103 | key: CompletionKey, 104 | port: *windows.HANDLE, 105 | ) !WatchEntry { 106 | const dir_handle = CreateFileA( 107 | path, 108 | windows.GENERIC_READ, // FILE_LIST_DIRECTORY, 109 | windows.FILE_SHARE_READ | windows.FILE_SHARE_WRITE | windows.FILE_SHARE_DELETE, 110 | null, 111 | windows.OPEN_EXISTING, 112 | windows.FILE_FLAG_BACKUP_SEMANTICS | windows.FILE_FLAG_OVERLAPPED, 113 | null, 114 | ); 115 | if (dir_handle == windows.INVALID_HANDLE_VALUE) { 116 | log.err( 117 | "Unable to open directory {s}: {s}", 118 | .{ path, @tagName(windows.kernel32.GetLastError()) }, 119 | ); 120 | return error.InvalidHandle; 121 | } 122 | 123 | if (port.* == windows.INVALID_HANDLE_VALUE) { 124 | port.* = try windows.CreateIoCompletionPort(dir_handle, null, key, 0); 125 | } else { 126 | _ = try windows.CreateIoCompletionPort(dir_handle, port.*, key, 0); 127 | } 128 | 129 | return .{ 130 | .dir_path = path, 131 | .dir_handle = dir_handle, 132 | .buf_idx = @intCast(ReadBufferEntrySize * key), 133 | }; 134 | } 135 | 136 | pub fn start(watcher: *WindowsWatcher) !void { 137 | const t = try std.Thread.spawn(.{}, WindowsWatcher.listen, .{watcher}); 138 | t.detach(); 139 | } 140 | 141 | pub fn listen(watcher: *WindowsWatcher) !void { 142 | var dont_care: struct { 143 | bytes_transferred: windows.DWORD = undefined, 144 | overlap: ?*windows.OVERLAPPED = undefined, 145 | } = .{}; 146 | 147 | var key: CompletionKey = undefined; 148 | while (true) { 149 | // Waits here until any of the directory handles associated with the iocp port 150 | // have been updated. 151 | const wait_result = windows.GetQueuedCompletionStatus( 152 | watcher.iocp_port, 153 | &dont_care.bytes_transferred, 154 | &key, 155 | &dont_care.overlap, 156 | windows.INFINITE, 157 | ); 158 | if (wait_result != .Normal) { 159 | log.err("GetQueuedCompletionStatus error: {s}", .{@tagName(wait_result)}); 160 | return error.WaitFailed; 161 | } 162 | 163 | const entry = watcher.entries.getPtr(key) orelse @panic("Invalid CompletionKey"); 164 | 165 | var info_iter = windows.FileInformationIterator(FILE_NOTIFY_INFORMATION){ 166 | .buf = watcher.read_buffer[entry.buf_idx..][0..ReadBufferEntrySize], 167 | }; 168 | var path_buf: [windows.MAX_PATH]u8 = undefined; 169 | while (info_iter.next()) |info| { 170 | const filename: []const u8 = blk: { 171 | const n = try std.unicode.utf16LeToUtf8( 172 | &path_buf, 173 | @as([*]u16, @ptrCast(&info.FileName))[0 .. info.FileNameLength / 2], 174 | ); 175 | break :blk path_buf[0..n]; 176 | }; 177 | 178 | const args = .{ entry.dir_path, filename }; 179 | switch (info.Action) { 180 | windows.FILE_ACTION_ADDED => log.debug("added {s}/{s}", args), 181 | windows.FILE_ACTION_REMOVED => log.debug("removed {s}/{s}", args), 182 | windows.FILE_ACTION_MODIFIED => log.debug("modified {s}/{s}", args), 183 | windows.FILE_ACTION_RENAMED_OLD_NAME => log.debug("renamed_old_name {s}/{s}", args), 184 | windows.FILE_ACTION_RENAMED_NEW_NAME => log.debug("renamed_new_name {s}/{s}", args), 185 | else => log.debug("Unknown Action {s}/{s}", args), 186 | } 187 | 188 | watcher.debouncer.newEvent(); 189 | } 190 | 191 | // Re-queue the directory entry 192 | if (windows.kernel32.ReadDirectoryChangesW( 193 | entry.dir_handle, 194 | @ptrCast(@alignCast(&watcher.read_buffer[entry.buf_idx])), 195 | ReadBufferEntrySize, 196 | @intFromBool(true), 197 | notify_filter, 198 | null, 199 | &entry.overlap, 200 | null, 201 | ) == 0) { 202 | log.err("ReadDirectoryChanges error: {s}", .{@tagName(windows.kernel32.GetLastError())}); 203 | return error.QueueFailed; 204 | } 205 | } 206 | } 207 | 208 | const FILE_NOTIFY_INFORMATION = extern struct { 209 | NextEntryOffset: windows.DWORD, 210 | Action: windows.DWORD, 211 | FileNameLength: windows.DWORD, 212 | /// Flexible array member 213 | FileName: windows.WCHAR, 214 | }; 215 | 216 | extern "kernel32" fn CreateFileA( 217 | lpFileName: windows.LPCSTR, 218 | dwDesiredAccess: windows.DWORD, 219 | dwShareMode: windows.DWORD, 220 | lpSecurityAttributes: ?*windows.SECURITY_ATTRIBUTES, 221 | dwCreationDisposition: windows.DWORD, 222 | dwFlagsAndAttributes: windows.DWORD, 223 | hTemplateFile: ?windows.HANDLE, 224 | ) callconv(windows.WINAPI) windows.HANDLE; 225 | -------------------------------------------------------------------------------- /src/watcher/channel.zig: -------------------------------------------------------------------------------- 1 | const std = @import("std"); 2 | 3 | pub fn Channel(comptime T: type) type { 4 | return struct { 5 | lock: std.Thread.Mutex = .{}, 6 | fifo: Fifo, 7 | writeable: std.Thread.Condition = .{}, 8 | readable: std.Thread.Condition = .{}, 9 | 10 | const Fifo = std.fifo.LinearFifo(T, .Slice); 11 | const Self = @This(); 12 | 13 | pub fn init(buffer: []T) Self { 14 | return Self{ .fifo = Fifo.init(buffer) }; 15 | } 16 | 17 | pub fn put(self: *Self, item: T) void { 18 | self.lock.lock(); 19 | defer { 20 | self.lock.unlock(); 21 | self.readable.signal(); 22 | } 23 | 24 | while (true) return self.fifo.writeItem(item) catch { 25 | self.writeable.wait(&self.lock); 26 | continue; 27 | }; 28 | } 29 | 30 | pub fn tryPut(self: *Self, item: T) !void { 31 | self.lock.lock(); 32 | defer self.lock.unlock(); 33 | 34 | try self.fifo.writeItem(item); 35 | 36 | // only signal on success 37 | self.readable.signal(); 38 | } 39 | 40 | pub fn get(self: *Self) T { 41 | self.lock.lock(); 42 | defer { 43 | self.lock.unlock(); 44 | self.writeable.signal(); 45 | } 46 | 47 | while (true) return self.fifo.readItem() orelse { 48 | self.readable.wait(&self.lock); 49 | continue; 50 | }; 51 | } 52 | 53 | pub fn getOrNull(self: *Self) ?T { 54 | self.lock.lock(); 55 | defer self.lock.unlock(); 56 | 57 | if (self.fifo.readItem()) |item| return item; 58 | 59 | // signal on empty queue 60 | self.writeable.signal(); 61 | } 62 | }; 63 | } 64 | -------------------------------------------------------------------------------- /src/watcher/hot.zig: -------------------------------------------------------------------------------- 1 | const std = @import("std"); 2 | const ws = @import("websocket.zig"); 3 | const Channel = @import("channel.zig").Channel; 4 | const builtin = @import("builtin"); 5 | const Watcher = switch (builtin.target.os.tag) { 6 | .linux => @import("LinuxWatcher.zig"), 7 | .macos => @import("MacosWatcher.zig"), 8 | .windows => @import("WindowsWatcher.zig"), 9 | else => @compileError("unsupported platform"), 10 | }; 11 | const runProgramHot = @import("../main.zig").runProgramHot; 12 | pub var stop_signal = std.atomic.Value(bool).init(false); 13 | var program_thread: ?std.Thread = null; 14 | 15 | pub const ServeEvent = union(enum) { 16 | change, 17 | connect: ws.Connection, 18 | disconnect: ws.Connection, 19 | }; 20 | 21 | const RunContext = struct { 22 | gpa: std.mem.Allocator, 23 | path: []const u8, 24 | }; 25 | 26 | pub const VmContext = struct { 27 | stop_signal: *std.atomic.Value(bool), 28 | }; 29 | 30 | fn runProgramThreadFn(context: RunContext) !void { 31 | // It's now the responsibility of `runProgram` to accept this 32 | // stop_signal and check it periodically inside its own loops. 33 | try runProgramHot(context.path, &stop_signal); 34 | } 35 | 36 | fn startProgramThread(gpa: std.mem.Allocator, path: []const u8) !void { 37 | // Ensure the signal is false before starting. 38 | stop_signal.store(false, .release); 39 | 40 | const context = RunContext{ 41 | .gpa = gpa, 42 | .path = path, 43 | }; 44 | 45 | // Spawn the new thread! 46 | program_thread = try std.Thread.spawn(.{}, runProgramThreadFn, .{context}); 47 | std.log.info("Program thread started.", .{}); 48 | } 49 | 50 | pub fn hot(gpa: std.mem.Allocator, path: []const u8) !void { 51 | std.log.info("hot {s}", .{path}); 52 | 53 | var buf: [64]ServeEvent = undefined; 54 | var channel: Channel(ServeEvent) = .init(&buf); 55 | 56 | var debouncer: Debouncer = .{ 57 | .cascade_window_ms = 25, 58 | .channel = &channel, 59 | }; 60 | debouncer.start() catch |err| { 61 | std.log.err( 62 | "error: unable to start debouncer: {s}", 63 | .{@errorName(err)}, 64 | ); 65 | return; 66 | }; 67 | 68 | var dirs_to_watch: std.ArrayListUnmanaged([:0]const u8) = .empty; 69 | defer if (builtin.mode == .Debug) dirs_to_watch.deinit(gpa); 70 | 71 | try dirs_to_watch.append(gpa, try std.fmt.allocPrintZ(gpa, "{s}", .{"."})); 72 | 73 | var dir = try std.fs.cwd().openDir(".", .{ .iterate = true }); 74 | defer dir.close(); 75 | 76 | var it = dir.iterate(); 77 | while (try it.next()) |entry| { 78 | if (entry.kind == .directory) { 79 | // entry.name is the relative path for the subdirectory 80 | try dirs_to_watch.append(gpa, try std.fmt.allocPrintZ(gpa, "{s}", .{entry.name})); 81 | } 82 | } 83 | 84 | var watcher: Watcher = try .init( 85 | gpa, 86 | &debouncer, 87 | dirs_to_watch.items, 88 | ); 89 | 90 | watcher.start() catch |err| { 91 | std.log.err( 92 | "error: unable to start file watcher: {s}", 93 | .{@errorName(err)}, 94 | ); 95 | return; 96 | }; 97 | 98 | var build_lock: std.Thread.RwLock = .{}; 99 | 100 | // try runProgram(path); 101 | 102 | try startProgramThread(gpa, path); 103 | 104 | var websockets: std.AutoArrayHashMapUnmanaged( 105 | std.posix.socket_t, 106 | ws.Connection, 107 | ) = .empty; 108 | 109 | while (true) { 110 | const event = channel.get(); 111 | std.log.debug("new event: {s}", .{@tagName(event)}); 112 | switch (event) { 113 | .change => { 114 | build_lock.lock(); 115 | // try runProgram(path); 116 | 117 | stop_signal.store(true, .release); 118 | 119 | if (program_thread) |thread| { 120 | thread.join(); 121 | program_thread = null; 122 | } 123 | 124 | stop_signal.store(true, .release); 125 | 126 | try startProgramThread(gpa, path); 127 | 128 | build_lock.unlock(); 129 | 130 | for (websockets.entries.items(.value)) |*conn| { 131 | conn.writeMessage( 132 | \\{ "command": "reload_all" } 133 | , .text) catch |err| { 134 | std.log.debug( 135 | "error writing to ws: {s}", 136 | .{@errorName(err)}, 137 | ); 138 | }; 139 | } 140 | }, 141 | .connect => |conn| { 142 | try websockets.put(gpa, conn.stream.handle, conn); 143 | }, 144 | .disconnect => |conn| { 145 | // the server thread will take care of closing the connection 146 | // as the corresponding thread shuts down 147 | _ = websockets.swapRemove(conn.stream.handle); 148 | }, 149 | } 150 | } 151 | 152 | // Debug print to check 153 | for (dirs_to_watch.items) |p| { 154 | std.debug.print("Watching: {s}\n", .{p}); 155 | } 156 | } 157 | 158 | pub const Debouncer = struct { 159 | cascade_window_ms: i64, 160 | 161 | cascade_mutex: std.Thread.Mutex = .{}, 162 | cascade_condition: std.Thread.Condition = .{}, 163 | cascade_start_ms: i64 = 0, 164 | channel: *Channel(ServeEvent), 165 | 166 | /// Thread-safe. To be called when a new event comes in 167 | pub fn newEvent(d: *Debouncer) void { 168 | { 169 | d.cascade_mutex.lock(); 170 | defer d.cascade_mutex.unlock(); 171 | d.cascade_start_ms = std.time.milliTimestamp(); 172 | } 173 | d.cascade_condition.signal(); 174 | } 175 | 176 | pub fn start(d: *Debouncer) !void { 177 | const t = try std.Thread.spawn(.{}, Debouncer.notify, .{d}); 178 | t.detach(); 179 | } 180 | 181 | pub fn notify(d: *Debouncer) void { 182 | while (true) { 183 | d.cascade_mutex.lock(); 184 | defer d.cascade_mutex.unlock(); 185 | 186 | while (d.cascade_start_ms == 0) { 187 | // no active cascade 188 | d.cascade_condition.wait(&d.cascade_mutex); 189 | } 190 | // cascade != 0 191 | while (true) { 192 | const time_passed = std.time.milliTimestamp() - d.cascade_start_ms; 193 | if (time_passed >= d.cascade_window_ms) break; 194 | d.cascade_mutex.unlock(); 195 | const sleep_ms = d.cascade_window_ms - time_passed; 196 | std.Thread.sleep(@intCast(sleep_ms * std.time.ns_per_ms)); 197 | d.cascade_mutex.lock(); 198 | } 199 | 200 | // We have slept enough, "commit" the cascade window and 201 | // trigger a new build. 202 | d.cascade_start_ms = 0; 203 | d.channel.put(.change); 204 | } 205 | } 206 | }; 207 | -------------------------------------------------------------------------------- /src/watcher/websocket.zig: -------------------------------------------------------------------------------- 1 | const std = @import("std"); 2 | 3 | const log = std.log.scoped(.websockets); 4 | 5 | pub const Connection = struct { 6 | stream: std.net.Stream, 7 | 8 | pub fn init(request: *std.http.Server.Request) !Connection { 9 | var it = request.iterateHeaders(); 10 | const key = while (it.next()) |header| { 11 | if (std.ascii.eqlIgnoreCase(header.name, "sec-websocket-key")) { 12 | break header.value; 13 | } 14 | } else { 15 | // req.serveError("missing sec-websocket-key header", .bad_request); 16 | return error.MissingSecWebsocketKey; 17 | }; 18 | 19 | const hash_byte_len = 20; 20 | var encoded_hash: [std.base64.standard.Encoder.calcSize(hash_byte_len)]u8 = undefined; 21 | { 22 | var hasher = std.crypto.hash.Sha1.init(.{}); 23 | hasher.update(key); 24 | hasher.update("258EAFA5-E914-47DA-95CA-C5AB0DC85B11"); 25 | 26 | var h: [hash_byte_len]u8 = undefined; 27 | hasher.final(&h); 28 | 29 | const written = std.base64.standard.Encoder.encode(&encoded_hash, &h); 30 | std.debug.assert(written.len == encoded_hash.len); 31 | } 32 | 33 | var buffer: [4000]u8 = undefined; 34 | var response = request.respondStreaming(.{ 35 | .send_buffer = &buffer, 36 | .respond_options = .{ 37 | .status = .switching_protocols, 38 | .extra_headers = &.{ 39 | .{ .name = "Upgrade", .value = "websocket" }, 40 | .{ .name = "Connection", .value = "upgrade" }, 41 | .{ .name = "Sec-Websocket-Accept", .value = &encoded_hash }, 42 | .{ .name = "connection", .value = "close" }, 43 | }, 44 | .transfer_encoding = .none, 45 | }, 46 | }); 47 | 48 | try response.flush(); 49 | 50 | return .{ 51 | .stream = request.server.connection.stream, 52 | }; 53 | } 54 | 55 | const MessageKind = enum { 56 | binary, 57 | text, 58 | }; 59 | 60 | /// Thread safe, lock ensures one message sent at a time. 61 | pub fn writeMessage( 62 | conn: *const Connection, 63 | bytes: []const u8, 64 | kind: MessageKind, 65 | ) !void { 66 | // NOT named `write` because websockets is a message protocol, not a stream protocol. 67 | 68 | const op_code: Header.OpCode = switch (kind) { 69 | .binary => .binary, 70 | .text => .text, 71 | }; 72 | 73 | const header: Header = .{ 74 | .finish = true, 75 | .op_code = op_code, 76 | .payload_len = @intCast(bytes.len), 77 | .mask = null, 78 | }; 79 | try conn.writeWithHeader(header, bytes); 80 | } 81 | 82 | fn writeWithHeader( 83 | conn: *const Connection, 84 | header: Header, 85 | payload: []const u8, 86 | ) !void { 87 | const writer = conn.stream.writer(); 88 | 89 | try header.write(writer); 90 | try writer.writeAll(payload); 91 | } 92 | 93 | /// Not thread safe, must be only called by one thread at a time. 94 | pub fn readMessage(conn: *const Connection, buffer: []u8) ![]u8 { 95 | // NOT named `read` because websockets is a message protocol, not a stream protocol. 96 | 97 | const reader = conn.stream.reader(); 98 | 99 | var current_length: u64 = 0; 100 | while (true) { 101 | const header = try Header.read(reader); 102 | if (current_length > 0 and (header.op_code == .binary or header.op_code == .text)) { 103 | return error.ExpectedContinuation; 104 | } 105 | const new_len = header.payload_len + current_length; 106 | if (new_len > buffer.len) { 107 | return error.NoSpaceLeft; 108 | } 109 | try reader.readNoEof(buffer[current_length..new_len]); 110 | 111 | if (header.mask) |mask| { 112 | for (0.., buffer[current_length..new_len]) |i, *b| { 113 | b.* ^= mask[i % 4]; 114 | } 115 | } 116 | current_length = new_len; 117 | 118 | switch (header.op_code) { 119 | .continuation, .text, .binary => { 120 | if (header.finish) { 121 | return buffer[0..current_length]; 122 | } 123 | }, 124 | 125 | .close => { 126 | return error.WebsocketClosed; 127 | }, 128 | 129 | .ping => { 130 | try conn.writeWithHeader(.{ 131 | .finish = true, 132 | .op_code = .pong, 133 | .payload_len = 0, 134 | .mask = null, 135 | }, &.{}); 136 | }, 137 | 138 | .pong => {}, 139 | } 140 | } 141 | } 142 | 143 | pub fn close(conn: *const Connection) void { 144 | conn.stream.close(); 145 | } 146 | }; 147 | 148 | const Header = struct { 149 | finish: bool, 150 | op_code: OpCode, 151 | payload_len: u64, 152 | mask: ?[4]u8, 153 | 154 | const OpCode = enum(u4) { 155 | continuation = 0, 156 | text = 1, 157 | binary = 2, 158 | close = 8, 159 | ping = 9, 160 | pong = 10, 161 | }; 162 | 163 | const Partial = packed struct(u16) { 164 | payload_len: enum(u7) { 165 | u16_len = 126, 166 | u64_len = 127, 167 | _, 168 | }, 169 | masked: bool, 170 | op_code: u4, 171 | reserved: u3, 172 | fin: bool, 173 | }; 174 | fn read(reader: anytype) !Header { 175 | const partial: Partial = @bitCast(try reader.readInt(u16, .big)); 176 | var r: Header = undefined; 177 | r.finish = partial.fin; 178 | 179 | inline for (std.meta.fields(OpCode)) |field| { 180 | if (field.value == partial.op_code) { 181 | r.op_code = @field(OpCode, field.name); 182 | break; 183 | } 184 | } else { 185 | return error.InvalidHeader; 186 | } 187 | 188 | r.payload_len = switch (partial.payload_len) { 189 | .u16_len => try reader.readInt(u16, .big), 190 | .u64_len => try reader.readInt(u64, .big), 191 | else => |v| @intFromEnum(v), 192 | }; 193 | 194 | if (partial.masked) { 195 | r.mask = try reader.readBytesNoEof(4); 196 | } else { 197 | r.mask = null; 198 | } 199 | 200 | return r; 201 | } 202 | 203 | fn write(h: Header, writer: anytype) !void { 204 | var p: Partial = .{ 205 | .payload_len = undefined, 206 | .masked = if (h.mask) |_| true else false, 207 | .op_code = @intFromEnum(h.op_code), 208 | .reserved = 0, 209 | .fin = h.finish, 210 | }; 211 | 212 | if (h.payload_len < 126) { 213 | p.payload_len = @enumFromInt(h.payload_len); 214 | } else if (h.payload_len <= std.math.maxInt(u16)) { 215 | p.payload_len = .u16_len; 216 | } else { 217 | p.payload_len = .u64_len; 218 | } 219 | 220 | try writer.writeInt(u16, @bitCast(p), .big); 221 | switch (p.payload_len) { 222 | .u16_len => try writer.writeInt(u16, @intCast(h.payload_len), .big), 223 | .u64_len => try writer.writeInt(u64, h.payload_len, .big), 224 | else => {}, 225 | } 226 | if (h.mask) |mask| { 227 | try writer.writeAll(&mask); 228 | } 229 | } 230 | }; 231 | 232 | fn testHeader(header_truth: Header, buffer_truth: []const u8) !void { 233 | { 234 | var stream = std.io.fixedBufferStream(buffer_truth); 235 | const header_result = Header.read(stream.reader()); 236 | try std.testing.expectEqualDeep(header_truth, header_result); 237 | try std.testing.expectEqual(buffer_truth.len, stream.getPos()); // consumed whole header 238 | } 239 | { 240 | var b: [20]u8 = undefined; 241 | var stream = std.io.fixedBufferStream(&b); 242 | try header_truth.write(stream.writer()); 243 | try std.testing.expectEqualSlices(u8, buffer_truth, stream.getWritten()); 244 | } 245 | } 246 | 247 | test Header { 248 | // Finish 249 | try testHeader(.{ .finish = true, .op_code = .continuation, .payload_len = 0, .mask = null }, &[_]u8{ 1 << 7, 0 }); 250 | // Op code 251 | try testHeader(.{ .finish = false, .op_code = .text, .payload_len = 0, .mask = null }, &[_]u8{ 1, 0 }); 252 | // Payload len 253 | try testHeader(.{ .finish = false, .op_code = .continuation, .payload_len = 125, .mask = null }, &[_]u8{ 0, 125 }); 254 | try testHeader(.{ .finish = false, .op_code = .continuation, .payload_len = 126, .mask = null }, &[_]u8{ 0, 126, 0, 126 }); 255 | try testHeader(.{ .finish = false, .op_code = .continuation, .payload_len = 65_535, .mask = null }, &[_]u8{ 0, 126, 255, 255 }); 256 | try testHeader(.{ .finish = false, .op_code = .continuation, .payload_len = 65_536, .mask = null }, &[_]u8{ 0, 127, 0, 0, 0, 0, 0, 1, 0, 0 }); 257 | // Mask 258 | try testHeader(.{ .finish = false, .op_code = .continuation, .payload_len = 0, .mask = .{ 1, 2, 3, 4 } }, &[_]u8{ 0, 1 << 7, 1, 2, 3, 4 }); 259 | // Bit of everthing 260 | try testHeader(.{ .finish = true, .op_code = .binary, .payload_len = 126, .mask = .{ 1, 2, 3, 4 } }, &[_]u8{ (1 << 7) | 2, 1 << 7 | 126, 0, 126, 1, 2, 3, 4 }); 261 | } 262 | --------------------------------------------------------------------------------