Compare commits
3 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
ad4f3d3f58 | ||
|
|
b23dca6934 | ||
|
|
8fba19c820 |
193
.github/workflows/build.yml
vendored
@@ -3,13 +3,10 @@ name: Build and Deploy
|
||||
on:
|
||||
push:
|
||||
branches: [ "*" ]
|
||||
tags: [ "v*" ]
|
||||
tags: [ "v*" ]
|
||||
pull_request:
|
||||
|
||||
jobs:
|
||||
# ──────────────────────────────────────────────────────────────
|
||||
# LINUX BUILD
|
||||
# ──────────────────────────────────────────────────────────────
|
||||
build-linux:
|
||||
runs-on: ubuntu-latest
|
||||
container:
|
||||
@@ -18,17 +15,20 @@ jobs:
|
||||
steps:
|
||||
- name: Check Out Code
|
||||
uses: actions/checkout@v4
|
||||
with: { fetch-depth: 0 }
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Build Prosperon (Linux)
|
||||
run: |
|
||||
meson setup build -Dbuildtype=release -Db_lto=true -Db_lto_mode=thin -Db_ndebug=true
|
||||
meson setup build -Dbuildtype=release -Db_lto=true -Db_ndebug=true
|
||||
meson compile -C build
|
||||
|
||||
- name: Test Prosperon (Linux)
|
||||
env: { TRACY_NO_INVARIANT_CHECK: 1 }
|
||||
env:
|
||||
TRACY_NO_INVARIANT_CHECK: 1
|
||||
run: |
|
||||
meson test --print-errorlogs -C build
|
||||
strip build/prosperon
|
||||
|
||||
- name: Upload Test Log (Linux)
|
||||
if: ${{ always() }}
|
||||
@@ -43,43 +43,12 @@ jobs:
|
||||
with:
|
||||
name: prosperon-artifacts-linux
|
||||
path: build/prosperon
|
||||
|
||||
- name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@v3
|
||||
|
||||
- name: Log in to Gitea Registry
|
||||
uses: docker/login-action@v3
|
||||
with:
|
||||
registry: gitea.pockle.world
|
||||
username: ${{ secrets.USER_GITEA }}
|
||||
password: ${{ secrets.TOKEN_GITEA }}
|
||||
|
||||
- name: Determine Docker Tag
|
||||
id: docker_tag
|
||||
run: |
|
||||
if [[ "${{ github.ref }}" =~ ^refs/tags/v.* ]]; then
|
||||
TAG=$(echo "${{ github.ref }}" | sed 's#refs/tags/##')
|
||||
echo "tag=$TAG" >> $GITHUB_OUTPUT
|
||||
else
|
||||
echo "tag=latest" >> $GITHUB_OUTPUT
|
||||
fi
|
||||
|
||||
- name: Build and Push Docker Image
|
||||
uses: docker/build-push-action@v6
|
||||
with:
|
||||
context: .
|
||||
file: ./Dockerfile
|
||||
push: true
|
||||
tags: gitea.pockle.world/john/prosperon:${{ steps.docker_tag.outputs.tag }}
|
||||
platforms: linux/amd64
|
||||
|
||||
# ──────────────────────────────────────────────────────────────
|
||||
# WINDOWS BUILD (MSYS2 / CLANG64)
|
||||
# ──────────────────────────────────────────────────────────────
|
||||
build-windows:
|
||||
runs-on: win-native
|
||||
strategy:
|
||||
matrix: { msystem: [ CLANG64 ] }
|
||||
matrix:
|
||||
msystem: [CLANG64]
|
||||
|
||||
steps:
|
||||
- name: Check Out Code
|
||||
@@ -92,26 +61,32 @@ jobs:
|
||||
update: true
|
||||
cache: true
|
||||
install: |
|
||||
git zip gzip tar base-devel
|
||||
git
|
||||
zip
|
||||
gzip
|
||||
tar
|
||||
base-devel
|
||||
pacboy: |
|
||||
meson
|
||||
cmake
|
||||
toolchain
|
||||
|
||||
- name: Build Prosperon (Windows)
|
||||
- name: Build Prosperon
|
||||
shell: msys2 {0}
|
||||
run: |
|
||||
meson setup build -Dbuildtype=release -Db_lto=true -Db_lto_mode=thin -Db_ndebug=true -Dtracy:only_localhost=true -Dtracy:no_broadcast=true
|
||||
meson setup build -Dbuildtype=release -Db_lto=true -Db_ndebug=true -Dtracy:only_localhost=true -Dtracy:no_broadcast=true
|
||||
meson compile -C build
|
||||
|
||||
- name: Test Prosperon (Windows)
|
||||
- name: Test Prosperon
|
||||
shell: msys2 {0}
|
||||
continue-on-error: true
|
||||
env:
|
||||
TRACY_NO_INVARIANT_CHECK: 1
|
||||
run: |
|
||||
meson test --print-errorlogs -C build
|
||||
strip build/prosperon.exe
|
||||
|
||||
- name: Upload Test Log (Windows)
|
||||
- name: Upload Test Log
|
||||
if: ${{ always() }}
|
||||
uses: actions/upload-artifact@v3
|
||||
with:
|
||||
@@ -125,52 +100,15 @@ jobs:
|
||||
name: prosperon-artifacts-windows
|
||||
path: build/prosperon.exe
|
||||
|
||||
# ──────────────────────────────────────────────────────────────
|
||||
# MACOS BUILD
|
||||
# ──────────────────────────────────────────────────────────────
|
||||
build-macos:
|
||||
runs-on: macos-latest
|
||||
|
||||
steps:
|
||||
- name: Check Out Code
|
||||
uses: actions/checkout@v4
|
||||
with: { fetch-depth: 0 }
|
||||
|
||||
- name: Build Prosperon (macOS)
|
||||
run: |
|
||||
meson setup build -Dbuildtype=release -Db_lto=true -Db_lto_mode=thin -Db_ndebug=true
|
||||
meson compile -C build
|
||||
|
||||
- name: Test Prosperon (macOS)
|
||||
run: |
|
||||
meson test --print-errorlogs -C build
|
||||
|
||||
- name: Upload Test Log (macOS)
|
||||
if: ${{ always() }}
|
||||
uses: actions/upload-artifact@v3
|
||||
with:
|
||||
name: testlog-macos
|
||||
path: build/meson-logs/testlog.txt
|
||||
|
||||
- name: Upload Artifact (macOS)
|
||||
if: startsWith(github.ref, 'refs/tags/v')
|
||||
uses: actions/upload-artifact@v3
|
||||
with:
|
||||
name: prosperon-artifacts-macos
|
||||
path: build/prosperon
|
||||
|
||||
# ──────────────────────────────────────────────────────────────
|
||||
# PACKAGE CROSS-PLATFORM DIST
|
||||
# ──────────────────────────────────────────────────────────────
|
||||
package-dist:
|
||||
needs: [ build-linux, build-windows, build-macos ]
|
||||
needs: [build-linux, build-windows]
|
||||
if: startsWith(github.ref, 'refs/tags/v')
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- name: Check Out Code
|
||||
uses: actions/checkout@v3
|
||||
with: { fetch-depth: 0 }
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Get Latest Tag
|
||||
id: get_tag
|
||||
@@ -190,21 +128,16 @@ jobs:
|
||||
name: prosperon-artifacts-windows
|
||||
path: windows_artifacts
|
||||
|
||||
- name: Download macOS Artifacts
|
||||
uses: actions/download-artifact@v3
|
||||
with:
|
||||
name: prosperon-artifacts-macos
|
||||
path: mac_artifacts
|
||||
|
||||
- name: Create Dist Folder
|
||||
- name: Create the Dist Folder
|
||||
run: |
|
||||
mkdir -p dist/linux dist/win dist/mac
|
||||
cp README.md dist/
|
||||
cp license.txt dist/
|
||||
cp -r examples dist/
|
||||
cp linux_artifacts/* dist/linux/
|
||||
mkdir dist
|
||||
cp README.md dist/
|
||||
cp license.txt dist/
|
||||
cp -r examples dist/
|
||||
mkdir dist/linux
|
||||
mkdir dist/win
|
||||
cp linux_artifacts/* dist/linux/
|
||||
cp windows_artifacts/* dist/win/
|
||||
cp mac_artifacts/* dist/mac/
|
||||
|
||||
- name: Package Final Dist
|
||||
run: |
|
||||
@@ -218,17 +151,15 @@ jobs:
|
||||
name: "prosperon-${{ steps.get_tag.outputs.tag }}"
|
||||
path: "prosperon-${{ steps.get_tag.outputs.tag }}.zip"
|
||||
|
||||
# ──────────────────────────────────────────────────────────────
|
||||
# DEPLOY TO ITCH.IO (single ZIP containing all OSes)
|
||||
# ──────────────────────────────────────────────────────────────
|
||||
deploy-itch:
|
||||
needs: [ package-dist ]
|
||||
needs: [package-dist]
|
||||
if: ${{ false }}
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- name: Check Out Code
|
||||
uses: actions/checkout@v3
|
||||
with: { fetch-depth: 0 }
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Get Latest Tag
|
||||
id: get_tag
|
||||
@@ -247,30 +178,25 @@ jobs:
|
||||
|
||||
- name: Push to itch.io
|
||||
run: |
|
||||
butler push "dist/prosperon-${{ steps.get_tag.outputs.tag }}.zip" \
|
||||
${{ secrets.ITCHIO_USERNAME }}/prosperon:universal \
|
||||
--userversion ${{ steps.get_tag.outputs.tag }}
|
||||
butler push "dist/prosperon-${{ steps.get_tag.outputs.tag }}.zip" ${{ secrets.ITCHIO_USERNAME }}/prosperon:win-linux --userversion ${{ steps.get_tag.outputs.tag }}
|
||||
env:
|
||||
BUTLER_API_KEY: ${{ secrets.ITCHIO_API_KEY }}
|
||||
|
||||
# ──────────────────────────────────────────────────────────────
|
||||
# DEPLOY TO SELF-HOSTED GITEA
|
||||
# ──────────────────────────────────────────────────────────────
|
||||
deploy-gitea:
|
||||
needs: [ package-dist ]
|
||||
needs: [package-dist]
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- name: Check Out Code
|
||||
uses: actions/checkout@v3
|
||||
with: { fetch-depth: 0 }
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Get Latest Tag & Commit Message
|
||||
- name: Get Latest Tag and Commit Message
|
||||
id: get_tag
|
||||
run: |
|
||||
TAG=$(git describe --tags --abbrev=0)
|
||||
COMMIT_MSG=$(git log -1 --pretty=%B "$TAG")
|
||||
echo "tag=$TAG" >> $GITHUB_OUTPUT
|
||||
COMMIT_MSG=$(git log -1 --pretty=%B "${TAG}")
|
||||
echo "tag=$TAG" >> $GITHUB_OUTPUT
|
||||
echo "commit_msg=$COMMIT_MSG" >> $GITHUB_OUTPUT
|
||||
|
||||
- name: Download Final Distribution
|
||||
@@ -279,26 +205,39 @@ jobs:
|
||||
name: "prosperon-${{ steps.get_tag.outputs.tag }}"
|
||||
path: dist
|
||||
|
||||
- name: Create / Update Gitea Release
|
||||
- name: Create or Update Gitea Release
|
||||
run: |
|
||||
TAG=${{ steps.get_tag.outputs.tag }}
|
||||
ZIP=dist/prosperon-${TAG}.zip
|
||||
BODY=$(echo "${{ steps.get_tag.outputs.commit_msg }}" | jq -R -s '.')
|
||||
RELEASE=$(curl -s -H "Authorization: token ${{ secrets.TOKEN_GITEA }}" \
|
||||
"https://gitea.pockle.world/api/v1/repos/john/prosperon/releases/tags/$TAG" | jq -r '.id')
|
||||
ZIP_FILE="dist/prosperon-${TAG}.zip"
|
||||
COMMIT_MSG=$(echo "${{ steps.get_tag.outputs.commit_msg }}" | jq -R -s '.')
|
||||
|
||||
if [ "$RELEASE" = "null" ] || [ -z "$RELEASE" ]; then
|
||||
RELEASE=$(curl -X POST \
|
||||
# Check if release exists
|
||||
RELEASE_ID=$(curl -s -H "Authorization: token ${{ secrets.TOKEN_GITEA }}" \
|
||||
"https://gitea.pockle.world/api/v1/repos/john/prosperon/releases/tags/${TAG}" | jq -r '.id')
|
||||
|
||||
if [ -z "$RELEASE_ID" ] || [ "$RELEASE_ID" == "null" ]; then
|
||||
# Create a new release if it doesn't exist
|
||||
echo "Creating new release for tag ${TAG}"
|
||||
RELEASE_ID=$(curl -X POST \
|
||||
-H "Authorization: token ${{ secrets.TOKEN_GITEA }}" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d "{\"tag_name\":\"$TAG\",\"target_commitish\":\"${{ github.sha }}\",\"name\":\"$TAG\",\"body\":$BODY,\"draft\":false,\"prerelease\":false}" \
|
||||
-d "{\"tag_name\":\"${TAG}\",\"target_commitish\":\"${{ github.sha }}\",\"name\":\"${TAG}\",\"body\":${COMMIT_MSG},\"draft\":false,\"prerelease\":false}" \
|
||||
"https://gitea.pockle.world/api/v1/repos/john/prosperon/releases" | jq -r '.id')
|
||||
|
||||
if [ -z "$RELEASE_ID" ] || [ "$RELEASE_ID" == "null" ]; then
|
||||
echo "Failed to create release for tag ${TAG}"
|
||||
exit 1
|
||||
fi
|
||||
echo "Created release with ID: ${RELEASE_ID}"
|
||||
else
|
||||
echo "Release already exists with ID: ${RELEASE_ID}"
|
||||
fi
|
||||
|
||||
# Upload the zip file as an asset
|
||||
curl -X POST \
|
||||
-H "Authorization: token ${{ secrets.TOKEN_GITEA }}" \
|
||||
-H "Content-Type: application/octet-stream" \
|
||||
--data-binary @"$ZIP" \
|
||||
"https://gitea.pockle.world/api/v1/repos/john/prosperon/releases/$RELEASE/assets?name=prosperon-${TAG}.zip"
|
||||
--data-binary @"${ZIP_FILE}" \
|
||||
"https://gitea.pockle.world/api/v1/repos/john/prosperon/releases/${RELEASE_ID}/assets?name=prosperon-${TAG}.zip"
|
||||
env:
|
||||
TOKEN_GITEA: ${{ secrets.TOKEN_GITEA }}
|
||||
TOKEN_GITEA: ${{ secrets.TOKEN_GITEA }}
|
||||
64
.github/workflows/macbuild.yml
vendored
Normal file
@@ -0,0 +1,64 @@
|
||||
name: Build
|
||||
|
||||
jobs:
|
||||
# ===============================================================
|
||||
# MACOS BUILD (Using Homebrew SDL3)
|
||||
# ===============================================================
|
||||
build-macos:
|
||||
runs-on: macos-latest
|
||||
continue-on-error: true
|
||||
steps:
|
||||
# 1) Check out code
|
||||
- name: Check Out Code
|
||||
uses: actions/checkout@v3
|
||||
|
||||
# 2) Install dependencies (SDL3 via Homebrew) + ccache
|
||||
- name: Install Dependencies (macOS)
|
||||
run: |
|
||||
brew update
|
||||
brew install sdl3 meson ninja cmake ccache
|
||||
|
||||
# 3) Configure ccache
|
||||
- name: Configure ccache
|
||||
run: |
|
||||
echo "CMAKE_C_COMPILER_LAUNCHER=ccache" >> $GITHUB_ENV
|
||||
echo "CMAKE_CXX_COMPILER_LAUNCHER=ccache" >> $GITHUB_ENV
|
||||
|
||||
# 4) Cache ccache
|
||||
- name: Cache ccache
|
||||
uses: actions/cache@v3
|
||||
with:
|
||||
path: ~/Library/Caches/ccache
|
||||
key: ccache-macos-${{ hashFiles('**/*.c', '**/*.cpp', '**/*.h', '**/CMakeLists.txt', '**/meson.build') }}
|
||||
restore-keys: |
|
||||
ccache-macos-
|
||||
|
||||
# 5) Build Prosperon (macOS) linking against Homebrew's SDL3
|
||||
- name: Build Prosperon (macOS)
|
||||
run: |
|
||||
# Ensure pkg-config can find Homebrew's SDL3 .pc files
|
||||
export PKG_CONFIG_PATH="$(brew --prefix sdl3)/lib/pkgconfig:$PKG_CONFIG_PATH"
|
||||
meson setup build_macos -Dbuildtype=release -Db_lto=true -Db_ndebug=true
|
||||
meson compile -C build_macos
|
||||
|
||||
# 6) Copy SDL3 .dylib from Homebrew for packaging
|
||||
- name: Copy SDL3 library for packaging
|
||||
run: |
|
||||
SDL3_PREFIX=$(brew --prefix sdl3)
|
||||
mkdir -p sdl3-macos
|
||||
# Copy all versions of the SDL3 dynamic library
|
||||
cp -a "${SDL3_PREFIX}/lib/libSDL3*.dylib" sdl3-macos/ || echo "No .dylib found, ignoring"
|
||||
|
||||
# 7) Create minimal artifact folder (macOS)
|
||||
- name: Create artifact folder (macOS)
|
||||
run: |
|
||||
mkdir _pack
|
||||
cp build_macos/prosperon _pack/
|
||||
cp sdl3-macos/libSDL3*.dylib _pack/ || echo "No .dylib found, ignoring"
|
||||
|
||||
# 8) Upload artifact (macOS)
|
||||
- name: Upload Artifact (macOS)
|
||||
uses: actions/upload-artifact@v3
|
||||
with:
|
||||
name: prosperon-artifacts-macos
|
||||
path: _pack
|
||||
1
.gitignore
vendored
@@ -29,4 +29,3 @@ game.zip
|
||||
icon.ico
|
||||
steam/
|
||||
subprojects/*/
|
||||
build_dbg/
|
||||
27
AGENTS.md
@@ -1,27 +0,0 @@
|
||||
# AGENTS.md
|
||||
|
||||
## Project Overview
|
||||
This is a game engine developed using a QuickJS fork as its scripting language. It is an actor based system, based on Douglas Crockford's Misty. It is a Meson compiled project with a number of dependencies.
|
||||
|
||||
## File Structure
|
||||
- `source/`: Contains the C source code
|
||||
- `scripts/`: Contains script code that is loaded on executable start, and modules
|
||||
- `shaders/`: Contains shaders that ship with the engine (for shader based backends)
|
||||
- `benchmarks/`: Benchmark programs for testing speed
|
||||
- `tests/`: Unit tests
|
||||
- `examples/`: Contains full game examples
|
||||
|
||||
## Coding Practices
|
||||
- Use K&R style C
|
||||
- Use as little whitespace as possible
|
||||
- Javascript style prefers objects and prototypical inheritence over ES6 classes, liberal use of closures, and var everywhere
|
||||
|
||||
## Instructions
|
||||
- When generating code, adhere to the coding practices outlined above.
|
||||
- When adding new features, ensure they align with the project's goals.
|
||||
- When fixing bugs, review the code carefully before making changes.
|
||||
- When writing unit tests, cover all important scenarios.
|
||||
|
||||
## Compiling, running, and testing
|
||||
- To compile the code, run "make", which generates a prosperon executable in build_dbg/, and copy it into the root folder
|
||||
- Run a test by giving it as its command: so ./prosperon tests/overling.js would run the test overling.js, ./prosperon tests/nota.js runs the nota benchmark
|
||||
400
CLAUDE.md
@@ -1,400 +0,0 @@
|
||||
# CLAUDE.md
|
||||
|
||||
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
|
||||
|
||||
## Build Commands
|
||||
|
||||
### Build variants
|
||||
- `make debug` - Build debug version (uses meson debug configuration)
|
||||
- `make fast` - Build optimized version
|
||||
- `make release` - Build release version with LTO and optimizations
|
||||
- `make small` - Build minimal size version
|
||||
- `make web` - Build for web/emscripten platform
|
||||
- `make crosswin` - Cross-compile for Windows using mingw32
|
||||
|
||||
### Testing
|
||||
- `meson test -C build_dbg` - Run all tests in debug build
|
||||
- `meson test -C build_<variant>` - Run tests in specific build variant
|
||||
- `./build_dbg/prosperon tests/<testname>.js` - Run specific test
|
||||
- Available tests: `spawn_actor`, `empty`, `nota`, `wota`, `portalspawner`, `overling`, `send`, `delay`
|
||||
|
||||
### Common development commands
|
||||
- `meson setup build_<variant>` - Configure build directory
|
||||
- `meson compile -C build_<variant>` - Compile in build directory
|
||||
- `./build_dbg/prosperon examples/<example>` - Run example from build directory
|
||||
- Copy prosperon to game directory and run: `cp build_dbg/prosperon <game-dir>/ && cd <game-dir> && ./prosperon`
|
||||
|
||||
## Architecture Overview
|
||||
|
||||
Prosperon is an actor-based game engine inspired by Douglas Crockford's Misty system. Key architectural principles:
|
||||
|
||||
### Actor Model
|
||||
- Each actor runs on its own thread
|
||||
- Communication only through message passing (no shared JavaScript objects)
|
||||
- Hierarchical actor system with spawning/killing
|
||||
- Actor lifecycle: awake, update, draw, garbage collection
|
||||
|
||||
### JavaScript Style Guide
|
||||
- Use `use()` function for imports (Misty-style, not ES6 import/export)
|
||||
- Prefer closures and javascript objects and prototypes over ES6 style classes
|
||||
- Follow existing JavaScript patterns in the codebase
|
||||
- Functions as first-class citizens
|
||||
- Do not use const or let; only var
|
||||
|
||||
### Core Systems
|
||||
1. **Actor System** (scripts/core/engine.js)
|
||||
- Message passing via `send()`, `$_.receive()`
|
||||
- Actor spawning/management
|
||||
- Register-based component system (update, draw, gui, etc.)
|
||||
|
||||
2. **Module System**
|
||||
- `use()` function for loading modules
|
||||
- Module paths: `scripts/modules/`, `scripts/modules/ext/`
|
||||
- Custom QuickJS build with embedded C modules
|
||||
|
||||
3. **Build System**
|
||||
- Meson build configuration (Makefile is convenience wrapper)
|
||||
- Multiple platform targets (Windows, macOS, Linux, Web)
|
||||
- Custom QuickJS build in `subprojects/`
|
||||
- Uses SDL3 for cross-platform support
|
||||
|
||||
### Engine Entry Points
|
||||
- `source/prosperon.c` - Main C entry point
|
||||
- `scripts/core/engine.js` - JavaScript engine initialization for system
|
||||
- `scripts/core/base.js` has modifications to this Javascript runtime (for example, additions to the base Array, String, etc)
|
||||
|
||||
### Subprojects
|
||||
- C code has many subprojects, who's source and sometimes documentation can be found in subprojects. subprojects/quickjs/doc has documentation for quickjs
|
||||
|
||||
### Resource System
|
||||
- Scripts are bundled into `core.zip` during build
|
||||
- Runtime module loading via PhysFS
|
||||
- Resource paths checked in order: `/`, `scripts/modules/`, `scripts/modules/ext/`
|
||||
|
||||
### Notable Dependencies
|
||||
- QuickJS (custom build) - JavaScript runtime
|
||||
- SDL3 - Platform abstraction
|
||||
- Chipmunk2D - Physics
|
||||
- ENet - Networking
|
||||
- Soloud - Audio
|
||||
- Tracy - Profiling (when enabled)
|
||||
|
||||
## Development Tips
|
||||
|
||||
### Running Games
|
||||
```bash
|
||||
# Build first
|
||||
make debug
|
||||
|
||||
# Run example from build directory
|
||||
./build_dbg/prosperon examples/chess
|
||||
|
||||
# Or copy to game directory
|
||||
cp build_dbg/prosperon examples/chess/
|
||||
cd examples/chess
|
||||
./prosperon
|
||||
```
|
||||
|
||||
### Documentation
|
||||
- Documentation is found in docs
|
||||
- Documentation for the JS modules loaded with 'use' is docs/api/modules
|
||||
- .md files directly in docs gives a high level overview
|
||||
- docs/dull is what this specific Javascript system is (including alterations from quickjs/es6)
|
||||
|
||||
### Shader Development
|
||||
- Shaders are in `shaders/` directory as HLSL
|
||||
- Compile script: `shaders/compile.sh`
|
||||
- Outputs to platform-specific formats: `dxil/`, `msl/`, `spv/`
|
||||
|
||||
### Example Games
|
||||
Located in `examples/` directory:
|
||||
- `chess` - Chess implementation (has its own Makefile)
|
||||
- `pong` - Classic pong game
|
||||
- `snake` - Snake game
|
||||
- `tetris` - Tetris clone
|
||||
- `bunnymark` - Performance test
|
||||
|
||||
### Testing
|
||||
```bash
|
||||
# Run all tests
|
||||
meson test -C build_dbg
|
||||
|
||||
# Run specific test
|
||||
./build_dbg/prosperon tests/spawn_actor.js
|
||||
```
|
||||
|
||||
### Debugging
|
||||
- Use debug build: `make debug`
|
||||
- Tracy profiler support when enabled
|
||||
- Console logging available via `console.log()`, `console.error()`, etc.
|
||||
- Log files written to `.prosperon/log.txt`
|
||||
|
||||
# Project Structure Notes
|
||||
|
||||
## Core JavaScript Modules
|
||||
|
||||
- JavaScript modules are defined using the MISTUSE macro in jsffi.c
|
||||
- The `js_os_funcs`, `js_io_funcs`, etc. arrays define the available functions for each module
|
||||
- New functions are added with MIST_FUNC_DEF(module, function, args_count)
|
||||
|
||||
## File I/O
|
||||
|
||||
- `io.slurp(path)` - Reads a file as text
|
||||
- `io.slurpbytes(path)` - Reads a file as an ArrayBuffer
|
||||
- `io.slurpwrite(path, data)` - Writes data (string or ArrayBuffer) to a file
|
||||
- `io.exists(path)` - Checks if a file exists
|
||||
|
||||
## Script Loading
|
||||
|
||||
- The `use(path)` function in engine.js loads JavaScript modules
|
||||
- Script loading happens in prosperon.c and the engine.js script
|
||||
- jsffi.c contains the C hooks for the QuickJS JavaScript engine
|
||||
- Added functionality for bytecode compilation and loading:
|
||||
- `os.compile_bytecode(source, filename)` - Compiles JS to bytecode, returns ArrayBuffer
|
||||
- `os.eval_bytecode(bytecode)` - Evaluates bytecode from an ArrayBuffer
|
||||
- `compile(scriptPath)` - Compiles a JS file to a .jso bytecode file
|
||||
- Modified `use()` to check for .jso files before loading .js files
|
||||
|
||||
## QuickJS Bytecode API
|
||||
|
||||
- `JS_Eval` with JS_EVAL_FLAG_COMPILE_ONLY - Compiles without executing
|
||||
- `JS_WriteObject` with JS_WRITE_OBJ_BYTECODE - Serializes to bytecode
|
||||
- `JS_ReadObject` with JS_READ_OBJ_BYTECODE - Deserializes and loads bytecode
|
||||
- Bytecode files use .jso extension alongside .js files
|
||||
|
||||
## Available JavaScript APIs
|
||||
|
||||
### Core APIs
|
||||
- `actor` - Base prototype for all actor objects
|
||||
- `$_` - Special global for actor messaging
|
||||
- `prosperon` - Global engine interface
|
||||
- `console` - Logging and debugging interface
|
||||
|
||||
### Framework APIs
|
||||
- `moth` - Higher-level game framework that simplifies Prosperon usage
|
||||
- Handles window creation, game loop, and event dispatching
|
||||
- Provides simple configuration via config.js
|
||||
- Auto-initializes systems like rendering and input
|
||||
- Manages camera, resolution, and FPS automatically
|
||||
|
||||
### Rendering
|
||||
- `draw2d` - 2D drawing primitives
|
||||
- `render` - Low-level rendering operations
|
||||
- `graphics` - Higher-level graphics utilities
|
||||
- `camera` - Camera controls and transformations
|
||||
- `sprite` - Sprite rendering and management
|
||||
|
||||
### Physics and Math
|
||||
- `math` - Mathematical utilities
|
||||
- `geometry` - Geometric calculations and shapes
|
||||
- `transform` - Object transformations
|
||||
|
||||
### Input and Events
|
||||
- `input` - Mouse, keyboard, and touch handling
|
||||
- `event` - Event management system
|
||||
|
||||
### Networking
|
||||
- `enet` - Networking through ENet library
|
||||
- `http` - HTTP client capabilities
|
||||
|
||||
### Audio
|
||||
- `sound` - Audio playback using SoLoud
|
||||
|
||||
### Utility Modules
|
||||
- `time` - Time management and delays
|
||||
- `io` - File I/O operations
|
||||
- `json` - JSON parsing and serialization
|
||||
- `util` - General utilities
|
||||
- `color` - Color manipulation
|
||||
- `miniz` - Compression utilities
|
||||
- `nota` - Structured data format
|
||||
- `wota` - Serialization format
|
||||
- `qr` - QR code generation/reading
|
||||
- `tween` - Animation tweening
|
||||
- `spline` - Spline calculations
|
||||
- `imgui` - Immediate mode GUI
|
||||
|
||||
## Game Development Patterns
|
||||
|
||||
### Project Structure
|
||||
- Game config is typically in `config.js`
|
||||
- Main entry point is `main.js`
|
||||
- Resource loading through `resources.js`
|
||||
|
||||
### Actor Pattern Usage
|
||||
- Create actors with `actor.spawn(script, config)`
|
||||
- Start actors with `$_.start(callback, script)` - the system automatically sends a greeting, callback receives {type: 'greet', actor: actor_ref}
|
||||
- No need to manually send greetings - `$_.start` handles this automatically
|
||||
- Manage actor hierarchy with overlings and underlings
|
||||
- Schedule actor tasks with `$_.delay()` method
|
||||
- Clean up with `kill()` and `garbage()`
|
||||
|
||||
### Actor Messaging with Callbacks
|
||||
When sending a message with a callback, respond by sending to the message itself:
|
||||
```javascript
|
||||
// Sender side:
|
||||
send(actor, {type: 'status'}, response => {
|
||||
console.log(response); // Handle the response
|
||||
});
|
||||
|
||||
// Receiver side:
|
||||
$_.receiver(msg => {
|
||||
if (msg.type === 'status') {
|
||||
send(msg, {status: 'ok'}); // Send response to the message itself
|
||||
}
|
||||
});
|
||||
```
|
||||
|
||||
**Critical Rules for Message Callbacks**:
|
||||
- **A message can only be used ONCE as a send target** - after sending a response to a message, it cannot be used again
|
||||
- If you need to send multiple updates (like progress), only the download request message should be used for the final response
|
||||
- Status requests should each get their own individual response
|
||||
- Actor objects and message headers are completely opaque - never try to access internal properties
|
||||
- Never access `msg.__HEADER__` or similar - the actor system handles routing internally
|
||||
- Use `$_.delay()` to schedule work and avoid blocking the message receiver
|
||||
|
||||
### Game Loop Registration
|
||||
- Register functions like `update`, `draw`, `gui`, etc.
|
||||
- Set function.layer property to control execution order
|
||||
- Use `Register` system to manage callbacks
|
||||
|
||||
### Program vs Module Pattern
|
||||
- Programs are actor scripts that don't return values, they execute top-to-bottom
|
||||
- Modules are files that return single values (usually objects) that get frozen
|
||||
- Programs can spawn other programs as underlings
|
||||
- Programs have lifecycle hooks: awake, update, draw, garbage, etc.
|
||||
|
||||
## Technical Capabilities
|
||||
|
||||
### Graphics Pipeline
|
||||
- Supports multiple render backends (Direct3D, Metal, Vulkan via SDL3)
|
||||
- Custom shader system with cross-platform compilation
|
||||
- Sprite batching for efficient 2D rendering
|
||||
- Camera systems for both 2D and 3D
|
||||
|
||||
### Asset Support
|
||||
- Images: PNG, JPG, QOI, etc.
|
||||
- Audio: Various formats through SoLoud
|
||||
- Models: Basic 3D model support
|
||||
- Custom formats: Aseprite animations, etc.
|
||||
|
||||
### Developer Tools
|
||||
- Built-in documentation system with `prosperon.DOC`
|
||||
- Tracy profiler integration for performance monitoring
|
||||
- Imgui debugging tools
|
||||
- Console logging with various severity levels
|
||||
|
||||
## Misty Networking Patterns
|
||||
|
||||
Prosperon implements the Misty actor networking model. Understanding these patterns is critical for building distributed applications.
|
||||
|
||||
### Portal Reply Pattern
|
||||
Portals must reply with an actor object, not application data:
|
||||
```javascript
|
||||
// CORRECT: Portal replies with actor
|
||||
$_.portal(e => {
|
||||
send(e, $_); // Reply with server actor
|
||||
}, 5678);
|
||||
|
||||
// WRONG: Portal sends application data
|
||||
$_.portal(e => {
|
||||
send(e, {type: 'game_start'}); // This breaks the pattern
|
||||
}, 5678);
|
||||
```
|
||||
|
||||
### Two-Phase Connection Protocol
|
||||
Proper Misty networking follows a two-phase pattern:
|
||||
|
||||
**Phase 1: Actor Connection**
|
||||
- Client contacts portal using `$_.contact()`
|
||||
- Portal replies with an actor object
|
||||
- This establishes the communication channel
|
||||
|
||||
**Phase 2: Application Communication**
|
||||
- Client sends application messages to the received actor
|
||||
- Normal bidirectional messaging begins
|
||||
- Application logic handles game/service initialization
|
||||
|
||||
### Message Handling Best Practices
|
||||
Messages should be treated as opaque objects with your application data:
|
||||
|
||||
```javascript
|
||||
// CORRECT: Store actor references separately
|
||||
var players = {};
|
||||
$_.receiver(msg => {
|
||||
if (msg.type === 'join_game' && msg.player_id) {
|
||||
// Store the message for later response
|
||||
players[msg.player_id] = msg;
|
||||
// Later, respond to the stored message
|
||||
send(players[msg.player_id], {type: 'game_start'});
|
||||
}
|
||||
});
|
||||
|
||||
// WRONG: Trying to access internal message properties
|
||||
$_.receiver(msg => {
|
||||
var sender = msg.__HEADER__.replycc; // Never do this!
|
||||
});
|
||||
```
|
||||
|
||||
### Return ID Lifecycle
|
||||
- Each reply callback gets a unique return ID
|
||||
- Return IDs are consumed once and then deleted
|
||||
- Reusing message objects with return headers causes "Could not find return function" errors
|
||||
- Always create clean actor references for ongoing communication
|
||||
|
||||
### Actor Object Transparency
|
||||
Actor objects must be completely opaque black boxes that work identically regardless of transport:
|
||||
|
||||
```javascript
|
||||
// Actor objects work transparently for:
|
||||
// - Same-process communication (fastest - uses mailbox)
|
||||
// - Inter-process communication (uses mailbox)
|
||||
// - Network communication (uses ENet)
|
||||
|
||||
// The actor shouldn't know or care about the transport mechanism
|
||||
send(opponent, {type: 'move', from: [0,0], to: [1,1]});
|
||||
```
|
||||
|
||||
**Key Implementation Details:**
|
||||
- `actor_send()` in `scripts/core/engine.js` handles routing based on available actor data
|
||||
- Actor objects sent in message data automatically get address/port populated when received over network
|
||||
- Three communication pathways: `os.mailbox_exist()` check → mailbox send → network send
|
||||
- Actor objects must contain all necessary routing information for transparent messaging
|
||||
|
||||
### Common Networking Bugs
|
||||
1. **Portal sending application data**: Portal should only establish actor connections
|
||||
2. **Return ID collision**: Reusing messages with return headers for multiple sends
|
||||
3. **Mixed phases**: Trying to do application logic during connection establishment
|
||||
4. **Header pollution**: Using received message objects as actor references
|
||||
5. **Missing actor address info**: Actor objects in message data need network address population (fixed in engine.js:746-766)
|
||||
|
||||
### Example: Correct Chess Networking
|
||||
```javascript
|
||||
// Server: Portal setup
|
||||
$_.portal(e => {
|
||||
send(e, $_); // Just reply with actor
|
||||
}, 5678);
|
||||
|
||||
// Client: Two-phase connection
|
||||
$_.contact((actor, reason) => {
|
||||
if (actor) {
|
||||
opponent = actor;
|
||||
send(opponent, {type: 'join_game'}); // Phase 2: app messaging
|
||||
}
|
||||
}, {address: "localhost", port: 5678});
|
||||
|
||||
// Server: Handle application messages
|
||||
$_.receiver(e => {
|
||||
if (e.type === 'join_game') {
|
||||
opponent = e.__HEADER__.replycc;
|
||||
send(opponent, {type: 'game_start', your_color: 'black'});
|
||||
}
|
||||
});
|
||||
```
|
||||
|
||||
## Memory Management
|
||||
|
||||
- When working with a conversational AI system like Claude, it's important to maintain a clean and focused memory
|
||||
- Regularly review and update memories to ensure they remain relevant and helpful
|
||||
- Delete or modify memories that are no longer accurate or useful
|
||||
- Prioritize information that can genuinely assist in future interactions
|
||||
54
Dockerfile
@@ -1,54 +0,0 @@
|
||||
# Builder stage
|
||||
FROM ubuntu:plucky AS builder
|
||||
|
||||
RUN apt-get update && apt-get install -y --no-install-recommends \
|
||||
python3 python3-pip \
|
||||
libasound2-dev \
|
||||
libpulse-dev \
|
||||
libudev-dev \
|
||||
libwayland-dev \
|
||||
wayland-protocols \
|
||||
libxkbcommon-dev \
|
||||
libx11-dev \
|
||||
libxext-dev \
|
||||
libxrandr-dev \
|
||||
libxcursor-dev \
|
||||
libxi-dev \
|
||||
libxinerama-dev \
|
||||
libxss-dev \
|
||||
libegl1-mesa-dev \
|
||||
libgl1-mesa-dev \
|
||||
cmake \
|
||||
ninja-build \
|
||||
git \
|
||||
build-essential \
|
||||
binutils \
|
||||
pkg-config \
|
||||
meson \
|
||||
zip && \
|
||||
rm -rf /var/lib/apt/lists/*
|
||||
|
||||
WORKDIR /app
|
||||
RUN git clone https://gitea.pockle.world/john/prosperon.git
|
||||
WORKDIR /app/prosperon
|
||||
RUN git checkout jsffi_refactor
|
||||
RUN meson setup build -Dbuildtype=release -Db_lto=true -Db_lto_mode=thin -Db_ndebug=true
|
||||
RUN meson compile -C build
|
||||
|
||||
# Runtime stage
|
||||
FROM ubuntu:latest
|
||||
|
||||
# Install minimal runtime dependencies (e.g., for dynamically linked libraries)
|
||||
RUN apt-get update && apt-get install -y libstdc++6 && rm -rf /var/lib/apt/lists/*
|
||||
|
||||
# Copy the compiled prosperon binary from the build stage
|
||||
COPY --from=builder /app/prosperon/build/prosperon /usr/local/bin/prosperon
|
||||
|
||||
# Create an entrypoint script
|
||||
RUN echo '#!/bin/bash' > /entrypoint.sh && \
|
||||
echo '/usr/local/bin/prosperon "$@" &' >> /entrypoint.sh && \
|
||||
echo 'tail -f /dev/null' >> /entrypoint.sh && \
|
||||
chmod +x /entrypoint.sh
|
||||
|
||||
WORKDIR /workdir
|
||||
ENTRYPOINT ["/entrypoint.sh"]
|
||||
4
Makefile
@@ -1,5 +1,5 @@
|
||||
debug: FORCE
|
||||
meson setup build_dbg -Dbuildtype=debug
|
||||
meson setup build_dbg -Dbuildtype=debugoptimized
|
||||
meson compile -C build_dbg
|
||||
|
||||
fast: FORCE
|
||||
@@ -7,7 +7,7 @@ fast: FORCE
|
||||
meson compile -C build_fast
|
||||
|
||||
release: FORCE
|
||||
meson setup -Dbuildtype=release -Db_lto=true -Db_lto_mode=thin -Db_ndebug=true build_release
|
||||
meson setup -Dbuildtype=release -Db_lto=true -Db_ndebug=true build_release
|
||||
meson compile -C build_release
|
||||
|
||||
sanitize: FORCE
|
||||
|
||||
@@ -2,6 +2,6 @@ Thank you for using Prosperon!
|
||||
|
||||
Provided are prosperon builds for all available platforms. Simply run prosperon for your platform in a game folder to play!
|
||||
|
||||
To get started, take a dive into the provided example games in the examples folder. You can either copy the prosperon executable into an example directory and run it there, or run `prosperon path/to/example` from the project root.
|
||||
To get started, take a dive into the provided example games in the examples folder. Just copy the prosperon executable for your platform, into any provided example folder, then run it!
|
||||
|
||||
You can take a look through the docs folder for the prosperon manual to learn all about it. The manual is available on the web at [docs.prosperon.dev](https://docs.prosperon.dev).
|
||||
|
||||
@@ -1,76 +0,0 @@
|
||||
var nota = use('nota')
|
||||
var os = use('os')
|
||||
var io = use('io')
|
||||
|
||||
var ll = io.slurp('benchmarks/nota.json')
|
||||
|
||||
var newarr = []
|
||||
var accstr = ""
|
||||
for (var i = 0; i < 10000; i++) {
|
||||
accstr += i;
|
||||
newarr.push(i.toString())
|
||||
}
|
||||
// Arrays to store timing results
|
||||
var jsonDecodeTimes = [];
|
||||
var jsonEncodeTimes = [];
|
||||
var notaEncodeTimes = [];
|
||||
var notaDecodeTimes = [];
|
||||
var notaSizes = [];
|
||||
|
||||
// Run 100 tests
|
||||
for (let i = 0; i < 100; i++) {
|
||||
// JSON Decode test
|
||||
let start = os.now();
|
||||
var jll = json.decode(ll);
|
||||
jsonDecodeTimes.push((os.now() - start) * 1000);
|
||||
|
||||
// JSON Encode test
|
||||
start = os.now();
|
||||
let jsonStr = JSON.stringify(jll);
|
||||
jsonEncodeTimes.push((os.now() - start) * 1000);
|
||||
|
||||
// NOTA Encode test
|
||||
start = os.now();
|
||||
var nll = nota.encode(jll);
|
||||
notaEncodeTimes.push((os.now() - start) * 1000);
|
||||
|
||||
// NOTA Decode test
|
||||
start = os.now();
|
||||
var oll = nota.decode(nll);
|
||||
notaDecodeTimes.push((os.now() - start) * 1000);
|
||||
}
|
||||
|
||||
// Calculate statistics
|
||||
function getStats(arr) {
|
||||
const avg = arr.reduce((a, b) => a + b) / arr.length;
|
||||
const min = Math.min(...arr);
|
||||
const max = Math.max(...arr);
|
||||
return { avg, min, max };
|
||||
}
|
||||
|
||||
// Pretty print results
|
||||
console.log("\n=== Performance Test Results (100 iterations) ===");
|
||||
console.log("\nJSON Decoding (ms):");
|
||||
const jsonDecStats = getStats(jsonDecodeTimes);
|
||||
console.log(`Average: ${jsonDecStats.avg.toFixed(2)} ms`);
|
||||
console.log(`Min: ${jsonDecStats.min.toFixed(2)} ms`);
|
||||
console.log(`Max: ${jsonDecStats.max.toFixed(2)} ms`);
|
||||
|
||||
console.log("\nJSON Encoding (ms):");
|
||||
const jsonEncStats = getStats(jsonEncodeTimes);
|
||||
console.log(`Average: ${jsonEncStats.avg.toFixed(2)} ms`);
|
||||
console.log(`Min: ${jsonEncStats.min.toFixed(2)} ms`);
|
||||
console.log(`Max: ${jsonEncStats.max.toFixed(2)} ms`);
|
||||
|
||||
console.log("\nNOTA Encoding (ms):");
|
||||
const notaEncStats = getStats(notaEncodeTimes);
|
||||
console.log(`Average: ${notaEncStats.avg.toFixed(2)} ms`);
|
||||
console.log(`Min: ${notaEncStats.min.toFixed(2)} ms`);
|
||||
console.log(`Max: ${notaEncStats.max.toFixed(2)} ms`);
|
||||
|
||||
console.log("\nNOTA Decoding (ms):");
|
||||
const notaDecStats = getStats(notaDecodeTimes);
|
||||
console.log(`Average: ${notaDecStats.avg.toFixed(2)} ms`);
|
||||
console.log(`Min: ${notaDecStats.min.toFixed(2)} ms`);
|
||||
console.log(`Max: ${notaDecStats.max.toFixed(2)} ms`);
|
||||
|
||||
2132
benchmarks/nota.json
@@ -1,106 +0,0 @@
|
||||
//
|
||||
// wota_benchmark.js
|
||||
//
|
||||
// Usage in QuickJS:
|
||||
// qjs wota_benchmark.js
|
||||
//
|
||||
// Prerequisite:
|
||||
var wota = use('wota');
|
||||
var os = use('os');
|
||||
// or otherwise ensure `wota` and `os` are available.
|
||||
// Make sure wota_benchmark.js is loaded after wota.js or combined with it.
|
||||
//
|
||||
|
||||
// Helper to run a function repeatedly and measure total time in seconds.
|
||||
// Returns elapsed time in seconds.
|
||||
function measureTime(fn, iterations) {
|
||||
let t1 = os.now();
|
||||
for (let i = 0; i < iterations; i++) {
|
||||
fn();
|
||||
}
|
||||
let t2 = os.now();
|
||||
return t2 - t1;
|
||||
}
|
||||
|
||||
// We'll define a function that does `encode -> decode` for a given value:
|
||||
function roundTripWota(value) {
|
||||
let encoded = wota.encode(value);
|
||||
let decoded = wota.decode(encoded);
|
||||
// Not doing a deep compare here, just measuring performance.
|
||||
// (We trust the test suite to verify correctness.)
|
||||
}
|
||||
|
||||
// A small suite of data we want to benchmark. Each entry includes:
|
||||
// name: label for printing
|
||||
// data: the test value(s) to encode/decode
|
||||
// iterations: how many times to loop
|
||||
//
|
||||
// You can tweak these as you like for heavier or lighter tests.
|
||||
const benchmarks = [
|
||||
{
|
||||
name: "Small Integers",
|
||||
data: [0, 42, -1, 2023],
|
||||
iterations: 100000
|
||||
},
|
||||
{
|
||||
name: "Strings (short, emoji)",
|
||||
data: ["Hello, Wota!", "short", "Emoji: \u{1f600}\u{1f64f}"],
|
||||
iterations: 100000
|
||||
},
|
||||
{
|
||||
name: "Small Objects",
|
||||
data: [
|
||||
{ a:1, b:2.2, c:"3", d:false },
|
||||
{ x:42, y:null, z:"test" }
|
||||
],
|
||||
iterations: 50000
|
||||
},
|
||||
{
|
||||
name: "Nested Arrays",
|
||||
data: [ [ [ [1,2], [3,4] ] ], [[[]]], [1, [2, [3, [4]]]] ],
|
||||
iterations: 50000
|
||||
},
|
||||
{
|
||||
name: "Large Array (1k numbers)",
|
||||
// A thousand random numbers
|
||||
data: [ Array.from({length:1000}, (_, i) => i * 0.5) ],
|
||||
iterations: 1000
|
||||
},
|
||||
{
|
||||
name: "Large Binary Blob (256KB)",
|
||||
// A 256KB ArrayBuffer
|
||||
data: [ new Uint8Array(256 * 1024).buffer ],
|
||||
iterations: 200
|
||||
}
|
||||
];
|
||||
|
||||
// Print a header
|
||||
console.log("Wota Encode/Decode Benchmark");
|
||||
console.log("============================\n");
|
||||
|
||||
// We'll run each benchmark scenario in turn.
|
||||
for (let bench of benchmarks) {
|
||||
// We'll measure how long it takes to do 'iterations' *for each test value*
|
||||
// in bench.data. The total loop count is `bench.iterations * bench.data.length`.
|
||||
// Then we compute an overall encode+decode throughput (ops/s).
|
||||
let totalIterations = bench.iterations * bench.data.length;
|
||||
|
||||
// We'll define a function that does a roundTrip for *each* data item in bench.data
|
||||
// to measure in one loop iteration. Then we multiply by bench.iterations.
|
||||
function runAllData() {
|
||||
for (let val of bench.data) {
|
||||
roundTripWota(val);
|
||||
}
|
||||
}
|
||||
|
||||
let elapsedSec = measureTime(runAllData, bench.iterations);
|
||||
let opsPerSec = (totalIterations / elapsedSec).toFixed(1);
|
||||
|
||||
console.log(`${bench.name}:`);
|
||||
console.log(` Iterations: ${bench.iterations} × ${bench.data.length} data items = ${totalIterations}`);
|
||||
console.log(` Elapsed: ${elapsedSec.toFixed(3)} s`);
|
||||
console.log(` Throughput: ${opsPerSec} encode+decode ops/sec\n`);
|
||||
}
|
||||
|
||||
// All done
|
||||
console.log("Benchmark completed.\n");
|
||||
@@ -1,182 +0,0 @@
|
||||
//
|
||||
// benchmark_wota_nota_json.js
|
||||
//
|
||||
// Usage in QuickJS:
|
||||
// qjs benchmark_wota_nota_json.js
|
||||
//
|
||||
// Ensure wota, nota, json, and os are all available, e.g.:
|
||||
var wota = use('wota');
|
||||
var nota = use('nota');
|
||||
var json = use('json');
|
||||
var os = use('os');
|
||||
//
|
||||
|
||||
////////////////////////////////////////////////////////////////////////////////
|
||||
// 1. Setup "libraries" array to easily switch among Wota, Nota, and JSON
|
||||
////////////////////////////////////////////////////////////////////////////////
|
||||
|
||||
const libraries = [
|
||||
{
|
||||
name: "Wota",
|
||||
encode: wota.encode,
|
||||
decode: wota.decode,
|
||||
// Wota produces an ArrayBuffer. We'll count `buffer.byteLength` as size.
|
||||
getSize(encoded) {
|
||||
return encoded.byteLength;
|
||||
}
|
||||
},
|
||||
{
|
||||
name: "Nota",
|
||||
encode: nota.encode,
|
||||
decode: nota.decode,
|
||||
// Nota also produces an ArrayBuffer:
|
||||
getSize(encoded) {
|
||||
return encoded.byteLength;
|
||||
}
|
||||
},
|
||||
{
|
||||
name: "JSON",
|
||||
encode: json.encode,
|
||||
decode: json.decode,
|
||||
// JSON produces a JS string. We'll measure its UTF-16 code unit length
|
||||
// as a rough "size". Alternatively, you could convert to UTF-8 for
|
||||
// a more accurate byte size. Here we just use `string.length`.
|
||||
getSize(encodedStr) {
|
||||
return encodedStr.length;
|
||||
}
|
||||
}
|
||||
];
|
||||
|
||||
////////////////////////////////////////////////////////////////////////////////
|
||||
// 2. Test data sets (similar to wota benchmarks).
|
||||
// Each scenario has { name, data, iterations }
|
||||
////////////////////////////////////////////////////////////////////////////////
|
||||
|
||||
const benchmarks = [
|
||||
{
|
||||
name: "Small Integers",
|
||||
data: [0, 42, -1, 2023],
|
||||
iterations: 100000
|
||||
},
|
||||
{
|
||||
name: "Floating point",
|
||||
data: [0.1, 1e-50, 3.14159265359],
|
||||
iterations: 100000
|
||||
},
|
||||
{
|
||||
name: "Strings (short, emoji)",
|
||||
data: ["Hello, Wota!", "short", "Emoji: \u{1f600}\u{1f64f}"],
|
||||
iterations: 100000
|
||||
},
|
||||
{
|
||||
name: "Small Objects",
|
||||
data: [
|
||||
{ a:1, b:2.2, c:"3", d:false },
|
||||
{ x:42, y:null, z:"test" }
|
||||
],
|
||||
iterations: 50000
|
||||
},
|
||||
{
|
||||
name: "Nested Arrays",
|
||||
data: [ [ [ [1,2], [3,4] ] ], [[[]]], [1, [2, [3, [4]]]] ],
|
||||
iterations: 50000
|
||||
},
|
||||
{
|
||||
name: "Large Array (1k integers)",
|
||||
data: [ Array.from({length:1000}, (_, i) => i) ],
|
||||
iterations: 1000
|
||||
},
|
||||
{
|
||||
name: "Large Binary Blob (256KB)",
|
||||
data: [ new Uint8Array(256 * 1024).buffer ],
|
||||
iterations: 200
|
||||
}
|
||||
];
|
||||
|
||||
////////////////////////////////////////////////////////////////////////////////
|
||||
// 3. Utility: measureTime(fn) => how long fn() takes in seconds.
|
||||
////////////////////////////////////////////////////////////////////////////////
|
||||
|
||||
function measureTime(fn) {
|
||||
let start = os.now();
|
||||
fn();
|
||||
let end = os.now();
|
||||
return (end - start); // in seconds
|
||||
}
|
||||
|
||||
////////////////////////////////////////////////////////////////////////////////
|
||||
// 4. For each library, we run each benchmark scenario and measure:
|
||||
// - Encoding time (seconds)
|
||||
// - Decoding time (seconds)
|
||||
// - Total encoded size (bytes or code units for JSON)
|
||||
//
|
||||
////////////////////////////////////////////////////////////////////////////////
|
||||
|
||||
function runBenchmarkForLibrary(lib, bench) {
|
||||
// We'll encode and decode each item in `bench.data`.
|
||||
// We do 'bench.iterations' times. Then sum up total time.
|
||||
|
||||
// Pre-store the encoded results for all items so we can measure decode time
|
||||
// in a separate pass. Also measure total size once.
|
||||
let encodedList = [];
|
||||
let totalSize = 0;
|
||||
|
||||
// 1) Measure ENCODING
|
||||
let encodeTime = measureTime(() => {
|
||||
for (let i = 0; i < bench.iterations; i++) {
|
||||
// For each data item, encode it
|
||||
for (let d of bench.data) {
|
||||
let e = lib.encode(d);
|
||||
// store only in the very first iteration, so we can decode them later
|
||||
// but do not store them every iteration or we blow up memory.
|
||||
if (i === 0) {
|
||||
encodedList.push(e);
|
||||
totalSize += lib.getSize(e);
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// 2) Measure DECODING
|
||||
let decodeTime = measureTime(() => {
|
||||
for (let i = 0; i < bench.iterations; i++) {
|
||||
// decode everything we stored during the first iteration
|
||||
for (let e of encodedList) {
|
||||
let decoded = lib.decode(e);
|
||||
// not verifying correctness here, just measuring speed
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
return { encodeTime, decodeTime, totalSize };
|
||||
}
|
||||
|
||||
////////////////////////////////////////////////////////////////////////////////
|
||||
// 5. Main driver: run across all benchmarks, for each library.
|
||||
////////////////////////////////////////////////////////////////////////////////
|
||||
|
||||
console.log("Benchmark: Wota vs Nota vs JSON");
|
||||
console.log("================================\n");
|
||||
|
||||
for (let bench of benchmarks) {
|
||||
console.log(`SCENARIO: ${bench.name}`);
|
||||
console.log(` Data length: ${bench.data.length} | Iterations: ${bench.iterations}\n`);
|
||||
|
||||
for (let lib of libraries) {
|
||||
let { encodeTime, decodeTime, totalSize } = runBenchmarkForLibrary(lib, bench);
|
||||
|
||||
// We'll compute total operations = bench.iterations * bench.data.length
|
||||
let totalOps = bench.iterations * bench.data.length;
|
||||
let encOpsPerSec = (totalOps / encodeTime).toFixed(1);
|
||||
let decOpsPerSec = (totalOps / decodeTime).toFixed(1);
|
||||
|
||||
console.log(` ${lib.name}:`);
|
||||
console.log(` Encode time: ${encodeTime.toFixed(3)}s => ${encOpsPerSec} encodes/sec`);
|
||||
console.log(` Decode time: ${decodeTime.toFixed(3)}s => ${decOpsPerSec} decodes/sec`);
|
||||
console.log(` Total size: ${totalSize} bytes (or code units for JSON)`);
|
||||
console.log("");
|
||||
}
|
||||
console.log("---------------------------------------------------------\n");
|
||||
}
|
||||
|
||||
console.log("Benchmark complete.\n");
|
||||
@@ -1,39 +0,0 @@
|
||||
# dmon
|
||||
|
||||
### watch() <sub>function</sub>
|
||||
|
||||
Start watching the root directory, recursively.
|
||||
|
||||
This function begins monitoring the specified directory and its subdirectories recursively for events such as file creation, deletion, modification, or movement. Events are queued and can be retrieved by calling poll.
|
||||
|
||||
:throws: An error if dmon is already watching.
|
||||
|
||||
|
||||
**Returns**: None
|
||||
|
||||
|
||||
### unwatch() <sub>function</sub>
|
||||
|
||||
Stop watching the currently monitored directory.
|
||||
|
||||
This function halts filesystem monitoring for the directory previously set by watch. It clears the watch state, allowing a new watch to be started.
|
||||
|
||||
:throws: An error if no directory is currently being watched.
|
||||
|
||||
|
||||
**Returns**: None
|
||||
|
||||
|
||||
### poll(callback) <sub>function</sub>
|
||||
|
||||
Retrieve and process queued filesystem events.
|
||||
|
||||
This function dequeues all pending filesystem events and invokes the provided callback for each one. The callback receives an event object with properties: 'action' (string: "create", "delete", "modify", or "move"), 'root' (string: watched directory), 'file' (string: affected file path), and 'old' (string: previous file path for move events, empty if not applicable).
|
||||
|
||||
|
||||
|
||||
**callback**: A function to call for each event, receiving an event object as its argument.
|
||||
|
||||
|
||||
**Returns**: None
|
||||
|
||||
@@ -1,45 +0,0 @@
|
||||
# enet
|
||||
|
||||
### initialize() <sub>function</sub>
|
||||
|
||||
|
||||
Initialize the ENet library. Must be called before using any ENet functionality.
|
||||
Throws an error if initialization fails.
|
||||
|
||||
|
||||
|
||||
**Returns**: None
|
||||
|
||||
|
||||
### deinitialize() <sub>function</sub>
|
||||
|
||||
|
||||
Deinitialize the ENet library, cleaning up all resources. Call this when you no longer
|
||||
need any ENet functionality.
|
||||
|
||||
|
||||
|
||||
**Returns**: None
|
||||
|
||||
|
||||
### create_host(address) <sub>function</sub>
|
||||
|
||||
|
||||
Create an ENet host for either a client-like unbound host or a server bound to a specific
|
||||
address and port:
|
||||
|
||||
- If no argument is provided, creates an unbound "client-like" host with default settings
|
||||
(maximum 32 peers, 2 channels, unlimited bandwidth).
|
||||
- If you pass an "ip:port" string (e.g. "127.0.0.1:7777"), it creates a server bound to
|
||||
that address. The server supports up to 32 peers, 2 channels, and unlimited bandwidth.
|
||||
|
||||
Throws an error if host creation fails for any reason.
|
||||
|
||||
omit to create an unbound client-like host.
|
||||
|
||||
|
||||
**address**: (optional) A string in 'ip:port' format to bind the host (server), or
|
||||
|
||||
|
||||
**Returns**: An ENetHost object.
|
||||
|
||||
@@ -1,30 +0,0 @@
|
||||
# nota
|
||||
|
||||
### encode(value) <sub>function</sub>
|
||||
|
||||
Convert a JavaScript value into a NOTA-encoded ArrayBuffer.
|
||||
|
||||
This function serializes JavaScript values (such as numbers, strings, booleans, arrays, objects, or ArrayBuffers) into the NOTA binary format. The resulting ArrayBuffer can be stored or transmitted and later decoded back into a JavaScript value.
|
||||
|
||||
:throws: An error if no argument is provided.
|
||||
|
||||
|
||||
**value**: The JavaScript value to encode (e.g., number, string, boolean, array, object, or ArrayBuffer).
|
||||
|
||||
|
||||
**Returns**: An ArrayBuffer containing the NOTA-encoded data.
|
||||
|
||||
|
||||
### decode(buffer) <sub>function</sub>
|
||||
|
||||
Decode a NOTA-encoded ArrayBuffer into a JavaScript value.
|
||||
|
||||
This function deserializes a NOTA-formatted ArrayBuffer into its corresponding JavaScript representation, such as a number, string, boolean, array, object, or ArrayBuffer. If the input is invalid or empty, it returns undefined.
|
||||
|
||||
|
||||
|
||||
**buffer**: An ArrayBuffer containing NOTA-encoded data to decode.
|
||||
|
||||
|
||||
**Returns**: The decoded JavaScript value (e.g., number, string, boolean, array, object, or ArrayBuffer), or undefined if no argument is provided.
|
||||
|
||||
@@ -1,66 +0,0 @@
|
||||
# enet_host
|
||||
|
||||
### service(callback, timeout) <sub>function</sub>
|
||||
|
||||
|
||||
Poll for and process any available network events (connect, receive, disconnect, or none)
|
||||
from this host, calling the provided callback for each event. This function loops until
|
||||
no more events are available in the current timeframe.
|
||||
|
||||
Event object properties:
|
||||
- type: String, one of "connect", "receive", "disconnect", or "none".
|
||||
- peer: (present if type = "connect") The ENetPeer object for the new connection.
|
||||
- channelID: (present if type = "receive") The channel on which the data was received.
|
||||
- data: (present if type = "receive") The received data as a *plain JavaScript object*.
|
||||
If the JSON parse fails or the data isn't an object, a JavaScript error is thrown.
|
||||
|
||||
object as its single argument.
|
||||
|
||||
|
||||
**callback**: A function called once for each available event, receiving an event
|
||||
|
||||
**timeout**: (optional) Timeout in milliseconds. Defaults to 0 (non-blocking).
|
||||
|
||||
|
||||
**Returns**: None
|
||||
|
||||
|
||||
### connect(host, port) <sub>function</sub>
|
||||
|
||||
|
||||
Initiate a connection from this host to a remote server. Throws an error if the
|
||||
connection cannot be started.
|
||||
|
||||
|
||||
|
||||
**host**: The hostname or IP address of the remote server (e.g. "example.com" or "127.0.0.1").
|
||||
|
||||
**port**: The port number to connect to.
|
||||
|
||||
|
||||
**Returns**: An ENetPeer object representing the connection.
|
||||
|
||||
|
||||
### flush() <sub>function</sub>
|
||||
|
||||
|
||||
Flush all pending outgoing packets for this host immediately.
|
||||
|
||||
|
||||
|
||||
**Returns**: None
|
||||
|
||||
|
||||
### broadcast(data) <sub>function</sub>
|
||||
|
||||
|
||||
Broadcast a JavaScript object to all connected peers on channel 0. The object is
|
||||
serialized to JSON, and the packet is sent reliably. Throws an error if serialization fails.
|
||||
|
||||
|
||||
|
||||
**data**: A JavaScript object to broadcast to all peers.
|
||||
|
||||
|
||||
**Returns**: None
|
||||
|
||||
@@ -1,102 +0,0 @@
|
||||
# enet_peer
|
||||
|
||||
### send(data) <sub>function</sub>
|
||||
|
||||
|
||||
Send a JavaScript object to this peer on channel 0. The object is serialized to JSON and
|
||||
sent reliably. Throws an error if serialization fails.
|
||||
|
||||
|
||||
|
||||
**data**: A JavaScript object to send.
|
||||
|
||||
|
||||
**Returns**: None
|
||||
|
||||
|
||||
### disconnect() <sub>function</sub>
|
||||
|
||||
|
||||
Request a graceful disconnection from this peer. The connection will close after
|
||||
pending data is sent.
|
||||
|
||||
|
||||
|
||||
**Returns**: None
|
||||
|
||||
|
||||
### disconnect_now() <sub>function</sub>
|
||||
|
||||
|
||||
Immediately terminate the connection to this peer, discarding any pending data.
|
||||
|
||||
|
||||
|
||||
**Returns**: None
|
||||
|
||||
|
||||
### disconnect_later() <sub>function</sub>
|
||||
|
||||
|
||||
Request a disconnection from this peer after all queued packets are sent.
|
||||
|
||||
|
||||
|
||||
**Returns**: None
|
||||
|
||||
|
||||
### reset() <sub>function</sub>
|
||||
|
||||
|
||||
Reset this peer's connection, immediately dropping it and clearing its internal state.
|
||||
|
||||
|
||||
|
||||
**Returns**: None
|
||||
|
||||
|
||||
### ping() <sub>function</sub>
|
||||
|
||||
|
||||
Send a ping request to this peer to measure latency.
|
||||
|
||||
|
||||
|
||||
**Returns**: None
|
||||
|
||||
|
||||
### throttle_configure(interval, acceleration, deceleration) <sub>function</sub>
|
||||
|
||||
|
||||
Configure the throttling behavior for this peer, controlling how ENet adjusts its sending
|
||||
rate based on packet loss or congestion.
|
||||
|
||||
|
||||
|
||||
**interval**: The interval (ms) between throttle adjustments.
|
||||
|
||||
**acceleration**: The factor to increase sending speed when conditions improve.
|
||||
|
||||
**deceleration**: The factor to decrease sending speed when conditions worsen.
|
||||
|
||||
|
||||
**Returns**: None
|
||||
|
||||
|
||||
### timeout(timeout_limit, timeout_min, timeout_max) <sub>function</sub>
|
||||
|
||||
|
||||
Set timeout parameters for this peer, determining how long ENet waits before considering
|
||||
the connection lost.
|
||||
|
||||
|
||||
|
||||
**timeout_limit**: The total time (ms) before the peer is disconnected.
|
||||
|
||||
**timeout_min**: The minimum timeout (ms) used for each timeout attempt.
|
||||
|
||||
**timeout_max**: The maximum timeout (ms) used for each timeout attempt.
|
||||
|
||||
|
||||
**Returns**: None
|
||||
|
||||
@@ -3,20 +3,9 @@ c = 'emcc'
|
||||
cpp = 'em++'
|
||||
ar = 'emar'
|
||||
strip = 'emstrip'
|
||||
pkg-config = 'pkg-config'
|
||||
exe_wrapper = 'node'
|
||||
|
||||
[host_machine]
|
||||
system = 'emscripten'
|
||||
cpu_family = 'wasm32'
|
||||
cpu = 'wasm32'
|
||||
endian = 'little'
|
||||
|
||||
[built-in options]
|
||||
pkg_config_path = '$EMSDK/upstream/emscripten/cache/sysroot/lib/pkgconfig'
|
||||
cmake_prefix_path = '$EMSDK/upstream/emscripten/cache/sysroot'
|
||||
|
||||
[properties]
|
||||
needs_exe_wrapper = true
|
||||
cmake_system_name = 'Emscripten'
|
||||
sys_root = '@env:EMSDK@/upstream/emscripten/cache/sysroot'
|
||||
cpu_family = 'wasm64'
|
||||
cpu = 'wasm64'
|
||||
endian = 'little'
|
||||
@@ -1,8 +0,0 @@
|
||||
var config = {
|
||||
title: "Box2D Physics Demo",
|
||||
width: 1280,
|
||||
height: 720,
|
||||
fullscreen: false
|
||||
}
|
||||
|
||||
return config
|
||||
@@ -1,343 +0,0 @@
|
||||
var box2d = use('box2d')
|
||||
var moth = use('moth', $_.delay)
|
||||
moth.initialize()
|
||||
var draw2d = use('draw2d')
|
||||
|
||||
// Physics world setup
|
||||
var world = new box2d.World({
|
||||
gravity: {x: 0, y: -10}
|
||||
})
|
||||
|
||||
// Ground body (static)
|
||||
var ground = world.createBody({
|
||||
type: 'static',
|
||||
position: {x: 0, y: -10}
|
||||
})
|
||||
|
||||
var groundShape = ground.createBoxShape({
|
||||
width: 50,
|
||||
height: 0.5,
|
||||
density: 0,
|
||||
friction: 0.7
|
||||
})
|
||||
|
||||
// Walls
|
||||
var leftWall = world.createBody({
|
||||
type: 'static',
|
||||
position: {x: -25, y: 0}
|
||||
})
|
||||
leftWall.createBoxShape({
|
||||
width: 0.5,
|
||||
height: 30,
|
||||
density: 0
|
||||
})
|
||||
|
||||
var rightWall = world.createBody({
|
||||
type: 'static',
|
||||
position: {x: 25, y: 0}
|
||||
})
|
||||
rightWall.createBoxShape({
|
||||
width: 0.5,
|
||||
height: 30,
|
||||
density: 0
|
||||
})
|
||||
|
||||
// Dynamic bodies array
|
||||
var boxes = []
|
||||
var circles = []
|
||||
|
||||
// Create some dynamic boxes
|
||||
for (var i = 0; i < 5; i++) {
|
||||
var box = world.createBody({
|
||||
type: 'dynamic',
|
||||
position: {x: -10 + i * 4, y: 5 + i * 3},
|
||||
angle: Math.random() * Math.PI
|
||||
})
|
||||
|
||||
box.createBoxShape({
|
||||
width: 2,
|
||||
height: 2,
|
||||
density: 1.0,
|
||||
friction: 0.3,
|
||||
restitution: 0.5
|
||||
})
|
||||
|
||||
boxes.push(box)
|
||||
}
|
||||
|
||||
// Create some circles
|
||||
for (var i = 0; i < 3; i++) {
|
||||
var circle = world.createBody({
|
||||
type: 'dynamic',
|
||||
position: {x: 5 + i * 3, y: 10 + i * 2}
|
||||
})
|
||||
|
||||
circle.createCircleShape({
|
||||
radius: 1,
|
||||
density: 0.5,
|
||||
friction: 0.2,
|
||||
restitution: 0.8
|
||||
})
|
||||
|
||||
circles.push(circle)
|
||||
}
|
||||
|
||||
// Connected bodies with distance joint
|
||||
var bodyA = world.createBody({
|
||||
type: 'dynamic',
|
||||
position: {x: -5, y: 15}
|
||||
})
|
||||
bodyA.createCircleShape({
|
||||
radius: 0.5,
|
||||
density: 1.0
|
||||
})
|
||||
|
||||
var bodyB = world.createBody({
|
||||
type: 'dynamic',
|
||||
position: {x: 0, y: 15}
|
||||
})
|
||||
bodyB.createCircleShape({
|
||||
radius: 0.5,
|
||||
density: 1.0
|
||||
})
|
||||
|
||||
var joint = world.createDistanceJoint({
|
||||
bodyA: bodyA,
|
||||
bodyB: bodyB,
|
||||
localAnchorA: {x: 0, y: 0},
|
||||
localAnchorB: {x: 0, y: 0},
|
||||
length: 5
|
||||
})
|
||||
|
||||
// Mouse interaction
|
||||
var mouseBody = null
|
||||
var mousePressed = false
|
||||
|
||||
// Game state
|
||||
var camera = {x: 0, y: 0, zoom: 10}
|
||||
|
||||
// Input state
|
||||
var keys = {}
|
||||
var mouse = {
|
||||
pos: {x: 0, y: 0},
|
||||
buttons: [false, false, false]
|
||||
}
|
||||
|
||||
// Main update function
|
||||
prosperon.on('update', function(dt) {
|
||||
// Step physics simulation
|
||||
world.step(dt, 4)
|
||||
|
||||
// Camera controls
|
||||
if (keys[4]) camera.x -= 20 * dt // A
|
||||
if (keys[7]) camera.x += 20 * dt // D
|
||||
if (keys[26]) camera.y += 20 * dt // W
|
||||
if (keys[22]) camera.y -= 20 * dt // S
|
||||
if (keys[20]) camera.zoom *= 1 + dt // Q
|
||||
if (keys[8]) camera.zoom *= 1 - dt // E
|
||||
|
||||
// Mouse interaction
|
||||
var mouseWorld = screenToWorld(mouse.pos)
|
||||
// Raycast to find body under mouse
|
||||
var result = world.rayCast(mouseWorld, {x: 0, y: -1}, 0.1)
|
||||
|
||||
if (!result.hit) {
|
||||
// Create new body at mouse position
|
||||
if (keys[225]) { // LSHIFT
|
||||
// Create circle
|
||||
var newCircle = world.createBody({
|
||||
type: 'dynamic',
|
||||
position: mouseWorld
|
||||
})
|
||||
newCircle.createCircleShape({
|
||||
radius: 0.5 + Math.random(),
|
||||
density: 1.0,
|
||||
restitution: 0.3 + Math.random() * 0.5
|
||||
})
|
||||
circles.push(newCircle)
|
||||
} else {
|
||||
// Create box
|
||||
var newBox = world.createBody({
|
||||
type: 'dynamic',
|
||||
position: mouseWorld,
|
||||
angle: Math.random() * Math.PI * 2
|
||||
})
|
||||
newBox.createBoxShape({
|
||||
width: 1 + Math.random() * 2,
|
||||
height: 1 + Math.random() * 2,
|
||||
density: 1.0,
|
||||
restitution: 0.2 + Math.random() * 0.3
|
||||
})
|
||||
boxes.push(newBox)
|
||||
}
|
||||
}
|
||||
|
||||
boxes.forEach(function(box) {
|
||||
var dir = {
|
||||
x: box.position.x - mouseWorld.x,
|
||||
y: box.position.y - mouseWorld.y
|
||||
}
|
||||
var dist = Math.sqrt(dir.x * dir.x + dir.y * dir.y)
|
||||
if (dist < 10 && dist > 0.1) {
|
||||
dir.x /= dist
|
||||
dir.y /= dist
|
||||
box.applyLinearImpulse({x: dir.x * 50, y: dir.y * 50})
|
||||
}
|
||||
})
|
||||
|
||||
|
||||
// Reset with R
|
||||
if (keys[21] && !keys[21 + '_prev']) { // R key pressed
|
||||
// Reset all dynamic bodies
|
||||
boxes.forEach(function(box) {
|
||||
box.position = {x: Math.random() * 20 - 10, y: 10 + Math.random() * 10}
|
||||
box.angle = Math.random() * Math.PI * 2
|
||||
box.linearVelocity = {x: 0, y: 0}
|
||||
box.angularVelocity = 0
|
||||
})
|
||||
|
||||
circles.forEach(function(circle) {
|
||||
circle.position = {x: Math.random() * 20 - 10, y: 10 + Math.random() * 10}
|
||||
circle.linearVelocity = {x: 0, y: 0}
|
||||
})
|
||||
}
|
||||
|
||||
// Update previous key states
|
||||
for (var k in keys) {
|
||||
if (k.indexOf('_prev') === -1) {
|
||||
keys[k + '_prev'] = keys[k]
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
// Event handlers
|
||||
prosperon.on('key_down', function(e) {
|
||||
keys[e.scancode] = true
|
||||
})
|
||||
|
||||
prosperon.on('key_up', function(e) {
|
||||
keys[e.scancode] = false
|
||||
})
|
||||
|
||||
prosperon.on('mouse_button_down', function(e) {
|
||||
mouse.buttons[e.which] = true
|
||||
|
||||
if (e.which === 0 && !mousePressed) {
|
||||
mousePressed = true
|
||||
var mouseWorld = screenToWorld(mouse.pos)
|
||||
|
||||
// Raycast to find body under mouse
|
||||
var result = world.rayCast(mouseWorld, {x: 0, y: -1}, 0.1)
|
||||
|
||||
if (!result.hit) {
|
||||
// Create new body at mouse position
|
||||
if (keys[225]) { // LSHIFT
|
||||
// Create circle
|
||||
var newCircle = world.createBody({
|
||||
type: 'dynamic',
|
||||
position: mouseWorld
|
||||
})
|
||||
newCircle.createCircleShape({
|
||||
radius: 0.5 + Math.random(),
|
||||
density: 1.0,
|
||||
restitution: 0.3 + Math.random() * 0.5
|
||||
})
|
||||
circles.push(newCircle)
|
||||
} else {
|
||||
// Create box
|
||||
var newBox = world.createBody({
|
||||
type: 'dynamic',
|
||||
position: mouseWorld,
|
||||
angle: Math.random() * Math.PI * 2
|
||||
})
|
||||
newBox.createBoxShape({
|
||||
width: 1 + Math.random() * 2,
|
||||
height: 1 + Math.random() * 2,
|
||||
density: 1.0,
|
||||
restitution: 0.2 + Math.random() * 0.3
|
||||
})
|
||||
boxes.push(newBox)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (e.which === 1) {
|
||||
// Apply impulse with right click
|
||||
var mouseWorld = screenToWorld(mouse.pos)
|
||||
boxes.forEach(function(box) {
|
||||
var dir = {
|
||||
x: box.position.x - mouseWorld.x,
|
||||
y: box.position.y - mouseWorld.y
|
||||
}
|
||||
var dist = Math.sqrt(dir.x * dir.x + dir.y * dir.y)
|
||||
if (dist < 10 && dist > 0.1) {
|
||||
dir.x /= dist
|
||||
dir.y /= dist
|
||||
box.applyLinearImpulse({x: dir.x * 50, y: dir.y * 50})
|
||||
}
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
prosperon.on('mouse_button_up', function(e) {
|
||||
mouse.buttons[e.which] = false
|
||||
if (e.which === 0) {
|
||||
mousePressed = false
|
||||
}
|
||||
})
|
||||
|
||||
prosperon.on('mouse_motion', function(e) {
|
||||
mouse.pos = e.pos
|
||||
})
|
||||
|
||||
// Rendering
|
||||
prosperon.on('draw', function() {
|
||||
// Clear background
|
||||
|
||||
// Draw ground
|
||||
drawBox(ground.position, 50, 0.5, ground.angle, {r: 0.5, g: 0.5, b: 0.5, a: 1})
|
||||
|
||||
// Draw walls
|
||||
drawBox(leftWall.position, 0.5, 30, 0, {r: 0.5, g: 0.5, b: 0.5, a: 1})
|
||||
drawBox(rightWall.position, 0.5, 30, 0, {r: 0.5, g: 0.5, b: 0.5, a: 1})
|
||||
|
||||
// Draw boxes
|
||||
boxes.forEach(function(box) {
|
||||
drawBox(box.position, 2, 2, box.angle, {r: 0.8, g: 0.3, b: 0.3, a: 1})
|
||||
})
|
||||
|
||||
// Draw circles
|
||||
circles.forEach(function(circle) {
|
||||
draw2d.circle(circle.position, 1, {r: 0.3, g: 0.8, b: 0.3, a: 1})
|
||||
})
|
||||
|
||||
// Draw connected bodies
|
||||
draw2d.circle(bodyA.position, 0.5, {r: 0.8, g: 0.8, b: 0.3, a: 1})
|
||||
draw2d.circle(bodyB.position, 0.5, {r: 0.8, g: 0.8, b: 0.3, a: 1})
|
||||
draw2d.line([bodyA.position, bodyB.position], {r: 1, g: 1, b: 0, a: 0.5})
|
||||
|
||||
// Draw UI
|
||||
draw2d.text("Box2D Demo", {x: 10, y: 10}, 20, {r: 1, g: 1, b: 1, a: 1})
|
||||
draw2d.text("Controls:", {x: 10, y: 40}, 16, {r: 1, g: 1, b: 1, a: 1})
|
||||
draw2d.text("- WASD: Move camera", {x: 10, y: 60}, 14, {r: 1, g: 1, b: 1, a: 1})
|
||||
draw2d.text("- Q/E: Zoom in/out", {x: 10, y: 80}, 14, {r: 1, g: 1, b: 1, a: 1})
|
||||
draw2d.text("- Left click: Create box", {x: 10, y: 100}, 14, {r: 1, g: 1, b: 1, a: 1})
|
||||
draw2d.text("- Shift + Left click: Create circle", {x: 10, y: 120}, 14, {r: 1, g: 1, b: 1, a: 1})
|
||||
draw2d.text("- Right click: Apply impulse", {x: 10, y: 140}, 14, {r: 1, g: 1, b: 1, a: 1})
|
||||
draw2d.text("- R: Reset bodies", {x: 10, y: 160}, 14, {r: 1, g: 1, b: 1, a: 1})
|
||||
|
||||
// Show physics stats
|
||||
draw2d.text("Bodies: " + (boxes.length + circles.length + 4), {x: 10, y: 200}, 14, {r: 1, g: 1, b: 1, a: 1})
|
||||
})
|
||||
|
||||
// Helper functions
|
||||
function drawBox(pos, width, height, angle, color) {
|
||||
draw2d.rectangle({x:pos.x,y:pos.y,width, height})
|
||||
}
|
||||
|
||||
function screenToWorld(screenPos) {
|
||||
return {
|
||||
x: (screenPos.x - prosperon.x * 0.5) / camera.zoom + camera.x,
|
||||
y: (screenPos.y - prosperon.y * 0.5) / camera.zoom + camera.y
|
||||
}
|
||||
}
|
||||
|
Before Width: | Height: | Size: 390 B |
|
Before Width: | Height: | Size: 438 B |
|
Before Width: | Height: | Size: 398 B |
|
Before Width: | Height: | Size: 337 B |
|
Before Width: | Height: | Size: 390 B |
|
Before Width: | Height: | Size: 379 B |
@@ -1,9 +0,0 @@
|
||||
// Chess game configuration for Moth framework
|
||||
return {
|
||||
title: "Chess",
|
||||
resolution: { width: 480, height: 480 },
|
||||
internal_resolution: { width: 480, height: 480 },
|
||||
fps: 60,
|
||||
clearColor: [22/255, 120/255, 194/255, 1],
|
||||
mode: 'stretch' // No letterboxing for chess
|
||||
};
|
||||
@@ -1,57 +0,0 @@
|
||||
var CELLS = Symbol()
|
||||
|
||||
var key = function key(x,y) { return `${x},${y}` }
|
||||
|
||||
function grid(w, h)
|
||||
{
|
||||
this[CELLS] = new Map()
|
||||
this.width = w;
|
||||
this.height = h;
|
||||
}
|
||||
|
||||
grid.prototype = {
|
||||
cell(x,y) {
|
||||
var k = key(x,y)
|
||||
if (!this[CELLS].has(k)) this[CELLS].set(k,[])
|
||||
return this[CELLS].get(k)
|
||||
},
|
||||
|
||||
add(entity, pos) {
|
||||
this.cell(pos.x, pos.y).push(entity);
|
||||
entity.coord = pos.slice();
|
||||
},
|
||||
|
||||
remove(entity, pos) {
|
||||
var c = this.cell(pos.x, pos.y);
|
||||
c.splice(c.indexOf(entity), 1);
|
||||
},
|
||||
|
||||
at(pos) {
|
||||
return this.cell(pos.x, pos.y);
|
||||
},
|
||||
|
||||
inBounds(pos) {
|
||||
return pos.x >= 0 && pos.x < this.width && pos.y >= 0 && pos.y < this.height;
|
||||
},
|
||||
|
||||
each(fn) {
|
||||
for (var [k, list] of this[CELLS])
|
||||
for (var p of list) fn(p, p.coord);
|
||||
},
|
||||
|
||||
toString() {
|
||||
var out = `grid [${this.width}x${this.height}]
|
||||
`
|
||||
for (var y = 0; y < this.height; y++) {
|
||||
for (var x = 0; x < this.width; x++) {
|
||||
var cell = this.at([x,y]);
|
||||
out += cell.length
|
||||
}
|
||||
if (y !== this.height - 1) out += "\n"
|
||||
}
|
||||
|
||||
return out
|
||||
},
|
||||
}
|
||||
|
||||
return grid
|
||||
@@ -1,403 +0,0 @@
|
||||
/* main.js – runs the demo with your prototype-based grid */
|
||||
|
||||
var moth = use('moth', $_.delay)
|
||||
var json = use('json')
|
||||
|
||||
var res = 160
|
||||
var internal_res = 480
|
||||
|
||||
moth.initialize({ width:res, height:res, resolution_x:internal_res, resolution_y:internal_res, mode:'letterbox' });
|
||||
|
||||
var os = use('os');
|
||||
var draw2d = use('draw2d');
|
||||
var gfx = use('graphics');
|
||||
|
||||
/*──── import our pieces + systems ───────────────────────────────────*/
|
||||
var Grid = use('grid'); // your new ctor
|
||||
var MovementSystem = use('movement').MovementSystem;
|
||||
var startingPos = use('pieces').startingPosition;
|
||||
var rules = use('rules');
|
||||
|
||||
/*──── build board ───────────────────────────────────────────────────*/
|
||||
var grid = new Grid(8, 8);
|
||||
grid.width = 8; // (the ctor didn't store them)
|
||||
grid.height = 8;
|
||||
|
||||
var mover = new MovementSystem(grid, rules);
|
||||
startingPos(grid);
|
||||
|
||||
/*──── networking and game state ─────────────────────────────────────*/
|
||||
var gameState = 'waiting'; // 'waiting', 'searching', 'server_waiting', 'connected'
|
||||
var isServer = false;
|
||||
var opponent = null;
|
||||
var myColor = null; // 'white' or 'black'
|
||||
var isMyTurn = false;
|
||||
|
||||
function updateTitle() {
|
||||
var title = "Misty Chess - ";
|
||||
|
||||
switch(gameState) {
|
||||
case 'waiting':
|
||||
title += "Press S to start server or J to join";
|
||||
break;
|
||||
case 'searching':
|
||||
title += "Searching for server...";
|
||||
break;
|
||||
case 'server_waiting':
|
||||
title += "Waiting for player to join...";
|
||||
break;
|
||||
case 'connected':
|
||||
if (myColor) {
|
||||
title += (mover.turn === myColor ? "Your turn (" + myColor + ")" : "Opponent's turn (" + mover.turn + ")");
|
||||
} else {
|
||||
title += mover.turn + " turn";
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
prosperon.window.title = title
|
||||
}
|
||||
|
||||
// Initialize title
|
||||
updateTitle();
|
||||
|
||||
/*──── mouse → click-to-move ─────────────────────────────────────────*/
|
||||
var selectPos = null;
|
||||
var hoverPos = null;
|
||||
var holdingPiece = false;
|
||||
|
||||
var opponentMousePos = null;
|
||||
var opponentHoldingPiece = false;
|
||||
var opponentSelectPos = null;
|
||||
|
||||
prosperon.on('mouse_button_down', function(e) {
|
||||
if (e.which !== 0) return;
|
||||
|
||||
// Don't allow piece selection unless we have an opponent
|
||||
if (gameState !== 'connected' || !opponent) return;
|
||||
|
||||
var mx = e.mouse.x;
|
||||
var my = e.mouse.y;
|
||||
|
||||
var c = [Math.floor(mx / 60), Math.floor(my / 60)];
|
||||
if (!grid.inBounds(c)) return;
|
||||
|
||||
var cell = grid.at(c);
|
||||
if (cell.length && cell[0].colour === mover.turn) {
|
||||
selectPos = c;
|
||||
holdingPiece = true;
|
||||
// Send pickup notification to opponent
|
||||
if (opponent) {
|
||||
send(opponent, {
|
||||
type: 'piece_pickup',
|
||||
pos: c
|
||||
});
|
||||
}
|
||||
} else {
|
||||
selectPos = null;
|
||||
}
|
||||
})
|
||||
|
||||
prosperon.on('mouse_button_up', function(e) {
|
||||
if (e.which !== 0 || !holdingPiece || !selectPos) return;
|
||||
|
||||
// Don't allow moves unless we have an opponent and it's our turn
|
||||
if (gameState !== 'connected' || !opponent || !isMyTurn) {
|
||||
holdingPiece = false;
|
||||
return;
|
||||
}
|
||||
|
||||
var mx = e.mouse.x;
|
||||
var my = e.mouse.y;
|
||||
|
||||
var c = [Math.floor(mx / 60), Math.floor(my / 60)];
|
||||
if (!grid.inBounds(c)) {
|
||||
holdingPiece = false;
|
||||
return;
|
||||
}
|
||||
|
||||
if (mover.tryMove(grid.at(selectPos)[0], c)) {
|
||||
console.log("Made move from", selectPos, "to", c);
|
||||
// Send move to opponent
|
||||
console.log("Sending move to opponent:", opponent);
|
||||
send(opponent, {
|
||||
type: 'move',
|
||||
from: selectPos,
|
||||
to: c
|
||||
});
|
||||
isMyTurn = false; // It's now opponent's turn
|
||||
console.log("Move sent, now opponent's turn");
|
||||
selectPos = null;
|
||||
updateTitle();
|
||||
}
|
||||
|
||||
holdingPiece = false;
|
||||
|
||||
// Send piece drop notification to opponent
|
||||
if (opponent) {
|
||||
send(opponent, {
|
||||
type: 'piece_drop'
|
||||
});
|
||||
}
|
||||
})
|
||||
|
||||
prosperon.on('mouse_motion', function(e) {
|
||||
var mx = e.pos.x;
|
||||
var my = e.pos.y;
|
||||
|
||||
var c = [Math.floor(mx / 60), Math.floor(my / 60)];
|
||||
if (!grid.inBounds(c)) {
|
||||
hoverPos = null;
|
||||
return;
|
||||
}
|
||||
|
||||
hoverPos = c;
|
||||
|
||||
// Send mouse position to opponent in real-time
|
||||
if (opponent && gameState === 'connected') {
|
||||
send(opponent, {
|
||||
type: 'mouse_move',
|
||||
pos: c,
|
||||
holding: holdingPiece,
|
||||
selectPos: selectPos
|
||||
});
|
||||
}
|
||||
})
|
||||
|
||||
/*──── drawing helpers ───────────────────────────────────────────────*/
|
||||
/* ── constants ─────────────────────────────────────────────────── */
|
||||
var S = 60; // square size in px
|
||||
var light = [0.93,0.93,0.93,1];
|
||||
var dark = [0.25,0.25,0.25,1];
|
||||
var allowedColor = [1.0, 0.84, 0.0, 1.0]; // Gold for allowed moves
|
||||
var myMouseColor = [0.0, 1.0, 0.0, 1.0]; // Green for my mouse
|
||||
var opponentMouseColor = [1.0, 0.0, 0.0, 1.0]; // Red for opponent mouse
|
||||
|
||||
/* ── draw one 8×8 chess board ──────────────────────────────────── */
|
||||
function drawBoard() {
|
||||
for (var y = 0; y < 8; ++y)
|
||||
for (var x = 0; x < 8; ++x) {
|
||||
var isMyHover = hoverPos && hoverPos[0] === x && hoverPos[1] === y;
|
||||
var isOpponentHover = opponentMousePos && opponentMousePos[0] === x && opponentMousePos[1] === y;
|
||||
var isValidMove = selectPos && holdingPiece && isValidMoveForTurn(selectPos, [x, y]);
|
||||
|
||||
var color = ((x+y)&1) ? dark : light;
|
||||
|
||||
if (isValidMove) {
|
||||
color = allowedColor; // Gold for allowed moves
|
||||
} else if (isMyHover && !isOpponentHover) {
|
||||
color = myMouseColor; // Green for my mouse
|
||||
} else if (isOpponentHover) {
|
||||
color = opponentMouseColor; // Red for opponent mouse
|
||||
}
|
||||
|
||||
draw2d.rectangle(
|
||||
{ x: x*S, y: y*S, width: S, height: S },
|
||||
{ thickness: 0, color: color }
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
function isValidMoveForTurn(from, to) {
|
||||
if (!grid.inBounds(to)) return false;
|
||||
|
||||
var piece = grid.at(from)[0];
|
||||
if (!piece) return false;
|
||||
|
||||
// Check if the destination has a piece of the same color
|
||||
var destCell = grid.at(to);
|
||||
if (destCell.length && destCell[0].colour === piece.colour) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return rules.canMove(piece, from, to, grid);
|
||||
}
|
||||
|
||||
/* ── draw every live piece ─────────────────────────────────────── */
|
||||
function drawPieces() {
|
||||
grid.each(function (piece) {
|
||||
if (piece.captured) return;
|
||||
|
||||
// Skip drawing the piece being held (by me or opponent)
|
||||
if (holdingPiece && selectPos &&
|
||||
piece.coord[0] === selectPos[0] &&
|
||||
piece.coord[1] === selectPos[1]) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Skip drawing the piece being held by opponent
|
||||
if (opponentHoldingPiece && opponentSelectPos &&
|
||||
piece.coord[0] === opponentSelectPos[0] &&
|
||||
piece.coord[1] === opponentSelectPos[1]) {
|
||||
return;
|
||||
}
|
||||
|
||||
var r = { x: piece.coord[0]*S, y: piece.coord[1]*S,
|
||||
width:S, height:S };
|
||||
|
||||
draw2d.image(piece.sprite, r, 0, [0,0], [0,0], {mode:"nearest"});
|
||||
});
|
||||
|
||||
// Draw the held piece at the mouse position if we're holding one
|
||||
if (holdingPiece && selectPos && hoverPos) {
|
||||
var piece = grid.at(selectPos)[0];
|
||||
if (piece) {
|
||||
var r = { x: hoverPos[0]*S, y: hoverPos[1]*S,
|
||||
width:S, height:S };
|
||||
|
||||
draw2d.image(piece.sprite, r, 0, [0,0], [0,0], {mode:"nearest"});
|
||||
}
|
||||
}
|
||||
|
||||
// Draw opponent's held piece if they're dragging one
|
||||
if (opponentHoldingPiece && opponentSelectPos && opponentMousePos) {
|
||||
var opponentPiece = grid.at(opponentSelectPos)[0];
|
||||
if (opponentPiece) {
|
||||
var r = { x: opponentMousePos[0]*S, y: opponentMousePos[1]*S,
|
||||
width:S, height:S };
|
||||
|
||||
// Draw with slight transparency to show it's the opponent's piece
|
||||
draw2d.image(opponentPiece.sprite, r, 0, [0,0], [0,0], {mode:"nearest", color: [1, 1, 1, 0.7]});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var graphics = use('graphics')
|
||||
|
||||
prosperon.on('draw', function() {
|
||||
drawBoard()
|
||||
drawPieces()
|
||||
draw2d.text("HELL", [100,100])
|
||||
})
|
||||
|
||||
prosperon.on('key_down', function(e) {
|
||||
// S key - start server
|
||||
if (e.scancode === 22 && gameState === 'waiting') { // S key
|
||||
startServer();
|
||||
}
|
||||
// J key - join server
|
||||
else if (e.scancode === 13 && gameState === 'waiting') { // J key
|
||||
joinServer();
|
||||
}
|
||||
})
|
||||
|
||||
function startServer() {
|
||||
gameState = 'server_waiting';
|
||||
isServer = true;
|
||||
myColor = 'white';
|
||||
isMyTurn = true;
|
||||
updateTitle();
|
||||
|
||||
$_.portal(e => {
|
||||
console.log("Portal received contact message");
|
||||
// Reply with this actor to establish connection
|
||||
console.log (json.encode($_))
|
||||
send(e, $_);
|
||||
console.log("Portal replied with server actor");
|
||||
}, 5678);
|
||||
}
|
||||
|
||||
function joinServer() {
|
||||
gameState = 'searching';
|
||||
updateTitle();
|
||||
|
||||
function contact_fn(actor, reason) {
|
||||
console.log("CONTACTED!", actor ? "SUCCESS" : "FAILED", reason);
|
||||
if (actor) {
|
||||
opponent = actor;
|
||||
console.log("Connection established with server, sending join request");
|
||||
|
||||
// Send a greet message with our actor object
|
||||
send(opponent, {
|
||||
type: 'greet',
|
||||
client_actor: $_
|
||||
});
|
||||
} else {
|
||||
console.log(`Failed to connect: ${json.encode(reason)}`);
|
||||
gameState = 'waiting';
|
||||
updateTitle();
|
||||
}
|
||||
}
|
||||
|
||||
$_.contact(contact_fn, {
|
||||
address: "192.168.0.149",
|
||||
port: 5678
|
||||
});
|
||||
}
|
||||
|
||||
var os = use('os')
|
||||
var actor = use('actor')
|
||||
for (var i in actor) console.log(i)
|
||||
|
||||
// Set up IO actor subscription
|
||||
var ioguy = {
|
||||
__ACTORDATA__: {
|
||||
id: actor.ioactor()
|
||||
}
|
||||
};
|
||||
|
||||
send(ioguy, {
|
||||
type: "subscribe",
|
||||
actor: $_
|
||||
});
|
||||
|
||||
$_.receiver(e => {
|
||||
if (e.type === 'game_start' || e.type === 'move' || e.type === 'greet')
|
||||
console.log("Receiver got message:", e.type, e);
|
||||
if (e.type === 'quit') os.exit()
|
||||
|
||||
if (e.type === 'greet') {
|
||||
console.log("Server received greet from client");
|
||||
// Store the client's actor object for ongoing communication
|
||||
opponent = e.client_actor;
|
||||
console.log("Stored client actor:", json.encode(opponent));
|
||||
gameState = 'connected';
|
||||
updateTitle();
|
||||
|
||||
// Send game_start to the client
|
||||
console.log("Sending game_start to client");
|
||||
send(opponent, {
|
||||
type: 'game_start',
|
||||
your_color: 'black'
|
||||
});
|
||||
console.log("game_start message sent to client");
|
||||
}
|
||||
else if (e.type === 'game_start') {
|
||||
console.log("Game starting, I am:", e.your_color);
|
||||
myColor = e.your_color;
|
||||
isMyTurn = (myColor === 'white');
|
||||
gameState = 'connected';
|
||||
updateTitle();
|
||||
} else if (e.type === 'move') {
|
||||
console.log("Received move from opponent:", e.from, "to", e.to);
|
||||
// Apply opponent's move
|
||||
var fromCell = grid.at(e.from);
|
||||
if (fromCell.length) {
|
||||
var piece = fromCell[0];
|
||||
if (mover.tryMove(piece, e.to)) {
|
||||
isMyTurn = true; // It's now our turn
|
||||
updateTitle();
|
||||
console.log("Applied opponent move, now my turn");
|
||||
} else {
|
||||
console.log("Failed to apply opponent move");
|
||||
}
|
||||
} else {
|
||||
console.log("No piece found at from position");
|
||||
}
|
||||
} else if (e.type === 'mouse_move') {
|
||||
// Update opponent's mouse position
|
||||
opponentMousePos = e.pos;
|
||||
opponentHoldingPiece = e.holding;
|
||||
opponentSelectPos = e.selectPos;
|
||||
} else if (e.type === 'piece_pickup') {
|
||||
// Opponent picked up a piece
|
||||
opponentSelectPos = e.pos;
|
||||
opponentHoldingPiece = true;
|
||||
} else if (e.type === 'piece_drop') {
|
||||
// Opponent dropped their piece
|
||||
opponentHoldingPiece = false;
|
||||
opponentSelectPos = null;
|
||||
}
|
||||
|
||||
prosperon.dispatch(e.type, e)
|
||||
})
|
||||
@@ -1,32 +0,0 @@
|
||||
var MovementSystem = function(grid, rules) {
|
||||
this.grid = grid;
|
||||
this.rules = rules || {}; // expects { canMove: fn }
|
||||
this.turn = 'white';
|
||||
}
|
||||
|
||||
MovementSystem.prototype.tryMove = function (piece, to) {
|
||||
if (piece.colour !== this.turn) return false;
|
||||
|
||||
// normalise ‘to’ into our hybrid coord
|
||||
var dest = [to.x !== undefined ? to.x : to[0],
|
||||
to.y !== undefined ? to.y : to[1]];
|
||||
|
||||
if (!this.grid.inBounds(dest)) return false;
|
||||
if (!this.rules.canMove(piece, piece.coord, dest, this.grid)) return false;
|
||||
|
||||
var victims = this.grid.at(dest);
|
||||
if (victims.length && victims[0].colour === piece.colour) return false;
|
||||
if (victims.length) victims[0].captured = true;
|
||||
|
||||
this.grid.remove(piece, piece.coord);
|
||||
this.grid.add (piece, dest);
|
||||
|
||||
// grid.add() re-creates coord; re-add .x/.y fields:
|
||||
piece.coord.x = dest.x;
|
||||
piece.coord.y = dest.y;
|
||||
|
||||
this.turn = (this.turn === 'white') ? 'black' : 'white';
|
||||
return true;
|
||||
};
|
||||
|
||||
return { MovementSystem: MovementSystem };
|
||||
@@ -1,29 +0,0 @@
|
||||
/* pieces.js – simple data holders + starting layout */
|
||||
function Piece(kind, colour) {
|
||||
this.kind = kind; // "pawn" etc.
|
||||
this.colour = colour; // "white"/"black"
|
||||
this.sprite = colour + '_' + kind; // for draw2d.image
|
||||
this.captured = false;
|
||||
this.coord = [0,0];
|
||||
}
|
||||
Piece.prototype.toString = function () {
|
||||
return this.colour.charAt(0) + this.kind.charAt(0).toUpperCase();
|
||||
};
|
||||
|
||||
function startingPosition(grid) {
|
||||
var W = 'white', B = 'black', x;
|
||||
|
||||
// pawns
|
||||
for (x = 0; x < 8; x++) {
|
||||
grid.add(new Piece('pawn', W), [x, 6]);
|
||||
grid.add(new Piece('pawn', B), [x, 1]);
|
||||
}
|
||||
// major pieces
|
||||
var back = ['rook','knight','bishop','queen','king','bishop','knight','rook'];
|
||||
for (x = 0; x < 8; x++) {
|
||||
grid.add(new Piece(back[x], W), [x, 7]);
|
||||
grid.add(new Piece(back[x], B), [x, 0]);
|
||||
}
|
||||
}
|
||||
|
||||
return { Piece, startingPosition };
|
||||
@@ -1,45 +0,0 @@
|
||||
/* helper – robust coord access */
|
||||
function cx(c) { return (c.x !== undefined) ? c.x : c[0]; }
|
||||
function cy(c) { return (c.y !== undefined) ? c.y : c[1]; }
|
||||
|
||||
/* simple move-shape checks */
|
||||
var deltas = {
|
||||
pawn: function (pc, dx, dy, grid, to) {
|
||||
var dir = (pc.colour === 'white') ? -1 : 1;
|
||||
var base = (pc.colour === 'white') ? 6 : 1;
|
||||
var one = (dy === dir && dx === 0 && grid.at(to).length === 0);
|
||||
var two = (dy === 2 * dir && dx === 0 && cy(pc.coord) === base &&
|
||||
grid.at({ x: cx(pc.coord), y: cy(pc.coord)+dir }).length === 0 &&
|
||||
grid.at(to).length === 0);
|
||||
var cap = (dy === dir && Math.abs(dx) === 1 && grid.at(to).length);
|
||||
return one || two || cap;
|
||||
},
|
||||
rook : function (pc, dx, dy) { return (dx === 0 || dy === 0); },
|
||||
bishop: function (pc, dx, dy) { return Math.abs(dx) === Math.abs(dy); },
|
||||
queen : function (pc, dx, dy) { return (dx === 0 || dy === 0 || Math.abs(dx) === Math.abs(dy)); },
|
||||
knight: function (pc, dx, dy) { return (Math.abs(dx) === 1 && Math.abs(dy) === 2) ||
|
||||
(Math.abs(dx) === 2 && Math.abs(dy) === 1); },
|
||||
king : function (pc, dx, dy) { return Math.max(Math.abs(dx), Math.abs(dy)) === 1; }
|
||||
};
|
||||
|
||||
function clearLine(from, to, grid) {
|
||||
var dx = Math.sign(cx(to) - cx(from));
|
||||
var dy = Math.sign(cy(to) - cy(from));
|
||||
var x = cx(from) + dx, y = cy(from) + dy;
|
||||
while (x !== cx(to) || y !== cy(to)) {
|
||||
if (grid.at({ x: x, y: y }).length) return false;
|
||||
x += dx; y += dy;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
function canMove(piece, from, to, grid) {
|
||||
var dx = cx(to) - cx(from);
|
||||
var dy = cy(to) - cy(from);
|
||||
var f = deltas[piece.kind];
|
||||
if (!f || !f(piece, dx, dy, grid, to)) return false;
|
||||
if (piece.kind === 'knight') return true;
|
||||
return clearLine(from, to, grid);
|
||||
}
|
||||
|
||||
return { canMove };
|
||||
|
Before Width: | Height: | Size: 376 B |
|
Before Width: | Height: | Size: 403 B |
|
Before Width: | Height: | Size: 381 B |
|
Before Width: | Height: | Size: 313 B |
|
Before Width: | Height: | Size: 378 B |
|
Before Width: | Height: | Size: 378 B |
@@ -1,234 +0,0 @@
|
||||
// HTTP Download Actor
|
||||
// Handles download requests and progress queries
|
||||
var http = use('http');
|
||||
var os = use('os');
|
||||
|
||||
// Actor state
|
||||
var state = {
|
||||
downloading: false,
|
||||
current_url: null,
|
||||
total_bytes: 0,
|
||||
downloaded_bytes: 0,
|
||||
start_time: 0,
|
||||
error: null,
|
||||
connection: null,
|
||||
download_msg: null,
|
||||
chunks: []
|
||||
};
|
||||
|
||||
// Helper to calculate progress percentage
|
||||
function get_progress() {
|
||||
if (state.total_bytes === 0) {
|
||||
return 0;
|
||||
}
|
||||
return Math.round((state.downloaded_bytes / state.total_bytes) * 100);
|
||||
}
|
||||
|
||||
// Helper to format status response
|
||||
function get_status() {
|
||||
if (!state.downloading) {
|
||||
return {
|
||||
status: 'idle',
|
||||
error: state.error
|
||||
};
|
||||
}
|
||||
|
||||
var elapsed = os.now() - state.start_time;
|
||||
var bytes_per_sec = elapsed > 0 ? state.downloaded_bytes / elapsed : 0;
|
||||
|
||||
return {
|
||||
status: 'downloading',
|
||||
url: state.current_url,
|
||||
progress: get_progress(),
|
||||
downloaded_bytes: state.downloaded_bytes,
|
||||
total_bytes: state.total_bytes,
|
||||
elapsed_seconds: elapsed,
|
||||
bytes_per_second: Math.round(bytes_per_sec)
|
||||
};
|
||||
}
|
||||
|
||||
// Main message receiver
|
||||
$_.receiver(function(msg) {
|
||||
switch (msg.type) {
|
||||
case 'download':
|
||||
if (state.downloading) {
|
||||
send(msg, {
|
||||
type: 'error',
|
||||
error: 'Already downloading',
|
||||
current_url: state.current_url
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
if (!msg.url) {
|
||||
send(msg, {
|
||||
type: 'error',
|
||||
error: 'No URL provided'
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
// Start download
|
||||
state.downloading = true;
|
||||
state.current_url = msg.url;
|
||||
state.total_bytes = 0;
|
||||
state.downloaded_bytes = 0;
|
||||
state.start_time = os.now();
|
||||
state.error = null;
|
||||
state.download_msg = msg;
|
||||
state.chunks = [];
|
||||
|
||||
try {
|
||||
// Start the connection
|
||||
state.connection = http.fetch_start(msg.url, msg.options || {});
|
||||
if (!state.connection) {
|
||||
throw new Error('Failed to start download');
|
||||
}
|
||||
|
||||
// Schedule the first chunk read
|
||||
$_.delay(read_next_chunk, 0);
|
||||
|
||||
} catch (e) {
|
||||
state.error = e.toString();
|
||||
state.downloading = false;
|
||||
|
||||
send(msg, {
|
||||
type: 'error',
|
||||
error: state.error,
|
||||
url: msg.url
|
||||
});
|
||||
}
|
||||
break;
|
||||
|
||||
case 'status':
|
||||
console.log(`got status request. current is ${json.encode(get_status())}`)
|
||||
send(msg, {
|
||||
type: 'status_response',
|
||||
...get_status()
|
||||
});
|
||||
break;
|
||||
|
||||
case 'cancel':
|
||||
if (state.downloading) {
|
||||
// Cancel the download
|
||||
if (state.connection) {
|
||||
http.fetch_close(state.connection);
|
||||
state.connection = null;
|
||||
}
|
||||
state.downloading = false;
|
||||
state.current_url = null;
|
||||
state.download_msg = null;
|
||||
state.chunks = [];
|
||||
|
||||
send(msg, {
|
||||
type: 'cancelled',
|
||||
message: 'Download cancelled',
|
||||
url: state.current_url
|
||||
});
|
||||
} else {
|
||||
send(msg, {
|
||||
type: 'error',
|
||||
error: 'No download in progress'
|
||||
});
|
||||
}
|
||||
break;
|
||||
|
||||
default:
|
||||
send(msg, {
|
||||
type: 'error',
|
||||
error: 'Unknown message type: ' + msg.type
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
// Non-blocking chunk reader
|
||||
function read_next_chunk() {
|
||||
if (!state.downloading || !state.connection) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
var chunk = http.fetch_read_chunk(state.connection);
|
||||
|
||||
if (chunk === null) {
|
||||
// Download complete
|
||||
finish_download();
|
||||
return;
|
||||
}
|
||||
|
||||
// Store chunk
|
||||
state.chunks.push(chunk);
|
||||
|
||||
// Update progress
|
||||
var info = http.fetch_info(state.connection);
|
||||
state.downloaded_bytes = info.bytes_read;
|
||||
if (info.headers_complete && info.content_length > 0) {
|
||||
state.total_bytes = info.content_length;
|
||||
}
|
||||
|
||||
// Schedule next chunk read
|
||||
$_.delay(read_next_chunk, 0);
|
||||
|
||||
} catch (e) {
|
||||
// Error during download
|
||||
state.error = e.toString();
|
||||
if (state.connection) {
|
||||
http.fetch_close(state.connection);
|
||||
}
|
||||
|
||||
if (state.download_msg) {
|
||||
send(state.download_msg, {
|
||||
type: 'error',
|
||||
error: state.error,
|
||||
url: state.current_url
|
||||
});
|
||||
}
|
||||
|
||||
// Reset state
|
||||
state.downloading = false;
|
||||
state.connection = null;
|
||||
state.download_msg = null;
|
||||
state.chunks = [];
|
||||
}
|
||||
}
|
||||
|
||||
// Complete the download and send result
|
||||
function finish_download() {
|
||||
if (state.connection) {
|
||||
http.fetch_close(state.connection);
|
||||
}
|
||||
|
||||
// Combine all chunks into single ArrayBuffer
|
||||
var total_size = 0;
|
||||
for (var i = 0; i < state.chunks.length; i++) {
|
||||
total_size += state.chunks[i].byteLength;
|
||||
}
|
||||
|
||||
var result = new ArrayBuffer(total_size);
|
||||
var view = new Uint8Array(result);
|
||||
var offset = 0;
|
||||
|
||||
for (var i = 0; i < state.chunks.length; i++) {
|
||||
var chunk_view = new Uint8Array(state.chunks[i]);
|
||||
view.set(chunk_view, offset);
|
||||
offset += state.chunks[i].byteLength;
|
||||
}
|
||||
|
||||
// Send complete message
|
||||
if (state.download_msg) {
|
||||
send(state.download_msg, {
|
||||
type: 'complete',
|
||||
url: state.current_url,
|
||||
data: result,
|
||||
size: result.byteLength,
|
||||
duration: os.now() - state.start_time
|
||||
});
|
||||
}
|
||||
|
||||
// Reset state
|
||||
state.downloading = false;
|
||||
state.connection = null;
|
||||
state.current_url = null;
|
||||
state.download_msg = null;
|
||||
state.chunks = [];
|
||||
}
|
||||
@@ -1,29 +0,0 @@
|
||||
// NAT Punchthrough Server
|
||||
// This server helps two chess clients find each other through NAT
|
||||
// The server coordinates the punchthrough by having both clients
|
||||
// connect to each other simultaneously
|
||||
|
||||
var json = use('json');
|
||||
var waiting_client = null;
|
||||
var match_id = 0;
|
||||
|
||||
$_.portal(e => {
|
||||
console.log("NAT server: received connection request");
|
||||
|
||||
if (!is_actor(e.actor))
|
||||
send(e, {reason: "Must provide the actor you want to connect."});
|
||||
|
||||
if (waiting_client) {
|
||||
console.log(`sending out messages! to ${json.encode(e.actor)} and ${json.encode(waiting_client.actor)}`)
|
||||
send(waiting_client, e.actor)
|
||||
send(e, waiting_client.actor)
|
||||
|
||||
waiting_client = undefined
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
waiting_client = e
|
||||
|
||||
console.log(`actor ${json.encode(e.actor)} is waiting ...`)
|
||||
}, 4000);
|
||||
@@ -1,22 +0,0 @@
|
||||
console.log(`nat client starting`)
|
||||
|
||||
$_.contact((actor, reason) => {
|
||||
if (actor) {
|
||||
console.log(`trying to message ${json.encode(actor)}`)
|
||||
send(actor, {type:"greet"})
|
||||
} else {
|
||||
console.log(json.encode(reason))
|
||||
}
|
||||
}, {
|
||||
address: "108.210.60.32", // NAT server's public IP
|
||||
port: 4000,
|
||||
actor: $_
|
||||
})
|
||||
|
||||
$_.receiver(e => {
|
||||
switch(e.type) {
|
||||
case 'greet':
|
||||
console.log(`hello!`)
|
||||
break
|
||||
}
|
||||
})
|
||||
@@ -1,187 +0,0 @@
|
||||
// Steam Integration Example
|
||||
// This example shows how to use Steam achievements and stats
|
||||
|
||||
var steam = use("steam");
|
||||
|
||||
// Achievement names (these should match your Steam app configuration)
|
||||
var ACHIEVEMENTS = {
|
||||
FIRST_WIN: "ACH_FIRST_WIN",
|
||||
PLAY_10_GAMES: "ACH_PLAY_10_GAMES",
|
||||
HIGH_SCORE: "ACH_HIGH_SCORE_1000"
|
||||
};
|
||||
|
||||
// Stat names
|
||||
var STATS = {
|
||||
GAMES_PLAYED: "stat_games_played",
|
||||
TOTAL_SCORE: "stat_total_score",
|
||||
PLAY_TIME: "stat_play_time"
|
||||
};
|
||||
|
||||
var steam_available = false;
|
||||
var stats_loaded = false;
|
||||
|
||||
// Initialize Steam
|
||||
function init_steam() {
|
||||
if (!steam) {
|
||||
console.log("Steam module not available");
|
||||
return false;
|
||||
}
|
||||
|
||||
console.log("Initializing Steam...");
|
||||
steam_available = steam.steam_init();
|
||||
|
||||
if (steam_available) {
|
||||
console.log("Steam initialized successfully");
|
||||
|
||||
// Request current stats/achievements
|
||||
if (steam.stats.stats_request()) {
|
||||
console.log("Stats requested");
|
||||
stats_loaded = true;
|
||||
}
|
||||
} else {
|
||||
console.log("Failed to initialize Steam");
|
||||
}
|
||||
|
||||
return steam_available;
|
||||
}
|
||||
|
||||
// Update Steam (call this regularly, e.g., once per frame)
|
||||
function update_steam() {
|
||||
if (steam_available) {
|
||||
steam.steam_run_callbacks();
|
||||
}
|
||||
}
|
||||
|
||||
// Unlock an achievement
|
||||
function unlock_achievement(achievement_name) {
|
||||
if (!steam_available || !stats_loaded) return false;
|
||||
|
||||
// Check if already unlocked
|
||||
var unlocked = steam.achievement.achievement_get(achievement_name);
|
||||
if (unlocked) {
|
||||
console.log("Achievement already unlocked:", achievement_name);
|
||||
return true;
|
||||
}
|
||||
|
||||
// Unlock it
|
||||
if (steam.achievement.achievement_set(achievement_name)) {
|
||||
console.log("Achievement unlocked:", achievement_name);
|
||||
|
||||
// Store stats to make it permanent
|
||||
steam.stats.stats_store();
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
// Update a stat
|
||||
function update_stat(stat_name, value, is_float) {
|
||||
if (!steam_available || !stats_loaded) return false;
|
||||
|
||||
var success;
|
||||
if (is_float) {
|
||||
success = steam.stats.stats_set_float(stat_name, value);
|
||||
} else {
|
||||
success = steam.stats.stats_set_int(stat_name, value);
|
||||
}
|
||||
|
||||
if (success) {
|
||||
console.log("Stat updated:", stat_name, "=", value);
|
||||
steam.stats.stats_store();
|
||||
}
|
||||
|
||||
return success;
|
||||
}
|
||||
|
||||
// Get a stat value
|
||||
function get_stat(stat_name, is_float) {
|
||||
if (!steam_available || !stats_loaded) return 0;
|
||||
|
||||
if (is_float) {
|
||||
return steam.stats.stats_get_float(stat_name) || 0;
|
||||
} else {
|
||||
return steam.stats.stats_get_int(stat_name) || 0;
|
||||
}
|
||||
}
|
||||
|
||||
// Example game logic
|
||||
var games_played = 0;
|
||||
var total_score = 0;
|
||||
var current_score = 0;
|
||||
|
||||
function start_game() {
|
||||
games_played = get_stat(STATS.GAMES_PLAYED, false);
|
||||
total_score = get_stat(STATS.TOTAL_SCORE, false);
|
||||
current_score = 0;
|
||||
|
||||
console.log("Starting game #" + (games_played + 1));
|
||||
}
|
||||
|
||||
function end_game(score) {
|
||||
current_score = score;
|
||||
games_played++;
|
||||
total_score += score;
|
||||
|
||||
// Update stats
|
||||
update_stat(STATS.GAMES_PLAYED, games_played, false);
|
||||
update_stat(STATS.TOTAL_SCORE, total_score, false);
|
||||
|
||||
// Check for achievements
|
||||
if (games_played === 1) {
|
||||
unlock_achievement(ACHIEVEMENTS.FIRST_WIN);
|
||||
}
|
||||
|
||||
if (games_played >= 10) {
|
||||
unlock_achievement(ACHIEVEMENTS.PLAY_10_GAMES);
|
||||
}
|
||||
|
||||
if (score >= 1000) {
|
||||
unlock_achievement(ACHIEVEMENTS.HIGH_SCORE);
|
||||
}
|
||||
}
|
||||
|
||||
// Cloud save example
|
||||
function save_to_cloud(save_data) {
|
||||
if (!steam_available) return false;
|
||||
|
||||
var json_data = JSON.stringify(save_data);
|
||||
return steam.cloud.cloud_write("savegame.json", json_data);
|
||||
}
|
||||
|
||||
function load_from_cloud() {
|
||||
if (!steam_available) return null;
|
||||
|
||||
var data = steam.cloud.cloud_read("savegame.json");
|
||||
if (data) {
|
||||
// Convert ArrayBuffer to string
|
||||
var decoder = new TextDecoder();
|
||||
var json_str = decoder.decode(data);
|
||||
return JSON.parse(json_str);
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
// Cleanup
|
||||
function cleanup_steam() {
|
||||
if (steam_available) {
|
||||
steam.steam_shutdown();
|
||||
console.log("Steam shut down");
|
||||
}
|
||||
}
|
||||
|
||||
// Export the API
|
||||
module.exports = {
|
||||
init: init_steam,
|
||||
update: update_steam,
|
||||
cleanup: cleanup_steam,
|
||||
unlock_achievement: unlock_achievement,
|
||||
update_stat: update_stat,
|
||||
get_stat: get_stat,
|
||||
start_game: start_game,
|
||||
end_game: end_game,
|
||||
save_to_cloud: save_to_cloud,
|
||||
load_from_cloud: load_from_cloud,
|
||||
is_available: function() { return steam_available; }
|
||||
};
|
||||
203
meson.build
@@ -1,5 +1,5 @@
|
||||
project('prosperon', ['c', 'cpp'],
|
||||
version: '0.9.3',
|
||||
version: '0.9.2',
|
||||
meson_version: '>=1.4',
|
||||
default_options : [ 'cpp_std=c++11'])
|
||||
|
||||
@@ -8,8 +8,6 @@ libtype = get_option('default_library')
|
||||
link = []
|
||||
src = []
|
||||
|
||||
fs = import('fs')
|
||||
|
||||
add_project_arguments('-pedantic', language: ['c'])
|
||||
|
||||
git_tag_cmd = run_command('git', 'describe', '--tags', '--abbrev=0', check: false)
|
||||
@@ -34,9 +32,8 @@ add_project_arguments(
|
||||
add_project_arguments('-Wno-incompatible-pointer-types', language: 'c')
|
||||
add_project_arguments('-Wno-narrowing', language: 'cpp')
|
||||
add_project_arguments('-Wno-missing-braces', language:'c')
|
||||
add_project_arguments('-Wno-strict-prototypes', language:'c')
|
||||
add_project_arguments('-Wno-unused-command-line-argument', language: 'c')
|
||||
add_project_arguments('-Wno-unused-command-line-argument', language: 'cpp')
|
||||
add_project_arguments('-Wl,--disable-new-dtags', language:'cpp')
|
||||
add_project_arguments('-Wl,--disable-new-dtags', language:'c')
|
||||
|
||||
deps = []
|
||||
|
||||
@@ -67,44 +64,25 @@ endif
|
||||
|
||||
cmake = import('cmake')
|
||||
|
||||
mbedtls_opts = cmake.subproject_options()
|
||||
mbedtls_opts.add_cmake_defines({
|
||||
'ENABLE_PROGRAMS': 'OFF', # Disable Mbed TLS programs
|
||||
'ENABLE_TESTING': 'OFF', # Disable Mbed TLS tests
|
||||
'CMAKE_BUILD_TYPE': 'Release', # Optimize for release
|
||||
'MBEDTLS_FATAL_WARNINGS': 'ON', # Treat warnings as errors
|
||||
'USE_STATIC_MBEDTLS_LIBRARY': 'ON',# Build static libraries
|
||||
'USE_SHARED_MBEDTLS_LIBRARY': 'OFF'# Disable shared libraries
|
||||
})
|
||||
mbedtls_proj = cmake.subproject('mbedtls', options: mbedtls_opts)
|
||||
deps += [
|
||||
mbedtls_proj.dependency('mbedtls'),
|
||||
mbedtls_proj.dependency('mbedx509'),
|
||||
mbedtls_proj.dependency('mbedcrypto')
|
||||
]
|
||||
|
||||
sdl3_opts = cmake.subproject_options()
|
||||
sdl3_opts.add_cmake_defines({
|
||||
'SDL_STATIC': 'ON',
|
||||
'SDL_SHARED': 'OFF',
|
||||
'SDL_TEST': 'OFF',
|
||||
'CMAKE_BUILD_TYPE': 'Release',
|
||||
'SDL_THREADS': 'ON',
|
||||
'SDL_PIPEWIRE': 'ON',
|
||||
'SDL_PULSEAUDIO': 'ON',
|
||||
'CMAKE_BUILD_TYPE': 'Release'
|
||||
})
|
||||
|
||||
box2d_opts = cmake.subproject_options()
|
||||
box2d_opts.add_cmake_defines({
|
||||
'BOX2D_SAMPLES': 'OFF',
|
||||
'BOX2D_BUILD_STATIC': 'ON',
|
||||
'BOX2d_AVX2': 'ON',
|
||||
'BOX2D_BUILD_SHARED': 'OFF',
|
||||
chipmunk_opts = cmake.subproject_options()
|
||||
chipmunk_opts.add_cmake_defines({
|
||||
'BUILD_DEMOS': 'OFF',
|
||||
'BUILD_SHARED': 'OFF',
|
||||
'BUILD_STATIC': 'ON',
|
||||
'CMAKE_BUILD_TYPE': 'Release',
|
||||
# uncomment to use floats instead of doubles
|
||||
# 'CP_USE_DOUBLES': 'OFF',
|
||||
})
|
||||
|
||||
box2d_proj = cmake.subproject('box2d', options: box2d_opts)
|
||||
deps += box2d_proj.dependency('box2d')
|
||||
chipmunk_proj = cmake.subproject('chipmunk', options: chipmunk_opts)
|
||||
deps += chipmunk_proj.dependency('chipmunk_static')
|
||||
|
||||
cc = meson.get_compiler('c')
|
||||
|
||||
@@ -128,119 +106,66 @@ if host_machine.system() == 'windows'
|
||||
deps += cc.find_library('imm32')
|
||||
deps += cc.find_library('version')
|
||||
deps += cc.find_library('cfgmgr32')
|
||||
deps += cc.find_library('bcrypt')
|
||||
sdl3_opts.add_cmake_defines({'HAVE_ISINF': '1'}) # Hack for MSYS2
|
||||
sdl3_opts.add_cmake_defines({'HAVE_ISNAN': '1'})
|
||||
sdl3_opts.add_cmake_defines({'HAVE_ISINF': '1'}) # TODO: A hack to get this to compile on MSYS2; otherwise it doesn't link correctly
|
||||
link += '-static'
|
||||
endif
|
||||
|
||||
if host_machine.system() == 'emscripten'
|
||||
link += '-sUSE_WEBGPU'
|
||||
# Use the pre-installed copy
|
||||
deps += dependency('sdl3',
|
||||
static : true,
|
||||
method : 'pkg-config', # or 'cmake' if you prefer
|
||||
required : true)
|
||||
else
|
||||
sdl3_proj = cmake.subproject('sdl3', options : sdl3_opts)
|
||||
deps += sdl3_proj.dependency('SDL3-static')
|
||||
endif
|
||||
|
||||
sdl3_proj = cmake.subproject('sdl3', options: sdl3_opts)
|
||||
|
||||
deps += sdl3_proj.dependency('SDL3-static')
|
||||
|
||||
tracy_opts = ['fibers=true', 'on_demand=true']
|
||||
quickjs_opts = []
|
||||
|
||||
src += 'qjs_tracy.c'
|
||||
add_project_arguments('-DTRACY_ENABLE', language:['c','cpp'])
|
||||
deps += dependency('tracy', static:true, default_options:tracy_opts)
|
||||
|
||||
quickjs_opts += 'default_library=static'
|
||||
|
||||
deps += dependency('quickjs', static:true, default_options:quickjs_opts)
|
||||
deps += dependency('qjs-layout', static:true)
|
||||
deps += dependency('qjs-miniz', static:true)
|
||||
deps += dependency('physfs', static:true)
|
||||
deps += dependency('threads')
|
||||
|
||||
if host_machine.system() != 'emscripten'
|
||||
deps += dependency('enet', static:true)
|
||||
src += 'qjs_enet.c'
|
||||
|
||||
src += 'qjs_tracy.c'
|
||||
tracy_opts = ['fibers=true', 'on_demand=true']
|
||||
add_project_arguments('-DTRACY_ENABLE', language:['c','cpp'])
|
||||
deps += dependency('tracy', static:true, default_options:tracy_opts)
|
||||
|
||||
src += 'qjs_dmon.c'
|
||||
endif
|
||||
|
||||
deps += dependency('soloud', static:true)
|
||||
deps += dependency('libqrencode', static:true)
|
||||
|
||||
# Storefront SDK support
|
||||
storefront = get_option('storefront')
|
||||
if storefront == 'steam'
|
||||
steam_sdk_path = meson.current_source_dir() / 'sdk'
|
||||
|
||||
if host_machine.system() == 'darwin'
|
||||
steam_lib_path = steam_sdk_path / 'redistributable_bin' / 'osx' / 'libsteam_api.dylib'
|
||||
elif host_machine.system() == 'linux'
|
||||
steam_lib_path = steam_sdk_path / 'redistributable_bin' / 'linux64' / 'libsteam_api.so'
|
||||
elif host_machine.system() == 'windows'
|
||||
steam_lib_path = steam_sdk_path / 'redistributable_bin' / 'win64' / 'steam_api64.lib'
|
||||
else
|
||||
steam_lib_path = ''
|
||||
endif
|
||||
|
||||
if fs.exists(steam_lib_path)
|
||||
steam_dep = declare_dependency(
|
||||
include_directories: include_directories('sdk/public'),
|
||||
link_args: [steam_lib_path]
|
||||
)
|
||||
deps += steam_dep
|
||||
src += 'qjs_steam.cpp'
|
||||
message('Steam SDK enabled')
|
||||
else
|
||||
error('Steam SDK required but not found at: ' + steam_lib_path)
|
||||
endif
|
||||
else
|
||||
add_project_arguments('-DNSTEAM', language: ['c', 'cpp'])
|
||||
message('Storefront: ' + storefront)
|
||||
deps += dependency('qjs-steam',static:false)
|
||||
endif
|
||||
|
||||
link_args = link
|
||||
deps += dependency('qjs-layout',static:true)
|
||||
deps += dependency('qjs-miniz',static:true)
|
||||
|
||||
deps += dependency('physfs', static:true)
|
||||
|
||||
#deps += dependency('opencv4')
|
||||
#deps += cc.find_library('opencv')
|
||||
|
||||
deps += dependency('threads')
|
||||
deps += dependency('enet', static:true)
|
||||
deps += dependency('soloud', static:true)
|
||||
|
||||
sources = []
|
||||
src += ['anim.c', 'config.c', 'datastream.c','font.c','HandmadeMath.c','jsffi.c','model.c','render.c','script.c','simplex.c','spline.c', 'timer.c', 'transform.c','prosperon.c', 'wildmatch.c', 'sprite.c', 'rtree.c', 'qjs_dmon.c', 'qjs_nota.c', 'qjs_enet.c', 'qjs_soloud.c', 'qjs_chipmunk.c']
|
||||
|
||||
src += [
|
||||
'anim.c', 'config.c', 'datastream.c','font.c','HandmadeMath.c','jsffi.c','model.c',
|
||||
'render.c','simplex.c','spline.c', 'transform.c','prosperon.c', 'wildmatch.c',
|
||||
'sprite.c', 'rtree.c', 'qjs_nota.c', 'qjs_soloud.c', 'qjs_sdl.c', 'qjs_math.c', 'qjs_geometry.c', 'qjs_transform.c', 'qjs_sprite.c', 'qjs_io.c', 'qjs_os.c', 'qjs_actor.c',
|
||||
'qjs_qr.c', 'qjs_wota.c', 'monocypher.c', 'qjs_blob.c', 'qjs_crypto.c', 'qjs_time.c', 'qjs_http.c', 'qjs_rtree.c', 'qjs_spline.c', 'qjs_js.c', 'qjs_debug.c'
|
||||
]
|
||||
src += 'qjs_box2d.c'
|
||||
# quirc src
|
||||
src += [
|
||||
'thirdparty/quirc/quirc.c', 'thirdparty/quirc/decode.c',
|
||||
'thirdparty/quirc/identify.c', 'thirdparty/quirc/version_db.c'
|
||||
]
|
||||
|
||||
imsrc = [
|
||||
'GraphEditor.cpp','ImCurveEdit.cpp','ImGradient.cpp','imgui_draw.cpp',
|
||||
'imgui_tables.cpp','imgui_widgets.cpp','imgui.cpp','ImGuizmo.cpp','imnodes.cpp',
|
||||
'implot_items.cpp','implot.cpp','imgui_impl_sdlrenderer3.cpp','imgui_impl_sdl3.cpp',
|
||||
'imgui_impl_sdlgpu3.cpp'
|
||||
]
|
||||
imsrc = ['GraphEditor.cpp','ImCurveEdit.cpp','ImGradient.cpp','imgui_draw.cpp','imgui_tables.cpp','imgui_widgets.cpp','imgui.cpp','ImGuizmo.cpp','imnodes.cpp','implot_items.cpp','implot.cpp', 'imgui_impl_sdlrenderer3.cpp', 'imgui_impl_sdl3.cpp', 'imgui_impl_sdlgpu3.cpp']
|
||||
|
||||
srceng = 'source'
|
||||
tp = srceng / 'thirdparty'
|
||||
includes = [
|
||||
srceng, tp / 'cgltf', tp / 'imgui', tp / 'par', tp / 'stb',
|
||||
tp, tp / 'pl_mpeg/include', tp / 'quirc'
|
||||
]
|
||||
|
||||
includes = [srceng,tp / 'cgltf',tp / 'imgui',tp / 'par',tp / 'stb',tp,tp / 'pl_mpeg/include']
|
||||
|
||||
foreach file : src
|
||||
full_path = join_paths('source', file)
|
||||
sources += files(full_path)
|
||||
full_path = join_paths('source', file)
|
||||
sources += files(full_path)
|
||||
endforeach
|
||||
|
||||
if get_option('editor')
|
||||
# sources += 'source/qjs_imgui.cpp'
|
||||
# foreach imgui : imsrc
|
||||
# sources += tp / 'imgui' / imgui
|
||||
# endforeach
|
||||
sources += 'source/qjs_imgui.cpp'
|
||||
foreach imgui : imsrc
|
||||
sources += tp / 'imgui' / imgui
|
||||
endforeach
|
||||
endif
|
||||
|
||||
includers = []
|
||||
@@ -254,7 +179,7 @@ foreach folder: zip_folders
|
||||
zip_paths += meson.project_source_root() / folder
|
||||
endforeach
|
||||
|
||||
# Produce core.zip
|
||||
# Now use the hash file as a dependency so that any change in the files causes a rebuild.
|
||||
core = custom_target('core.zip',
|
||||
output : 'core.zip',
|
||||
command : ['sh', '-c',
|
||||
@@ -274,26 +199,6 @@ prosperon_raw = executable('prosperon_raw', sources,
|
||||
install:false
|
||||
)
|
||||
|
||||
strip_enabled = ['release', 'minsize'].contains(get_option('buildtype'))
|
||||
|
||||
if strip_enabled
|
||||
prosperon_raw_stripped = custom_target('prosperon_raw_stripped',
|
||||
input: prosperon_raw,
|
||||
output: 'prosperon_raw_stripped',
|
||||
command: [
|
||||
'sh', '-c',
|
||||
'strip "$1" && cp "$1" "$2"',
|
||||
'stripper',
|
||||
'@INPUT@',
|
||||
'@OUTPUT@'
|
||||
],
|
||||
build_by_default: true
|
||||
)
|
||||
exe_for_concat = prosperon_raw_stripped
|
||||
else
|
||||
exe_for_concat = prosperon_raw
|
||||
endif
|
||||
|
||||
if host_machine.system() == 'windows'
|
||||
exe_ext = '.exe'
|
||||
else
|
||||
@@ -302,7 +207,7 @@ endif
|
||||
|
||||
prosperon = custom_target('prosperon',
|
||||
output: 'prosperon' + exe_ext,
|
||||
input: [exe_for_concat, core],
|
||||
input: [prosperon_raw, core],
|
||||
command: [
|
||||
'sh', '-c',
|
||||
'cat "$1" "$2" > "$3" && chmod +x "$3" >/dev/null 2>&1',
|
||||
@@ -316,7 +221,7 @@ prosperon = custom_target('prosperon',
|
||||
)
|
||||
|
||||
prosperon_dep = declare_dependency(
|
||||
link_with: prosperon
|
||||
link_with:prosperon
|
||||
)
|
||||
|
||||
copy_tests = custom_target(
|
||||
@@ -335,13 +240,11 @@ tests = [
|
||||
'spawn_actor',
|
||||
'empty',
|
||||
'nota',
|
||||
'wota',
|
||||
'portalspawner',
|
||||
'overling',
|
||||
'send',
|
||||
'delay'
|
||||
'enet',
|
||||
'chipmunk2d'
|
||||
]
|
||||
|
||||
foreach file : tests
|
||||
test(file, prosperon_raw, args:['tests/' + file + '.js'], depends:copy_tests)
|
||||
endforeach
|
||||
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
option('editor', type:'boolean', value:true)
|
||||
option('chipmunk', type:'boolean', value:true)
|
||||
option('enet', type:'boolean', value:true)
|
||||
option('storefront', type:'combo', choices:['none','steam', 'gog', 'egs'], value:'none')
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
site_name: Prosperon Documentation
|
||||
edit_uri: edit/master/doc/docs
|
||||
|
||||
plugins:
|
||||
- search
|
||||
- awesome-pages
|
||||
- mike
|
||||
|
||||
extra_css:
|
||||
- style.css
|
||||
@@ -32,9 +32,6 @@ extra:
|
||||
analytics:
|
||||
provider: google
|
||||
property: G-85ECSFGCBV
|
||||
version:
|
||||
default: latest
|
||||
provider: mike
|
||||
|
||||
markdown_extensions:
|
||||
- admonition
|
||||
|
||||
@@ -625,6 +625,13 @@ debug or serialization. Implementation details may vary.
|
||||
:return: A quoted version of the string.
|
||||
`;
|
||||
|
||||
(String.prototype.localeCompare)[prosperon.DOC] = `Return a number indicating whether this string is less than, equal to, or
|
||||
greater than 'compareString' in sort order, according to the current locale.
|
||||
|
||||
:param compareString: The string to compare against.
|
||||
:return: A negative number if less, 0 if the same, or a positive number if greater.
|
||||
`;
|
||||
|
||||
(String.prototype.toLowerCase)[prosperon.DOC] = `Return a new string with all alphabetic characters converted to lowercase.
|
||||
|
||||
:return: The lowercase version of this string.
|
||||
|
||||
1129
scripts/core/doc.js
@@ -1,164 +0,0 @@
|
||||
$_.unneeded(_ => {
|
||||
}, Infinity)
|
||||
|
||||
var subscribers = []
|
||||
|
||||
var windows = []
|
||||
var renderers = []
|
||||
|
||||
var os = use('os')
|
||||
|
||||
$_.receiver(e => {
|
||||
if (e.type === "subscribe") {
|
||||
if (!e.actor) throw Error('Got a subscribe message with no actor.');
|
||||
subscribers.push(e.actor)
|
||||
return;
|
||||
}
|
||||
|
||||
if (e.type === "window") {
|
||||
var window = windows[e.id]
|
||||
|
||||
switch (e.fn) {
|
||||
case "create":
|
||||
window = os.engine_start(e.config)
|
||||
windows[e.id] = window
|
||||
break;
|
||||
|
||||
case "fullscreen":
|
||||
window.fullscreen()
|
||||
break;
|
||||
|
||||
case "make_renderer":
|
||||
var renderer = window.make_renderer(e.config || {})
|
||||
renderers[e.renderer_id] = renderer
|
||||
break;
|
||||
|
||||
case "keyboard_shown":
|
||||
return window.keyboard_shown()
|
||||
|
||||
case "theme":
|
||||
return window.theme()
|
||||
|
||||
case "safe_area":
|
||||
return window.safe_area()
|
||||
|
||||
case "bordered":
|
||||
window.bordered(e.value)
|
||||
break;
|
||||
|
||||
case "set_icon":
|
||||
window.set_icon(e.icon)
|
||||
break;
|
||||
|
||||
case "set_title":
|
||||
window.title = e.title
|
||||
break;
|
||||
|
||||
case "get_title":
|
||||
return window.title
|
||||
|
||||
case "set_size":
|
||||
window.size = e.size
|
||||
break;
|
||||
|
||||
case "get_size":
|
||||
return window.size
|
||||
|
||||
case "mouse_grab":
|
||||
window.mouse_grab(e.value)
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (e.type === "render") {
|
||||
var renderer = renderers[e.id]
|
||||
|
||||
switch (e.fn) {
|
||||
case "draw_color":
|
||||
renderer.draw_color(e.color)
|
||||
break;
|
||||
|
||||
case "present":
|
||||
renderer.present()
|
||||
break;
|
||||
|
||||
case "clear":
|
||||
renderer.clear()
|
||||
break;
|
||||
|
||||
case "line":
|
||||
renderer.line(e.config)
|
||||
break;
|
||||
|
||||
case "point":
|
||||
renderer.point(e.config)
|
||||
break;
|
||||
|
||||
case "texture":
|
||||
renderer.texture(e.texture, e.src_rect, e.dst_rect, e.angle, e.center)
|
||||
break;
|
||||
|
||||
case "rects":
|
||||
renderer.rects(e.rects)
|
||||
break;
|
||||
|
||||
case "geometry":
|
||||
renderer.geometry(e.vertices, e.indices)
|
||||
break;
|
||||
|
||||
case "geometry2":
|
||||
renderer.geometry2(e.vertices, e.indices)
|
||||
break;
|
||||
|
||||
case "sprite":
|
||||
renderer.sprite(e.config)
|
||||
break;
|
||||
|
||||
case "load_texture":
|
||||
renderer.load_texture(e.path)
|
||||
break;
|
||||
|
||||
case "get_image":
|
||||
renderer.get_image(e.config)
|
||||
break;
|
||||
|
||||
case "scale":
|
||||
renderer.scale(e.scale)
|
||||
break;
|
||||
|
||||
case "logical_size":
|
||||
renderer.logical_size(e.size)
|
||||
break;
|
||||
|
||||
case "viewport":
|
||||
renderer.viewport(e.viewport)
|
||||
break;
|
||||
|
||||
case "clip":
|
||||
renderer.clip(e.rect)
|
||||
break;
|
||||
|
||||
case "vsync":
|
||||
renderer.vsync(e.enable)
|
||||
break;
|
||||
|
||||
case "coords":
|
||||
renderer.coords(e.config)
|
||||
break;
|
||||
|
||||
case "camera":
|
||||
renderer.camera(e.cam, e.layer)
|
||||
break;
|
||||
|
||||
case "screen2world":
|
||||
return renderer.screen2world(e.point)
|
||||
|
||||
case "target":
|
||||
renderer.target(e.target)
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
for (var a of subscribers)
|
||||
send(a, e);
|
||||
});
|
||||
78
scripts/modules/actor.js
Normal file
@@ -0,0 +1,78 @@
|
||||
var ex = {}
|
||||
ex[prosperon.DOC] = `
|
||||
A set of utilities for iterating over a hierarchy of actor-like objects, as well
|
||||
as managing tag-based lookups. Objects are assumed to have a "objects" property,
|
||||
pointing to children or sub-objects, forming a tree.
|
||||
`
|
||||
|
||||
function eachobj(obj, fn) {
|
||||
var val = fn(obj)
|
||||
if (val) return val
|
||||
for (var o in obj.objects) {
|
||||
if (obj.objects[o] === obj) console.error(`Object ${obj.toString()} is referenced by itself.`)
|
||||
val = eachobj(obj.objects[o], fn)
|
||||
if (val) return val
|
||||
}
|
||||
}
|
||||
|
||||
ex.all_objects = function (fn, startobj = world) {
|
||||
return eachobj(startobj, fn)
|
||||
}
|
||||
ex.all_objects[prosperon.DOC] = `
|
||||
:param fn: A callback function that receives each object. If it returns a truthy value, iteration stops and that value is returned.
|
||||
:param startobj: The root object at which iteration begins, default is the global "world".
|
||||
:return: The first truthy value returned by fn, or undefined if none.
|
||||
Iterate over each object (and its sub-objects) in the hierarchy, calling fn for each one.
|
||||
`
|
||||
|
||||
ex.find_object = function (fn, startobj = world) {}
|
||||
ex.find_object[prosperon.DOC] = `
|
||||
:param fn: A callback or criteria to locate a particular object.
|
||||
:param startobj: The root object at which search begins, default "world".
|
||||
:return: Not yet implemented.
|
||||
Intended to find a matching object within the hierarchy.
|
||||
`
|
||||
|
||||
var gtags = {}
|
||||
|
||||
ex.tag_add = function (tag, obj) {
|
||||
gtags[tag] ??= new Set()
|
||||
gtags[tag].add(obj)
|
||||
}
|
||||
ex.tag_add[prosperon.DOC] = `
|
||||
:param tag: A string tag to associate with the object.
|
||||
:param obj: The object to add under this tag.
|
||||
:return: None
|
||||
Associate the given object with the specified tag. Creates a new tag set if it does not exist.
|
||||
`
|
||||
|
||||
ex.tag_rm = function (tag, obj) {
|
||||
delete gtags[tag].delete(obj)
|
||||
}
|
||||
ex.tag_rm[prosperon.DOC] = `
|
||||
:param tag: The tag to remove the object from.
|
||||
:param obj: The object to remove from the tag set.
|
||||
:return: None
|
||||
Remove the given object from the specified tag’s set, if it exists.
|
||||
`
|
||||
|
||||
ex.tag_clear_guid = function (obj) {
|
||||
for (var tag in gtags) gtags[tag].delete(obj)
|
||||
}
|
||||
ex.tag_clear_guid[prosperon.DOC] = `
|
||||
:param obj: The object whose tags should be cleared.
|
||||
:return: None
|
||||
Remove the object from all tag sets.
|
||||
`
|
||||
|
||||
ex.objects_with_tag = function (tag) {
|
||||
if (!gtags[tag]) return []
|
||||
return Array.from(gtags[tag])
|
||||
}
|
||||
ex.objects_with_tag[prosperon.DOC] = `
|
||||
:param tag: A string tag to look up.
|
||||
:return: An array of objects associated with the given tag.
|
||||
Retrieve all objects currently tagged with the specified tag.
|
||||
`
|
||||
|
||||
return ex
|
||||
564
scripts/modules/chipmunk2d.js
Normal file
@@ -0,0 +1,564 @@
|
||||
var chipmunk = this;
|
||||
return chipmunk
|
||||
|
||||
//------------------------------------------------
|
||||
// Top-level Chipmunk2D functions
|
||||
//------------------------------------------------
|
||||
|
||||
chipmunk.make_space[prosperon.DOC] = `
|
||||
Create and return a new Chipmunk2D cpSpace instance. By default, this space has
|
||||
no bodies or shapes. You can add bodies via space.add_body() and step the simulation
|
||||
with space.step().
|
||||
|
||||
:return: A newly created Space object.
|
||||
`;
|
||||
|
||||
//------------------------------------------------
|
||||
// Space methods and properties
|
||||
//------------------------------------------------
|
||||
|
||||
var cpSpace = prosperon.c_types.cpSpace;
|
||||
|
||||
cpSpace.step[prosperon.DOC] = `
|
||||
Advance the physics simulation by the specified timestep.
|
||||
|
||||
:param dt: A number representing the time step to simulate (e.g., 1/60 for 60 FPS).
|
||||
:return: None
|
||||
`;
|
||||
|
||||
cpSpace.add_body[prosperon.DOC] = `
|
||||
Create and add a new dynamic body to this space. Returns the newly created cpBody
|
||||
object, which you can then configure (mass, moment, etc.) or attach shapes to.
|
||||
|
||||
:return: A cpBody object representing the new body.
|
||||
`;
|
||||
|
||||
cpSpace.eachBody[prosperon.DOC] = `
|
||||
Iterate over every cpBody in this space, calling the provided callback for each one.
|
||||
Useful for enumerating or modifying all bodies.
|
||||
|
||||
:param callback: A function(body) called for each cpBody in this space.
|
||||
:return: None
|
||||
`;
|
||||
|
||||
cpSpace.eachShape[prosperon.DOC] = `
|
||||
Iterate over every cpShape in this space, calling the provided callback for each one.
|
||||
|
||||
:param callback: A function(shape) called for each cpShape in this space.
|
||||
:return: None
|
||||
`;
|
||||
|
||||
cpSpace.eachConstraint[prosperon.DOC] = `
|
||||
Iterate over every cpConstraint in this space, calling the provided callback for each.
|
||||
|
||||
:param callback: A function(constraint) called for each cpConstraint in this space.
|
||||
:return: None
|
||||
`;
|
||||
|
||||
cpSpace.gravity[prosperon.DOC] = `
|
||||
The gravity vector for this space, typically something like { x: 0, y: -9.8 } for
|
||||
Earth-like gravity in 2D. You can read or write this property to change gravity.
|
||||
|
||||
:return: An object { x, y } for read. Assign a similar object to set.
|
||||
`;
|
||||
|
||||
cpSpace.iterations[prosperon.DOC] = `
|
||||
The number of solver iterations that Chipmunk2D performs each time step. Higher values
|
||||
improve stability at a performance cost. You can read or write this property.
|
||||
|
||||
:return: Number of iterations.
|
||||
`;
|
||||
|
||||
cpSpace.idle_speed[prosperon.DOC] = `
|
||||
Bodies with a speed (velocity magnitude) lower than idle_speed for a certain amount of
|
||||
time can enter sleep mode, which saves CPU. Read or set this threshold.
|
||||
|
||||
:return: A number indicating the idle speed threshold.
|
||||
`;
|
||||
|
||||
cpSpace.sleep_time[prosperon.DOC] = `
|
||||
A duration threshold (in seconds) for which a body must remain below idle_speed before it
|
||||
can sleep. Read or set this property.
|
||||
|
||||
:return: Number of seconds for the sleep threshold.
|
||||
`;
|
||||
|
||||
cpSpace.collision_slop[prosperon.DOC] = `
|
||||
Extra distance allowed to mitigate floating-point issues, preventing shapes from
|
||||
interpenetrating excessively. Can be read or set.
|
||||
|
||||
:return: A number representing the collision slop distance.
|
||||
`;
|
||||
|
||||
cpSpace.collision_bias[prosperon.DOC] = `
|
||||
Rate at which interpenetration errors are corrected each step. Usually a number close to
|
||||
1.0 (a bit less). Can be read or set.
|
||||
|
||||
:return: A number controlling bias for shape collision resolution.
|
||||
`;
|
||||
|
||||
cpSpace.collision_persistence[prosperon.DOC] = `
|
||||
How many steps a contact persists if the two shapes are still overlapping. Read or set.
|
||||
|
||||
:return: An integer count of persistence steps.
|
||||
`;
|
||||
|
||||
//------------------------------------------------
|
||||
// Space joint-creation methods
|
||||
//------------------------------------------------
|
||||
|
||||
cpSpace.pin[prosperon.DOC] = `
|
||||
Create a cpPinJoint between two bodies (argv[0] and argv[1]) and add it to this space.
|
||||
If called without arguments, returns the PinJoint prototype object.
|
||||
|
||||
:return: A cpPinJoint constraint object if arguments are provided, or the prototype if none.
|
||||
`;
|
||||
|
||||
cpSpace.pivot[prosperon.DOC] = `
|
||||
Create a cpPivotJoint between two bodies at a given pivot point. If called without
|
||||
arguments, returns the PivotJoint prototype object.
|
||||
|
||||
:param bodyA: The first cpBody.
|
||||
:param bodyB: The second cpBody.
|
||||
:param pivotPoint: An object { x, y } for the pivot location in world coordinates.
|
||||
:return: A cpPivotJoint constraint object or the prototype if no arguments.
|
||||
`;
|
||||
|
||||
cpSpace.gear[prosperon.DOC] = `
|
||||
Create a cpGearJoint between two bodies, controlling their relative angular motion.
|
||||
If called without arguments, returns the GearJoint prototype object.
|
||||
|
||||
:param bodyA: The first cpBody.
|
||||
:param bodyB: The second cpBody.
|
||||
:param phase: Initial angular offset in radians.
|
||||
:param ratio: Gear ratio linking rotations of bodyA and bodyB.
|
||||
:return: A cpGearJoint constraint object or prototype if no arguments.
|
||||
`;
|
||||
|
||||
cpSpace.rotary[prosperon.DOC] = `
|
||||
Create a cpRotaryLimitJoint that constrains rotation between two bodies to a min and max
|
||||
angle. Without arguments, returns the RotaryLimitJoint prototype.
|
||||
|
||||
:param bodyA: The first cpBody.
|
||||
:param bodyB: The second cpBody.
|
||||
:param minAngle: Minimum relative angle in radians.
|
||||
:param maxAngle: Maximum relative angle in radians.
|
||||
:return: A cpRotaryLimitJoint constraint object or prototype if no arguments.
|
||||
`;
|
||||
|
||||
cpSpace.damped_rotary[prosperon.DOC] = `
|
||||
Create a cpDampedRotarySpring that uses a spring-damper system to keep two bodies at a
|
||||
relative rest angle. Without arguments, returns the prototype.
|
||||
|
||||
:param bodyA: The first cpBody.
|
||||
:param bodyB: The second cpBody.
|
||||
:param restAngle: Rest angle between the bodies.
|
||||
:param stiffness: Spring stiffness.
|
||||
:param damping: Damping factor.
|
||||
:return: A cpDampedRotarySpring object or prototype if no arguments.
|
||||
`;
|
||||
|
||||
cpSpace.damped_spring[prosperon.DOC] = `
|
||||
Create a cpDampedSpring between two bodies, each with an anchor point and a rest length.
|
||||
Without arguments, returns the prototype.
|
||||
|
||||
:param bodyA: The first cpBody.
|
||||
:param bodyB: The second cpBody.
|
||||
:param anchorA: { x, y } anchor relative to bodyA.
|
||||
:param anchorB: { x, y } anchor relative to bodyB.
|
||||
:param restLength: The spring's natural rest length.
|
||||
:param stiffness: The spring stiffness.
|
||||
:param damping: The damping factor.
|
||||
:return: A cpDampedSpring constraint or prototype if no arguments.
|
||||
`;
|
||||
|
||||
cpSpace.groove[prosperon.DOC] = `
|
||||
Create a cpGrooveJoint, which allows one body’s anchor to slide along a groove defined
|
||||
in the other body. Without arguments, returns the prototype.
|
||||
|
||||
:param bodyA: The first cpBody.
|
||||
:param bodyB: The second cpBody.
|
||||
:param grooveA: { x, y } start of the groove in bodyA.
|
||||
:param grooveB: { x, y } end of the groove in bodyA.
|
||||
:param anchorB: { x, y } anchor point on bodyB.
|
||||
:return: A cpGrooveJoint object or prototype if no arguments.
|
||||
`;
|
||||
|
||||
cpSpace.slide[prosperon.DOC] = `
|
||||
Create a cpSlideJoint that limits the distance between two anchor points on two bodies
|
||||
to a min and max. Without arguments, returns the prototype.
|
||||
|
||||
:param bodyA: The first cpBody.
|
||||
:param bodyB: The second cpBody.
|
||||
:param anchorA: { x, y } anchor relative to bodyA.
|
||||
:param anchorB: { x, y } anchor relative to bodyB.
|
||||
:param minDistance: Minimum distance allowed.
|
||||
:param maxDistance: Maximum distance allowed.
|
||||
:return: A cpSlideJoint object or prototype if no arguments.
|
||||
`;
|
||||
|
||||
cpSpace.ratchet[prosperon.DOC] = `
|
||||
Create a cpRatchetJoint, allowing relative rotation in steps (like a ratchet). Without
|
||||
arguments, returns the prototype.
|
||||
|
||||
:param bodyA: The first cpBody.
|
||||
:param bodyB: The second cpBody.
|
||||
:param phase: Initial angular offset.
|
||||
:param ratchet: The angle increment for each "click" of the ratchet.
|
||||
:return: A cpRatchetJoint object or prototype if no arguments.
|
||||
`;
|
||||
|
||||
cpSpace.motor[prosperon.DOC] = `
|
||||
Create a cpSimpleMotor that enforces a relative angular velocity between two bodies.
|
||||
Without arguments, returns the prototype.
|
||||
|
||||
:param bodyA: The first cpBody.
|
||||
:param bodyB: The second cpBody.
|
||||
:param rate: The desired relative angular speed (radians per second).
|
||||
:return: A cpSimpleMotor object or prototype if no arguments.
|
||||
`;
|
||||
|
||||
//------------------------------------------------
|
||||
// Body methods and properties
|
||||
//------------------------------------------------
|
||||
|
||||
var cpBody = prosperon.c_types.cpBody;
|
||||
|
||||
cpBody.position[prosperon.DOC] = `
|
||||
The current position (x, y) of this body in world coordinates. Read or assign { x, y }.
|
||||
`;
|
||||
|
||||
cpBody.angle[prosperon.DOC] = `
|
||||
The body's rotation in radians. 0 is "no rotation". Read or set this property.
|
||||
`;
|
||||
|
||||
cpBody.velocity[prosperon.DOC] = `
|
||||
The linear velocity of this body (x, y). Typically updated each time step, but you
|
||||
can also set it directly.
|
||||
`;
|
||||
|
||||
cpBody.angularVelocity[prosperon.DOC] = `
|
||||
The body's angular velocity (radians per second).
|
||||
`;
|
||||
|
||||
cpBody.moment[prosperon.DOC] = `
|
||||
The moment of inertia (rotational inertia) for this body. Must not be changed if
|
||||
the body is static or kinematic.
|
||||
`;
|
||||
|
||||
cpBody.torque[prosperon.DOC] = `
|
||||
Accumulated torque on this body. Setting this directly allows you to apply a net
|
||||
torque each frame.
|
||||
`;
|
||||
|
||||
cpBody.mass[prosperon.DOC] = `
|
||||
Mass of the body. Must not be changed if the body is static or kinematic.
|
||||
`;
|
||||
|
||||
cpBody.centerOfGravity[prosperon.DOC] = `
|
||||
Offset of the center of gravity (relative to the body's local origin). Typically
|
||||
set before shapes are attached.
|
||||
`;
|
||||
|
||||
cpBody.force[prosperon.DOC] = `
|
||||
Accumulated force on this body. Setting this is another way to apply a direct force
|
||||
each frame.
|
||||
`;
|
||||
|
||||
cpBody.type[prosperon.DOC] = `
|
||||
The body's type. 0 = CP_BODY_TYPE_DYNAMIC, 1 = CP_BODY_TYPE_KINEMATIC, 2 = CP_BODY_TYPE_STATIC.
|
||||
Changing the type for a body can move it in or out of the simulation.
|
||||
`;
|
||||
|
||||
cpBody.isSleeping[prosperon.DOC] = `
|
||||
Returns true if this body is currently sleeping. Sleep is managed automatically.
|
||||
`;
|
||||
|
||||
cpBody.activate[prosperon.DOC] = `
|
||||
Wake up this body if it is sleeping, making it active again. Typically needed if
|
||||
you manually move a sleeping body.
|
||||
`;
|
||||
|
||||
cpBody.sleep[prosperon.DOC] = `
|
||||
Force this body to sleep immediately. Useful if you know it won't move for a while.
|
||||
`;
|
||||
|
||||
cpBody.activateStatic[prosperon.DOC] = `
|
||||
Wake up any dynamic bodies touching this static/kinematic body. Optionally pass a shape
|
||||
to wake only bodies that touch that shape.
|
||||
:param shape: (optional) A cpShape that is part of this body.
|
||||
:return: None
|
||||
`;
|
||||
|
||||
cpBody.sleepWithGroup[prosperon.DOC] = `
|
||||
Put this body to sleep as though it were in the same group as another sleeping body.
|
||||
Allows linking bodies for group sleep.
|
||||
|
||||
:param otherBody: Another body that is already sleeping.
|
||||
:return: None
|
||||
`;
|
||||
|
||||
cpBody.applyForceAtWorldPoint[prosperon.DOC] = `
|
||||
Apply a force to this body at a specific world space point. This can move or rotate the
|
||||
body depending on the offset from its center of gravity.
|
||||
|
||||
:param force: { x, y } force vector in world coordinates.
|
||||
:param point: { x, y } point in world coordinates.
|
||||
:return: None
|
||||
`;
|
||||
|
||||
cpBody.applyForceAtLocalPoint[prosperon.DOC] = `
|
||||
Apply a force at a local point on the body (local coords). The body’s transform is used
|
||||
to find its world effect.
|
||||
|
||||
:param force: { x, y } force vector in body local coordinates.
|
||||
:param point: { x, y } local point relative to the body's origin.
|
||||
:return: None
|
||||
`;
|
||||
|
||||
cpBody.applyImpulseAtWorldPoint[prosperon.DOC] = `
|
||||
Apply an instantaneous impulse (like a collision) at a point in world coordinates.
|
||||
|
||||
:param impulse: { x, y } impulse vector in world coordinates.
|
||||
:param point: { x, y } where to apply the impulse, in world coordinates.
|
||||
:return: None
|
||||
`;
|
||||
|
||||
cpBody.applyImpulseAtLocalPoint[prosperon.DOC] = `
|
||||
Apply an instantaneous impulse at a local point. Similar to applyForceAtLocalPoint, but
|
||||
impulse is used instead of a continuous force.
|
||||
|
||||
:param impulse: { x, y } impulse vector in local coordinates.
|
||||
:param point: { x, y } local point on the body.
|
||||
:return: None
|
||||
`;
|
||||
|
||||
cpBody.eachShape[prosperon.DOC] = `
|
||||
Iterate over all cpShape objects attached to this body, calling the provided callback.
|
||||
|
||||
:param callback: A function(shape) that is called once per shape.
|
||||
:return: None
|
||||
`;
|
||||
|
||||
cpBody.eachConstraint[prosperon.DOC] = `
|
||||
Iterate over all cpConstraint objects attached to this body, calling the provided
|
||||
callback.
|
||||
|
||||
:param callback: A function(constraint) that is called once per constraint.
|
||||
:return: None
|
||||
`;
|
||||
|
||||
cpBody.eachArbiter[prosperon.DOC] = `
|
||||
Iterate over all cpArbiters (contact pairs) for this body. Not currently implemented
|
||||
for JavaScript, so it’s a no-op in this binding.
|
||||
|
||||
:return: None
|
||||
`;
|
||||
|
||||
cpBody.add_circle_shape[prosperon.DOC] = `
|
||||
Attach a circle shape to this body. Does not automatically add the shape to the space.
|
||||
You may need to space.addShape(...) or store it.
|
||||
|
||||
:param radius: The radius of the circle shape.
|
||||
:param offset: { x, y } local offset of the circle center from the body’s origin.
|
||||
:return: A cpShape representing the circle.
|
||||
`;
|
||||
|
||||
cpBody.add_segment_shape[prosperon.DOC] = `
|
||||
Attach a line segment shape to this body. Does not automatically add it to the space.
|
||||
Useful for edges or walls.
|
||||
|
||||
:param a: { x, y } start point of the segment in local coords.
|
||||
:param b: { x, y } end point of the segment in local coords.
|
||||
:param radius: Thickness radius for the segment.
|
||||
:return: A cpShape representing the segment.
|
||||
`;
|
||||
|
||||
cpBody.add_poly_shape[prosperon.DOC] = `
|
||||
Attach a polygon shape to this body. Currently a stub that uses a placeholder for verts.
|
||||
Does not automatically add the shape to the space.
|
||||
|
||||
:param count: Number of vertices. (Actual vertex data is not fully implemented in the stub.)
|
||||
:return: A cpShape representing the polygon.
|
||||
`;
|
||||
|
||||
//------------------------------------------------
|
||||
// Shape methods and properties
|
||||
//------------------------------------------------
|
||||
|
||||
var cpShape = prosperon.c_types.cpShape;
|
||||
|
||||
cpShape.getBB[prosperon.DOC] = `
|
||||
Retrieve the bounding box of this shape. Returns an object with x, y, width, and height
|
||||
corresponding to the bounding box in world coordinates.
|
||||
|
||||
:return: An object { x, y, width, height }.
|
||||
`;
|
||||
|
||||
cpShape.collisionType[prosperon.DOC] = `
|
||||
An integer used to identify the collision type of this shape. Used with collision handlers
|
||||
if you have custom collision code. Assign any int to set.
|
||||
|
||||
:return: The collision type integer.
|
||||
`;
|
||||
|
||||
cpShape.sensor[prosperon.DOC] = `
|
||||
If true, the shape is a sensor that detects collisions without generating contact forces.
|
||||
Set to false for normal collisions.
|
||||
|
||||
:return: Boolean indicating sensor status.
|
||||
`;
|
||||
|
||||
cpShape.elasticity[prosperon.DOC] = `
|
||||
Coefficient of restitution (bounciness). Ranges from 0 (inelastic) to 1 (fully elastic),
|
||||
though values above 1 are possible (super bouncy).
|
||||
|
||||
:return: A number for shape elasticity.
|
||||
`;
|
||||
|
||||
cpShape.friction[prosperon.DOC] = `
|
||||
Coefficient of friction for this shape. Typically 0 for no friction to 1 or more for
|
||||
high friction.
|
||||
|
||||
:return: A number for friction.
|
||||
`;
|
||||
|
||||
cpShape.surfaceVelocity[prosperon.DOC] = `
|
||||
Relative velocity of the shape’s surface, useful for conveyors. Typically { x:0, y:0 }
|
||||
for normal shapes.
|
||||
|
||||
:return: { x, y } velocity vector.
|
||||
`;
|
||||
|
||||
cpShape.filter[prosperon.DOC] = `
|
||||
Collision filtering parameters. An object { categories, mask, group } controlling which
|
||||
objects this shape collides with. E.g., shape.filter = { categories: 1, mask: 0xFFFFFFFF, group: 0 }
|
||||
|
||||
:return: Object with fields categories, mask, and group.
|
||||
`;
|
||||
|
||||
//------------------------------------------------
|
||||
// Circle shape (subtype of cpShape)
|
||||
//------------------------------------------------
|
||||
|
||||
var cpCircleShape = prosperon.c_types.cpCircleShape;
|
||||
|
||||
cpCircleShape.radius[prosperon.DOC] = `
|
||||
The radius of this circle shape.
|
||||
|
||||
:return: A number representing circle radius.
|
||||
`;
|
||||
|
||||
cpCircleShape.offset[prosperon.DOC] = `
|
||||
A local offset of the circle center relative to the body's origin. Typically { x: 0, y: 0 }.
|
||||
|
||||
:return: { x, y } local offset.
|
||||
`;
|
||||
|
||||
//------------------------------------------------
|
||||
// Segment shape (subtype of cpShape)
|
||||
//------------------------------------------------
|
||||
|
||||
var cpSegmentShape = prosperon.c_types.cpSegmentShape;
|
||||
|
||||
cpSegmentShape.setEndpoints[prosperon.DOC] = `
|
||||
Change the endpoints of this line segment. Each endpoint is specified as an { x, y }
|
||||
vector in the body's local coordinates.
|
||||
|
||||
:param startPoint: { x, y } local coordinates for the segment start.
|
||||
:param endPoint: { x, y } local coordinates for the segment end.
|
||||
:return: None
|
||||
`;
|
||||
|
||||
cpSegmentShape.radius[prosperon.DOC] = `
|
||||
Thickness radius of this segment shape.
|
||||
|
||||
:return: Number representing the segment's thickness radius.
|
||||
`;
|
||||
|
||||
//------------------------------------------------
|
||||
// Poly shape (subtype of cpShape)
|
||||
//------------------------------------------------
|
||||
|
||||
var cpPolyShape = prosperon.c_types.cpPolyShape;
|
||||
|
||||
cpPolyShape.setVerts[prosperon.DOC] = `
|
||||
Set the vertices of this polygon shape. Currently a stub: in reality you’d pass an array
|
||||
of { x, y } points to define the polygon outline.
|
||||
|
||||
:param count: Number of vertices (integer).
|
||||
:param verts: (stub) Array of { x, y } points in local coords.
|
||||
:return: None
|
||||
`;
|
||||
|
||||
cpPolyShape.radius[prosperon.DOC] = `
|
||||
Radius used to give the polygon a beveled edge. 0 for a sharp polygon, >0 for smoothing
|
||||
corners.
|
||||
|
||||
:return: Number representing the bevel radius.
|
||||
`;
|
||||
|
||||
//------------------------------------------------
|
||||
// Constraint methods and properties
|
||||
//------------------------------------------------
|
||||
|
||||
var cpConstraint = prosperon.c_types.cpConstraint;
|
||||
|
||||
cpConstraint.bodyA[prosperon.DOC] = `
|
||||
Return the first body attached to this constraint.
|
||||
|
||||
:return: The cpBody object for body A.
|
||||
`;
|
||||
|
||||
cpConstraint.bodyB[prosperon.DOC] = `
|
||||
Return the second body attached to this constraint.
|
||||
|
||||
:return: The cpBody object for body B.
|
||||
`;
|
||||
|
||||
cpConstraint.max_force[prosperon.DOC] = `
|
||||
The maximum force the constraint can apply. If the constraint would need more than this
|
||||
force to maintain its constraints, it won't hold them fully.
|
||||
|
||||
:return: Number for max force.
|
||||
`;
|
||||
|
||||
cpConstraint.max_bias[prosperon.DOC] = `
|
||||
Limits how quickly the constraint can correct errors each step. Setting a maxBias too low
|
||||
can cause "soft" constraints.
|
||||
|
||||
:return: Number controlling maximum correction speed.
|
||||
`;
|
||||
|
||||
cpConstraint.error_bias[prosperon.DOC] = `
|
||||
Bias factor controlling how quickly overlap/penetration is corrected. Typically close to 1.0.
|
||||
|
||||
:return: Number (0..1 range).
|
||||
`;
|
||||
|
||||
cpConstraint.collide_bodies[prosperon.DOC] = `
|
||||
If true, the connected bodies can still collide with each other. If false, the bodies
|
||||
won't collide. Usually false for "connected" objects.
|
||||
|
||||
:return: Boolean indicating if bodies collide.
|
||||
`;
|
||||
|
||||
cpConstraint.broken[prosperon.DOC] = `
|
||||
Check if the constraint is still in the space. Returns true if the space still contains
|
||||
this constraint, false otherwise.
|
||||
|
||||
:return: Boolean indicating whether the constraint remains in the space.
|
||||
`;
|
||||
|
||||
cpConstraint.break[prosperon.DOC] = `
|
||||
Remove this constraint from the space immediately, effectively "breaking" it.
|
||||
|
||||
:return: None
|
||||
`;
|
||||
|
||||
//------------------------------------------------
|
||||
// Return the chipmunk object
|
||||
//------------------------------------------------
|
||||
|
||||
|
||||
return chipmunk;
|
||||
@@ -1,6 +1,7 @@
|
||||
var io = use('io')
|
||||
var util = use('util')
|
||||
|
||||
|
||||
var dumpfolder = ".prosperon";
|
||||
|
||||
io.mkdir(dumpfolder)
|
||||
@@ -30,12 +31,20 @@ Cmdline.register_order(
|
||||
|
||||
var gs = ['console', 'prosperon', 'actor', 'use']
|
||||
|
||||
Object.getOwnPropertyDescriptor(prosperon.c_types.transform, 'pos')[prosperon.DOC] = 'TEST DOC'
|
||||
|
||||
console.log(Object.getOwnPropertyDescriptor(prosperon.c_types.transform,'pos')[prosperon.DOC])
|
||||
|
||||
for (var g of gs)
|
||||
io.slurpwrite(`.src/docs/api/${g}.md`, doc.writeDocFile(globalThis[g], g))
|
||||
|
||||
var coredocs = io.enumerate("scripts/modules", 0)
|
||||
coredocs = coredocs.filter(x => io.match("**/*.js", x)).map(x => x.name())
|
||||
|
||||
var TYPEPATH = '.src/docs/api/types/'
|
||||
for (var c in prosperon.c_types)
|
||||
io.slurpwrite(`${TYPEPATH}${c}.md`, doc.writeDocFile(prosperon.c_types[c], c))
|
||||
|
||||
var APIPATH = '.src/docs/api/modules/'
|
||||
|
||||
for (var m of coredocs) {
|
||||
@@ -43,10 +52,6 @@ Cmdline.register_order(
|
||||
var path = `${APIPATH}${m}.md`
|
||||
io.slurpwrite(path, doc.writeDocFile(u, m))
|
||||
}
|
||||
|
||||
var TYPEPATH = '.src/docs/api/types/'
|
||||
for (var c in prosperon.c_types)
|
||||
io.slurpwrite(`${TYPEPATH}${c}.md`, doc.writeDocFile(prosperon.c_types[c], c))
|
||||
|
||||
var DULLPATH = '.src/docs/dull/'
|
||||
var mixins = ['Object', 'String', 'Array', 'Map', 'WeakMap', 'Symbol','Set', 'WeakSet', 'ArrayBuffer', 'Function']
|
||||
@@ -64,14 +69,31 @@ Cmdline.register_order(
|
||||
"Make documentation."
|
||||
})
|
||||
|
||||
function spawn_root(script)
|
||||
{
|
||||
return actor.spawn(script, {}, function(underling, msg) {
|
||||
if (msg.message !== "created") return;
|
||||
Object.defineProperty(underling, 'then', {
|
||||
configurable:false,
|
||||
writable:false,
|
||||
value:function() {
|
||||
os.exit(0);
|
||||
}
|
||||
});
|
||||
})
|
||||
}
|
||||
|
||||
Cmdline.register_order(
|
||||
"play",
|
||||
function (argv) {
|
||||
var app
|
||||
if (io.exists("main.js"))
|
||||
app = actor.spawn("main.js")
|
||||
app = spawn_root("main.js")
|
||||
else
|
||||
app = actor.spawn("nogame.js")
|
||||
app = spawn_root("nogame.js")
|
||||
|
||||
// rm actor so it can't be tampered
|
||||
globalThis.actor = undefined
|
||||
|
||||
var loop = use('loop')
|
||||
while(1) loop.step();
|
||||
@@ -118,6 +140,28 @@ Cmdline.register_order(
|
||||
"OBJECT ?FILE?",
|
||||
);
|
||||
|
||||
Cmdline.register_order(
|
||||
"run",
|
||||
function (script) {
|
||||
var s = os.now()
|
||||
script = script.join(" ");
|
||||
if (!script) {
|
||||
console.print("Need something to run.");
|
||||
return;
|
||||
}
|
||||
try {
|
||||
spawn_root(script)
|
||||
} catch(e) {
|
||||
console.error(e);
|
||||
os.exit(1);
|
||||
}
|
||||
},
|
||||
"Run a given script. SCRIPT can be the script itself, or a file containing the script",
|
||||
"SCRIPT",
|
||||
);
|
||||
|
||||
Cmdline.orders.script = Cmdline.orders.run;
|
||||
|
||||
Cmdline.print_order = function (fn) {
|
||||
if (typeof fn === "string") fn = Cmdline.orders[fn];
|
||||
|
||||
@@ -126,50 +170,6 @@ Cmdline.print_order = function (fn) {
|
||||
console.print(fn.doc + "\n");
|
||||
};
|
||||
|
||||
function parse_args(argv)
|
||||
{
|
||||
var args = {};
|
||||
for (var i = 0; i < argv.length; i++) {
|
||||
if (argv[i].startsWith("--")) {
|
||||
var key = argv[i].slice(2);
|
||||
if (i + 1 < argv.length && !argv[i + 1].startsWith("--")) {
|
||||
args[key] = argv[i + 1];
|
||||
i++; // Skip the value
|
||||
} else {
|
||||
args[key] = true; // Flag without value
|
||||
}
|
||||
}
|
||||
}
|
||||
return args;
|
||||
}
|
||||
|
||||
function unparse_args(args) {
|
||||
var argv = [];
|
||||
for (var key in args) {
|
||||
if (args.hasOwnProperty(key)) {
|
||||
argv.push("--" + key); // Add the flag with "--" prefix
|
||||
if (args[key] !== true) {
|
||||
argv.push(args[key]); // Add the value if it's not a boolean true flag
|
||||
}
|
||||
}
|
||||
}
|
||||
return argv;
|
||||
}
|
||||
|
||||
Cmdline.register_order(
|
||||
"spawn",
|
||||
function(argv) {
|
||||
prosperon.args = parse_args(argv)
|
||||
if (!prosperon.args.cwd) prosperon.args.cwd = '.'
|
||||
io.mount(prosperon.args.cwd)
|
||||
|
||||
if (!prosperon.args.program)
|
||||
os.exit()
|
||||
},
|
||||
"Spawn a new prosperon actor.",
|
||||
"TOPIC"
|
||||
);
|
||||
|
||||
Cmdline.register_order(
|
||||
"help",
|
||||
function (order) {
|
||||
@@ -204,37 +204,15 @@ Cmdline.register_order(
|
||||
);
|
||||
|
||||
function cmd_args(cmds) {
|
||||
// Remove the leading 'prosperon'
|
||||
cmds.shift();
|
||||
|
||||
// If there are no arguments left, assume we want to spawn main.js
|
||||
if (!cmds[0]) {
|
||||
// => effectively do: prosperon spawn --program main.js
|
||||
cmds.unshift("main.js");
|
||||
cmds.unshift("--program");
|
||||
cmds.unshift("spawn");
|
||||
} else if (!Cmdline.orders[cmds[0]]) {
|
||||
// If the first token isn't a recognized command, treat it as either
|
||||
// a directory containing main.js, or a script to run directly.
|
||||
var arg0 = cmds.shift();
|
||||
if (io.is_directory(arg0)) {
|
||||
var script = cmds[0] ? cmds.shift() : "main.js";
|
||||
cmds.unshift(script);
|
||||
cmds.unshift("--program");
|
||||
cmds.unshift(arg0);
|
||||
cmds.unshift("--cwd");
|
||||
} else {
|
||||
cmds.unshift(arg0);
|
||||
cmds.unshift("--program");
|
||||
}
|
||||
cmds.unshift("spawn");
|
||||
cmds.shift()
|
||||
if (cmds.length === 0) cmds[0] = "play";
|
||||
else if (!Cmdline.orders[cmds[0]]) {
|
||||
// assume it's a script
|
||||
cmds[1] = cmds[0]
|
||||
cmds[0] = "run"
|
||||
}
|
||||
|
||||
Cmdline.orders[cmds[0]](cmds.slice(1));
|
||||
}
|
||||
|
||||
return {
|
||||
process: cmd_args,
|
||||
encode: parse_args,
|
||||
decode: unparse_args,
|
||||
}
|
||||
return cmd_args;
|
||||
|
||||
@@ -1,35 +0,0 @@
|
||||
// helpful render devices. width and height in pixels; diagonal in inches.
|
||||
return {
|
||||
pc: { width: 1920, height: 1080 },
|
||||
macbook_m2: { width: 2560, height: 1664, diagonal: 13.6 },
|
||||
ds_top: { width: 400, height: 240, diagonal: 3.53 },
|
||||
ds_bottom: { width: 320, height: 240, diagonal: 3.02 },
|
||||
playdate: { width: 400, height: 240, diagonal: 2.7 },
|
||||
switch: { width: 1280, height: 720, diagonal: 6.2 },
|
||||
switch_lite: { width: 1280, height: 720, diagonal: 5.5 },
|
||||
switch_oled: { width: 1280, height: 720, diagonal: 7 },
|
||||
dsi: { width: 256, height: 192, diagonal: 3.268 },
|
||||
ds: { width: 256, height: 192, diagonal: 3 },
|
||||
dsixl: { width: 256, height: 192, diagonal: 4.2 },
|
||||
ipad_air_m2: { width: 2360, height: 1640, diagonal: 11.97 },
|
||||
iphone_se: { width: 1334, height: 750, diagonal: 4.7 },
|
||||
iphone_12_pro: { width: 2532, height: 1170, diagonal: 6.06 },
|
||||
iphone_15: { width: 2556, height: 1179, diagonal: 6.1 },
|
||||
gba: { width: 240, height: 160, diagonal: 2.9 },
|
||||
gameboy: { width: 160, height: 144, diagonal: 2.48 },
|
||||
gbc: { width: 160, height: 144, diagonal: 2.28 },
|
||||
steamdeck: { width: 1280, height: 800, diagonal: 7 },
|
||||
vita: { width: 960, height: 544, diagonal: 5 },
|
||||
psp: { width: 480, height: 272, diagonal: 4.3 },
|
||||
imac_m3: { width: 4480, height: 2520, diagonal: 23.5 },
|
||||
macbook_pro_m3: { width: 3024, height: 1964, diagonal: 14.2 },
|
||||
ps1: { width: 320, height: 240, diagonal: 5 },
|
||||
ps2: { width: 640, height: 480 },
|
||||
snes: { width: 256, height: 224 },
|
||||
gamecube: { width: 640, height: 480 },
|
||||
n64: { width: 320, height: 240 },
|
||||
c64: { width: 320, height: 200 },
|
||||
macintosh: { width: 512, height: 342 },
|
||||
gamegear: { width: 160, height: 144, diagonal: 3.2 }
|
||||
};
|
||||
|
||||
@@ -3,374 +3,123 @@ var graphics = use('graphics')
|
||||
var math = use('math')
|
||||
var util = use('util')
|
||||
var os = use('os')
|
||||
var geometry = use('geometry')
|
||||
|
||||
var draw = {}
|
||||
draw[prosperon.DOC] = `
|
||||
A collection of 2D drawing functions that operate in screen space. Provides primitives
|
||||
for lines, rectangles, text, sprite drawing, etc. Immediate mode.
|
||||
for lines, rectangles, text, sprite drawing, etc.
|
||||
`
|
||||
|
||||
/*var whiteimage = {}
|
||||
whiteimage = graphics.make_surface([1,1])
|
||||
whiteimage.rect({x:0,y:0,width:1,height:1}, [1,1,1,1])
|
||||
render.load_texture(whiteimage)
|
||||
*/
|
||||
var whiteimage = {}
|
||||
whiteimage.surface = graphics.make_surface([1,1])
|
||||
whiteimage.surface.rect({x:0,y:0,width:1,height:1}, [1,1,1,1])
|
||||
whiteimage.texture = prosperon.gpu.load_texture(whiteimage.surface)
|
||||
|
||||
if (render.point)
|
||||
draw.point = function(pos,size,opt = {color:Color.white}, pipeline) {
|
||||
render.settings(opt)
|
||||
render.pipeline(pipeline)
|
||||
render.point([pos])
|
||||
}
|
||||
else
|
||||
draw.point = function() { throw new Error('Backend cannot draw points.') }
|
||||
draw.point = function (pos, size, color = Color.blue) {
|
||||
render._main.point(pos, color)
|
||||
}
|
||||
draw.point[prosperon.DOC] = `
|
||||
:param pos: A 2D position ([x, y]) where the point should be drawn.
|
||||
:param size: The size of the point.
|
||||
:param color: The color of the point, defaults to white.
|
||||
:param size: The size of the point (not currently affecting rendering).
|
||||
:param color: The color of the point, defaults to Color.blue.
|
||||
:return: None
|
||||
`
|
||||
|
||||
/* ------------------------------------------------------------------------
|
||||
* helper – is (dx,dy) inside the desired wedge?
|
||||
* -------------------------------------------------------------------- */
|
||||
function within_wedge(dx, dy, start, end, full_circle)
|
||||
{
|
||||
if (full_circle) return true
|
||||
|
||||
var ang = Math.atan2(dy, dx) // [-π,π]
|
||||
if (ang < 0) ang += Math.PI * 2
|
||||
var t = ang / (Math.PI * 2) // turn ∈ [0,1)
|
||||
|
||||
if (start <= end) return t >= start && t <= end
|
||||
return t >= start || t <= end // wrap-around arc
|
||||
draw.line = function render_line(points, color = Color.white, thickness = 1, pipeline) {
|
||||
var mesh = graphics.make_line_prim(points, thickness, 0, 0, color)
|
||||
render.queue({
|
||||
type: 'geometry',
|
||||
mesh,
|
||||
pipeline,
|
||||
first_index: 0,
|
||||
num_indices: mesh.num_indices,
|
||||
image:whiteimage
|
||||
})
|
||||
}
|
||||
draw.line[prosperon.DOC] = `
|
||||
:param points: An array of 2D positions representing the line vertices.
|
||||
:param color: The color of the line, default Color.white.
|
||||
:param thickness: The line thickness, default 1.
|
||||
:param pipeline: (Optional) A pipeline or rendering state object.
|
||||
:return: None
|
||||
`
|
||||
|
||||
/* ------------------------------------------------------------------------
|
||||
* software ellipse – outline / ring / fill via inner_radius
|
||||
* -------------------------------------------------------------------- */
|
||||
function software_ellipse(pos, radii, opt)
|
||||
{
|
||||
var rx = radii[0], ry = radii[1]
|
||||
if (rx <= 0 || ry <= 0) return
|
||||
|
||||
var cx = pos[0], cy = pos[1]
|
||||
var raw_start = opt.start
|
||||
var raw_end = opt.end
|
||||
var full_circle = Math.abs(raw_end - raw_start) >= 1 - 1e-9
|
||||
var start = (raw_start % 1 + 1) % 1
|
||||
var end = (raw_end % 1 + 1) % 1
|
||||
var thickness = Math.max(1, opt.thickness|0)
|
||||
|
||||
/* inner ellipse radii (may be zero or negative → treat as no hole) */
|
||||
var rx_i = rx - thickness,
|
||||
ry_i = ry - thickness
|
||||
var hole = (rx_i > 0 && ry_i > 0)
|
||||
|
||||
/* fast one-pixel outline ? --------------------------------------- */
|
||||
if (!hole && thickness === 1) {
|
||||
/* (same midpoint algorithm as before, filtered by wedge) --------*/
|
||||
var rx_sq = rx * rx, ry_sq = ry * ry
|
||||
var two_rx_sq = rx_sq << 1, two_ry_sq = ry_sq << 1
|
||||
var x = 0, y = ry, px = 0, py = two_rx_sq * y
|
||||
var p = ry_sq - rx_sq * ry + 0.25 * rx_sq
|
||||
|
||||
function plot_pts(x, y) {
|
||||
var pts = [
|
||||
[cx + x, cy + y], [cx - x, cy + y],
|
||||
[cx + x, cy - y], [cx - x, cy - y]
|
||||
].filter(pt => within_wedge(pt[0]-cx, pt[1]-cy, start, end, full_circle))
|
||||
if (pts.length) render.point(pts)
|
||||
}
|
||||
|
||||
while (px < py) {
|
||||
plot_pts(x, y)
|
||||
++x; px += two_ry_sq
|
||||
if (p < 0) p += ry_sq + px
|
||||
else { --y; py -= two_rx_sq; p += ry_sq + px - py }
|
||||
}
|
||||
p = ry_sq*(x+.5)*(x+.5) + rx_sq*(y-1)*(y-1) - rx_sq*ry_sq
|
||||
while (y >= 0) {
|
||||
plot_pts(x, y)
|
||||
--y; py -= two_rx_sq
|
||||
if (p > 0) p += rx_sq - py
|
||||
else { ++x; px += two_ry_sq; p += rx_sq - py + px }
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
/* ------------------------------------------------------------------
|
||||
* FILL or RING (thickness ≥ 2 OR hole == false -> full fill)
|
||||
* ---------------------------------------------------------------- */
|
||||
var strips = []
|
||||
var rx_sq = rx * rx, ry_sq = ry * ry
|
||||
var rx_i_sq = rx_i * rx_i, ry_i_sq = ry_i * ry_i
|
||||
|
||||
for (var dy = -ry; dy <= ry; ++dy) {
|
||||
var yy = dy * dy
|
||||
var x_out = Math.floor(rx * Math.sqrt(1 - yy / ry_sq))
|
||||
var y_screen = cy + dy
|
||||
|
||||
/* optional inner span */
|
||||
var x_in = hole ? Math.floor(rx_i * Math.sqrt(1 - yy / ry_i_sq)) : -1
|
||||
|
||||
var run_start = null
|
||||
for (var dx = -x_out; dx <= x_out; ++dx) {
|
||||
if (hole && Math.abs(dx) <= x_in) { run_start = null; continue }
|
||||
if (!within_wedge(dx, dy, start, end, full_circle)) { run_start = null; continue }
|
||||
|
||||
if (run_start === null) run_start = cx + dx
|
||||
|
||||
var last = (dx === x_out)
|
||||
var next_in_ring =
|
||||
!last &&
|
||||
!(hole && Math.abs(dx+1) <= x_in) &&
|
||||
within_wedge(dx+1, dy, start, end, full_circle)
|
||||
|
||||
if (last || !next_in_ring) {
|
||||
strips.push({
|
||||
x: run_start,
|
||||
y: y_screen,
|
||||
width: (cx + dx) - run_start + 1,
|
||||
height: 1
|
||||
})
|
||||
run_start = null
|
||||
}
|
||||
}
|
||||
}
|
||||
if (strips.length) render.rects(strips)
|
||||
}
|
||||
|
||||
var ellipse_def = {
|
||||
color: Color.white,
|
||||
start: 0,
|
||||
end: 1,
|
||||
mode: 'fill',
|
||||
thickness: 1,
|
||||
}
|
||||
|
||||
if (render.ellipse)
|
||||
draw.ellipse = function(pos, radii, def, pipeline) {
|
||||
}
|
||||
else
|
||||
draw.ellipse = function(pos, radii, def, pipeline) {
|
||||
var opt = def ? {...ellipse_def, ...def} : ellipse_def
|
||||
render.settings(opt)
|
||||
render.pipeline(pipeline)
|
||||
if (opt.thickness <= 0) opt.thickness = Math.max(radii[0], radii[1])
|
||||
software_ellipse(pos, radii, opt)
|
||||
}
|
||||
|
||||
var line_def = {
|
||||
color: Color.white,
|
||||
thickness: 1,
|
||||
cap:"butt",
|
||||
}
|
||||
|
||||
draw.line = function(points, def, pipeline)
|
||||
{
|
||||
var opt
|
||||
if (def)
|
||||
opt = {...line_def, ...def}
|
||||
else
|
||||
opt = line_def
|
||||
|
||||
render.settings(opt)
|
||||
render.pipeline(pipeline)
|
||||
render.line(points)
|
||||
}
|
||||
|
||||
draw.cross = function render_cross(pos, size, def, pipe) {
|
||||
draw.cross = function render_cross(pos, size, color = Color.red, thickness = 1, pipe) {
|
||||
var a = [pos.add([0, size]), pos.add([0, -size])]
|
||||
var b = [pos.add([size, 0]), pos.add([-size, 0])]
|
||||
draw.line(a, def, pipe)
|
||||
draw.line(b, def,pipe)
|
||||
draw.line(a, color, thickness)
|
||||
draw.line(b, color, thickness)
|
||||
}
|
||||
draw.cross[prosperon.DOC] = `
|
||||
:param pos: The center of the cross as a 2D position ([x, y]).
|
||||
:param size: Half the size of each cross arm.
|
||||
:param color: The color of the cross, default Color.red.
|
||||
:param thickness: The thickness of each line, default 1.
|
||||
:param pipe: (Optional) A pipeline or rendering state object.
|
||||
:return: None
|
||||
`
|
||||
|
||||
draw.arrow = function render_arrow(start, end, wingspan = 4, wingangle = 10, def, pipe) {
|
||||
draw.arrow = function render_arrow(start, end, color = Color.red, wingspan = 4, wingangle = 10, pipe) {
|
||||
var dir = math.norm(end.sub(start))
|
||||
var wing1 = [math.rotate(dir, wingangle).scale(wingspan).add(end), end]
|
||||
var wing2 = [math.rotate(dir, -wingangle).scale(wingspan).add(end), end]
|
||||
draw.line([start, end], def, pipe)
|
||||
draw.line(wing1, def, pipe)
|
||||
draw.line(wing2, def, pipe)
|
||||
render.line([start, end], color)
|
||||
render.line(wing1, color)
|
||||
render.line(wing2, color)
|
||||
}
|
||||
draw.arrow[prosperon.DOC] = `
|
||||
:param start: The start position of the arrow ([x, y]).
|
||||
:param end: The end (tip) position of the arrow ([x, y]).
|
||||
:param color: The color, default Color.red.
|
||||
:param wingspan: The length of each arrowhead 'wing', default 4.
|
||||
:param wingangle: Wing rotation in degrees, default 10.
|
||||
:param pipe: (Optional) A pipeline or rendering state object.
|
||||
:return: None
|
||||
`
|
||||
|
||||
/* ------------------------------------------------------------------------
|
||||
* helper – plain rectangle outline of arbitrary thickness (radius=0)
|
||||
* -------------------------------------------------------------------- */
|
||||
function software_outline_rect(rect, thickness)
|
||||
{
|
||||
if (thickness <= 0) {
|
||||
render.rectangle(rect);
|
||||
return;
|
||||
}
|
||||
|
||||
/* stroke swallows the whole thing → fill instead */
|
||||
if ((thickness << 1) >= rect.width ||
|
||||
(thickness << 1) >= rect.height) {
|
||||
render.rectangle(rect) // filled
|
||||
return
|
||||
}
|
||||
|
||||
const x0 = rect.x,
|
||||
y0 = rect.y,
|
||||
x1 = rect.x + rect.width,
|
||||
y1 = rect.y + rect.height
|
||||
|
||||
render.rects([
|
||||
{ x:x0, y:y0, width:rect.width, height:thickness }, // top
|
||||
{ x:x0, y:y1-thickness, width:rect.width, height:thickness }, // bottom
|
||||
{ x:x0, y:y0+thickness, width:thickness,
|
||||
height:rect.height - (thickness<<1) }, // left
|
||||
{ x:x1-thickness, y:y0+thickness, width:thickness,
|
||||
height:rect.height - (thickness<<1) } // right
|
||||
])
|
||||
draw.rectangle = function render_rectangle(rect, color = Color.white, pipeline) {
|
||||
var T = os.make_transform()
|
||||
T.rect(rect)
|
||||
render.queue({
|
||||
type: 'sprite',
|
||||
transform: T,
|
||||
color,
|
||||
pipeline,
|
||||
image: whiteimage
|
||||
})
|
||||
}
|
||||
draw.rectangle[prosperon.DOC] = `
|
||||
:param rect: A rectangle object with {x, y, width, height}.
|
||||
:param color: The fill color, default Color.white.
|
||||
:param pipeline: (Optional) A pipeline or rendering state object.
|
||||
:return: None
|
||||
`
|
||||
|
||||
/* ------------------------------------------------------------------------
|
||||
* ROUNDED rectangle outline (was software_round_rect)
|
||||
* -------------------------------------------------------------------- */
|
||||
function software_round_rect(rect, radius, thickness = 1)
|
||||
{
|
||||
if (thickness <= 0) {
|
||||
software_fill_round_rect(rect, radius)
|
||||
return
|
||||
}
|
||||
var tile_def = {repeat_x:true, repeat_y:true}
|
||||
|
||||
radius = Math.min(radius, rect.width >> 1, rect.height >> 1)
|
||||
|
||||
/* stroke covers whole rect → fall back to fill ------------------- */
|
||||
if ((thickness << 1) >= rect.width ||
|
||||
(thickness << 1) >= rect.height ||
|
||||
thickness >= radius) {
|
||||
software_fill_round_rect(rect, radius)
|
||||
return
|
||||
}
|
||||
|
||||
const x0 = rect.x,
|
||||
y0 = rect.y,
|
||||
x1 = rect.x + rect.width - 1, // inclusive
|
||||
y1 = rect.y + rect.height - 1
|
||||
|
||||
const cx_l = x0 + radius, cx_r = x1 - radius
|
||||
const cy_t = y0 + radius, cy_b = y1 - radius
|
||||
const r_out = radius
|
||||
const r_in = radius - thickness
|
||||
|
||||
/* straight bands (top/bottom/left/right) ------------------------- */
|
||||
render.rects([
|
||||
{ x:x0 + radius, y:y0, width:rect.width - (radius << 1),
|
||||
height:thickness }, // top
|
||||
{ x:x0 + radius, y:y1 - thickness + 1,
|
||||
width:rect.width - (radius << 1), height:thickness }, // bottom
|
||||
{ x:x0, y:y0 + radius, width:thickness,
|
||||
height:rect.height - (radius << 1) }, // left
|
||||
{ x:x1 - thickness + 1, y:y0 + radius, width:thickness,
|
||||
height:rect.height - (radius << 1) } // right
|
||||
])
|
||||
|
||||
/* corner arcs ---------------------------------------------------- */
|
||||
const strips = []
|
||||
|
||||
for (let dy = 0; dy < radius; ++dy) {
|
||||
const dy_sq = dy * dy
|
||||
const dx_out = Math.floor(Math.sqrt(r_out * r_out - dy_sq))
|
||||
const dx_in = (r_in > 0 && dy < r_in)
|
||||
? Math.floor(Math.sqrt(r_in * r_in - dy_sq))
|
||||
: -1 // no inner rim
|
||||
const w = dx_out - dx_in // strip width
|
||||
if (w <= 0) continue
|
||||
|
||||
/* top */
|
||||
strips.push(
|
||||
{ x:cx_l - dx_out, y:cy_t - dy, width:w, height:1 }, // NW
|
||||
{ x:cx_r + dx_in + 1, y:cy_t - dy, width:w, height:1 } // NE
|
||||
)
|
||||
|
||||
/* bottom */
|
||||
strips.push(
|
||||
{ x:cx_l - dx_out, y:cy_b + dy, width:w, height:1 }, // SW
|
||||
{ x:cx_r + dx_in + 1, y:cy_b + dy, width:w, height:1 } // SE
|
||||
)
|
||||
}
|
||||
|
||||
render.rects(strips)
|
||||
}
|
||||
|
||||
/* ------------------------------------------------------------------------
|
||||
* filled rounded rect (unchanged)
|
||||
* -------------------------------------------------------------------- */
|
||||
function software_fill_round_rect(rect, radius)
|
||||
{
|
||||
radius = Math.min(radius, rect.width >> 1, rect.height >> 1)
|
||||
|
||||
const x0 = rect.x,
|
||||
y0 = rect.y,
|
||||
x1 = rect.x + rect.width - 1,
|
||||
y1 = rect.y + rect.height - 1
|
||||
|
||||
/* main column */
|
||||
render.rects([
|
||||
{ x:x0 + radius, y:y0, width:rect.width - (radius << 1),
|
||||
height:rect.height }
|
||||
])
|
||||
|
||||
/* side columns */
|
||||
render.rects([
|
||||
{ x:x0, y:y0 + radius, width:radius,
|
||||
height:rect.height - (radius << 1) },
|
||||
{ x:x1 - radius + 1, y:y0 + radius, width:radius,
|
||||
height:rect.height - (radius << 1) }
|
||||
])
|
||||
|
||||
/* corner caps */
|
||||
const cx_l = x0 + radius, cx_r = x1 - radius
|
||||
const cy_t = y0 + radius, cy_b = y1 - radius
|
||||
const caps = []
|
||||
|
||||
for (let dy = 0; dy < radius; ++dy) {
|
||||
const dx = Math.floor(Math.sqrt(radius * radius - dy * dy))
|
||||
const w = (dx << 1) + 1
|
||||
|
||||
caps.push(
|
||||
{ x:cx_l - dx, y:cy_t - dy, width:w, height:1 },
|
||||
{ x:cx_r - dx, y:cy_t - dy, width:w, height:1 },
|
||||
{ x:cx_l - dx, y:cy_b + dy, width:w, height:1 },
|
||||
{ x:cx_r - dx, y:cy_b + dy, width:w, height:1 }
|
||||
)
|
||||
}
|
||||
|
||||
render.rects(caps)
|
||||
}
|
||||
|
||||
var rect_def = {
|
||||
thickness:1,
|
||||
color: Color.white,
|
||||
radius: 0
|
||||
}
|
||||
draw.rectangle = function render_rectangle(rect, def, pipeline) {
|
||||
var opt = def ? {...rect_def, ...def} : rect_def
|
||||
render.settings(opt)
|
||||
render.pipeline(pipeline)
|
||||
|
||||
var t = opt.thickness|0
|
||||
|
||||
if (t <= 0) {
|
||||
if (opt.radius)
|
||||
software_fill_round_rect(rect, opt.radius)
|
||||
else
|
||||
render.rectangle(rect)
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
if (opt.radius)
|
||||
software_round_rect(rect, opt.radius, t)
|
||||
else
|
||||
software_outline_rect(rect,t)
|
||||
draw.tile = function(image, rect, color = Color.white, tile = tile_def, pipeline) {
|
||||
if (!image) throw Error('Need an image to render.')
|
||||
if (typeof image === "string")
|
||||
image = graphics.texture(image)
|
||||
var mesh = render._main.tile(image.texture, {x:0,y:0,width:image.texture.width,height:image.texture.height}, rect, tile)
|
||||
render.queue({
|
||||
type:'geometry',
|
||||
mesh,
|
||||
image,
|
||||
pipeline,
|
||||
first_index:0,
|
||||
num_indices:mesh.num_indices
|
||||
})
|
||||
}
|
||||
draw.tile[prosperon.DOC] = `
|
||||
:param image: An image object or string path to a texture.
|
||||
:param rect: A rectangle specifying draw location/size ({x, y, width, height}).
|
||||
:param color: The color tint, default Color.white.
|
||||
:param tile: A tiling definition ({repeat_x, repeat_y}), default tile_def.
|
||||
:param pipeline: (Optional) A pipeline or rendering state object.
|
||||
:return: None
|
||||
:raises Error: If no image is provided.
|
||||
`
|
||||
|
||||
var slice9_info = {
|
||||
tile_top:true,
|
||||
@@ -378,21 +127,28 @@ var slice9_info = {
|
||||
tile_left:true,
|
||||
tile_right:true,
|
||||
tile_center_x:true,
|
||||
tile_center_right:true,
|
||||
color: Color.white,
|
||||
tile_center_right:true
|
||||
}
|
||||
|
||||
draw.slice9 = function slice9(image, rect = [0,0], slice = 0, info = slice9_info, pipeline) {
|
||||
draw.slice9 = function slice9(image, rect = [0,0], slice = 0, color = Color.white, info = slice9_info, pipeline) {
|
||||
if (!image) throw Error('Need an image to render.')
|
||||
if (typeof image === "string")
|
||||
image = graphics.texture(image)
|
||||
|
||||
render.slice9(image, rect, slice, slice9_info, pipeline);
|
||||
var mesh = render._main.slice9(image.texture, rect, util.normalizeSpacing(slice), info)
|
||||
render.queue({
|
||||
type: 'geometry',
|
||||
mesh,
|
||||
image,
|
||||
pipeline,
|
||||
first_index:0,
|
||||
num_indices:mesh.num_indices
|
||||
})
|
||||
}
|
||||
draw.slice9[prosperon.DOC] = `
|
||||
:param image: An image object or string path to a texture.
|
||||
:param rect: A rectangle specifying draw location/size, default [0, 0].
|
||||
:param slice: The pixel inset or spacing for the 9-slice (number or object).
|
||||
:param color: The color tint, default Color.white.
|
||||
:param info: A slice9 info object controlling tiling of edges/corners.
|
||||
:param pipeline: (Optional) A pipeline or rendering state object.
|
||||
:return: None
|
||||
@@ -400,67 +156,99 @@ draw.slice9[prosperon.DOC] = `
|
||||
`
|
||||
|
||||
var std_sprite_cmd = {type:'sprite', color:[1,1,1,1]}
|
||||
var image_info = {
|
||||
tile_x: false,
|
||||
tile_y: false,
|
||||
flip_x: false,
|
||||
flip_y: false,
|
||||
color: Color.white,
|
||||
mode: 'linear'
|
||||
}
|
||||
|
||||
draw.image = function image(image, rect = [0,0], rotation = 0, anchor = [0,0], shear = [0,0], info = {}, pipeline) {
|
||||
draw.image = function image(image, rect = [0,0], rotation = 0, color, pipeline) {
|
||||
if (!image) throw Error('Need an image to render.')
|
||||
if (typeof image === "string")
|
||||
image = graphics.texture(image)
|
||||
rect.width ??= image.texture.width
|
||||
rect.height ??= image.texture.height
|
||||
info ??= image_info;
|
||||
render.settings(info)
|
||||
render.image(image, rect, rotation, anchor, shear, info)
|
||||
var cmd = Object.create(std_sprite_cmd)
|
||||
cmd.image = image
|
||||
cmd.rect = rect
|
||||
if (pipeline) cmd.pipeline = pipeline
|
||||
if (color) cmd.color = color
|
||||
render.queue(cmd)
|
||||
var sprite = graphics.make_sprite()
|
||||
sprite.set_image(image)
|
||||
sprite.set_rect(rect)
|
||||
return sprite
|
||||
}
|
||||
draw.image[prosperon.DOC] = `
|
||||
:param image: An image object or string path to a texture.
|
||||
:param rect: A rectangle specifying draw location/size, default [0,0]; width/height default to image size.
|
||||
:param rotation: Rotation in degrees (not currently used).
|
||||
:param color: The color tint, default none.
|
||||
:param pipeline: (Optional) A pipeline or rendering state object.
|
||||
:return: A sprite object that was created for this draw call.
|
||||
:raises Error: If no image is provided.
|
||||
`
|
||||
|
||||
function software_circle(pos, radius)
|
||||
{
|
||||
if (radius <= 0) return // nothing to draw
|
||||
|
||||
var cx = pos[0], cy = pos[1]
|
||||
var x = 0, y = radius
|
||||
var d = 3 - (radius << 1) // decision parameter
|
||||
|
||||
while (x <= y) {
|
||||
draw.point([
|
||||
[cx + x, cy + y], [cx - x, cy + y],
|
||||
[cx + x, cy - y], [cx - x, cy - y],
|
||||
[cx + y, cy + x], [cx - y, cy + x],
|
||||
[cx + y, cy - x], [cx - y, cy - x]
|
||||
])
|
||||
|
||||
if (d < 0) d += (x << 2) + 6
|
||||
else {
|
||||
d += ((x - y) << 2) + 10
|
||||
y--
|
||||
}
|
||||
x++
|
||||
draw.images = function images(image, rects, config) {
|
||||
if (!image) throw Error('Need an image to render.')
|
||||
if (typeof image === "string") image = graphics.texture(image)
|
||||
var bb = []
|
||||
bb.width = image.texture.width
|
||||
bb.height = image.texture.height
|
||||
var sprites = []
|
||||
for (var rect of rects) {
|
||||
rect.__proto__ = bb
|
||||
var sprite = graphics.make_sprite()
|
||||
sprite.set_rect(rect)
|
||||
sprite.set_image(image)
|
||||
sprites.push(sprite)
|
||||
}
|
||||
var cmds = graphics.make_sprite_queue(sprites, prosperon.camera, undefined)
|
||||
for (var i = 0; i < cmds.length; i++)
|
||||
render.queue(cmds[i])
|
||||
return sprites
|
||||
}
|
||||
draw.images[prosperon.DOC] = `
|
||||
:param image: An image object or string path to a texture.
|
||||
:param rects: An array of rectangle objects ({x, y, width, height}) to draw.
|
||||
:param config: (Unused) Additional config data if needed.
|
||||
:return: An array of sprite objects created and queued for rendering.
|
||||
:raises Error: If no image is provided.
|
||||
`
|
||||
|
||||
var circle_def = {
|
||||
inner_radius:1, // percentage: 1 means filled circle
|
||||
color: Color.white,
|
||||
start:0,
|
||||
end: 1,
|
||||
draw.sprites = function(sprites, sort = 0, pipeline) {
|
||||
var cmds = graphics.make_sprite_queue(sprites, prosperon.camera, pipeline, sort)
|
||||
for (var i = 0; i < cmds.length; i++)
|
||||
render.queue(cmds[i])
|
||||
}
|
||||
draw.circle = function render_circle(pos, radius, def, pipeline) {
|
||||
draw.ellipse(pos, [radius,radius], def, pipeline)
|
||||
draw.sprites[prosperon.DOC] = `
|
||||
:param sprites: An array of sprite objects to draw.
|
||||
:param sort: Sorting mode or order, default 0.
|
||||
:param pipeline: (Optional) A pipeline or rendering state object.
|
||||
:return: None
|
||||
`
|
||||
|
||||
draw.circle = function render_circle(pos, radius, color, inner_radius = 1, pipeline) {
|
||||
render.rectangle({x:pos.x, y:pos.y, width:radius*2,height:radius*2}, color, circle_pipeline)
|
||||
}
|
||||
draw.circle[prosperon.DOC] = `
|
||||
:param pos: Center of the circle ([x, y]).
|
||||
:param radius: The circle radius.
|
||||
:param color: The fill color of the circle, default none.
|
||||
:param inner_radius: (Unused) Possibly ring thickness, default 1.
|
||||
:param pipeline: (Optional) A pipeline or rendering state object.
|
||||
:return: None
|
||||
`
|
||||
|
||||
var sysfont = graphics.get_font('fonts/c64.ttf', 8)
|
||||
|
||||
draw.text = function text(text, rect, font = sysfont, size = 0, color = Color.white, wrap = 0, pipeline) {
|
||||
if (typeof font === 'string') font = graphics.get_font(font)
|
||||
var mesh = graphics.make_text_buffer(text, rect, 0, color, wrap, font)
|
||||
render.geometry(font, mesh)
|
||||
render.queue({
|
||||
type: 'geometry',
|
||||
mesh,
|
||||
image: font,
|
||||
texture: font.texture,
|
||||
pipeline,
|
||||
first_index:0,
|
||||
num_indices:mesh.num_indices
|
||||
})
|
||||
}
|
||||
draw.text[prosperon.DOC] = `
|
||||
:param text: The string to draw.
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
var cam = {}
|
||||
|
||||
var os = use('os')
|
||||
var transform = use('transform')
|
||||
|
||||
var basecam = {}
|
||||
basecam.draw_rect = function(size)
|
||||
@@ -89,7 +88,7 @@ function mode_rect(src,dst,mode = "stretch")
|
||||
cam.make = function()
|
||||
{
|
||||
var c = Object.create(basecam)
|
||||
c.transform = new transform
|
||||
c.transform = os.make_transform()
|
||||
c.transform.unit()
|
||||
c.zoom = 1
|
||||
c.size = [640,360]
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
var Color = use('color')
|
||||
var os = use('os')
|
||||
var graphics = use('graphics')
|
||||
var transform = use('transform')
|
||||
//var render = use('render')
|
||||
|
||||
var ex = {}
|
||||
|
||||
@@ -77,7 +77,7 @@ ex.spawn = function(t)
|
||||
}
|
||||
|
||||
par = {
|
||||
transform: new transform,
|
||||
transform: os.make_transform(),
|
||||
life: this.life,
|
||||
time: 0,
|
||||
color: this.color,
|
||||
@@ -111,7 +111,7 @@ ex.draw = function()
|
||||
/* var diff = graphics.texture(this.diffuse)
|
||||
if (!diff) throw new Error("emitter does not have a proper diffuse texture")
|
||||
|
||||
var mesh = graphics.make_sprite_mesh(this.particles)
|
||||
var mesh = render._main.make_sprite_mesh(this.particles)
|
||||
if (mesh.num_indices === 0) return
|
||||
render.queue({
|
||||
type:'geometry',
|
||||
|
||||
@@ -184,9 +184,7 @@ sprite.inputs.kp1 = function () {
|
||||
this.setanchor("ul");
|
||||
};
|
||||
|
||||
var rtree = use('rtree')
|
||||
|
||||
var tree = new rtree
|
||||
var tree = graphics.make_rtree()
|
||||
sprite.tree = tree;
|
||||
|
||||
var IN = Symbol()
|
||||
@@ -224,15 +222,15 @@ return sprite;
|
||||
---
|
||||
|
||||
var Color = use('color')
|
||||
var transform = use('transform')
|
||||
var sprite = use('sprite')
|
||||
var os = use('os')
|
||||
var graphics = use('graphics')
|
||||
|
||||
this.transform = new transform;
|
||||
this.transform = os.make_transform();
|
||||
if (this.overling.transform)
|
||||
this.transform.parent = this.overling.transform;
|
||||
|
||||
this.transform.change_hook = $.t_hook;
|
||||
var msp = new sprite
|
||||
var msp = graphics.make_sprite();
|
||||
this._sprite = msp;
|
||||
msp.color = Color.white;
|
||||
this.transform.sprite = this
|
||||
|
||||
33
scripts/modules/ext/transform.js
Normal file
@@ -0,0 +1,33 @@
|
||||
var ret = {
|
||||
get pos() {
|
||||
if (!this.transform) return
|
||||
return this.transform.pos;
|
||||
},
|
||||
set pos(x) {
|
||||
this.transform.pos = x;
|
||||
},
|
||||
get angle() {
|
||||
return this.transform.angle;
|
||||
},
|
||||
set angle(x) {
|
||||
this.transform.angle = x;
|
||||
},
|
||||
get scale() {
|
||||
return this.transform.scale;
|
||||
},
|
||||
set scale(x) {
|
||||
this.transform.scale = x;
|
||||
},
|
||||
move(vec) {
|
||||
this.pos = this.pos.add(vec);
|
||||
},
|
||||
rotate(x) {
|
||||
this.transform.rotate([0, 0, -1],x);
|
||||
},
|
||||
grow(vec) {
|
||||
if (typeof vec === "number") vec = [vec, vec];
|
||||
this.scale = this.scale.map((x, i) => x * vec[i]);
|
||||
},
|
||||
}
|
||||
|
||||
return ret
|
||||
@@ -1,5 +1,4 @@
|
||||
var graphics = this
|
||||
|
||||
graphics[prosperon.DOC] = `
|
||||
Provides functionality for loading and managing images, fonts, textures, and sprite meshes.
|
||||
Includes both JavaScript and C-implemented routines for creating geometry buffers, performing
|
||||
@@ -7,151 +6,50 @@ rectangle packing, etc.
|
||||
`
|
||||
|
||||
var io = use('io')
|
||||
var os = use('os')
|
||||
var res = use('resources')
|
||||
var render = use('render')
|
||||
|
||||
var GPU = Symbol()
|
||||
var CPU = Symbol()
|
||||
var LASTUSE = Symbol()
|
||||
|
||||
var cache = new Map()
|
||||
|
||||
// When creating an image, do an Object.create(graphics.Image)
|
||||
graphics.Image = {
|
||||
get gpu() {
|
||||
this[LASTUSE] = os.now();
|
||||
if (!this[GPU]) {
|
||||
this[GPU] = render.load_texture(this[CPU]);
|
||||
decorate_rect_px(this);
|
||||
}
|
||||
|
||||
return this[GPU]
|
||||
},
|
||||
|
||||
get texture() { return this.gpu },
|
||||
|
||||
get cpu() {
|
||||
this[LASTUSE] = os.now();
|
||||
if (!this[CPU]) this[CPU] = render.read_texture(this[GPU])
|
||||
return this[CPU]
|
||||
},
|
||||
|
||||
get surface() { return this.cpu },
|
||||
|
||||
get width() {
|
||||
return this[GPU].width
|
||||
},
|
||||
|
||||
get height() {
|
||||
return this[GPU].height
|
||||
},
|
||||
|
||||
unload_gpu() {
|
||||
this[GPU] = undefined
|
||||
},
|
||||
|
||||
unload_cpu() {
|
||||
this[CPU] = undefined
|
||||
},
|
||||
}
|
||||
|
||||
function calc_image_size(img) {
|
||||
if (!img.texture || !img.rect) return
|
||||
return [img.texture.width * img.rect.width, img.texture.height * img.rect.height]
|
||||
}
|
||||
|
||||
function decorate_rect_px(img) {
|
||||
// needs a GPU texture to measure
|
||||
if (!img || !img.texture) return
|
||||
|
||||
// default UV rect is the whole image if none supplied
|
||||
img.rect ??= {x:0, y:0, width:1, height:1} // [u0,v0,uw,vh] in 0-1
|
||||
|
||||
// store pixel-space version: [x, y, w, h] in texels
|
||||
img.rect_px = {
|
||||
x:Math.round(img.rect.x * img.texture.width),
|
||||
y:Math.round(img.rect.y * img.texture.height),
|
||||
width:Math.round(img.rect.width * img.texture.width),
|
||||
height:Math.round(img.rect.height * img.texture.height)
|
||||
}
|
||||
}
|
||||
|
||||
function make_handle(obj)
|
||||
{
|
||||
var image = Object.create(graphics.Image);
|
||||
|
||||
if (obj.surface) {
|
||||
im
|
||||
}
|
||||
return Object.assign(Object.create(graphics.Image), {
|
||||
rect:{x:0,y:0,width:1,height:1},
|
||||
[CPU]:obj,
|
||||
[GPU]:undefined,
|
||||
[LASTUSE]:os.now()
|
||||
})
|
||||
}
|
||||
|
||||
function wrapSurface(surf, maybeRect){
|
||||
const h = make_handle(surf);
|
||||
if(maybeRect) h.rect = maybeRect; /* honour frame sub-rect */
|
||||
return h;
|
||||
}
|
||||
function wrapFrames(arr){ /* [{surface,time,rect}, …] → [{image,time}] */
|
||||
return arr.map(f => ({
|
||||
image : wrapSurface(f.surface || f), /* accept bare surface too */
|
||||
time: f.time,
|
||||
rect: f.rect /* keep for reference */
|
||||
}));
|
||||
}
|
||||
function makeAnim(frames, loop=true){
|
||||
return { frames, loop }
|
||||
}
|
||||
|
||||
function decode_image(bytes, ext)
|
||||
{
|
||||
switch(ext) {
|
||||
case 'gif': return graphics.make_gif(bytes)
|
||||
/**
|
||||
Internally loads image data from disk and prepares a GPU texture. Used by graphics.texture().
|
||||
Not intended for direct user calls.
|
||||
*/
|
||||
function create_image(path) {
|
||||
var data = io.slurpbytes(path)
|
||||
var newimg
|
||||
switch (path.ext()) {
|
||||
case 'gif':
|
||||
newimg = graphics.make_gif(data)
|
||||
if (newimg.surface)
|
||||
newimg.texture = prosperon.gpu.load_texture(newimg.surface)
|
||||
else
|
||||
for (var frame of newimg.frames)
|
||||
frame.texture = prosperon.gpu.load_texture(frame.surface)
|
||||
break
|
||||
case 'ase':
|
||||
case 'aseprite': return graphics.make_aseprite(bytes)
|
||||
default: return {surface:graphics.make_texture(bytes)}
|
||||
}
|
||||
}
|
||||
|
||||
function create_image(path){
|
||||
try{
|
||||
const bytes = io.slurpbytes(path);
|
||||
let raw = decode_image(bytes, path.ext());
|
||||
|
||||
/* ── Case A: static image ─────────────────────────────────── */
|
||||
if(raw.surface)
|
||||
return make_handle(raw.surface);
|
||||
|
||||
/* ── Case B: GIF helpers returned array [surf, …] ─────────── */
|
||||
if(Array.isArray(raw))
|
||||
return makeAnim( wrapFrames(raw), true );
|
||||
|
||||
/* ── Case C: GIF helpers returned {frames,loop} ───────────── */
|
||||
if(raw.frames && Array.isArray(raw.frames))
|
||||
return makeAnim( wrapFrames(raw.frames), !!raw.loop );
|
||||
|
||||
/* ── Case D: ASE helpers returned { animName:{frames,loop}, … } ── */
|
||||
const anims = {};
|
||||
for(const [name, anim] of Object.entries(raw)){
|
||||
if(anim && Array.isArray(anim.frames))
|
||||
anims[name] = makeAnim( wrapFrames(anim.frames), !!anim.loop );
|
||||
else if(anim && anim.surface) /* ase with flat surface */
|
||||
anims[name] = makeAnim(
|
||||
[{image:make_handle(anim.surface),time:0}], true );
|
||||
}
|
||||
if(Object.keys(anims).length) return anims;
|
||||
|
||||
throw new Error('Unsupported image structure from decoder');
|
||||
|
||||
}catch(e){
|
||||
console.error(`Error loading image ${path}: ${e.message}`);
|
||||
throw e;
|
||||
case 'aseprite':
|
||||
newimg = graphics.make_aseprite(data)
|
||||
if (newimg.surface)
|
||||
newimg.texture = prosperon.gpu.load_texture(newimg.surface)
|
||||
else {
|
||||
for (var anim in newimg) {
|
||||
var a = newimg[anim]
|
||||
for (var frame of a.frames)
|
||||
frame.texture = prosperon.gpu.load_texture(frame.surface)
|
||||
}
|
||||
}
|
||||
break
|
||||
default:
|
||||
newimg = {
|
||||
surface: graphics.make_texture(data)
|
||||
}
|
||||
newimg.texture = prosperon.gpu.load_texture(newimg.surface)
|
||||
break
|
||||
}
|
||||
return newimg
|
||||
}
|
||||
|
||||
var image = {}
|
||||
@@ -184,49 +82,15 @@ graphics.is_image[prosperon.DOC] = `
|
||||
:return: True if 'obj' has a .texture and a .rect property, indicating it's an image object.
|
||||
`
|
||||
|
||||
graphics.texture_from_data = function(data)
|
||||
{
|
||||
if (!(data instanceof ArrayBuffer)) return undefined
|
||||
|
||||
var img = {
|
||||
surface: graphics.make_texture(data)
|
||||
}
|
||||
render.load_texture(img)
|
||||
decorate_rect_px(img)
|
||||
|
||||
return img
|
||||
}
|
||||
|
||||
graphics.from_surface = function(id, surf)
|
||||
{
|
||||
return make_handle(surf)
|
||||
var img = { surface: surf }
|
||||
}
|
||||
|
||||
graphics.from = function(id, data)
|
||||
{
|
||||
if (typeof id !== 'string')
|
||||
throw new Error('Expected a string ID')
|
||||
|
||||
if (data instanceof ArrayBuffer)
|
||||
return graphics.texture_from_data(data)
|
||||
}
|
||||
|
||||
graphics.texture = function texture(path) {
|
||||
if (path.__proto__ === graphics.Image) return path
|
||||
|
||||
if (typeof path !== 'string')
|
||||
if (typeof path !== 'string') {
|
||||
return path // fallback if already an image object
|
||||
throw new Error('need a string for graphics.texture')
|
||||
|
||||
var id = path.split(':')[0]
|
||||
if (cache.has(id)) return cache.get(id)
|
||||
|
||||
var ipath = res.find_image(id)
|
||||
if (!ipath) throw new Error(`unknown image ${id}`)
|
||||
|
||||
var image = create_image(ipath)
|
||||
cache.set(id, image)
|
||||
return image
|
||||
}
|
||||
var parts = path.split(':')
|
||||
var ipath = res.find_image(parts[0])
|
||||
graphics.texture.cache[ipath] ??= create_image(ipath)
|
||||
return graphics.texture.cache[ipath]
|
||||
}
|
||||
graphics.texture[prosperon.DOC] = `
|
||||
:param path: A string path to an image file or an already-loaded image object.
|
||||
@@ -306,15 +170,9 @@ graphics.get_font = function get_font(path, size) {
|
||||
if (fontcache[fontstr]) return fontcache[fontstr]
|
||||
|
||||
var data = io.slurpbytes(fullpath)
|
||||
var font = graphics.make_font(data,size)
|
||||
font.texture = render.load_texture(font.surface)
|
||||
|
||||
console.log('loaded font texture')
|
||||
console.log(json.encode(font.texture))
|
||||
|
||||
fontcache[fontstr] = font
|
||||
|
||||
return font
|
||||
fontcache[fontstr] = graphics.make_font(data, size)
|
||||
fontcache[fontstr].texture = prosperon.gpu.load_texture(fontcache[fontstr].surface)
|
||||
return fontcache[fontstr]
|
||||
}
|
||||
graphics.get_font[prosperon.DOC] = `
|
||||
:param path: A string path to a font file, optionally with ".size" appended.
|
||||
@@ -377,6 +235,11 @@ graphics.rectpack[prosperon.DOC] = `
|
||||
Perform a rectangle packing using the stbrp library. Return positions for each rect.
|
||||
`
|
||||
|
||||
graphics.make_rtree[prosperon.DOC] = `
|
||||
:return: An R-Tree object for quickly querying many rectangles or sprite bounds.
|
||||
Create a new R-Tree for geometry queries.
|
||||
`
|
||||
|
||||
graphics.make_texture[prosperon.DOC] = `
|
||||
:param data: Raw image bytes (PNG, JPG, etc.) as an ArrayBuffer.
|
||||
:return: An SDL_Surface object representing the decoded image in RAM, for use with GPU or software rendering.
|
||||
@@ -402,6 +265,13 @@ graphics.cull_sprites[prosperon.DOC] = `
|
||||
Filter an array of sprites to only those visible in the provided camera’s view.
|
||||
`
|
||||
|
||||
graphics.rects_to_sprites[prosperon.DOC] = `
|
||||
:param rects: An array of rect coords or objects.
|
||||
:param image: An image object (with .texture).
|
||||
:return: An array of sprite objects referencing the 'image' and each rect for UV or position.
|
||||
Convert an array of rect coords into sprite objects referencing a single image.
|
||||
`
|
||||
|
||||
graphics.make_surface[prosperon.DOC] = `
|
||||
:param dimensions: The size object {width, height}, or an array [w,h].
|
||||
:return: A blank RGBA surface with the given dimensions, typically for software rendering or icons.
|
||||
@@ -420,6 +290,11 @@ graphics.make_font[prosperon.DOC] = `
|
||||
Load a font from TTF/OTF data at the given size.
|
||||
`
|
||||
|
||||
graphics.make_sprite[prosperon.DOC] = `
|
||||
:return: A new sprite object, which typically has .rect, .color, .layer, .image, etc.
|
||||
Create a new sprite object, storing default properties.
|
||||
`
|
||||
|
||||
graphics.make_line_prim[prosperon.DOC] = `
|
||||
:param points: An array of [x,y] points forming the line.
|
||||
:param thickness: The thickness (width) of the polyline.
|
||||
|
||||
@@ -1,18 +0,0 @@
|
||||
var http = this
|
||||
|
||||
http.fetch[prosperon.DOC] = `Initiate an asynchronous HTTP GET request.
|
||||
|
||||
This function enqueues an HTTP GET request for the specified URL. It supports both buffered responses (full response collected in memory) and streaming data (processed as it arrives). The request is executed asynchronously, and its completion or streaming data is handled via callbacks provided in the options. Use 'poll' to process the results.
|
||||
|
||||
:param url: A string representing the URL to fetch.
|
||||
:param options: Either a callback function or an object with optional properties:
|
||||
- 'callback': A function invoked upon request completion, receiving an object with 'data' (string or null) and 'error' (string or null) properties.
|
||||
- 'on_data': A function invoked for each chunk of streaming data, receiving a string chunk as its argument. If supplied, 'callback.data' will be null.
|
||||
:return: undefined
|
||||
:throws:
|
||||
- An error if the URL is not a string or is invalid.
|
||||
- An error if the options argument is neither a function nor an object.
|
||||
- An error if memory allocation or CURL initialization fails.
|
||||
`
|
||||
|
||||
return http
|
||||
@@ -56,7 +56,7 @@ if (render_menu) {
|
||||
imtoggle("Console [f9]", debug, "console");
|
||||
});
|
||||
|
||||
// imgui.sokol_gfx();
|
||||
imgui.sokol_gfx();
|
||||
|
||||
imgui.menu("Graphics", _ => {
|
||||
// imtoggle("Draw sprites", render, "draw_sprites");
|
||||
|
||||
@@ -4,8 +4,6 @@ Provides functions for introspecting and configuring the QuickJS runtime engine.
|
||||
Includes debug info, memory usage, GC controls, code evaluation, etc.
|
||||
`
|
||||
|
||||
js.rt_info[prosperon.DOC] = "Return internal QuickJS runtime info, such as object counts."
|
||||
|
||||
js.dump_shapes[prosperon.DOC] = `
|
||||
:return: A debug string describing the internal shape hierarchy used by QuickJS.
|
||||
Use this for internal debugging of object shapes.
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
var json = {}
|
||||
|
||||
json.encode = function encode(val,replacer,space = 1,whitelist)
|
||||
json.encode = function(val,space,replacer,whitelist)
|
||||
{
|
||||
return JSON.stringify(val, replacer, space)
|
||||
return JSON.stringify(val, replacer, space ? 1 : 0)
|
||||
}
|
||||
|
||||
json.encode[prosperon.DOC] = `Produce a JSON text from a Javascript object. If a record value, at any level, contains a json() method, it will be called, and the value it returns (usually a simpler record) will be JSONified.
|
||||
@@ -11,7 +11,7 @@ If the record does not have a json() method, and if whitelist is a record, then
|
||||
|
||||
If the space input is true, then line breaks and extra whitespace will be included in the text.`
|
||||
|
||||
json.decode = function decode(text,reviver)
|
||||
json.decode = function(text,reviver)
|
||||
{
|
||||
return JSON.parse(text,reviver)
|
||||
}
|
||||
|
||||
@@ -1,107 +0,0 @@
|
||||
var sprite = {}
|
||||
|
||||
var graphics = use('graphics')
|
||||
var render = use('render')
|
||||
var draw2d = use('draw2d')
|
||||
var sprite = use('sprite')
|
||||
|
||||
var SPRITE = Symbol() /* raw C sprite */
|
||||
var POS = Symbol() /* cached JS copies of simple data */
|
||||
var ROT = Symbol()
|
||||
var SCALE = Symbol()
|
||||
var SKEW = Symbol()
|
||||
var CENTER = Symbol()
|
||||
var COLOR = Symbol()
|
||||
var IMAGE = Symbol()
|
||||
|
||||
var ursprite = {
|
||||
get pos() { return this[POS] },
|
||||
set pos(v) {
|
||||
this[POS] = v
|
||||
this[SPRITE].pos = v
|
||||
},
|
||||
|
||||
get center() { return this[CENTER] },
|
||||
set center(v){
|
||||
this[CENTER] = v
|
||||
this[SPRITE].center = v
|
||||
},
|
||||
|
||||
get rotation() { return this[ROT] },
|
||||
set rotation(t){
|
||||
this[ROT] = t
|
||||
this[SPRITE].rotation = t * 2*Math.PI /* C expects radians */
|
||||
this[SPRITE].set_affine()
|
||||
},
|
||||
|
||||
get scale() { return this[SCALE] },
|
||||
set scale(v){
|
||||
this[SCALE] = v
|
||||
this[SPRITE].scale = v
|
||||
this[SPRITE].set_affine()
|
||||
},
|
||||
|
||||
get skew() { return this[SKEW] },
|
||||
set skew(v){
|
||||
this[SKEW] = v
|
||||
this[SPRITE].skew = v
|
||||
this[SPRITE].set_affine()
|
||||
},
|
||||
|
||||
get layer() { return this[SPRITE].layer },
|
||||
set layer(n){ this[SPRITE].layer = n },
|
||||
|
||||
get color() { return this[COLOR] },
|
||||
set color(v){
|
||||
this[COLOR] = v
|
||||
this[SPRITE].color = v
|
||||
},
|
||||
|
||||
move(mv) { this[SPRITE].move(mv) },
|
||||
moveto(p){
|
||||
this.pos = p
|
||||
},
|
||||
|
||||
get image() { return this[IMAGE] },
|
||||
set image(img) {
|
||||
this[IMAGE] = img
|
||||
this[SPRITE].set_image(img)
|
||||
}
|
||||
}
|
||||
|
||||
var _sprites = []
|
||||
|
||||
var def_sprite = Object.freeze({
|
||||
pos: [0,0],
|
||||
rotation: 0,
|
||||
scale: [1,1],
|
||||
center: [0,0],
|
||||
color: [1,1,1,1],
|
||||
skew: [0,0],
|
||||
layer: 0,
|
||||
})
|
||||
|
||||
sprite.create = function(image, info) {
|
||||
info.__proto__ = def_sprite
|
||||
var sp = Object.create(ursprite)
|
||||
sp[SPRITE] = new sprite(info)
|
||||
sp.image = graphics.texture(image)
|
||||
|
||||
_sprites.push(sp)
|
||||
return sp
|
||||
}
|
||||
|
||||
sprite.forEach = fn => { for (let s of _sprites) fn(s) }
|
||||
sprite.values = () => _sprites.slice()
|
||||
sprite.geometry= () => graphics.make_sprite_mesh(_sprites)
|
||||
|
||||
var raws = []
|
||||
sprite.queue = function() {
|
||||
if (raws.length != _sprites.length)
|
||||
raws.length = _sprites.length
|
||||
|
||||
for (var i = 0; i < _sprites.length; i++) raws[i] = _sprites[i]
|
||||
return graphics.make_sprite_queue(_sprites.map(x => x[SPRITE]))
|
||||
}
|
||||
|
||||
return sprite
|
||||
@@ -1,33 +1,42 @@
|
||||
/*
|
||||
the "dull" game engine
|
||||
|
||||
This sets up a lot of different modules to be used altogether
|
||||
*/
|
||||
|
||||
var render = use('render')
|
||||
// loop.js
|
||||
var render = use('render') // The refactored file above
|
||||
var layout = use('clay')
|
||||
var input = use('input')
|
||||
var emitter = use('emitter')
|
||||
var os = use('os')
|
||||
var event = use('event')
|
||||
var imgui = use('imgui')
|
||||
var tracy = use('tracy')
|
||||
|
||||
var waittime = 1/240
|
||||
var last_frame_time = 0
|
||||
var frame_t = 0
|
||||
var fpses = []
|
||||
var timescale = 1
|
||||
|
||||
last_frame_time = os.now()
|
||||
var dmon = use('dmon')
|
||||
|
||||
function dmon_cb(e) { prosperon.dispatch('dmon', e) }
|
||||
|
||||
function step() {
|
||||
if (dmon)
|
||||
dmon.poll(dmon_cb)
|
||||
|
||||
var now = os.now()
|
||||
var dt = now - last_frame_time
|
||||
last_frame_time = now
|
||||
if (dt < waittime) os.sleep(waittime - dt)
|
||||
last_frame_time = os.now()
|
||||
|
||||
// event.engine_input(e => prosperon.dispatch(e.type, e))
|
||||
dt = last_frame_time - frame_t
|
||||
frame_t = last_frame_time
|
||||
|
||||
event.engine_input(e => prosperon.dispatch(e.type, e))
|
||||
layout.newframe()
|
||||
|
||||
prosperon.appupdate(dt)
|
||||
input.procdown()
|
||||
emitter.update(dt * timescale)
|
||||
os.update_timers(dt * timescale)
|
||||
prosperon.update(dt * timescale)
|
||||
|
||||
render.setup_draw()
|
||||
@@ -36,18 +45,12 @@ function step() {
|
||||
if (imgui) imgui.prosperon_menu();
|
||||
|
||||
// Now do the GPU present (calls gpupresent in render.js)
|
||||
render.present()
|
||||
render._main.present()
|
||||
|
||||
tracy.end_frame()
|
||||
}
|
||||
|
||||
function start()
|
||||
{
|
||||
|
||||
}
|
||||
|
||||
// Return or export them so you can call from a main script
|
||||
return {
|
||||
start,
|
||||
step
|
||||
}
|
||||
@@ -1,114 +0,0 @@
|
||||
/**
|
||||
* Moth Game Framework
|
||||
* Higher-level game development framework built on top of Prosperon
|
||||
*/
|
||||
|
||||
// Check if we received the required delay function
|
||||
if (!arg || arg.length === 0 || typeof arg[0] !== 'function') {
|
||||
throw new Error("moth module requires $_.delay function as first argument. Usage: use('moth', $_.delay)");
|
||||
}
|
||||
|
||||
// Verify it's actually a delay function by checking if it has the expected behavior
|
||||
var delay = arg[0];
|
||||
if (!delay.name || (delay.name !== 'delay' && !delay.name.includes('delay'))) {
|
||||
console.warn("Warning: The function passed to moth module may not be $_.delay");
|
||||
}
|
||||
|
||||
var os = use('os');
|
||||
var io = use('io');
|
||||
var render = use('render');
|
||||
var actor = use('actor');
|
||||
var transform = use('transform');
|
||||
|
||||
var gameConfig = {};
|
||||
var gameDir = "";
|
||||
|
||||
// Framework initialization
|
||||
function initialize() {
|
||||
var configPath = `config.js`;
|
||||
if (io.exists(configPath))
|
||||
gameConfig = use(configPath);
|
||||
|
||||
// Set up default config values
|
||||
gameConfig.resolution = gameConfig.resolution || { width: 640, height: 480 };
|
||||
gameConfig.internal_resolution = gameConfig.internal_resolution || gameConfig.resolution;
|
||||
gameConfig.title = gameConfig.title || "Moth Game";
|
||||
gameConfig.fps = gameConfig.fps || 60;
|
||||
gameConfig.clearColor = gameConfig.clearColor || [0, 0, 0, 1];
|
||||
|
||||
// Initialize render system
|
||||
render.initialize({
|
||||
width: gameConfig.resolution.width,
|
||||
height: gameConfig.resolution.height,
|
||||
resolution_x: gameConfig.internal_resolution.width,
|
||||
resolution_y: gameConfig.internal_resolution.height,
|
||||
mode: gameConfig.mode || 'letterbox'
|
||||
});
|
||||
|
||||
// Set up default camera
|
||||
gameConfig.camera = gameConfig.camera || {
|
||||
size: [gameConfig.internal_resolution.width, gameConfig.internal_resolution.height],
|
||||
transform: new transform,
|
||||
fov: 50,
|
||||
near_z: 0,
|
||||
far_z: 1000,
|
||||
surface: undefined,
|
||||
viewport: {x: 0, y: 0, width: 1, height: 1},
|
||||
ortho: true,
|
||||
anchor: [0, 0]
|
||||
};
|
||||
|
||||
// Set window title
|
||||
prosperon.window.title = gameConfig.title;
|
||||
|
||||
startGameLoop();
|
||||
}
|
||||
|
||||
// Main game loop
|
||||
function startGameLoop() {
|
||||
var last = os.now();
|
||||
var fpsTimer = 0;
|
||||
var fpsCount = 0;
|
||||
var targetFrameTime = 1 / gameConfig.fps;
|
||||
|
||||
function loop() {
|
||||
var now = os.now();
|
||||
var dt = now - last;
|
||||
last = now;
|
||||
|
||||
// Dispatch update event
|
||||
prosperon.dispatch('update', dt);
|
||||
|
||||
// Clear and set up rendering
|
||||
render.clear(gameConfig.clearColor);
|
||||
render.camera(gameConfig.camera);
|
||||
|
||||
// Dispatch draw event
|
||||
prosperon.dispatch('draw');
|
||||
|
||||
// Present the frame
|
||||
render.present();
|
||||
|
||||
// FPS counter
|
||||
fpsTimer += dt;
|
||||
fpsCount++;
|
||||
if (fpsTimer >= 0.5) {
|
||||
// Only update if the title wasn't changed by the game
|
||||
if (prosperon.window.title === gameConfig.title) {
|
||||
prosperon.window.title = `${gameConfig.title} - FPS ${(fpsCount / fpsTimer).toFixed(1)}`;
|
||||
}
|
||||
fpsTimer = fpsCount = 0;
|
||||
}
|
||||
|
||||
// Schedule next frame
|
||||
delay(loop, Math.max(0, targetFrameTime - (os.now() - now)));
|
||||
}
|
||||
|
||||
loop();
|
||||
}
|
||||
|
||||
// Public API
|
||||
return {
|
||||
initialize: initialize,
|
||||
config: gameConfig
|
||||
};
|
||||
@@ -1,25 +1,5 @@
|
||||
var nota = this
|
||||
|
||||
var json = use('json')
|
||||
|
||||
var encode = nota.encode
|
||||
|
||||
function nota_tostring()
|
||||
{
|
||||
return json.encode(nota.decode(this))
|
||||
}
|
||||
|
||||
var nota_obj = {
|
||||
toString: nota_tostring
|
||||
}
|
||||
|
||||
nota.encode = function(obj, replacer)
|
||||
{
|
||||
var result = encode(obj,replacer)
|
||||
result.toString = nota_tostring
|
||||
return result
|
||||
}
|
||||
|
||||
nota.encode[prosperon.DOC] = `Convert a JavaScript value into a NOTA-encoded ArrayBuffer.
|
||||
|
||||
This function serializes JavaScript values (such as numbers, strings, booleans, arrays, objects, or ArrayBuffers) into the NOTA binary format. The resulting ArrayBuffer can be stored or transmitted and later decoded back into a JavaScript value.
|
||||
|
||||
@@ -1,5 +1,8 @@
|
||||
var os = this
|
||||
|
||||
os.make_transform[prosperon.DOC] = "Create a new transform object that can be used for 2D/3D positioning, scaling, and rotation."
|
||||
os.clean_transforms[prosperon.DOC] = "Force an update on all transforms to remove dangling references or perform house-keeping."
|
||||
|
||||
os.platform[prosperon.DOC] = "Return a string with the underlying platform name, like 'Windows', 'Linux', or 'macOS'."
|
||||
os.arch[prosperon.DOC] = "Return the CPU architecture string for this system (e.g. 'x64', 'arm64')."
|
||||
os.totalmem[prosperon.DOC] = "Return the total system RAM in bytes."
|
||||
@@ -7,13 +10,25 @@ os.freemem[prosperon.DOC] = "Return the amount of free system RAM in bytes, if k
|
||||
os.hostname[prosperon.DOC] = "Return the system's hostname, or an empty string if not available."
|
||||
os.version[prosperon.DOC] = "Return the OS or kernel version string, if the platform provides it."
|
||||
|
||||
os.kill[prosperon.DOC] = "Send a signal (e.g., 'SIGINT', 'SIGTERM', etc.) to the current process."
|
||||
os.exit[prosperon.DOC] = "Exit the application with the specified exit code."
|
||||
os.now[prosperon.DOC] = "Return current time (in seconds as a float) with high resolution."
|
||||
os.openurl[prosperon.DOC] = "Open the provided URL in the default web browser, if possible."
|
||||
|
||||
os.make_timer[prosperon.DOC] = "Create a new timer object that will call a specified function after a certain delay."
|
||||
os.update_timers[prosperon.DOC] = "Advance all timers by the provided time delta (in seconds)."
|
||||
|
||||
os.sleep[prosperon.DOC] = "Block execution for the specified number of seconds."
|
||||
os.battery_pct[prosperon.DOC] = "Return the battery level (percentage) or negative if unknown."
|
||||
os.battery_voltage[prosperon.DOC] = "Return the current battery voltage in volts, if available."
|
||||
os.battery_seconds[prosperon.DOC] = "Return the estimated remaining battery time in seconds, or negative if unknown."
|
||||
os.power_state[prosperon.DOC] = "Return a string describing power status: 'on battery', 'charging', 'charged', etc."
|
||||
|
||||
os.on[prosperon.DOC] = "Register a global callback for certain engine-wide or system-level events."
|
||||
os.rt_info[prosperon.DOC] = "Return internal QuickJS runtime info, such as object counts."
|
||||
os.rusage[prosperon.DOC] = "Return resource usage stats for this process, if the platform supports it."
|
||||
os.mallinfo[prosperon.DOC] = "Return detailed memory allocation info (arena size, free blocks, etc.) on some platforms."
|
||||
os.env[prosperon.DOC] = "Fetch the value of a given environment variable, or undefined if it doesn't exist."
|
||||
os.system[prosperon.DOC] = "Execute a shell command using the system() call. Returns the command's exit code."
|
||||
|
||||
return os
|
||||
return os
|
||||
@@ -1,584 +0,0 @@
|
||||
// parseq.js
|
||||
// Douglas Crockford
|
||||
// 2020-11-09
|
||||
|
||||
// Better living thru eventuality!
|
||||
|
||||
// You can access the parseq object in your module by importing it.
|
||||
// import parseq from "./parseq.js";
|
||||
|
||||
/*jslint node */
|
||||
|
||||
/*property
|
||||
concat, create, evidence, fallback, forEach, freeze, isArray, isSafeInteger,
|
||||
keys, length, min, parallel, parallel_object, pop, push, race, sequence,
|
||||
some
|
||||
*/
|
||||
|
||||
function make_reason(factory_name, excuse, evidence) {
|
||||
|
||||
// Make a reason object. These are used for exceptions and cancellations.
|
||||
// They are made from Error objects.
|
||||
|
||||
const reason = new Error("parseq." + factory_name + (
|
||||
excuse === undefined
|
||||
? ""
|
||||
: ": " + excuse
|
||||
));
|
||||
reason.evidence = evidence;
|
||||
return reason;
|
||||
}
|
||||
|
||||
function get_array_length(array, factory_name) {
|
||||
if (Array.isArray(array)) {
|
||||
return array.length;
|
||||
}
|
||||
if (array === undefined) {
|
||||
return 0;
|
||||
}
|
||||
throw make_reason(factory_name, "Not an array.", array);
|
||||
}
|
||||
|
||||
function check_callback(callback, factory_name) {
|
||||
if (typeof callback !== "function" || callback.length !== 2) {
|
||||
throw make_reason(factory_name, "Not a callback function.", callback);
|
||||
}
|
||||
}
|
||||
|
||||
function check_requestors(requestor_array, factory_name) {
|
||||
|
||||
// A requestor array contains only requestors. A requestor is a function that
|
||||
// takes wun or two arguments: 'callback' and optionally 'initial_value'.
|
||||
|
||||
if (requestor_array.some(function (requestor) {
|
||||
return (
|
||||
typeof requestor !== "function"
|
||||
|| requestor.length < 1
|
||||
|| requestor.length > 2
|
||||
);
|
||||
})) {
|
||||
throw make_reason(
|
||||
factory_name,
|
||||
"Bad requestors array.",
|
||||
requestor_array
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
function run(
|
||||
factory_name,
|
||||
requestor_array,
|
||||
initial_value,
|
||||
action,
|
||||
timeout,
|
||||
time_limit,
|
||||
throttle = 0
|
||||
) {
|
||||
|
||||
// The 'run' function does the work that is common to all of the Parseq
|
||||
// factories. It takes the name of the factory, an array of requestors, an
|
||||
// initial value, an action callback, a timeout callback, a time limit in
|
||||
// milliseconds, and a throttle.
|
||||
|
||||
// If all goes well, we call all of the requestor functions in the array. Each
|
||||
// of them might return a cancel function that is kept in the 'cancel_array'.
|
||||
|
||||
let cancel_array = new Array(requestor_array.length);
|
||||
let next_number = 0;
|
||||
let timer_id;
|
||||
|
||||
// We need 'cancel' and 'start_requestor' functions.
|
||||
|
||||
function cancel(reason = make_reason(factory_name, "Cancel.")) {
|
||||
|
||||
// Stop all unfinished business. This can be called when a requestor fails.
|
||||
// It can also be called when a requestor succeeds, such as 'race' stopping
|
||||
// its losers, or 'parallel' stopping the unfinished optionals.
|
||||
|
||||
// If a timer is running, stop it.
|
||||
|
||||
if (timer_id !== undefined) {
|
||||
clearTimeout(timer_id);
|
||||
timer_id = undefined;
|
||||
}
|
||||
|
||||
// If anything is still going, cancel it.
|
||||
|
||||
if (cancel_array !== undefined) {
|
||||
cancel_array.forEach(function (cancel) {
|
||||
try {
|
||||
if (typeof cancel === "function") {
|
||||
return cancel(reason);
|
||||
}
|
||||
} catch (ignore) {}
|
||||
});
|
||||
cancel_array = undefined;
|
||||
}
|
||||
}
|
||||
|
||||
function start_requestor(value) {
|
||||
|
||||
// The 'start_requestor' function is not recursive, exactly. It does not
|
||||
// directly call itself, but it does return a function that might call
|
||||
// 'start_requestor'.
|
||||
|
||||
// Start the execution of a requestor, if there are any still waiting.
|
||||
|
||||
if (
|
||||
cancel_array !== undefined
|
||||
&& next_number < requestor_array.length
|
||||
) {
|
||||
|
||||
// Each requestor has a number.
|
||||
|
||||
let number = next_number;
|
||||
next_number += 1;
|
||||
|
||||
// Call the next requestor, passing in a callback function,
|
||||
// saving the cancel function that the requestor might return.
|
||||
|
||||
const requestor = requestor_array[number];
|
||||
try {
|
||||
cancel_array[number] = requestor(
|
||||
function start_requestor_callback(value, reason) {
|
||||
|
||||
// This callback function is called by the 'requestor' when it is done.
|
||||
// If we are no longer running, then this call is ignored.
|
||||
// For example, it might be a result that is sent back after the time
|
||||
// limit has expired. This callback function can only be called wunce.
|
||||
|
||||
if (
|
||||
cancel_array !== undefined
|
||||
&& number !== undefined
|
||||
) {
|
||||
|
||||
// We no longer need the cancel associated with this requestor.
|
||||
|
||||
cancel_array[number] = undefined;
|
||||
|
||||
// Call the 'action' function to let the requestor know what happened.
|
||||
|
||||
action(value, reason, number);
|
||||
|
||||
// Clear 'number' so this callback can not be used again.
|
||||
|
||||
number = undefined;
|
||||
|
||||
// If there are any requestors that are still waiting to start, then
|
||||
// start the next wun. If the next requestor is in a sequence, then it
|
||||
// gets the most recent 'value'. The others get the 'initial_value'.
|
||||
|
||||
setTimeout(start_requestor, 0, (
|
||||
factory_name === "sequence"
|
||||
? value
|
||||
: initial_value
|
||||
));
|
||||
}
|
||||
},
|
||||
value
|
||||
);
|
||||
|
||||
// Requestors are required to report their failure thru the callback.
|
||||
// They are not allowed to throw exceptions. If we happen to catch wun,
|
||||
// it is treated as a failure.
|
||||
|
||||
} catch (exception) {
|
||||
action(undefined, exception, number);
|
||||
number = undefined;
|
||||
start_requestor(value);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// With the 'cancel' and the 'start_requestor' functions in hand,
|
||||
// we can now get to work.
|
||||
|
||||
// If a timeout was requested, start the timer.
|
||||
|
||||
if (time_limit !== undefined) {
|
||||
if (typeof time_limit === "number" && time_limit >= 0) {
|
||||
if (time_limit > 0) {
|
||||
timer_id = setTimeout(timeout, time_limit);
|
||||
}
|
||||
} else {
|
||||
throw make_reason(factory_name, "Bad time limit.", time_limit);
|
||||
}
|
||||
}
|
||||
|
||||
// If we are doing 'race' or 'parallel', we want to start all of the requestors
|
||||
// at wunce. However, if there is a 'throttle' in place then we start as many
|
||||
// as the 'throttle' allows, and then as each requestor finishes, another is
|
||||
// started.
|
||||
|
||||
// The 'sequence' and 'fallback' factories set 'throttle' to 1 because they
|
||||
// process wun at a time and always start another requestor when the
|
||||
// previous requestor finishes.
|
||||
|
||||
if (!Number.isSafeInteger(throttle) || throttle < 0) {
|
||||
throw make_reason(factory_name, "Bad throttle.", throttle);
|
||||
}
|
||||
let repeat = Math.min(throttle || Infinity, requestor_array.length);
|
||||
while (repeat > 0) {
|
||||
setTimeout(start_requestor, 0, initial_value);
|
||||
repeat -= 1;
|
||||
}
|
||||
|
||||
// We return 'cancel' which allows the requestor to cancel this work.
|
||||
|
||||
return cancel;
|
||||
}
|
||||
|
||||
// The factories ///////////////////////////////////////////////////////////////
|
||||
|
||||
function parallel(
|
||||
required_array,
|
||||
optional_array,
|
||||
time_limit,
|
||||
time_option,
|
||||
throttle,
|
||||
factory_name = "parallel"
|
||||
) {
|
||||
|
||||
// The parallel factory is the most complex of these factories. It can take
|
||||
// a second array of requestors that get a more forgiving failure policy.
|
||||
// It returns a requestor that produces an array of values.
|
||||
|
||||
let requestor_array;
|
||||
|
||||
// There are four cases because 'required_array' and 'optional_array'
|
||||
// can both be empty.
|
||||
|
||||
let number_of_required = get_array_length(required_array, factory_name);
|
||||
if (number_of_required === 0) {
|
||||
if (get_array_length(optional_array, factory_name) === 0) {
|
||||
|
||||
// If both are empty, then 'requestor_array' is empty.
|
||||
|
||||
requestor_array = [];
|
||||
} else {
|
||||
|
||||
// If there is only 'optional_array', then it is the 'requestor_array'.
|
||||
|
||||
requestor_array = optional_array;
|
||||
time_option = true;
|
||||
}
|
||||
} else {
|
||||
|
||||
// If there is only 'required_array', then it is the 'requestor_array'.
|
||||
|
||||
if (get_array_length(optional_array, factory_name) === 0) {
|
||||
requestor_array = required_array;
|
||||
time_option = undefined;
|
||||
|
||||
// If both arrays are provided, we concatenate them together.
|
||||
|
||||
} else {
|
||||
requestor_array = required_array.concat(optional_array);
|
||||
if (time_option !== undefined && typeof time_option !== "boolean") {
|
||||
throw make_reason(
|
||||
factory_name,
|
||||
"Bad time_option.",
|
||||
time_option
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// We check the array and return the requestor.
|
||||
|
||||
check_requestors(requestor_array, factory_name);
|
||||
return function parallel_requestor(callback, initial_value) {
|
||||
check_callback(callback, factory_name);
|
||||
let number_of_pending = requestor_array.length;
|
||||
let number_of_pending_required = number_of_required;
|
||||
let results = [];
|
||||
if (number_of_pending === 0) {
|
||||
callback(
|
||||
factory_name === "sequence"
|
||||
? initial_value
|
||||
: results
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
// 'run' gets it started.
|
||||
|
||||
let cancel = run(
|
||||
factory_name,
|
||||
requestor_array,
|
||||
initial_value,
|
||||
function parallel_action(value, reason, number) {
|
||||
|
||||
// The action function gets the result of each requestor in the array.
|
||||
// 'parallel' wants to return an array of all of the values it sees.
|
||||
|
||||
results[number] = value;
|
||||
number_of_pending -= 1;
|
||||
|
||||
// If the requestor was wun of the requireds, make sure it was successful.
|
||||
// If it failed, then the parallel operation fails. If an optionals requestor
|
||||
// fails, we can still continue.
|
||||
|
||||
if (number < number_of_required) {
|
||||
number_of_pending_required -= 1;
|
||||
if (value === undefined) {
|
||||
cancel(reason);
|
||||
callback(undefined, reason);
|
||||
callback = undefined;
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// If all have been processed, or if the requireds have all succeeded
|
||||
// and we do not have a 'time_option', then we are done.
|
||||
|
||||
if (
|
||||
number_of_pending < 1
|
||||
|| (
|
||||
time_option === undefined
|
||||
&& number_of_pending_required < 1
|
||||
)
|
||||
) {
|
||||
cancel(make_reason(factory_name, "Optional."));
|
||||
callback(
|
||||
factory_name === "sequence"
|
||||
? results.pop()
|
||||
: results
|
||||
);
|
||||
callback = undefined;
|
||||
}
|
||||
},
|
||||
function parallel_timeout() {
|
||||
|
||||
// When the timer fires, work stops unless we were under the 'false'
|
||||
// time option. The 'false' time option puts no time limits on the
|
||||
// requireds, allowing the optionals to run until the requireds finish
|
||||
// or the time expires, whichever happens last.
|
||||
|
||||
const reason = make_reason(
|
||||
factory_name,
|
||||
"Timeout.",
|
||||
time_limit
|
||||
);
|
||||
if (time_option === false) {
|
||||
time_option = undefined;
|
||||
if (number_of_pending_required < 1) {
|
||||
cancel(reason);
|
||||
callback(results);
|
||||
}
|
||||
} else {
|
||||
|
||||
// Time has expired. If all of the requireds were successful,
|
||||
// then the parallel operation is successful.
|
||||
|
||||
cancel(reason);
|
||||
if (number_of_pending_required < 1) {
|
||||
callback(results);
|
||||
} else {
|
||||
callback(undefined, reason);
|
||||
}
|
||||
callback = undefined;
|
||||
}
|
||||
},
|
||||
time_limit,
|
||||
throttle
|
||||
);
|
||||
return cancel;
|
||||
};
|
||||
}
|
||||
|
||||
function parallel_object(
|
||||
required_object,
|
||||
optional_object,
|
||||
time_limit,
|
||||
time_option,
|
||||
throttle
|
||||
) {
|
||||
|
||||
// 'parallel_object' is similar to 'parallel' except that it takes and
|
||||
// produces objects of requestors instead of arrays of requestors. This
|
||||
// factory converts the objects to arrays, and the requestor it returns
|
||||
// turns them back again. It lets 'parallel' do most of the work.
|
||||
|
||||
const names = [];
|
||||
let required_array = [];
|
||||
let optional_array = [];
|
||||
|
||||
// Extract the names and requestors from 'required_object'.
|
||||
// We only collect functions with an arity of 1 or 2.
|
||||
|
||||
if (required_object) {
|
||||
if (typeof required_object !== "object") {
|
||||
throw make_reason(
|
||||
"parallel_object",
|
||||
"Type mismatch.",
|
||||
required_object
|
||||
);
|
||||
}
|
||||
Object.keys(required_object).forEach(function (name) {
|
||||
let requestor = required_object[name];
|
||||
if (
|
||||
typeof requestor === "function"
|
||||
&& (requestor.length === 1 || requestor.length === 2)
|
||||
) {
|
||||
names.push(name);
|
||||
required_array.push(requestor);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Extract the names and requestors from 'optional_object'.
|
||||
// Look for duplicate keys.
|
||||
|
||||
if (optional_object) {
|
||||
if (typeof optional_object !== "object") {
|
||||
throw make_reason(
|
||||
"parallel_object",
|
||||
"Type mismatch.",
|
||||
optional_object
|
||||
);
|
||||
}
|
||||
Object.keys(optional_object).forEach(function (name) {
|
||||
let requestor = optional_object[name];
|
||||
if (
|
||||
typeof requestor === "function"
|
||||
&& (requestor.length === 1 || requestor.length === 2)
|
||||
) {
|
||||
if (required_object && required_object[name] !== undefined) {
|
||||
throw make_reason(
|
||||
"parallel_object",
|
||||
"Duplicate name.",
|
||||
name
|
||||
);
|
||||
}
|
||||
names.push(name);
|
||||
optional_array.push(requestor);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Call 'parallel' to get a requestor.
|
||||
|
||||
const parallel_requestor = parallel(
|
||||
required_array,
|
||||
optional_array,
|
||||
time_limit,
|
||||
time_option,
|
||||
throttle,
|
||||
"parallel_object"
|
||||
);
|
||||
|
||||
// Return the parallel object requestor.
|
||||
|
||||
return function parallel_object_requestor(callback, initial_value) {
|
||||
|
||||
// When our requestor is called, we return the result of our parallel requestor.
|
||||
|
||||
return parallel_requestor(
|
||||
|
||||
// We pass our callback to the parallel requestor,
|
||||
// converting its value into an object.
|
||||
|
||||
function parallel_object_callback(value, reason) {
|
||||
if (value === undefined) {
|
||||
return callback(undefined, reason);
|
||||
}
|
||||
const object = Object.create(null);
|
||||
names.forEach(function (name, index) {
|
||||
object[name] = value[index];
|
||||
});
|
||||
return callback(object);
|
||||
},
|
||||
initial_value
|
||||
);
|
||||
};
|
||||
}
|
||||
|
||||
function race(requestor_array, time_limit, throttle) {
|
||||
|
||||
// The 'race' factory returns a requestor that starts all of the
|
||||
// requestors in 'requestor_array' at wunce. The first success wins.
|
||||
|
||||
const factory_name = (
|
||||
throttle === 1
|
||||
? "fallback"
|
||||
: "race"
|
||||
);
|
||||
|
||||
if (get_array_length(requestor_array, factory_name) === 0) {
|
||||
throw make_reason(factory_name, "No requestors.");
|
||||
}
|
||||
check_requestors(requestor_array, factory_name);
|
||||
return function race_requestor(callback, initial_value) {
|
||||
check_callback(callback, factory_name);
|
||||
let number_of_pending = requestor_array.length;
|
||||
let cancel = run(
|
||||
factory_name,
|
||||
requestor_array,
|
||||
initial_value,
|
||||
function race_action(value, reason, number) {
|
||||
number_of_pending -= 1;
|
||||
if (value !== undefined) {
|
||||
|
||||
// We have a winner. Cancel the losers and pass the value to the 'callback'.
|
||||
|
||||
cancel(make_reason(factory_name, "Loser.", number));
|
||||
callback(value);
|
||||
callback = undefined;
|
||||
} else if (number_of_pending < 1) {
|
||||
|
||||
// There was no winner. Signal a failure.
|
||||
|
||||
cancel(reason);
|
||||
callback(undefined, reason);
|
||||
callback = undefined;
|
||||
}
|
||||
},
|
||||
function race_timeout() {
|
||||
let reason = make_reason(
|
||||
factory_name,
|
||||
"Timeout.",
|
||||
time_limit
|
||||
);
|
||||
cancel(reason);
|
||||
callback(undefined, reason);
|
||||
callback = undefined;
|
||||
},
|
||||
time_limit,
|
||||
throttle
|
||||
);
|
||||
return cancel;
|
||||
};
|
||||
}
|
||||
|
||||
function fallback(requestor_array, time_limit) {
|
||||
|
||||
// The 'fallback' factory returns a requestor that tries each requestor
|
||||
// in 'requestor_array', wun at a time, until it finds a successful wun.
|
||||
|
||||
return race(requestor_array, time_limit, 1);
|
||||
}
|
||||
|
||||
function sequence(requestor_array, time_limit) {
|
||||
|
||||
// A sequence runs each requestor in order, passing results to the next,
|
||||
// as long as they are all successful. A sequence is a throttled parallel.
|
||||
|
||||
return parallel(
|
||||
requestor_array,
|
||||
undefined,
|
||||
time_limit,
|
||||
undefined,
|
||||
1,
|
||||
"sequence"
|
||||
);
|
||||
|
||||
}
|
||||
|
||||
return {
|
||||
fallback,
|
||||
parallel,
|
||||
parallel_object,
|
||||
race,
|
||||
sequence
|
||||
}
|
||||
@@ -1 +1,842 @@
|
||||
return use('sdl_render')
|
||||
var render = {}
|
||||
|
||||
var io = use('io')
|
||||
var os = use('os')
|
||||
var controller = use('controller')
|
||||
var tracy = use('tracy')
|
||||
var graphics = use('graphics')
|
||||
|
||||
var default_conf = {
|
||||
title:`Prosperon [${prosperon.version}-${prosperon.revision}]`,
|
||||
width: 1280,
|
||||
height: 720,
|
||||
icon: graphics.make_texture(io.slurpbytes('icons/moon.gif')),
|
||||
high_dpi:0,
|
||||
alpha:1,
|
||||
fullscreen:0,
|
||||
sample_count:1,
|
||||
enable_clipboard:true,
|
||||
enable_dragndrop: true,
|
||||
max_dropped_files: 1,
|
||||
swap_interval: 1,
|
||||
name: "Prosperon",
|
||||
version:prosperon.version + "-" + prosperon.revision,
|
||||
identifier: "world.pockle.prosperon",
|
||||
creator: "Pockle World LLC",
|
||||
copyright: "Copyright Pockle World 2025",
|
||||
type: "game",
|
||||
url: "https://prosperon.dev"
|
||||
}
|
||||
|
||||
var config = use('config.js')
|
||||
config.__proto__ = default_conf
|
||||
|
||||
prosperon.camera = use('ext/camera').make()
|
||||
prosperon.camera.size = [config.width,config.height]
|
||||
|
||||
var base_pipeline = {
|
||||
vertex: "sprite.vert",
|
||||
fragment: "sprite.frag",
|
||||
primitive: "triangle", // point, line, linestrip, triangle, trianglestrip
|
||||
fill: true, // false for lines
|
||||
depth: {
|
||||
compare: "greater_equal", // never/less/equal/less_equal/greater/not_equal/greater_equal/always
|
||||
test: false,
|
||||
write: false,
|
||||
bias: 0,
|
||||
bias_slope_scale: 0,
|
||||
bias_clamp: 0
|
||||
},
|
||||
stencil: {
|
||||
enabled: true,
|
||||
front: {
|
||||
compare: "equal", // never/less/equal/less_equal/greater/neq/greq/always
|
||||
fail: "keep", // keep/zero/replace/incr_clamp/decr_clamp/invert/incr_wrap/decr_wrap
|
||||
depth_fail: "keep",
|
||||
pass: "keep"
|
||||
},
|
||||
back: {
|
||||
compare: "equal", // never/less/equal/less_equal/greater/neq/greq/always
|
||||
fail: "keep", // keep/zero/replace/incr_clamp/decr_clamp/invert/incr_wrap/decr_wrap
|
||||
depth_fail: "keep",
|
||||
pass: "keep"
|
||||
},
|
||||
test: true,
|
||||
compare_mask: 0,
|
||||
write_mask: 0
|
||||
},
|
||||
blend: {
|
||||
enabled: false,
|
||||
src_rgb: "zero", // zero/one/src_color/one_minus_src_color/dst_color/one_minus_dst_color/src_alpha/one_minus_src_alpha/dst_alpha/one_minus_dst_alpha/constant_color/one_minus_constant_color/src_alpha_saturate
|
||||
dst_rgb: "zero",
|
||||
op_rgb: "add", // add/sub/rev_sub/min/max
|
||||
src_alpha: "one",
|
||||
dst_alpha: "zero",
|
||||
op_alpha: "add"
|
||||
},
|
||||
cull: "none", // none/front/back
|
||||
face: "cw", // cw/ccw
|
||||
alpha_to_coverage: false,
|
||||
multisample: {
|
||||
count: 1, // number of multisamples
|
||||
mask: 0xFFFFFFFF,
|
||||
domask: false
|
||||
},
|
||||
label: "scripted pipeline",
|
||||
target: {}
|
||||
}
|
||||
|
||||
var sprite_pipeline = Object.create(base_pipeline);
|
||||
sprite_pipeline.blend = {
|
||||
enabled:true,
|
||||
src_rgb: "src_alpha", // zero/one/src_color/one_minus_src_color/dst_color/one_minus_dst_color/src_alpha/one_minus_src_alpha/dst_alpha/one_minus_dst_alpha/constant_color/one_minus_constant_color/src_alpha_saturate
|
||||
dst_rgb: "one_minus_src_alpha",
|
||||
op_rgb: "add", // add/sub/rev_sub/min/max
|
||||
src_alpha: "one",
|
||||
dst_alpha: "zero",
|
||||
op_alpha: "add"
|
||||
};
|
||||
|
||||
sprite_pipeline.target = {
|
||||
color_targets: [{
|
||||
format:"rgba8",
|
||||
blend:sprite_pipeline.blend
|
||||
}],
|
||||
depth: "d32 float s8"
|
||||
};
|
||||
|
||||
var appy = {};
|
||||
appy.inputs = {};
|
||||
if (os.platform() === "macos") {
|
||||
appy.inputs["S-q"] = os.exit;
|
||||
}
|
||||
|
||||
appy.inputs["M-f4"] = os.exit;
|
||||
|
||||
controller.player[0].control(appy);
|
||||
|
||||
prosperon.window = prosperon.engine_start(config);
|
||||
|
||||
var driver = "vulkan"
|
||||
switch(os.platform()) {
|
||||
case "Linux":
|
||||
driver = "vulkan"
|
||||
break
|
||||
case "Windows":
|
||||
// driver = "direct3d12"
|
||||
driver = "vulkan"
|
||||
break
|
||||
case "macOS":
|
||||
driver = "metal"
|
||||
break
|
||||
}
|
||||
|
||||
render._main = prosperon.window.make_gpu(false,driver)
|
||||
prosperon.gpu = render._main
|
||||
render._main.window = prosperon.window
|
||||
render._main.claim_window(prosperon.window)
|
||||
render._main.set_swapchain('sdr', 'vsync')
|
||||
|
||||
var whiteimage = {}
|
||||
whiteimage.surface = graphics.make_surface([1,1])
|
||||
whiteimage.surface.rect({x:0,y:0,width:1,height:1}, [1,1,1,1])
|
||||
whiteimage.texture = render._main.load_texture(whiteimage.surface)
|
||||
|
||||
var imgui = use('imgui')
|
||||
if (imgui) imgui.init(render._main, prosperon.window)
|
||||
|
||||
var unit_transform = os.make_transform();
|
||||
|
||||
var cur = {};
|
||||
cur.images = [];
|
||||
cur.samplers = [];
|
||||
|
||||
var tbuffer;
|
||||
function full_upload(buffers) {
|
||||
var cmds = render._main.acquire_cmd_buffer();
|
||||
tbuffer = render._main.upload(cmds, buffers, tbuffer);
|
||||
cmds.submit();
|
||||
}
|
||||
|
||||
full_upload[prosperon.DOC] = `Acquire a command buffer and upload the provided data buffers to the GPU, then submit.
|
||||
|
||||
:param buffers: An array of data buffers to be uploaded.
|
||||
:return: None
|
||||
`
|
||||
|
||||
function bind_pipeline(pass, pipeline) {
|
||||
make_pipeline(pipeline)
|
||||
pass.bind_pipeline(pipeline.gpu)
|
||||
pass.pipeline = pipeline;
|
||||
}
|
||||
|
||||
bind_pipeline[prosperon.DOC] = `Ensure the specified pipeline is created on the GPU and bind it to the given render pass.
|
||||
|
||||
:param pass: The current render pass to bind the pipeline to.
|
||||
:param pipeline: The pipeline object containing shader and state info.
|
||||
:return: None
|
||||
`
|
||||
|
||||
var main_pass;
|
||||
|
||||
var cornflower = [62/255,96/255,113/255,1];
|
||||
|
||||
function get_pipeline_ubo_slot(pipeline, name) {
|
||||
if (!pipeline.vertex.reflection.ubos) return;
|
||||
for (var i = 0; i < pipeline.vertex.reflection.ubos.length; i++) {
|
||||
var ubo = pipeline.vertex.reflection.ubos[i];
|
||||
if (ubo.name.endsWith(name))
|
||||
return i;
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
|
||||
get_pipeline_ubo_slot[prosperon.DOC] = `Return the index of a uniform buffer block within the pipeline's vertex reflection data by name suffix.
|
||||
|
||||
:param pipeline: The pipeline whose vertex reflection is inspected.
|
||||
:param name: A string suffix to match against the uniform buffer block name.
|
||||
:return: The integer index of the matching UBO, or undefined if not found.
|
||||
`
|
||||
|
||||
function transpose4x4(val) {
|
||||
var out = [];
|
||||
out[0] = val[0]; out[1] = val[4]; out[2] = val[8]; out[3] = val[12];
|
||||
out[4] = val[1]; out[5] = val[5]; out[6] = val[9]; out[7] = val[13];
|
||||
out[8] = val[2]; out[9] = val[6]; out[10] = val[10];out[11] = val[14];
|
||||
out[12] = val[3];out[13] = val[7];out[14] = val[11];out[15] = val[15];
|
||||
return out;
|
||||
}
|
||||
|
||||
transpose4x4[prosperon.DOC] = `Return a new 4x4 matrix array that is the transpose of the passed matrix.
|
||||
|
||||
:param val: An array of length 16 representing a 4x4 matrix in row-major format.
|
||||
:return: A new array of length 16 representing the transposed matrix.
|
||||
`
|
||||
|
||||
function ubo_obj_to_array(pipeline, name, obj) {
|
||||
var ubo;
|
||||
for (var i = 0; i < pipeline.vertex.reflection.ubos.length; i++) {
|
||||
ubo = pipeline.vertex.reflection.ubos[i];
|
||||
if (ubo.name.endsWith(name)) break;
|
||||
}
|
||||
var type = pipeline.vertex.reflection.types[ubo.type];
|
||||
var len = 0;
|
||||
for (var mem of type.members)
|
||||
len += type_to_byte_count(mem.type);
|
||||
|
||||
var buf = new ArrayBuffer(len);
|
||||
var view = new DataView(buf);
|
||||
|
||||
for (var mem of type.members) {
|
||||
var val = obj[mem.name];
|
||||
if (!val) throw new Error (`Could not find ${mem.name} on supplied object`);
|
||||
|
||||
if (mem.name === 'model')
|
||||
val = transpose4x4(val.array());
|
||||
|
||||
for (var i = 0; i < val.length; i++)
|
||||
view.setFloat32(mem.offset + i*4, val[i],true);
|
||||
}
|
||||
return buf;
|
||||
}
|
||||
|
||||
ubo_obj_to_array[prosperon.DOC] = `Construct an ArrayBuffer containing UBO data from the provided object, matching the pipeline's reflection info.
|
||||
|
||||
:param pipeline: The pipeline whose vertex reflection is read for UBO structure.
|
||||
:param name: The name suffix that identifies the target UBO in the reflection data.
|
||||
:param obj: An object whose properties match the UBO members.
|
||||
:return: An ArrayBuffer containing packed UBO data.
|
||||
`
|
||||
|
||||
function type_to_byte_count(type) {
|
||||
switch (type) {
|
||||
case 'float': return 4;
|
||||
case 'vec2': return 8;
|
||||
case 'vec3': return 12;
|
||||
case 'vec4': return 16;
|
||||
case 'mat4': return 64;
|
||||
default: throw new Error("Unknown or unsupported float-based type: " + type);
|
||||
}
|
||||
}
|
||||
|
||||
type_to_byte_count[prosperon.DOC] = `Return the byte size for known float-based types.
|
||||
|
||||
:param type: A string type identifier (e.g., 'float', 'vec2', 'vec3', 'vec4', 'mat4').
|
||||
:return: Integer number of bytes.
|
||||
`
|
||||
|
||||
var sprite_model_ubo = {
|
||||
model: unit_transform,
|
||||
color: [1,1,1,1]
|
||||
};
|
||||
|
||||
var shader_cache = {};
|
||||
var shader_times = {};
|
||||
|
||||
function make_pipeline(pipeline) {
|
||||
if (pipeline.hasOwnProperty("gpu")) return; // this pipeline has already been made
|
||||
|
||||
if (typeof pipeline.vertex === 'string')
|
||||
pipeline.vertex = make_shader(pipeline.vertex);
|
||||
if (typeof pipeline.fragment === 'string')
|
||||
pipeline.fragment = make_shader(pipeline.fragment)
|
||||
|
||||
// 1) Reflection data for vertex shader
|
||||
var refl = pipeline.vertex.reflection
|
||||
if (!refl || !refl.inputs || !Array.isArray(refl.inputs)) {
|
||||
pipeline.gpu = render._main.make_pipeline(pipeline);
|
||||
return;
|
||||
}
|
||||
|
||||
var inputs = refl.inputs
|
||||
var buffer_descriptions = []
|
||||
var attributes = []
|
||||
|
||||
// 2) Build buffer + attribute for each reflection input
|
||||
for (var i = 0; i < inputs.length; i++) {
|
||||
var inp = inputs[i]
|
||||
var typeStr = inp.type
|
||||
var nameStr = (inp.name || "").toUpperCase()
|
||||
var pitch = 4
|
||||
var fmt = "float1"
|
||||
|
||||
if (typeStr == "vec2") {
|
||||
pitch = 8
|
||||
fmt = "float2"
|
||||
} else if (typeStr == "vec3") {
|
||||
pitch = 12
|
||||
fmt = "float3"
|
||||
} else if (typeStr == "vec4") {
|
||||
if (nameStr.indexOf("COLOR") >= 0) {
|
||||
pitch = 16
|
||||
fmt = "color"
|
||||
} else {
|
||||
pitch = 16
|
||||
fmt = "float4"
|
||||
}
|
||||
}
|
||||
|
||||
buffer_descriptions.push({
|
||||
slot: i,
|
||||
pitch: pitch,
|
||||
input_rate: "vertex",
|
||||
instance_step_rate: 0,
|
||||
name:inp.name.split(".").pop()
|
||||
})
|
||||
|
||||
attributes.push({
|
||||
location: inp.location,
|
||||
buffer_slot: i,
|
||||
format: fmt,
|
||||
offset: 0
|
||||
})
|
||||
}
|
||||
|
||||
pipeline.vertex_buffer_descriptions = buffer_descriptions
|
||||
pipeline.vertex_attributes = attributes
|
||||
|
||||
pipeline.gpu = render._main.make_pipeline(pipeline);
|
||||
}
|
||||
|
||||
make_pipeline[prosperon.DOC] = `Create and store a GPU pipeline object if it has not already been created.
|
||||
|
||||
:param pipeline: An object describing the pipeline state, shaders, and reflection data.
|
||||
:return: None
|
||||
`
|
||||
|
||||
var shader_type;
|
||||
|
||||
function make_shader(sh_file) {
|
||||
var file = `shaders/${shader_type}/${sh_file}.${shader_type}`
|
||||
if (shader_cache[file]) return shader_cache[file]
|
||||
var refl = json.decode(io.slurp(`shaders/reflection/${sh_file}.json`))
|
||||
|
||||
var shader = {
|
||||
code: io.slurpbytes(file),
|
||||
format: shader_type,
|
||||
stage: sh_file.endsWith("vert") ? "vertex" : "fragment",
|
||||
num_samplers: refl.separate_samplers ? refl.separate_samplers.length : 0,
|
||||
num_textures: 0,
|
||||
num_storage_buffers: refl.separate_storage_buffers ? refl.separate_storage_buffers.length : 0,
|
||||
num_uniform_buffers: refl.ubos ? refl.ubos.length : 0,
|
||||
entrypoint: shader_type === "msl" ? "main0" : "main"
|
||||
}
|
||||
|
||||
shader.gpu = render._main.make_shader(shader)
|
||||
shader.reflection = refl;
|
||||
shader_cache[file] = shader
|
||||
shader.file = sh_file
|
||||
return shader
|
||||
}
|
||||
|
||||
make_shader[prosperon.DOC] = `Load and compile a shader from disk, caching the result. Reflective metadata is also loaded.
|
||||
|
||||
:param sh_file: The base filename (without extension) of the shader to compile.
|
||||
:return: A shader object with GPU and reflection data attached.
|
||||
`
|
||||
|
||||
// helpful render devices. width and height in pixels; diagonal in inches.
|
||||
render.device = {
|
||||
pc: { width: 1920, height: 1080 },
|
||||
macbook_m2: { width: 2560, height: 1664, diagonal: 13.6 },
|
||||
ds_top: { width: 400, height: 240, diagonal: 3.53 },
|
||||
ds_bottom: { width: 320, height: 240, diagonal: 3.02 },
|
||||
playdate: { width: 400, height: 240, diagonal: 2.7 },
|
||||
switch: { width: 1280, height: 720, diagonal: 6.2 },
|
||||
switch_lite: { width: 1280, height: 720, diagonal: 5.5 },
|
||||
switch_oled: { width: 1280, height: 720, diagonal: 7 },
|
||||
dsi: { width: 256, height: 192, diagonal: 3.268 },
|
||||
ds: { width: 256, height: 192, diagonal: 3 },
|
||||
dsixl: { width: 256, height: 192, diagonal: 4.2 },
|
||||
ipad_air_m2: { width: 2360, height: 1640, diagonal: 11.97 },
|
||||
iphone_se: { width: 1334, height: 750, diagonal: 4.7 },
|
||||
iphone_12_pro: { width: 2532, height: 1170, diagonal: 6.06 },
|
||||
iphone_15: { width: 2556, height: 1179, diagonal: 6.1 },
|
||||
gba: { width: 240, height: 160, diagonal: 2.9 },
|
||||
gameboy: { width: 160, height: 144, diagonal: 2.48 },
|
||||
gbc: { width: 160, height: 144, diagonal: 2.28 },
|
||||
steamdeck: { width: 1280, height: 800, diagonal: 7 },
|
||||
vita: { width: 960, height: 544, diagonal: 5 },
|
||||
psp: { width: 480, height: 272, diagonal: 4.3 },
|
||||
imac_m3: { width: 4480, height: 2520, diagonal: 23.5 },
|
||||
macbook_pro_m3: { width: 3024, height: 1964, diagonal: 14.2 },
|
||||
ps1: { width: 320, height: 240, diagonal: 5 },
|
||||
ps2: { width: 640, height: 480 },
|
||||
snes: { width: 256, height: 224 },
|
||||
gamecube: { width: 640, height: 480 },
|
||||
n64: { width: 320, height: 240 },
|
||||
c64: { width: 320, height: 200 },
|
||||
macintosh: { width: 512, height: 342 },
|
||||
gamegear: { width: 160, height: 144, diagonal: 3.2 }
|
||||
};
|
||||
|
||||
render.device.doc = `Device resolutions given as [x,y,inches diagonal].`;
|
||||
|
||||
var render_queue = [];
|
||||
var hud_queue = [];
|
||||
|
||||
var current_queue = render_queue;
|
||||
|
||||
var std_sampler = {
|
||||
min_filter: "nearest",
|
||||
mag_filter: "nearest",
|
||||
mipmap: "linear",
|
||||
u: "repeat",
|
||||
v: "repeat",
|
||||
w: "repeat",
|
||||
mip_bias: 0,
|
||||
max_anisotropy: 0,
|
||||
compare_op: "none",
|
||||
min_lod: 0,
|
||||
max_lod: 0,
|
||||
anisotropy: false,
|
||||
compare: false
|
||||
};
|
||||
|
||||
function upload_model(model) {
|
||||
var bufs = [];
|
||||
for (var i in model) {
|
||||
if (typeof model[i] !== 'object') continue;
|
||||
bufs.push(model[i]);
|
||||
}
|
||||
render._main.upload(this, bufs);
|
||||
}
|
||||
|
||||
upload_model[prosperon.DOC] = `Upload all buffer-like properties of the given model to the GPU.
|
||||
|
||||
:param model: An object whose buffer properties are to be uploaded.
|
||||
:return: None
|
||||
`
|
||||
|
||||
function bind_model(pass, pipeline, model) {
|
||||
var buffers = pipeline.vertex_buffer_descriptions;
|
||||
var bufs = [];
|
||||
if (buffers)
|
||||
for (var b of buffers) {
|
||||
if (b.name in model) bufs.push(model[b.name])
|
||||
else throw Error (`could not find buffer ${b.name} on model`);
|
||||
}
|
||||
pass.bind_buffers(0,bufs);
|
||||
pass.bind_index_buffer(model.indices);
|
||||
}
|
||||
|
||||
bind_model[prosperon.DOC] = `Bind the model's vertex and index buffers for the given pipeline and render pass.
|
||||
|
||||
:param pass: The current render pass.
|
||||
:param pipeline: The pipeline object with vertex buffer descriptions.
|
||||
:param model: The model object containing matching buffers and an index buffer.
|
||||
:return: None
|
||||
`
|
||||
|
||||
function bind_mat(pass, pipeline, mat) {
|
||||
var imgs = [];
|
||||
var refl = pipeline.fragment.reflection;
|
||||
if (refl.separate_images) {
|
||||
for (var i of refl.separate_images) {
|
||||
if (i.name in mat) {
|
||||
var tex = mat[i.name];
|
||||
imgs.push({texture:tex.texture, sampler:tex.sampler});
|
||||
} else
|
||||
throw Error (`could not find all necessary images: ${i.name}`)
|
||||
}
|
||||
pass.bind_samplers(false, 0,imgs);
|
||||
}
|
||||
}
|
||||
|
||||
bind_mat[prosperon.DOC] = `Bind the material images and samplers needed by the pipeline's fragment shader.
|
||||
|
||||
:param pass: The current render pass.
|
||||
:param pipeline: The pipeline whose fragment shader reflection indicates required textures.
|
||||
:param mat: An object mapping the required image names to {texture, sampler}.
|
||||
:return: None
|
||||
`
|
||||
|
||||
function group_sprites_by_texture(sprites, mesh) {
|
||||
if (sprites.length === 0) return;
|
||||
for (var i = 0; i < sprites.length; i++) {
|
||||
sprites[i].mesh = mesh;
|
||||
sprites[i].first_index = i*6;
|
||||
sprites[i].num_indices = 6;
|
||||
}
|
||||
return;
|
||||
// The code below is an alternate approach to grouping by image. Currently not in use.
|
||||
/*
|
||||
var groups = [];
|
||||
var group = {image:sprites[0].image, first_index:0};
|
||||
var count = 1;
|
||||
for (var i = 1; i < sprites.length; i++) {
|
||||
if (sprites[i].image === group.image) {
|
||||
count++;
|
||||
continue;
|
||||
}
|
||||
group.num_indices = count*6;
|
||||
var newgroup = {image:sprites[i].image, first_index:group.first_index+group.num_indices};
|
||||
group = newgroup;
|
||||
groups.push(group);
|
||||
count=1;
|
||||
}
|
||||
group.num_indices = count*6;
|
||||
return groups;
|
||||
*/
|
||||
}
|
||||
|
||||
group_sprites_by_texture[prosperon.DOC] = `Assign each sprite to the provided mesh, generating index data as needed.
|
||||
|
||||
:param sprites: An array of sprite objects.
|
||||
:param mesh: A mesh object (pos, color, uv, indices, etc.) to link to each sprite.
|
||||
:return: None
|
||||
`
|
||||
|
||||
var main_color = {
|
||||
type:"2d",
|
||||
format: "rgba8",
|
||||
layers: 1,
|
||||
mip_levels: 1,
|
||||
samples: 0,
|
||||
sampler:true,
|
||||
color_target:true
|
||||
};
|
||||
|
||||
var main_depth = {
|
||||
type: "2d",
|
||||
format: "d32 float s8",
|
||||
layers:1,
|
||||
mip_levels:1,
|
||||
samples:0,
|
||||
sampler:true,
|
||||
depth_target:true
|
||||
};
|
||||
|
||||
function render_camera(cmds, camera) {
|
||||
var pass;
|
||||
delete camera.target // TODO: HORRIBLE
|
||||
if (!camera.target) {
|
||||
main_color.width = main_depth.width = camera.size.x;
|
||||
main_color.height = main_depth.height = camera.size.y;
|
||||
camera.target = {
|
||||
color_targets: [{
|
||||
texture: render._main.texture(main_color),
|
||||
mip_level:0,
|
||||
layer: 0,
|
||||
load:"clear",
|
||||
store:"store",
|
||||
clear: cornflower
|
||||
}],
|
||||
depth_stencil: {
|
||||
texture: render._main.texture(main_depth),
|
||||
clear:1,
|
||||
load:"dont_care",
|
||||
store:"dont_care",
|
||||
stencil_load:"dont_care",
|
||||
stencil_store:"dont_care",
|
||||
stencil_clear:0
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
var buffers = [];
|
||||
buffers = buffers.concat(graphics.queue_sprite_mesh(render_queue));
|
||||
var unique_meshes = [...new Set(render_queue.map(x => x.mesh))];
|
||||
for (var q of unique_meshes)
|
||||
buffers = buffers.concat([q.pos, q.color, q.uv, q.indices]);
|
||||
|
||||
buffers = buffers.concat(graphics.queue_sprite_mesh(hud_queue));
|
||||
for (var q of hud_queue)
|
||||
if (q.type === 'geometry') buffers = buffers.concat([q.mesh.pos, q.mesh.color, q.mesh.uv, q.mesh.indices]);
|
||||
|
||||
full_upload(buffers)
|
||||
|
||||
var pass = cmds.render_pass(camera.target);
|
||||
|
||||
var pipeline = sprite_pipeline;
|
||||
bind_pipeline(pass,pipeline);
|
||||
|
||||
var camslot = get_pipeline_ubo_slot(pipeline, 'TransformBuffer');
|
||||
if (typeof camslot !== 'undefined')
|
||||
cmds.camera(camera, camslot);
|
||||
|
||||
modelslot = get_pipeline_ubo_slot(pipeline, "model");
|
||||
if (typeof modelslot !== 'undefined') {
|
||||
var ubo = ubo_obj_to_array(pipeline, 'model', sprite_model_ubo);
|
||||
cmds.push_vertex_uniform_data(modelslot, ubo);
|
||||
}
|
||||
|
||||
var mesh;
|
||||
var img;
|
||||
var modelslot;
|
||||
|
||||
cmds.push_debug_group("draw")
|
||||
for (var group of render_queue) {
|
||||
if (mesh != group.mesh) {
|
||||
mesh = group.mesh;
|
||||
bind_model(pass,pipeline,mesh);
|
||||
}
|
||||
|
||||
if (group.image && img != group.image) {
|
||||
img = group.image;
|
||||
img.sampler = std_sampler;
|
||||
bind_mat(pass,pipeline,{diffuse:img});
|
||||
}
|
||||
|
||||
pass.draw_indexed(group.num_indices, 1, group.first_index, 0, 0);
|
||||
}
|
||||
cmds.pop_debug_group()
|
||||
|
||||
cmds.push_debug_group("hud")
|
||||
var camslot = get_pipeline_ubo_slot(pipeline, 'TransformBuffer');
|
||||
if (typeof camslot !== 'undefined')
|
||||
cmds.hud(camera.size, camslot);
|
||||
|
||||
for (var group of hud_queue) {
|
||||
if (mesh != group.mesh) {
|
||||
mesh = group.mesh;
|
||||
bind_model(pass,pipeline,mesh);
|
||||
}
|
||||
|
||||
if (group.image && img != group.image) {
|
||||
img = group.image;
|
||||
img.sampler = std_sampler;
|
||||
bind_mat(pass,pipeline,{diffuse:img});
|
||||
}
|
||||
|
||||
pass.draw_indexed(group.num_indices, 1, group.first_index, 0, 0);
|
||||
}
|
||||
cmds.pop_debug_group();
|
||||
|
||||
pass?.end();
|
||||
|
||||
render_queue = [];
|
||||
hud_queue = [];
|
||||
}
|
||||
|
||||
render_camera[prosperon.DOC] = `Render a scene using the provided camera, drawing both render queue and HUD queue items.
|
||||
|
||||
:param cmds: A command buffer obtained from the GPU context.
|
||||
:param camera: The camera object (with size, optional target, etc.).
|
||||
:return: None
|
||||
`
|
||||
|
||||
var swaps = [];
|
||||
function gpupresent() {
|
||||
os.clean_transforms();
|
||||
var cmds = render._main.acquire_cmd_buffer();
|
||||
render_camera(cmds, prosperon.camera);
|
||||
var swapchain_tex = cmds.acquire_swapchain();
|
||||
if (!swapchain_tex)
|
||||
cmds.cancel();
|
||||
else {
|
||||
var torect = prosperon.camera.draw_rect(prosperon.window.size);
|
||||
torect.texture = swapchain_tex;
|
||||
if (swapchain_tex) {
|
||||
cmds.blit({
|
||||
src: prosperon.camera.target.color_targets[0].texture,
|
||||
dst: torect,
|
||||
filter:"nearest",
|
||||
load: "clear"
|
||||
});
|
||||
|
||||
if (imgui) { // draws any imgui commands present
|
||||
cmds.push_debug_group("imgui")
|
||||
imgui.prepend(cmds);
|
||||
var pass = cmds.render_pass({
|
||||
color_targets:[{texture:swapchain_tex}]});
|
||||
imgui.endframe(cmds,pass);
|
||||
pass.end();
|
||||
cmds.pop_debug_group()
|
||||
}
|
||||
}
|
||||
cmds.submit()
|
||||
}
|
||||
}
|
||||
|
||||
gpupresent[prosperon.DOC] = `Perform the per-frame rendering and present the final swapchain image, including imgui pass if available.
|
||||
|
||||
:return: None
|
||||
`
|
||||
|
||||
var stencil_write = {
|
||||
compare: "always",
|
||||
fail_op: "replace",
|
||||
depth_fail_op: "replace",
|
||||
pass_op: "replace"
|
||||
};
|
||||
|
||||
var stencil_writer = function stencil_writer(ref) {
|
||||
var pipe = Object.create(base_pipeline);
|
||||
Object.assign(pipe, {
|
||||
stencil: {
|
||||
enabled: true,
|
||||
front: stencil_write,
|
||||
back: stencil_write,
|
||||
write:true,
|
||||
read:true,
|
||||
ref:ref
|
||||
},
|
||||
write_mask: colormask.none
|
||||
});
|
||||
return pipe;
|
||||
}.hashify();
|
||||
|
||||
render.stencil_writer = stencil_writer;
|
||||
|
||||
// objects by default draw where the stencil buffer is 0
|
||||
render.fillmask = function fillmask(ref) {
|
||||
var pipe = stencil_writer(ref);
|
||||
render.use_shader('screenfill.cg', pipe);
|
||||
render.draw(shape.quad);
|
||||
}
|
||||
|
||||
render.fillmask[prosperon.DOC] = `Draw a fullscreen shape using a 'screenfill' shader to populate the stencil buffer with a given reference.
|
||||
|
||||
:param ref: The stencil reference value to write.
|
||||
:return: None
|
||||
`
|
||||
|
||||
var stencil_invert = {
|
||||
compare: "always",
|
||||
fail_op: "invert",
|
||||
depth_fail_op: "invert",
|
||||
pass_op: "invert"
|
||||
};
|
||||
|
||||
render.mask = function mask(image, pos, scale, rotation = 0, ref = 1) {
|
||||
if (typeof image === 'string')
|
||||
image = graphics.texture(image);
|
||||
|
||||
var tex = image.texture;
|
||||
if (scale) scale = scale.div([tex.width,tex.height]);
|
||||
else scale = [1,1,1]
|
||||
|
||||
var pipe = stencil_writer(ref);
|
||||
render.use_shader('sprite.cg', pipe);
|
||||
var t = os.make_transform();
|
||||
t.trs(pos, undefined, scale);
|
||||
set_model(t);
|
||||
render.use_mat({
|
||||
diffuse:image.texture,
|
||||
rect: image.rect,
|
||||
shade: Color.white
|
||||
});
|
||||
render.draw(shape.quad);
|
||||
}
|
||||
|
||||
render.mask[prosperon.DOC] = `Draw an image to the stencil buffer, marking its area with a specified reference value.
|
||||
|
||||
:param image: A texture or string path (which is converted to a texture).
|
||||
:param pos: The translation (x, y) for the image placement.
|
||||
:param scale: Optional scaling applied to the texture.
|
||||
:param rotation: Optional rotation in radians (unused by default).
|
||||
:param ref: The stencil reference value to write.
|
||||
:return: None
|
||||
`
|
||||
|
||||
render.viewport = function(rect) {
|
||||
render._main.viewport(rect);
|
||||
}
|
||||
|
||||
render.viewport[prosperon.DOC] = `Set the GPU viewport to the specified rectangle.
|
||||
|
||||
:param rect: A rectangle [x, y, width, height].
|
||||
:return: None
|
||||
`
|
||||
|
||||
render.scissor = function(rect) {
|
||||
render.viewport(rect)
|
||||
}
|
||||
|
||||
render.scissor[prosperon.DOC] = `Set the GPU scissor region to the specified rectangle (alias of render.viewport).
|
||||
|
||||
:param rect: A rectangle [x, y, width, height].
|
||||
:return: None
|
||||
`
|
||||
|
||||
// Some initialization
|
||||
shader_type = render._main.shader_format()[0];
|
||||
|
||||
std_sampler = render._main.make_sampler({
|
||||
min_filter: "nearest",
|
||||
mag_filter: "nearest",
|
||||
mipmap_mode: "nearest",
|
||||
address_mode_u: "repeat",
|
||||
address_mode_v: "repeat",
|
||||
address_mode_w: "repeat"
|
||||
});
|
||||
|
||||
render._main.present = gpupresent;
|
||||
|
||||
if (tracy) tracy.gpu_init()
|
||||
|
||||
render.queue = function(cmd) {
|
||||
if (Array.isArray(cmd))
|
||||
for (var i of cmd) current_queue.push(i)
|
||||
else
|
||||
current_queue.push(cmd)
|
||||
}
|
||||
|
||||
render.queue[prosperon.DOC] = `Enqueue one or more draw commands. These commands are batched until render_camera is called.
|
||||
|
||||
:param cmd: Either a single command object or an array of command objects.
|
||||
:return: None
|
||||
`
|
||||
|
||||
render.setup_draw = function() {
|
||||
current_queue = render_queue;
|
||||
prosperon.draw();
|
||||
}
|
||||
|
||||
render.setup_draw[prosperon.DOC] = `Switch the current queue to the primary scene render queue, then invoke 'prosperon.draw' if defined.
|
||||
|
||||
:return: None
|
||||
`
|
||||
|
||||
render.setup_hud = function() {
|
||||
current_queue = hud_queue;
|
||||
prosperon.hud();
|
||||
}
|
||||
|
||||
render.setup_hud[prosperon.DOC] = `Switch the current queue to the HUD render queue, then invoke 'prosperon.hud' if defined.
|
||||
|
||||
:return: None
|
||||
`
|
||||
|
||||
return render
|
||||
|
||||
@@ -1,19 +1,6 @@
|
||||
var io = use_embed('io');
|
||||
var miniz = use_embed('miniz');
|
||||
var os = use_embed('os');
|
||||
|
||||
Object.defineProperty(Function.prototype, "hashify", {
|
||||
value: function () {
|
||||
var hash = new Map()
|
||||
var fn = this
|
||||
function hashified(...args) {
|
||||
var key = args[0]
|
||||
if (!hash.has(key)) hash.set(key, fn(...args))
|
||||
return hash.get(key)
|
||||
}
|
||||
return hashified
|
||||
},
|
||||
})
|
||||
var io = use_embed('io')
|
||||
var miniz = use_embed('miniz')
|
||||
var os = use_embed('os')
|
||||
|
||||
// Merge of the old resources.js and packer.js functionalities
|
||||
var Resources = {}
|
||||
@@ -54,9 +41,9 @@ function isRecognizedExtension(ext) {
|
||||
return false
|
||||
}
|
||||
|
||||
// Attempt to find file with or without extension from the current PATH
|
||||
// (From the original resources.js, unchanged except for code style)
|
||||
function find_in_path(filename, exts = []) {
|
||||
if (typeof filename !== 'string') return undefined
|
||||
|
||||
if (filename.includes('.')) {
|
||||
for (var dir of prosperon.PATH) {
|
||||
var candidate = dir + filename
|
||||
@@ -66,15 +53,10 @@ function find_in_path(filename, exts = []) {
|
||||
}
|
||||
|
||||
for (var dir of prosperon.PATH) {
|
||||
// Only check extensions if exts is provided and not empty
|
||||
if (exts.length > 0) {
|
||||
for (var ext of exts) {
|
||||
var candidate = dir + filename + '.' + ext
|
||||
if (io.exists(candidate) && !io.is_directory(candidate)) return candidate
|
||||
}
|
||||
} else {
|
||||
// Fallback to extensionless file only if no extensions are specified
|
||||
var candidate = dir + filename
|
||||
var candidate = dir + filename
|
||||
if (io.exists(candidate) && !io.is_directory(candidate)) return candidate
|
||||
for (var ext of exts) {
|
||||
candidate = dir + filename + '.' + ext
|
||||
if (io.exists(candidate) && !io.is_directory(candidate)) return candidate
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,397 +0,0 @@
|
||||
var ex = this
|
||||
|
||||
var input = use('input')
|
||||
|
||||
var DEAD = Symbol()
|
||||
var GARBAGE = Symbol()
|
||||
var FILE = Symbol()
|
||||
var TIMERS = Symbol()
|
||||
var REGGIES = Symbol()
|
||||
var UNDERLINGS = Symbol()
|
||||
var OVERLING = Symbol()
|
||||
|
||||
function add_timer(obj, fn, seconds) {
|
||||
var timers = obj[TIMERS]
|
||||
|
||||
var stop = function () {
|
||||
if (!timer) return
|
||||
timers.delete(stop)
|
||||
timer.fn = undefined
|
||||
timer = undefined
|
||||
}
|
||||
|
||||
function execute() {
|
||||
if (fn) timer.remain = fn(stop.seconds)
|
||||
if (!timer) return
|
||||
if (!timer.remain) stop()
|
||||
else stop.seconds = timer.remain
|
||||
}
|
||||
|
||||
// var timer = os.make_timer(execute)
|
||||
timer.remain = seconds
|
||||
|
||||
stop.remain = seconds
|
||||
stop.seconds = seconds
|
||||
|
||||
timers.push(stop)
|
||||
return stop
|
||||
}
|
||||
|
||||
globalThis.Register = {
|
||||
registries: [],
|
||||
|
||||
add_cb(name) {
|
||||
var n = {}
|
||||
var fns = []
|
||||
|
||||
n.register = function (fn, oname) {
|
||||
if (typeof fn !== 'function') return
|
||||
|
||||
var dofn = function (...args) {
|
||||
fn(...args)
|
||||
}
|
||||
Object.defineProperty(dofn, 'name', {value:`do_${oname}`})
|
||||
|
||||
var left = 0
|
||||
var right = fns.length - 1
|
||||
dofn.layer = fn.layer
|
||||
dofn.layer ??= 0
|
||||
|
||||
while (left <= right) {
|
||||
var mid = Math.floor((left + right) / 2)
|
||||
if (fns[mid] === dofn.layer) {
|
||||
left = mid
|
||||
break
|
||||
} else if (fns[mid].layer < dofn.layer) left = mid + 1
|
||||
else right = mid - 1
|
||||
}
|
||||
|
||||
fns.splice(left, 0, dofn)
|
||||
return function () {
|
||||
fns.delete(dofn)
|
||||
}
|
||||
}
|
||||
|
||||
prosperon[name] = function (...args) {
|
||||
fns.forEach(fn => {
|
||||
fn(...args)
|
||||
})
|
||||
}
|
||||
Object.defineProperty(prosperon[name], 'name', {value:name})
|
||||
prosperon[name].fns = fns
|
||||
|
||||
n.clear = function () {
|
||||
fns = []
|
||||
}
|
||||
|
||||
Register[name] = n
|
||||
Register.registries[name] = n
|
||||
return n
|
||||
},
|
||||
}
|
||||
|
||||
Register.pull_registers = function pull_registers(obj) {
|
||||
var reggies = []
|
||||
for (var reg in Register.registries) {
|
||||
if (typeof obj[reg] === "function")
|
||||
reggies.push(reg)
|
||||
}
|
||||
return reggies
|
||||
}
|
||||
|
||||
Register.register_obj = function register_obj(obj, reg) {
|
||||
var fn = obj[reg].bind(obj)
|
||||
fn.layer = obj[reg].layer
|
||||
var name = obj.ur ? obj.ur.name : obj.toString()
|
||||
obj[TIMERS].push(Register.registries[reg].register(fn, name))
|
||||
if (!obj[reg].name) Object.defineProperty(obj[reg], 'name', {value:`${obj._file}_${reg}`})
|
||||
}
|
||||
|
||||
Register.check_registers = function check_registers(obj) {
|
||||
if (obj[REGGIES]) {
|
||||
if (obj[REGGIES].length == 0) return
|
||||
for (var reg of obj[REGGIES])
|
||||
Register.register_obj(obj,reg)
|
||||
return
|
||||
}
|
||||
for (var reg in Register.registries) {
|
||||
if (typeof obj[reg] === "function")
|
||||
Register.register_obj(obj,reg)
|
||||
}
|
||||
}
|
||||
|
||||
Register.add_cb("appupdate")
|
||||
Register.add_cb("update").doc = "Called once per frame."
|
||||
Register.add_cb("physupdate")
|
||||
Register.add_cb("gui")
|
||||
Register.add_cb("hud")
|
||||
Register.add_cb("draw")
|
||||
Register.add_cb("imgui")
|
||||
Register.add_cb("app")
|
||||
|
||||
|
||||
var actor = {}
|
||||
|
||||
actor.toString = function() { return this[FILE] }
|
||||
|
||||
actor.spawn = function spawn(script, config, actor_context) {
|
||||
if (this[DEAD]) throw new Error("Attempting to spawn on a dead actor")
|
||||
var prog
|
||||
if (!script) {
|
||||
prog = {}
|
||||
prog.module_ret = {}
|
||||
prog.prog_fn = function() {}
|
||||
} else {
|
||||
prog = script_fn(script)
|
||||
if (!prog.prog_fn) throw new Error(`Script ${script} is not an actor script or has no actor component`)
|
||||
}
|
||||
|
||||
var underling
|
||||
prog.module_ret.__proto__ = actor
|
||||
underling = Object.create(prog.module_ret)
|
||||
underling[OVERLING] = this
|
||||
underling[FILE] = script
|
||||
underling[TIMERS] = []
|
||||
underling[UNDERLINGS] = new Set()
|
||||
|
||||
// Make $_ available to the actor (either passed context or the engine's $_)
|
||||
var actor_dollar = actor_context || $_
|
||||
Object.defineProperty(underling, '$_', {
|
||||
value: actor_dollar,
|
||||
writable:false,
|
||||
enumerable:false,
|
||||
configurable:false
|
||||
})
|
||||
|
||||
Object.defineProperty(underling, 'overling', {
|
||||
get() { return this[OVERLING] },
|
||||
enumerable:true,
|
||||
configurable:false
|
||||
})
|
||||
|
||||
Object.defineProperty(underling, 'underlings', {
|
||||
get() { return new Set(this[UNDERLINGS]) },
|
||||
enumerable:true,
|
||||
configurable:false
|
||||
})
|
||||
|
||||
Object.defineProperty(underling, 'spawn', {
|
||||
value: function(script, config) {
|
||||
return actor.spawn.call(this, script, config, actor_dollar)
|
||||
},
|
||||
writable:false,
|
||||
enumerable:true,
|
||||
configurable:false
|
||||
})
|
||||
|
||||
Object.defineProperty(underling, 'kill', {
|
||||
value: actor.kill,
|
||||
writable:false,
|
||||
enumerable:true,
|
||||
configurable:false
|
||||
})
|
||||
|
||||
Object.defineProperty(underling, 'delay', {
|
||||
value: actor.delay,
|
||||
writable:false,
|
||||
enumerable:true,
|
||||
configurable:false
|
||||
})
|
||||
|
||||
try {
|
||||
// Pass $_ as a parameter to actor scripts
|
||||
prog.prog_fn.call(underling, actor_dollar)
|
||||
} catch(e) { throw e; }
|
||||
|
||||
if (underling[DEAD]) return undefined;
|
||||
|
||||
if (typeof config === 'object') Object.assign(underling, config)
|
||||
|
||||
if (!underling[REGGIES])
|
||||
underling.__proto__[REGGIES] = Register.pull_registers(underling)
|
||||
|
||||
Register.check_registers(underling)
|
||||
if (underling.awake) underling.awake()
|
||||
|
||||
this[UNDERLINGS].add(underling)
|
||||
if (underling.tag) act.tag_add(underling.tag, underling)
|
||||
|
||||
underling[GARBAGE] = underling.garbage
|
||||
return underling
|
||||
}
|
||||
|
||||
actor.clear = function actor_clear() {
|
||||
this[UNDERLINGS].forEach(p => {
|
||||
p.kill()
|
||||
})
|
||||
this[UNDERLINGS].clear()
|
||||
}
|
||||
|
||||
actor.kill = function kill() {
|
||||
if (this[DEAD]) return
|
||||
this[DEAD] = true
|
||||
this[TIMERS].slice().forEach(t => t())
|
||||
delete this[TIMERS]
|
||||
input.do_uncontrol(this)
|
||||
|
||||
this.clear()
|
||||
this[OVERLING][UNDERLINGS].delete(this)
|
||||
delete this[UNDERLINGS]
|
||||
|
||||
if (typeof this.garbage === "function") this.garbage()
|
||||
if (typeof this.then === "function") this.then()
|
||||
|
||||
act.tag_clear_guid(this)
|
||||
}
|
||||
actor.kill.doc = `Remove this actor and all its underlings from existence.`
|
||||
|
||||
actor.delay = function(fn, seconds) {
|
||||
if (this[DEAD]) return
|
||||
add_timer(this, fn, seconds)
|
||||
}
|
||||
actor.delay.doc = `Call 'fn' after 'seconds' with 'this' set to the actor.`
|
||||
|
||||
actor[UNDERLINGS] = new Set()
|
||||
|
||||
|
||||
ex[prosperon.DOC] = `
|
||||
A set of utilities for iterating over a hierarchy of actor-like objects, as well
|
||||
as managing tag-based lookups. Objects are assumed to have a "objects" property,
|
||||
pointing to children or sub-objects, forming a tree.
|
||||
`
|
||||
|
||||
function eachobj(obj, fn) {
|
||||
var val = fn(obj)
|
||||
if (val) return val
|
||||
for (var o in obj.objects) {
|
||||
if (obj.objects[o] === obj) console.error(`Object ${obj.toString()} is referenced by itself.`)
|
||||
val = eachobj(obj.objects[o], fn)
|
||||
if (val) return val
|
||||
}
|
||||
}
|
||||
|
||||
ex.all_objects = function (fn, startobj = world) {
|
||||
return eachobj(startobj, fn)
|
||||
}
|
||||
ex.all_objects[prosperon.DOC] = `
|
||||
:param fn: A callback function that receives each object. If it returns a truthy value, iteration stops and that value is returned.
|
||||
:param startobj: The root object at which iteration begins, default is the global "world".
|
||||
:return: The first truthy value returned by fn, or undefined if none.
|
||||
Iterate over each object (and its sub-objects) in the hierarchy, calling fn for each one.
|
||||
`
|
||||
|
||||
ex.find_object = function (fn, startobj = world) {}
|
||||
ex.find_object[prosperon.DOC] = `
|
||||
:param fn: A callback or criteria to locate a particular object.
|
||||
:param startobj: The root object at which search begins, default "world".
|
||||
:return: Not yet implemented.
|
||||
Intended to find a matching object within the hierarchy.
|
||||
`
|
||||
|
||||
var gtags = {}
|
||||
|
||||
ex.tag_add = function (tag, obj) {
|
||||
gtags[tag] ??= new Set()
|
||||
gtags[tag].add(obj)
|
||||
}
|
||||
ex.tag_add[prosperon.DOC] = `
|
||||
:param tag: A string tag to associate with the object.
|
||||
:param obj: The object to add under this tag.
|
||||
:return: None
|
||||
Associate the given object with the specified tag. Creates a new tag set if it does not exist.
|
||||
`
|
||||
|
||||
ex.tag_rm = function (tag, obj) {
|
||||
delete gtags[tag].delete(obj)
|
||||
}
|
||||
ex.tag_rm[prosperon.DOC] = `
|
||||
:param tag: The tag to remove the object from.
|
||||
:param obj: The object to remove from the tag set.
|
||||
:return: None
|
||||
Remove the given object from the specified tag’s set, if it exists.
|
||||
`
|
||||
|
||||
ex.tag_clear_guid = function (obj) {
|
||||
for (var tag in gtags) gtags[tag].delete(obj)
|
||||
}
|
||||
ex.tag_clear_guid[prosperon.DOC] = `
|
||||
:param obj: The object whose tags should be cleared.
|
||||
:return: None
|
||||
Remove the object from all tag sets.
|
||||
`
|
||||
|
||||
ex.objects_with_tag = function (tag) {
|
||||
if (!gtags[tag]) return []
|
||||
return Array.from(gtags[tag])
|
||||
}
|
||||
ex.objects_with_tag[prosperon.DOC] = `
|
||||
:param tag: A string tag to look up.
|
||||
:return: An array of objects associated with the given tag.
|
||||
Retrieve all objects currently tagged with the specified tag.
|
||||
`
|
||||
|
||||
function parse_file(content, file) {
|
||||
if (!content) return {}
|
||||
if (content.match()
|
||||
if (!/^\s*---\s*$/m.test(content)) {
|
||||
var part = content.trim()
|
||||
if (part.match(/return\s+[^;]+;?\s*$/)) {
|
||||
return { module: part }
|
||||
}
|
||||
return { program: part }
|
||||
}
|
||||
var parts = content.split(/\n\s*---\s*\n/)
|
||||
var module = parts[0]
|
||||
if (!/\breturn\b/.test(module))
|
||||
throw new Error(`Malformed file: ${file}. Module section must end with a return statement.`)
|
||||
|
||||
try {
|
||||
new Function(module)()
|
||||
} catch (e) {
|
||||
throw new Error(`Malformed file: ${file}. Module section must end with a return statement.\n` + e.message)
|
||||
}
|
||||
|
||||
var pad = '\n'.repeat(module.split('\n').length + 4)
|
||||
return {
|
||||
module,
|
||||
program: pad + parts[1]
|
||||
}
|
||||
}
|
||||
|
||||
// path is the path of a module or script to resolve
|
||||
var script_fn = function script_fn(path, args) {
|
||||
var parsed = {}
|
||||
var file = resources.find_script(path)
|
||||
|
||||
if (!file) {
|
||||
parsed.module_ret = bare_load(path)
|
||||
if (!parsed.module_ret) throw new Error(`Module ${path} could not be created`)
|
||||
return parsed
|
||||
}
|
||||
|
||||
var content = io.slurp(file)
|
||||
var parsed = parse_file(content, file)
|
||||
var module_name = file.name()
|
||||
parsed.module_ret = bare_load(path)
|
||||
parsed.module_ret ??= {}
|
||||
|
||||
if (parsed.module) {
|
||||
// Create a context object with args
|
||||
var context = Object.create(parsed.module_ret)
|
||||
context.__args__ = args || []
|
||||
var mod_script = `(function setup_${module_name}_module(){ var self = this; var $ = this; var exports = {}; var module = {exports: exports}; var define = undefined; var arg = this.__args__; ${parsed.module}})`
|
||||
var module_fn = js.eval(file, mod_script)
|
||||
parsed.module_ret = module_fn.call(context)
|
||||
if (parsed.module_ret === undefined || parsed.module_ret === null)
|
||||
throw new Error(`Module ${module_name} must return a value`)
|
||||
parsed.module_fn = module_fn
|
||||
}
|
||||
|
||||
parsed.program ??= ""
|
||||
var prog_script = `(function use_${module_name}($_) { var self = this; var $ = this.__proto__; ${parsed.program}})`
|
||||
parsed.prog_fn = js.eval(file, prog_script)
|
||||
|
||||
return parsed
|
||||
}.hashify()
|
||||
|
||||
return ex
|
||||
@@ -1,792 +0,0 @@
|
||||
var render = {}
|
||||
|
||||
var io = use('io')
|
||||
var os = use('os')
|
||||
var controller = use('controller')
|
||||
var tracy = use('tracy')
|
||||
var graphics = use('graphics')
|
||||
var imgui = use('imgui')
|
||||
var transform = use('transform')
|
||||
|
||||
var base_pipeline = {
|
||||
vertex: "sprite.vert",
|
||||
fragment: "sprite.frag",
|
||||
primitive: "triangle", // point, line, linestrip, triangle, trianglestrip
|
||||
fill: true, // false for lines
|
||||
depth: {
|
||||
compare: "greater_equal", // never/less/equal/less_equal/greater/not_equal/greater_equal/always
|
||||
test: false,
|
||||
write: false,
|
||||
bias: 0,
|
||||
bias_slope_scale: 0,
|
||||
bias_clamp: 0
|
||||
},
|
||||
stencil: {
|
||||
enabled: true,
|
||||
front: {
|
||||
compare: "equal", // never/less/equal/less_equal/greater/neq/greq/always
|
||||
fail: "keep", // keep/zero/replace/incr_clamp/decr_clamp/invert/incr_wrap/decr_wrap
|
||||
depth_fail: "keep",
|
||||
pass: "keep"
|
||||
},
|
||||
back: {
|
||||
compare: "equal", // never/less/equal/less_equal/greater/neq/greq/always
|
||||
fail: "keep", // keep/zero/replace/incr_clamp/decr_clamp/invert/incr_wrap/decr_wrap
|
||||
depth_fail: "keep",
|
||||
pass: "keep"
|
||||
},
|
||||
test: true,
|
||||
compare_mask: 0,
|
||||
write_mask: 0
|
||||
},
|
||||
blend: {
|
||||
enabled: false,
|
||||
src_rgb: "zero", // zero/one/src_color/one_minus_src_color/dst_color/one_minus_dst_color/src_alpha/one_minus_src_alpha/dst_alpha/one_minus_dst_alpha/constant_color/one_minus_constant_color/src_alpha_saturate
|
||||
dst_rgb: "zero",
|
||||
op_rgb: "add", // add/sub/rev_sub/min/max
|
||||
src_alpha: "one",
|
||||
dst_alpha: "zero",
|
||||
op_alpha: "add"
|
||||
},
|
||||
cull: "none", // none/front/back
|
||||
face: "cw", // cw/ccw
|
||||
alpha_to_coverage: false,
|
||||
multisample: {
|
||||
count: 1, // number of multisamples
|
||||
mask: 0xFFFFFFFF,
|
||||
domask: false
|
||||
},
|
||||
label: "scripted pipeline",
|
||||
target: {}
|
||||
}
|
||||
|
||||
var sprite_pipeline = Object.create(base_pipeline);
|
||||
sprite_pipeline.blend = {
|
||||
enabled:true,
|
||||
src_rgb: "src_alpha", // zero/one/src_color/one_minus_src_color/dst_color/one_minus_dst_color/src_alpha/one_minus_src_alpha/dst_alpha/one_minus_dst_alpha/constant_color/one_minus_constant_color/src_alpha_saturate
|
||||
dst_rgb: "one_minus_src_alpha",
|
||||
op_rgb: "add", // add/sub/rev_sub/min/max
|
||||
src_alpha: "one",
|
||||
dst_alpha: "zero",
|
||||
op_alpha: "add"
|
||||
};
|
||||
|
||||
var context;
|
||||
|
||||
sprite_pipeline.target = {
|
||||
color_targets: [{
|
||||
format:"rgba8",
|
||||
blend:sprite_pipeline.blend
|
||||
}],
|
||||
depth: "d32 float s8"
|
||||
};
|
||||
|
||||
var driver = "vulkan"
|
||||
switch(os.platform()) {
|
||||
case "Linux":
|
||||
driver = "vulkan"
|
||||
break
|
||||
case "Windows":
|
||||
// driver = "direct3d12"
|
||||
driver = "vulkan"
|
||||
break
|
||||
case "macOS":
|
||||
driver = "metal"
|
||||
break
|
||||
}
|
||||
|
||||
var unit_transform = new transform;
|
||||
|
||||
var cur = {};
|
||||
cur.images = [];
|
||||
cur.samplers = [];
|
||||
|
||||
var tbuffer;
|
||||
function full_upload(buffers) {
|
||||
var cmds = context.acquire_cmd_buffer();
|
||||
tbuffer = context.upload(cmds, buffers, tbuffer);
|
||||
cmds.submit();
|
||||
}
|
||||
|
||||
full_upload[prosperon.DOC] = `Acquire a command buffer and upload the provided data buffers to the GPU, then submit.
|
||||
|
||||
:param buffers: An array of data buffers to be uploaded.
|
||||
:return: None
|
||||
`
|
||||
|
||||
function bind_pipeline(pass, pipeline) {
|
||||
make_pipeline(pipeline)
|
||||
pass.bind_pipeline(pipeline.gpu)
|
||||
pass.pipeline = pipeline;
|
||||
}
|
||||
|
||||
bind_pipeline[prosperon.DOC] = `Ensure the specified pipeline is created on the GPU and bind it to the given render pass.
|
||||
|
||||
:param pass: The current render pass to bind the pipeline to.
|
||||
:param pipeline: The pipeline object containing shader and state info.
|
||||
:return: None
|
||||
`
|
||||
|
||||
var main_pass;
|
||||
|
||||
var cornflower = [62/255,96/255,113/255,1];
|
||||
|
||||
function get_pipeline_ubo_slot(pipeline, name) {
|
||||
if (!pipeline.vertex.reflection.ubos) return;
|
||||
for (var i = 0; i < pipeline.vertex.reflection.ubos.length; i++) {
|
||||
var ubo = pipeline.vertex.reflection.ubos[i];
|
||||
if (ubo.name.endsWith(name))
|
||||
return i;
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
|
||||
get_pipeline_ubo_slot[prosperon.DOC] = `Return the index of a uniform buffer block within the pipeline's vertex reflection data by name suffix.
|
||||
|
||||
:param pipeline: The pipeline whose vertex reflection is inspected.
|
||||
:param name: A string suffix to match against the uniform buffer block name.
|
||||
:return: The integer index of the matching UBO, or undefined if not found.
|
||||
`
|
||||
|
||||
function transpose4x4(val) {
|
||||
var out = [];
|
||||
out[0] = val[0]; out[1] = val[4]; out[2] = val[8]; out[3] = val[12];
|
||||
out[4] = val[1]; out[5] = val[5]; out[6] = val[9]; out[7] = val[13];
|
||||
out[8] = val[2]; out[9] = val[6]; out[10] = val[10];out[11] = val[14];
|
||||
out[12] = val[3];out[13] = val[7];out[14] = val[11];out[15] = val[15];
|
||||
return out;
|
||||
}
|
||||
|
||||
transpose4x4[prosperon.DOC] = `Return a new 4x4 matrix array that is the transpose of the passed matrix.
|
||||
|
||||
:param val: An array of length 16 representing a 4x4 matrix in row-major format.
|
||||
:return: A new array of length 16 representing the transposed matrix.
|
||||
`
|
||||
|
||||
function ubo_obj_to_array(pipeline, name, obj) {
|
||||
var ubo;
|
||||
for (var i = 0; i < pipeline.vertex.reflection.ubos.length; i++) {
|
||||
ubo = pipeline.vertex.reflection.ubos[i];
|
||||
if (ubo.name.endsWith(name)) break;
|
||||
}
|
||||
var type = pipeline.vertex.reflection.types[ubo.type];
|
||||
var len = 0;
|
||||
for (var mem of type.members)
|
||||
len += type_to_byte_count(mem.type);
|
||||
|
||||
var buf = new ArrayBuffer(len);
|
||||
var view = new DataView(buf);
|
||||
|
||||
for (var mem of type.members) {
|
||||
var val = obj[mem.name];
|
||||
if (!val) throw new Error (`Could not find ${mem.name} on supplied object`);
|
||||
|
||||
if (mem.name === 'model')
|
||||
val = transpose4x4(val.array());
|
||||
|
||||
for (var i = 0; i < val.length; i++)
|
||||
view.setFloat32(mem.offset + i*4, val[i],true);
|
||||
}
|
||||
return buf;
|
||||
}
|
||||
|
||||
ubo_obj_to_array[prosperon.DOC] = `Construct an ArrayBuffer containing UBO data from the provided object, matching the pipeline's reflection info.
|
||||
|
||||
:param pipeline: The pipeline whose vertex reflection is read for UBO structure.
|
||||
:param name: The name suffix that identifies the target UBO in the reflection data.
|
||||
:param obj: An object whose properties match the UBO members.
|
||||
:return: An ArrayBuffer containing packed UBO data.
|
||||
`
|
||||
|
||||
function type_to_byte_count(type) {
|
||||
switch (type) {
|
||||
case 'float': return 4;
|
||||
case 'vec2': return 8;
|
||||
case 'vec3': return 12;
|
||||
case 'vec4': return 16;
|
||||
case 'mat4': return 64;
|
||||
default: throw new Error("Unknown or unsupported float-based type: " + type);
|
||||
}
|
||||
}
|
||||
|
||||
type_to_byte_count[prosperon.DOC] = `Return the byte size for known float-based types.
|
||||
|
||||
:param type: A string type identifier (e.g., 'float', 'vec2', 'vec3', 'vec4', 'mat4').
|
||||
:return: Integer number of bytes.
|
||||
`
|
||||
|
||||
var sprite_model_ubo = {
|
||||
model: unit_transform,
|
||||
color: [1,1,1,1]
|
||||
};
|
||||
|
||||
var shader_cache = {};
|
||||
var shader_times = {};
|
||||
|
||||
function make_pipeline(pipeline) {
|
||||
if (pipeline.hasOwnProperty("gpu")) return; // this pipeline has already been made
|
||||
|
||||
if (typeof pipeline.vertex === 'string')
|
||||
pipeline.vertex = make_shader(pipeline.vertex);
|
||||
if (typeof pipeline.fragment === 'string')
|
||||
pipeline.fragment = make_shader(pipeline.fragment)
|
||||
|
||||
// 1) Reflection data for vertex shader
|
||||
var refl = pipeline.vertex.reflection
|
||||
if (!refl || !refl.inputs || !Array.isArray(refl.inputs)) {
|
||||
pipeline.gpu = context.make_pipeline(pipeline);
|
||||
return;
|
||||
}
|
||||
|
||||
var inputs = refl.inputs
|
||||
var buffer_descriptions = []
|
||||
var attributes = []
|
||||
|
||||
// 2) Build buffer + attribute for each reflection input
|
||||
for (var i = 0; i < inputs.length; i++) {
|
||||
var inp = inputs[i]
|
||||
var typeStr = inp.type
|
||||
var nameStr = (inp.name || "").toUpperCase()
|
||||
var pitch = 4
|
||||
var fmt = "float1"
|
||||
|
||||
if (typeStr == "vec2") {
|
||||
pitch = 8
|
||||
fmt = "float2"
|
||||
} else if (typeStr == "vec3") {
|
||||
pitch = 12
|
||||
fmt = "float3"
|
||||
} else if (typeStr == "vec4") {
|
||||
if (nameStr.indexOf("COLOR") >= 0) {
|
||||
pitch = 16
|
||||
fmt = "color"
|
||||
} else {
|
||||
pitch = 16
|
||||
fmt = "float4"
|
||||
}
|
||||
}
|
||||
|
||||
buffer_descriptions.push({
|
||||
slot: i,
|
||||
pitch: pitch,
|
||||
input_rate: "vertex",
|
||||
instance_step_rate: 0,
|
||||
name:inp.name.split(".").pop()
|
||||
})
|
||||
|
||||
attributes.push({
|
||||
location: inp.location,
|
||||
buffer_slot: i,
|
||||
format: fmt,
|
||||
offset: 0
|
||||
})
|
||||
}
|
||||
|
||||
pipeline.vertex_buffer_descriptions = buffer_descriptions
|
||||
pipeline.vertex_attributes = attributes
|
||||
|
||||
pipeline.gpu = context.make_pipeline(pipeline);
|
||||
}
|
||||
|
||||
make_pipeline[prosperon.DOC] = `Create and store a GPU pipeline object if it has not already been created.
|
||||
|
||||
:param pipeline: An object describing the pipeline state, shaders, and reflection data.
|
||||
:return: None
|
||||
`
|
||||
|
||||
var shader_type;
|
||||
|
||||
function make_shader(sh_file) {
|
||||
var file = `shaders/${shader_type}/${sh_file}.${shader_type}`
|
||||
if (shader_cache[file]) return shader_cache[file]
|
||||
var refl = json.decode(io.slurp(`shaders/reflection/${sh_file}.json`))
|
||||
|
||||
var shader = {
|
||||
code: io.slurpbytes(file),
|
||||
format: shader_type,
|
||||
stage: sh_file.endsWith("vert") ? "vertex" : "fragment",
|
||||
num_samplers: refl.separate_samplers ? refl.separate_samplers.length : 0,
|
||||
num_textures: 0,
|
||||
num_storage_buffers: refl.separate_storage_buffers ? refl.separate_storage_buffers.length : 0,
|
||||
num_uniform_buffers: refl.ubos ? refl.ubos.length : 0,
|
||||
entrypoint: shader_type === "msl" ? "main0" : "main"
|
||||
}
|
||||
|
||||
shader.gpu = context.make_shader(shader)
|
||||
shader.reflection = refl;
|
||||
shader_cache[file] = shader
|
||||
shader.file = sh_file
|
||||
return shader
|
||||
}
|
||||
|
||||
make_shader[prosperon.DOC] = `Load and compile a shader from disk, caching the result. Reflective metadata is also loaded.
|
||||
|
||||
:param sh_file: The base filename (without extension) of the shader to compile.
|
||||
:return: A shader object with GPU and reflection data attached.
|
||||
`
|
||||
|
||||
var render_queue = [];
|
||||
var hud_queue = [];
|
||||
|
||||
var current_queue = render_queue;
|
||||
|
||||
var std_sampler = {
|
||||
min_filter: "nearest",
|
||||
mag_filter: "nearest",
|
||||
mipmap: "linear",
|
||||
u: "repeat",
|
||||
v: "repeat",
|
||||
w: "repeat",
|
||||
mip_bias: 0,
|
||||
max_anisotropy: 0,
|
||||
compare_op: "none",
|
||||
min_lod: 0,
|
||||
max_lod: 0,
|
||||
anisotropy: false,
|
||||
compare: false
|
||||
};
|
||||
|
||||
function upload_model(model) {
|
||||
var bufs = [];
|
||||
for (var i in model) {
|
||||
if (typeof model[i] !== 'object') continue;
|
||||
bufs.push(model[i]);
|
||||
}
|
||||
context.upload(this, bufs);
|
||||
}
|
||||
|
||||
upload_model[prosperon.DOC] = `Upload all buffer-like properties of the given model to the GPU.
|
||||
|
||||
:param model: An object whose buffer properties are to be uploaded.
|
||||
:return: None
|
||||
`
|
||||
|
||||
function bind_model(pass, pipeline, model) {
|
||||
var buffers = pipeline.vertex_buffer_descriptions;
|
||||
var bufs = [];
|
||||
if (buffers)
|
||||
for (var b of buffers) {
|
||||
if (b.name in model) bufs.push(model[b.name])
|
||||
else throw Error (`could not find buffer ${b.name} on model`);
|
||||
}
|
||||
pass.bind_buffers(0,bufs);
|
||||
pass.bind_index_buffer(model.indices);
|
||||
}
|
||||
|
||||
bind_model[prosperon.DOC] = `Bind the model's vertex and index buffers for the given pipeline and render pass.
|
||||
|
||||
:param pass: The current render pass.
|
||||
:param pipeline: The pipeline object with vertex buffer descriptions.
|
||||
:param model: The model object containing matching buffers and an index buffer.
|
||||
:return: None
|
||||
`
|
||||
|
||||
function bind_mat(pass, pipeline, mat) {
|
||||
var imgs = [];
|
||||
var refl = pipeline.fragment.reflection;
|
||||
if (refl.separate_images) {
|
||||
for (var i of refl.separate_images) {
|
||||
if (i.name in mat) {
|
||||
var tex = mat[i.name];
|
||||
imgs.push({texture:tex.texture, sampler:tex.sampler});
|
||||
} else
|
||||
throw Error (`could not find all necessary images: ${i.name}`)
|
||||
}
|
||||
pass.bind_samplers(false, 0,imgs);
|
||||
}
|
||||
}
|
||||
|
||||
bind_mat[prosperon.DOC] = `Bind the material images and samplers needed by the pipeline's fragment shader.
|
||||
|
||||
:param pass: The current render pass.
|
||||
:param pipeline: The pipeline whose fragment shader reflection indicates required textures.
|
||||
:param mat: An object mapping the required image names to {texture, sampler}.
|
||||
:return: None
|
||||
`
|
||||
|
||||
function group_sprites_by_texture(sprites, mesh) {
|
||||
if (sprites.length === 0) return;
|
||||
for (var i = 0; i < sprites.length; i++) {
|
||||
sprites[i].mesh = mesh;
|
||||
sprites[i].first_index = i*6;
|
||||
sprites[i].num_indices = 6;
|
||||
}
|
||||
return;
|
||||
// The code below is an alternate approach to grouping by image. Currently not in use.
|
||||
/*
|
||||
var groups = [];
|
||||
var group = {image:sprites[0].image, first_index:0};
|
||||
var count = 1;
|
||||
for (var i = 1; i < sprites.length; i++) {
|
||||
if (sprites[i].image === group.image) {
|
||||
count++;
|
||||
continue;
|
||||
}
|
||||
group.num_indices = count*6;
|
||||
var newgroup = {image:sprites[i].image, first_index:group.first_index+group.num_indices};
|
||||
group = newgroup;
|
||||
groups.push(group);
|
||||
count=1;
|
||||
}
|
||||
group.num_indices = count*6;
|
||||
return groups;
|
||||
*/
|
||||
}
|
||||
|
||||
group_sprites_by_texture[prosperon.DOC] = `Assign each sprite to the provided mesh, generating index data as needed.
|
||||
|
||||
:param sprites: An array of sprite objects.
|
||||
:param mesh: A mesh object (pos, color, uv, indices, etc.) to link to each sprite.
|
||||
:return: None
|
||||
`
|
||||
|
||||
var main_color = {
|
||||
type:"2d",
|
||||
format: "rgba8",
|
||||
layers: 1,
|
||||
mip_levels: 1,
|
||||
samples: 0,
|
||||
sampler:true,
|
||||
color_target:true
|
||||
};
|
||||
|
||||
var main_depth = {
|
||||
type: "2d",
|
||||
format: "d32 float s8",
|
||||
layers:1,
|
||||
mip_levels:1,
|
||||
samples:0,
|
||||
sampler:true,
|
||||
depth_target:true
|
||||
};
|
||||
|
||||
function render_camera(cmds, camera) {
|
||||
var pass;
|
||||
delete camera.target // TODO: HORRIBLE
|
||||
if (!camera.target) {
|
||||
main_color.width = main_depth.width = camera.size.x;
|
||||
main_color.height = main_depth.height = camera.size.y;
|
||||
camera.target = {
|
||||
color_targets: [{
|
||||
texture: context.texture(main_color),
|
||||
mip_level:0,
|
||||
layer: 0,
|
||||
load:"clear",
|
||||
store:"store",
|
||||
clear: cornflower
|
||||
}],
|
||||
depth_stencil: {
|
||||
texture: context.texture(main_depth),
|
||||
clear:1,
|
||||
load:"dont_care",
|
||||
store:"dont_care",
|
||||
stencil_load:"dont_care",
|
||||
stencil_store:"dont_care",
|
||||
stencil_clear:0
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
var buffers = [];
|
||||
buffers = buffers.concat(graphics.queue_sprite_mesh(render_queue));
|
||||
var unique_meshes = [...new Set(render_queue.map(x => x.mesh))];
|
||||
for (var q of unique_meshes)
|
||||
buffers = buffers.concat([q.pos, q.color, q.uv, q.indices]);
|
||||
|
||||
buffers = buffers.concat(graphics.queue_sprite_mesh(hud_queue));
|
||||
for (var q of hud_queue)
|
||||
if (q.type === 'geometry') buffers = buffers.concat([q.mesh.pos, q.mesh.color, q.mesh.uv, q.mesh.indices]);
|
||||
|
||||
full_upload(buffers)
|
||||
|
||||
var pass = cmds.render_pass(camera.target);
|
||||
|
||||
var pipeline = sprite_pipeline;
|
||||
bind_pipeline(pass,pipeline);
|
||||
|
||||
var camslot = get_pipeline_ubo_slot(pipeline, 'TransformBuffer');
|
||||
if (typeof camslot !== 'undefined')
|
||||
cmds.camera(camera, camslot);
|
||||
|
||||
modelslot = get_pipeline_ubo_slot(pipeline, "model");
|
||||
if (typeof modelslot !== 'undefined') {
|
||||
var ubo = ubo_obj_to_array(pipeline, 'model', sprite_model_ubo);
|
||||
cmds.push_vertex_uniform_data(modelslot, ubo);
|
||||
}
|
||||
|
||||
var mesh;
|
||||
var img;
|
||||
var modelslot;
|
||||
|
||||
cmds.push_debug_group("draw")
|
||||
for (var group of render_queue) {
|
||||
if (mesh != group.mesh) {
|
||||
mesh = group.mesh;
|
||||
bind_model(pass,pipeline,mesh);
|
||||
}
|
||||
|
||||
if (group.image && img != group.image) {
|
||||
img = group.image;
|
||||
img.sampler = std_sampler;
|
||||
bind_mat(pass,pipeline,{diffuse:img});
|
||||
}
|
||||
|
||||
pass.draw_indexed(group.num_indices, 1, group.first_index, 0, 0);
|
||||
}
|
||||
cmds.pop_debug_group()
|
||||
|
||||
cmds.push_debug_group("hud")
|
||||
var camslot = get_pipeline_ubo_slot(pipeline, 'TransformBuffer');
|
||||
if (typeof camslot !== 'undefined')
|
||||
cmds.hud(camera.size, camslot);
|
||||
|
||||
for (var group of hud_queue) {
|
||||
if (mesh != group.mesh) {
|
||||
mesh = group.mesh;
|
||||
bind_model(pass,pipeline,mesh);
|
||||
}
|
||||
|
||||
if (group.image && img != group.image) {
|
||||
img = group.image;
|
||||
img.sampler = std_sampler;
|
||||
bind_mat(pass,pipeline,{diffuse:img});
|
||||
}
|
||||
|
||||
pass.draw_indexed(group.num_indices, 1, group.first_index, 0, 0);
|
||||
}
|
||||
cmds.pop_debug_group();
|
||||
|
||||
pass?.end();
|
||||
|
||||
render_queue = [];
|
||||
hud_queue = [];
|
||||
}
|
||||
|
||||
render_camera[prosperon.DOC] = `Render a scene using the provided camera, drawing both render queue and HUD queue items.
|
||||
|
||||
:param cmds: A command buffer obtained from the GPU context.
|
||||
:param camera: The camera object (with size, optional target, etc.).
|
||||
:return: None
|
||||
`
|
||||
|
||||
var swaps = [];
|
||||
render.present = function() {
|
||||
os.clean_transforms();
|
||||
var cmds = context.acquire_cmd_buffer();
|
||||
render_camera(cmds, prosperon.camera);
|
||||
var swapchain_tex = cmds.acquire_swapchain();
|
||||
if (!swapchain_tex)
|
||||
cmds.cancel();
|
||||
else {
|
||||
var torect = prosperon.camera.draw_rect(prosperon.window.size);
|
||||
torect.texture = swapchain_tex;
|
||||
if (swapchain_tex) {
|
||||
cmds.blit({
|
||||
src: prosperon.camera.target.color_targets[0].texture,
|
||||
dst: torect,
|
||||
filter:"nearest",
|
||||
load: "clear"
|
||||
});
|
||||
|
||||
if (imgui) { // draws any imgui commands present
|
||||
cmds.push_debug_group("imgui")
|
||||
imgui.prepend(cmds);
|
||||
var pass = cmds.render_pass({
|
||||
color_targets:[{texture:swapchain_tex}]});
|
||||
imgui.endframe(cmds,pass);
|
||||
pass.end();
|
||||
cmds.pop_debug_group()
|
||||
}
|
||||
}
|
||||
cmds.submit()
|
||||
}
|
||||
}
|
||||
|
||||
render.present[prosperon.DOC] = `Perform the per-frame rendering and present the final swapchain image, including imgui pass if available.
|
||||
|
||||
:return: None
|
||||
`
|
||||
|
||||
var stencil_write = {
|
||||
compare: "always",
|
||||
fail_op: "replace",
|
||||
depth_fail_op: "replace",
|
||||
pass_op: "replace"
|
||||
};
|
||||
|
||||
function stencil_writer(ref) {
|
||||
var pipe = Object.create(base_pipeline);
|
||||
Object.assign(pipe, {
|
||||
stencil: {
|
||||
enabled: true,
|
||||
front: stencil_write,
|
||||
back: stencil_write,
|
||||
write:true,
|
||||
read:true,
|
||||
ref:ref
|
||||
},
|
||||
write_mask: colormask.none
|
||||
});
|
||||
return pipe;
|
||||
}.hashify();
|
||||
|
||||
// objects by default draw where the stencil buffer is 0
|
||||
function fillmask(ref) {
|
||||
var pipe = stencil_writer(ref);
|
||||
render.use_shader('screenfill.cg', pipe);
|
||||
render.draw(shape.quad);
|
||||
}
|
||||
|
||||
render.fillmask[prosperon.DOC] = `Draw a fullscreen shape using a 'screenfill' shader to populate the stencil buffer with a given reference.
|
||||
|
||||
:param ref: The stencil reference value to write.
|
||||
:return: None
|
||||
`
|
||||
|
||||
var stencil_invert = {
|
||||
compare: "always",
|
||||
fail_op: "invert",
|
||||
depth_fail_op: "invert",
|
||||
pass_op: "invert"
|
||||
};
|
||||
|
||||
function mask(image, pos, scale, rotation = 0, ref = 1) {
|
||||
if (typeof image === 'string')
|
||||
image = graphics.texture(image);
|
||||
|
||||
var tex = image.texture;
|
||||
if (scale) scale = scale.div([tex.width,tex.height]);
|
||||
else scale = [1,1,1]
|
||||
|
||||
var pipe = stencil_writer(ref);
|
||||
render.use_shader('sprite.cg', pipe);
|
||||
var t = new transform;
|
||||
t.trs(pos, undefined, scale);
|
||||
set_model(t);
|
||||
render.use_mat({
|
||||
diffuse:image.texture,
|
||||
rect: image.rect,
|
||||
shade: Color.white
|
||||
});
|
||||
render.draw(shape.quad);
|
||||
}
|
||||
|
||||
render.mask[prosperon.DOC] = `Draw an image to the stencil buffer, marking its area with a specified reference value.
|
||||
|
||||
:param image: A texture or string path (which is converted to a texture).
|
||||
:param pos: The translation (x, y) for the image placement.
|
||||
:param scale: Optional scaling applied to the texture.
|
||||
:param rotation: Optional rotation in radians (unused by default).
|
||||
:param ref: The stencil reference value to write.
|
||||
:return: None
|
||||
`
|
||||
|
||||
render.viewport = function(rect) {
|
||||
context.viewport(rect);
|
||||
}
|
||||
|
||||
render.viewport[prosperon.DOC] = `Set the GPU viewport to the specified rectangle.
|
||||
|
||||
:param rect: A rectangle [x, y, width, height].
|
||||
:return: None
|
||||
`
|
||||
|
||||
render.scissor = function(rect) {
|
||||
render.viewport(rect)
|
||||
}
|
||||
|
||||
render.scissor[prosperon.DOC] = `Set the GPU scissor region to the specified rectangle (alias of render.viewport).
|
||||
|
||||
:param rect: A rectangle [x, y, width, height].
|
||||
:return: None
|
||||
`
|
||||
|
||||
var std_sampler
|
||||
|
||||
if (tracy) tracy.gpu_init()
|
||||
|
||||
render.queue = function(cmd) {
|
||||
if (Array.isArray(cmd))
|
||||
for (var i of cmd) current_queue.push(i)
|
||||
else
|
||||
current_queue.push(cmd)
|
||||
}
|
||||
|
||||
render.queue[prosperon.DOC] = `Enqueue one or more draw commands. These commands are batched until render_camera is called.
|
||||
|
||||
:param cmd: Either a single command object or an array of command objects.
|
||||
:return: None
|
||||
`
|
||||
|
||||
render.setup_draw = function() {
|
||||
current_queue = render_queue;
|
||||
prosperon.draw();
|
||||
}
|
||||
|
||||
render.setup_draw[prosperon.DOC] = `Switch the current queue to the primary scene render queue, then invoke 'prosperon.draw' if defined.
|
||||
|
||||
:return: None
|
||||
`
|
||||
|
||||
render.setup_hud = function() {
|
||||
current_queue = hud_queue;
|
||||
prosperon.hud();
|
||||
}
|
||||
|
||||
render.setup_hud[prosperon.DOC] = `Switch the current queue to the HUD render queue, then invoke 'prosperon.hud' if defined.
|
||||
|
||||
:return: None
|
||||
`
|
||||
|
||||
render.initialize = function(config)
|
||||
{
|
||||
var default_conf = {
|
||||
title:`Prosperon [${prosperon.version}-${prosperon.revision}]`,
|
||||
width: 1280,
|
||||
height: 720,
|
||||
icon: graphics.make_texture(io.slurpbytes('icons/moon.gif')),
|
||||
high_dpi:0,
|
||||
alpha:1,
|
||||
fullscreen:0,
|
||||
sample_count:1,
|
||||
enable_clipboard:true,
|
||||
enable_dragndrop: true,
|
||||
max_dropped_files: 1,
|
||||
swap_interval: 1,
|
||||
name: "Prosperon",
|
||||
version:prosperon.version + "-" + prosperon.revision,
|
||||
identifier: "world.pockle.prosperon",
|
||||
creator: "Pockle World LLC",
|
||||
copyright: "Copyright Pockle World 2025",
|
||||
type: "game",
|
||||
url: "https://prosperon.dev"
|
||||
}
|
||||
|
||||
config.__proto__ = default_conf
|
||||
|
||||
prosperon.camera = use('ext/camera').make()
|
||||
prosperon.camera.size = [config.width,config.height]
|
||||
|
||||
prosperon.window = prosperon.engine_start(config)
|
||||
|
||||
context = prosperon.window.make_gpu(false,driver)
|
||||
context.window = prosperon.window
|
||||
context.claim_window(prosperon.window)
|
||||
context.set_swapchain('sdr', 'vsync')
|
||||
|
||||
if (imgui) imgui.init(context, prosperon.window)
|
||||
|
||||
shader_type = context.shader_format()[0];
|
||||
|
||||
std_sampler = context.make_sampler({
|
||||
min_filter: "nearest",
|
||||
mag_filter: "nearest",
|
||||
mipmap_mode: "nearest",
|
||||
address_mode_u: "repeat",
|
||||
address_mode_v: "repeat",
|
||||
address_mode_w: "repeat"
|
||||
});
|
||||
}
|
||||
|
||||
return render
|
||||
|
||||
@@ -1,125 +0,0 @@
|
||||
var render = {}
|
||||
|
||||
var context
|
||||
|
||||
var util = use('util')
|
||||
|
||||
render.initialize = function(config)
|
||||
{
|
||||
var default_conf = {
|
||||
title:`Prosperon [${prosperon.version}-${prosperon.revision}]`,
|
||||
width: 1280,
|
||||
height: 720,
|
||||
// icon: graphics.make_texture(io.slurpbytes('icons/moon.gif')),
|
||||
high_dpi:0,
|
||||
alpha:1,
|
||||
fullscreen:0,
|
||||
sample_count:1,
|
||||
enable_clipboard:true,
|
||||
enable_dragndrop: true,
|
||||
max_dropped_files: 1,
|
||||
swap_interval: 1,
|
||||
name: "Prosperon",
|
||||
version:prosperon.version + "-" + prosperon.revision,
|
||||
identifier: "world.pockle.prosperon",
|
||||
creator: "Pockle World LLC",
|
||||
copyright: "Copyright Pockle World 2025",
|
||||
type: "game",
|
||||
url: "https://prosperon.dev"
|
||||
}
|
||||
|
||||
config.__proto__ = default_conf
|
||||
prosperon.window = prosperon.engine_start(config)
|
||||
context = prosperon.window.make_renderer()
|
||||
context.logical_size([config.resolution_x, config.resolution_y], config.mode)
|
||||
}
|
||||
|
||||
render.sprite = function(sprite)
|
||||
{
|
||||
context.sprite(sprite)
|
||||
}
|
||||
|
||||
// img here is the engine surface
|
||||
render.load_texture = function(surface)
|
||||
{
|
||||
return context.load_texture(surface)
|
||||
}
|
||||
|
||||
var current_color = Color.white
|
||||
|
||||
render.image = function(image, rect, rotation, anchor, shear, info)
|
||||
{
|
||||
// rect.width = image.rect_px.width;
|
||||
// rect.height = image.rect_px.height;
|
||||
image.texture.mode(info.mode)
|
||||
context.texture(image.texture, image.rect_px, rect, rotation, anchor);
|
||||
}
|
||||
|
||||
render.clip = function(rect)
|
||||
{
|
||||
context.clip(rect)
|
||||
}
|
||||
|
||||
render.line = function(points)
|
||||
{
|
||||
context.line(points)
|
||||
}
|
||||
|
||||
render.point = function(pos)
|
||||
{
|
||||
context.point(pos)
|
||||
}
|
||||
|
||||
render.rectangle = function(rect)
|
||||
{
|
||||
context.rects([rect])
|
||||
}
|
||||
|
||||
render.rects = function(rects)
|
||||
{
|
||||
context.rects(rects)
|
||||
}
|
||||
|
||||
render.pipeline = function(pipe)
|
||||
{
|
||||
// any changes here
|
||||
}
|
||||
|
||||
render.settings = function(set)
|
||||
{
|
||||
if (!set.color) return
|
||||
context.draw_color(set.color)
|
||||
}
|
||||
|
||||
render.geometry = function(image, mesh, pipeline)
|
||||
{
|
||||
context.geometry(image, mesh)
|
||||
}
|
||||
|
||||
render.slice9 = function(image, rect, slice, info, pipeline)
|
||||
{
|
||||
context.slice9(image.texture, image.rect_px, util.normalizeSpacing(slice), rect);
|
||||
}
|
||||
|
||||
render.get_image = function(rect)
|
||||
{
|
||||
return context.get_image(rect)
|
||||
}
|
||||
|
||||
render.clear = function(color)
|
||||
{
|
||||
if (color) context.draw_color(color)
|
||||
context.clear()
|
||||
}
|
||||
|
||||
render.present = function()
|
||||
{
|
||||
context.present()
|
||||
}
|
||||
|
||||
render.camera = function(cam)
|
||||
{
|
||||
context.camera(cam);
|
||||
}
|
||||
|
||||
return render
|
||||
@@ -16,7 +16,6 @@ audio.pcm = function pcm(file)
|
||||
file = res.find_sound(file);
|
||||
if (!file) throw new Error(`Could not findfile ${file}`);
|
||||
if (pcms[file]) return pcms[file];
|
||||
var bytes = io.slurpbytes(file)
|
||||
var newpcm = soloud.load_wav_mem(io.slurpbytes(file));
|
||||
pcms[file] = newpcm;
|
||||
return newpcm;
|
||||
@@ -74,33 +73,4 @@ audio.music = function music(file, fade = 0.5) {
|
||||
};
|
||||
audio.music[doc.sym] = `Play the given music file, with an optional cross fade. The song will loop. When this is invoked again, the previous music is replaced.`
|
||||
|
||||
var ss = use('sdl_audio')
|
||||
|
||||
var feeder = ss.open_stream("playback")
|
||||
|
||||
feeder.set_format({format:"f32", channels:2,samplerate:44100})
|
||||
|
||||
feeder.resume()
|
||||
|
||||
var FRAMES = 1024
|
||||
var CHANNELS = 2
|
||||
var BYTES_PER_F = 4
|
||||
var SAMPLES = FRAMES * CHANNELS
|
||||
var CHUNK_BYTES = FRAMES * CHANNELS * BYTES_PER_F
|
||||
|
||||
var mixview = new Float32Array(FRAMES*CHANNELS)
|
||||
var mixbuf = mixview.buffer
|
||||
|
||||
function pump()
|
||||
{
|
||||
if (feeder.queued() < CHUNK_BYTES*3) {
|
||||
var mm = soloud.mix(FRAMES)
|
||||
feeder.put(mm)
|
||||
}
|
||||
|
||||
$_.delay(pump, 1/240)
|
||||
}
|
||||
|
||||
pump()
|
||||
|
||||
return audio;
|
||||
|
||||
@@ -42,16 +42,16 @@ time.strparse = {
|
||||
s: "second",
|
||||
};
|
||||
|
||||
time.isleap = function isleap(year) {
|
||||
time.isleap = function(year) {
|
||||
return this.yearsize(year) === 366;
|
||||
};
|
||||
|
||||
time.yearsize = function yearsize(y) {
|
||||
time.yearsize = function(y) {
|
||||
if (y % 4 === 0 && (y % 100 !== 0 || y % 400 === 0)) return 366;
|
||||
return 365;
|
||||
};
|
||||
|
||||
time.timecode = function timecode(t, fps = 24) {
|
||||
time.timecode = function(t, fps = 24) {
|
||||
var s = Math.trunc(t);
|
||||
t -= s;
|
||||
return `${s}:${Math.trunc(fps * s)}`;
|
||||
@@ -60,7 +60,7 @@ time.timecode = function timecode(t, fps = 24) {
|
||||
time.monthdays = [31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31];
|
||||
time.zones = { "-12": "IDLW" };
|
||||
|
||||
time.record = function record(num, zone = this.computer_zone()) {
|
||||
time.record = function(num, zone = this.computer_zone()) {
|
||||
if (typeof num === "object") {
|
||||
return num;
|
||||
} else if (typeof num === "number") {
|
||||
@@ -113,7 +113,7 @@ time.record = function record(num, zone = this.computer_zone()) {
|
||||
}
|
||||
};
|
||||
|
||||
time.number = function number(rec) {
|
||||
time.number = function(rec) {
|
||||
if (typeof rec === "number") {
|
||||
return rec;
|
||||
} else if (typeof rec === "object") {
|
||||
@@ -153,7 +153,7 @@ time.number = function number(rec) {
|
||||
|
||||
time.fmt = "vB mB d h:nn:ss TZz a y c";
|
||||
|
||||
time.text = function text(num, fmt = this.fmt, zone) {
|
||||
time.text = function(num, fmt = this.fmt, zone) {
|
||||
var rec = (typeof num === "number") ? time.record(num, zone) : num;
|
||||
zone = rec.zone;
|
||||
if (fmt.match("a")) {
|
||||
@@ -187,7 +187,7 @@ time.text = function text(num, fmt = this.fmt, zone) {
|
||||
fmt = fmt.replaceAll("h", rec.hour);
|
||||
fmt = fmt.replaceAll("nn", rec.minute.toString().padStart(2, "0"));
|
||||
fmt = fmt.replaceAll("n", rec.minute);
|
||||
fmt = fmt.replaceAll("ss", rec.second.toFixed(2).padStart(2, "0"));
|
||||
fmt = fmt.replaceAll("ss", rec.second.toString().padStart(2, "0"));
|
||||
fmt = fmt.replaceAll("s", rec.second);
|
||||
fmt = fmt.replaceAll("z", zone >= 0 ? "+" + zone : zone);
|
||||
fmt = fmt.replaceAll(/mm[^bB]/g, rec.month + 1);
|
||||
|
||||
@@ -1,43 +0,0 @@
|
||||
// wota
|
||||
var wota = this
|
||||
|
||||
var json = use('json')
|
||||
|
||||
var encode = wota.encode
|
||||
|
||||
function wota_tostring()
|
||||
{
|
||||
return json.encode(wota.decode(this))
|
||||
}
|
||||
|
||||
var wota_obj = {
|
||||
toString: wota_tostring
|
||||
}
|
||||
|
||||
wota.encode = function(obj, replacer)
|
||||
{
|
||||
var result = encode(obj, replacer)
|
||||
result.toString = wota_tostring
|
||||
return result
|
||||
}
|
||||
|
||||
wota.encode[prosperon.DOC] = `Convert a JavaScript value into a WOTA-encoded ArrayBuffer.
|
||||
|
||||
This function serializes JavaScript values (such as numbers, strings, booleans, arrays, objects, or ArrayBuffers) into the WOTA binary format. The resulting ArrayBuffer can be stored or transmitted and later decoded back into a JavaScript value.
|
||||
|
||||
:param value: The JavaScript value to encode (e.g., number, string, boolean, array, object, or ArrayBuffer).
|
||||
:param replacer: An optional function that alters the encoding behavior for specific values.
|
||||
:return: An ArrayBuffer containing the WOTA-encoded data.
|
||||
:throws: An error if no argument is provided or if a cyclic object is encountered.
|
||||
`
|
||||
|
||||
wota.decode[prosperon.DOC] = `Decode a WOTA-encoded ArrayBuffer into a JavaScript value.
|
||||
|
||||
This function deserializes a WOTA-formatted ArrayBuffer into its corresponding JavaScript representation, such as a number, string, boolean, array, object, or ArrayBuffer. If the input is invalid or empty, it returns undefined.
|
||||
|
||||
:param buffer: An ArrayBuffer containing WOTA-encoded data to decode.
|
||||
:param reviver: An optional function that transforms the decoded values.
|
||||
:return: The decoded JavaScript value (e.g., number, string, boolean, array, object, or ArrayBuffer), or undefined if no argument is provided.
|
||||
`
|
||||
|
||||
return wota
|
||||
@@ -1,7 +1,5 @@
|
||||
#include "HandmadeMath.h"
|
||||
|
||||
#include <stdlib.h>
|
||||
|
||||
const HMM_Vec2 v2zero = {0,0};
|
||||
const HMM_Vec2 v2one = {1,1};
|
||||
const HMM_Vec3 v3zero = {0,0,0};
|
||||
|
||||
@@ -96,6 +96,8 @@
|
||||
#ifndef HANDMADE_MATH_H
|
||||
#define HANDMADE_MATH_H
|
||||
|
||||
#include <chipmunk/chipmunk.h>
|
||||
|
||||
#if !defined(HANDMADE_MATH_NO_SIMD)
|
||||
#if defined(__ARM_NEON) || defined(__ARM_NEON__)
|
||||
#define HANDMADE_MATH__USE_NEON 1
|
||||
@@ -270,6 +272,12 @@ typedef union HMM_Vec3 {
|
||||
HMM_Vec2 VW;
|
||||
};
|
||||
|
||||
struct
|
||||
{
|
||||
HMM_Vec2 cp;
|
||||
float _Ignored5;
|
||||
};
|
||||
|
||||
float Elements[3];
|
||||
float e[3];
|
||||
|
||||
@@ -365,6 +373,12 @@ typedef union HMM_Vec4 {
|
||||
HMM_Vec2 ZW;
|
||||
};
|
||||
|
||||
struct
|
||||
{
|
||||
HMM_Vec2 cp;
|
||||
HMM_Vec2 wh;
|
||||
};
|
||||
|
||||
HMM_Quat quat;
|
||||
struct {float x, y, z, w; };
|
||||
struct {float r, g, b, a; };
|
||||
|
||||
@@ -306,7 +306,7 @@ struct ase_t
|
||||
void* mem_ctx;
|
||||
};
|
||||
|
||||
const char *aseprite_GetError(void);
|
||||
const char *aseprite_GetError();
|
||||
|
||||
#endif // CUTE_ASEPRITE_H
|
||||
|
||||
@@ -317,11 +317,11 @@ const char *aseprite_GetError(void);
|
||||
#define ASEPRITE_ERROR_MAX 256
|
||||
char aseprite_error[ASEPRITE_ERROR_MAX] = {0};
|
||||
|
||||
const char *aseprite_GetError(void) {
|
||||
const char *aseprite_GetError() {
|
||||
return aseprite_error;
|
||||
}
|
||||
|
||||
void aseprite_clear_error(void) {
|
||||
void aseprite_clear_error() {
|
||||
aseprite_error[0] = 0;
|
||||
}
|
||||
|
||||
@@ -726,6 +726,7 @@ static int s_inflate(const void* in, int in_bytes, void* out, int out_bytes, voi
|
||||
s->out_end = s->out + out_bytes;
|
||||
s->begin = (char*)out;
|
||||
|
||||
int count = 0;
|
||||
uint32_t bfinal;
|
||||
do
|
||||
{
|
||||
@@ -739,6 +740,8 @@ static int s_inflate(const void* in, int in_bytes, void* out, int out_bytes, voi
|
||||
case 2: s_dynamic(s); CUTE_ASEPRITE_CALL(s_block(s)); break;
|
||||
case 3: CUTE_ASEPRITE_CHECK(0, "Detected unknown block type within input stream.");
|
||||
}
|
||||
|
||||
++count;
|
||||
}
|
||||
while (!bfinal);
|
||||
|
||||
|
||||
@@ -399,7 +399,7 @@ typedef struct dmon__watch_state {
|
||||
typedef struct dmon__state {
|
||||
int num_watches;
|
||||
dmon__watch_state* watches[DMON_MAX_WATCHES];
|
||||
int freelist[DMON_MAX_WATCHES];
|
||||
int freelist[DMON_MAX_WATCHES];
|
||||
HANDLE thread_handle;
|
||||
CRITICAL_SECTION mutex;
|
||||
volatile LONG modify_watches;
|
||||
@@ -589,7 +589,7 @@ DMON_API_IMPL void dmon_init(void)
|
||||
_dmon.thread_handle = CreateThread(NULL, 0, (LPTHREAD_START_ROUTINE)_dmon_thread, NULL, 0, NULL);
|
||||
DMON_ASSERT(_dmon.thread_handle);
|
||||
|
||||
for (int i = 0; i < DMON_MAX_WATCHES; i++)
|
||||
for (int i = 0; i < DMON_MAX_WATCHES; i++)
|
||||
_dmon.freelist[i] = DMON_MAX_WATCHES - i - 1;
|
||||
|
||||
_dmon_init = true;
|
||||
@@ -627,7 +627,7 @@ DMON_API_IMPL dmon_watch_id dmon_watch(const char* rootdir,
|
||||
const char* oldname, void* user),
|
||||
uint32_t flags, void* user_data)
|
||||
{
|
||||
DMON_ASSERT(_dmon_init);
|
||||
DMON_ASSERT(_dmon_init);
|
||||
DMON_ASSERT(watch_cb);
|
||||
DMON_ASSERT(rootdir && rootdir[0]);
|
||||
|
||||
@@ -706,7 +706,7 @@ DMON_API_IMPL dmon_watch_id dmon_watch(const char* rootdir,
|
||||
|
||||
DMON_API_IMPL void dmon_unwatch(dmon_watch_id id)
|
||||
{
|
||||
DMON_ASSERT(_dmon_init);
|
||||
DMON_ASSERT(_dmon_init);
|
||||
DMON_ASSERT(id.id > 0);
|
||||
int index = id.id - 1;
|
||||
DMON_ASSERT(index < DMON_MAX_WATCHES);
|
||||
@@ -761,7 +761,7 @@ typedef struct dmon__watch_state {
|
||||
|
||||
typedef struct dmon__state {
|
||||
dmon__watch_state* watches[DMON_MAX_WATCHES];
|
||||
int freelist[DMON_MAX_WATCHES];
|
||||
int freelist[DMON_MAX_WATCHES];
|
||||
dmon__inotify_event* events;
|
||||
int num_watches;
|
||||
pthread_t thread_handle;
|
||||
@@ -891,7 +891,9 @@ _DMON_PRIVATE void _dmon_inotify_process_events(void)
|
||||
ev->skip = true;
|
||||
break;
|
||||
} else if ((ev->mask & IN_ISDIR) && (check_ev->mask & (IN_ISDIR|IN_MODIFY))) {
|
||||
// handle trailing slashes
|
||||
// in some cases, particularly when created files under sub directories
|
||||
// there can be two modify events for a single subdir one with trailing slash and one without
|
||||
// remove trailing slash from both cases and test
|
||||
int l1 = (int)strlen(ev->filepath);
|
||||
int l2 = (int)strlen(check_ev->filepath);
|
||||
if (ev->filepath[l1-1] == '/') ev->filepath[l1-1] = '\0';
|
||||
@@ -908,12 +910,14 @@ _DMON_PRIVATE void _dmon_inotify_process_events(void)
|
||||
for (j = i + 1; j < c && !loop_break; j++) {
|
||||
dmon__inotify_event* check_ev = &_dmon.events[j];
|
||||
if ((check_ev->mask & IN_MOVED_FROM) && strcmp(ev->filepath, check_ev->filepath) == 0) {
|
||||
// check for rename sequences
|
||||
// there is a case where some programs (like gedit):
|
||||
// when we save, it creates a temp file, and moves it to the file being modified
|
||||
// search for these cases and remove all of them
|
||||
int k;
|
||||
for (k = j + 1; k < c; k++) {
|
||||
dmon__inotify_event* third_ev = &_dmon.events[k];
|
||||
if (third_ev->mask & IN_MOVED_TO && check_ev->cookie == third_ev->cookie) {
|
||||
third_ev->mask = IN_MODIFY; // treat as a modify
|
||||
third_ev->mask = IN_MODIFY; // change to modified
|
||||
ev->skip = check_ev->skip = true;
|
||||
loop_break = true;
|
||||
break;
|
||||
@@ -921,6 +925,7 @@ _DMON_PRIVATE void _dmon_inotify_process_events(void)
|
||||
}
|
||||
} else if ((check_ev->mask & IN_MODIFY) && strcmp(ev->filepath, check_ev->filepath) == 0) {
|
||||
// Another case is that file is copied. CREATE and MODIFY happens sequentially
|
||||
// so we ignore MODIFY event
|
||||
check_ev->skip = true;
|
||||
}
|
||||
}
|
||||
@@ -934,7 +939,10 @@ _DMON_PRIVATE void _dmon_inotify_process_events(void)
|
||||
break;
|
||||
}
|
||||
}
|
||||
// If destination is not valid, treat as delete
|
||||
|
||||
// in some environments like nautilus file explorer:
|
||||
// when a file is deleted, it is moved to recycle bin
|
||||
// so if the destination of the move is not valid, it's probably DELETE
|
||||
if (!move_valid) {
|
||||
ev->mask = IN_DELETE;
|
||||
}
|
||||
@@ -948,7 +956,10 @@ _DMON_PRIVATE void _dmon_inotify_process_events(void)
|
||||
break;
|
||||
}
|
||||
}
|
||||
// If source is not valid, treat as create
|
||||
|
||||
// in some environments like nautilus file explorer:
|
||||
// when a file is deleted, it is moved to recycle bin, on undo it is moved back it
|
||||
// so if the destination of the move is not valid, it's probably CREATE
|
||||
if (!move_valid) {
|
||||
ev->mask = IN_CREATE;
|
||||
}
|
||||
@@ -956,6 +967,7 @@ _DMON_PRIVATE void _dmon_inotify_process_events(void)
|
||||
int j;
|
||||
for (j = i + 1; j < c; j++) {
|
||||
dmon__inotify_event* check_ev = &_dmon.events[j];
|
||||
// if the file is DELETED and then MODIFIED after, just ignore the modify event
|
||||
if ((check_ev->mask & IN_MODIFY) && strcmp(ev->filepath, check_ev->filepath) == 0) {
|
||||
check_ev->skip = true;
|
||||
break;
|
||||
@@ -997,9 +1009,10 @@ _DMON_PRIVATE void _dmon_inotify_process_events(void)
|
||||
stb_sb_push(watch->subdirs, subdir);
|
||||
stb_sb_push(watch->wds, wd);
|
||||
|
||||
// gather newly created subdirs
|
||||
// some directories may be already created, for instance, with the command: mkdir -p
|
||||
// so we will enumerate them manually and add them to the events
|
||||
_dmon_gather_recursive(watch, watchdir);
|
||||
ev = &_dmon.events[i];
|
||||
ev = &_dmon.events[i]; // gotta refresh the pointer because it may be relocated
|
||||
}
|
||||
}
|
||||
watch->watch_cb(ev->watch_id, DMON_ACTION_CREATE, watch->rootdir, ev->filepath, NULL, watch->user_data);
|
||||
@@ -1159,7 +1172,7 @@ DMON_API_IMPL dmon_watch_id dmon_watch(const char* rootdir,
|
||||
const char* oldname, void* user),
|
||||
uint32_t flags, void* user_data)
|
||||
{
|
||||
DMON_ASSERT(_dmon_init);
|
||||
DMON_ASSERT(_dmon_init);
|
||||
DMON_ASSERT(watch_cb);
|
||||
DMON_ASSERT(rootdir && rootdir[0]);
|
||||
|
||||
@@ -1243,7 +1256,7 @@ DMON_API_IMPL dmon_watch_id dmon_watch(const char* rootdir,
|
||||
return _dmon_make_id(0);
|
||||
}
|
||||
dmon__watch_subdir subdir;
|
||||
_dmon_strcpy(subdir.rootdir, sizeof(subdir.rootdir), ""); // root dir is a dummy entry
|
||||
_dmon_strcpy(subdir.rootdir, sizeof(subdir.rootdir), ""); // root dir is just a dummy entry
|
||||
stb_sb_push(watch->subdirs, subdir);
|
||||
stb_sb_push(watch->wds, wd);
|
||||
|
||||
@@ -1260,7 +1273,7 @@ DMON_API_IMPL dmon_watch_id dmon_watch(const char* rootdir,
|
||||
|
||||
DMON_API_IMPL void dmon_unwatch(dmon_watch_id id)
|
||||
{
|
||||
DMON_ASSERT(_dmon_init);
|
||||
DMON_ASSERT(_dmon_init);
|
||||
DMON_ASSERT(id.id > 0);
|
||||
int index = id.id - 1;
|
||||
DMON_ASSERT(index < DMON_MAX_WATCHES);
|
||||
@@ -1307,7 +1320,7 @@ typedef struct dmon__watch_state {
|
||||
|
||||
typedef struct dmon__state {
|
||||
dmon__watch_state* watches[DMON_MAX_WATCHES];
|
||||
int freelist[DMON_MAX_WATCHES];
|
||||
int freelist[DMON_MAX_WATCHES];
|
||||
dmon__fsevent_event* events;
|
||||
int num_watches;
|
||||
volatile int modify_watches;
|
||||
@@ -1356,7 +1369,7 @@ _DMON_PRIVATE void _dmon_fsevent_process_events(void)
|
||||
continue;
|
||||
}
|
||||
|
||||
// remove redundant modifies on a single file
|
||||
// remove redundant modify events on a single file
|
||||
if (ev->event_flags & kFSEventStreamEventFlagItemModified) {
|
||||
int j;
|
||||
for (j = i + 1; j < c; j++) {
|
||||
@@ -1367,8 +1380,7 @@ _DMON_PRIVATE void _dmon_fsevent_process_events(void)
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
else if ((ev->event_flags & kFSEventStreamEventFlagItemRenamed) && !ev->move_valid) {
|
||||
} else if ((ev->event_flags & kFSEventStreamEventFlagItemRenamed) && !ev->move_valid) {
|
||||
int j;
|
||||
for (j = i + 1; j < c; j++) {
|
||||
dmon__fsevent_event* check_ev = &_dmon.events[j];
|
||||
@@ -1379,7 +1391,10 @@ _DMON_PRIVATE void _dmon_fsevent_process_events(void)
|
||||
}
|
||||
}
|
||||
|
||||
// if not valid rename, treat as remove or create
|
||||
// in some environments like finder file explorer:
|
||||
// when a file is deleted, it is moved to recycle bin
|
||||
// so if the destination of the move is not valid, it's probably DELETE or CREATE
|
||||
// decide CREATE if file exists
|
||||
if (!ev->move_valid) {
|
||||
ev->event_flags &= ~kFSEventStreamEventFlagItemRenamed;
|
||||
|
||||
@@ -1414,10 +1429,10 @@ _DMON_PRIVATE void _dmon_fsevent_process_events(void)
|
||||
watch->watch_cb(ev->watch_id, DMON_ACTION_CREATE, watch->rootdir_unmod, ev->filepath, NULL,
|
||||
watch->user_data);
|
||||
}
|
||||
|
||||
if (ev->event_flags & kFSEventStreamEventFlagItemModified) {
|
||||
watch->watch_cb(ev->watch_id, DMON_ACTION_MODIFY, watch->rootdir_unmod, ev->filepath, NULL, watch->user_data);
|
||||
}
|
||||
else if (ev->event_flags & kFSEventStreamEventFlagItemRenamed) {
|
||||
} else if (ev->event_flags & kFSEventStreamEventFlagItemRenamed) {
|
||||
int j;
|
||||
for (j = i + 1; j < c; j++) {
|
||||
dmon__fsevent_event* check_ev = &_dmon.events[j];
|
||||
@@ -1427,8 +1442,7 @@ _DMON_PRIVATE void _dmon_fsevent_process_events(void)
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
else if (ev->event_flags & kFSEventStreamEventFlagItemRemoved) {
|
||||
} else if (ev->event_flags & kFSEventStreamEventFlagItemRemoved) {
|
||||
watch->watch_cb(ev->watch_id, DMON_ACTION_DELETE, watch->rootdir_unmod, ev->filepath, NULL,
|
||||
watch->user_data);
|
||||
}
|
||||
@@ -1464,19 +1478,8 @@ _DMON_PRIVATE void* _dmon_thread(void* arg)
|
||||
dmon__watch_state* watch = _dmon.watches[i];
|
||||
if (!watch->init) {
|
||||
DMON_ASSERT(watch->fsev_stream_ref);
|
||||
// Modified block: Use dispatch queue if macOS >= 13, else run loop
|
||||
#if __ENVIRONMENT_MAC_OS_X_VERSION_MIN_REQUIRED__ >= 130000
|
||||
{
|
||||
dispatch_queue_t queue = dispatch_queue_create("com.dmon.fsevents", DISPATCH_QUEUE_SERIAL);
|
||||
FSEventStreamSetDispatchQueue(watch->fsev_stream_ref, queue);
|
||||
FSEventStreamStart(watch->fsev_stream_ref);
|
||||
}
|
||||
#else
|
||||
{
|
||||
FSEventStreamScheduleWithRunLoop(watch->fsev_stream_ref, _dmon.cf_loop_ref, kCFRunLoopDefaultMode);
|
||||
FSEventStreamStart(watch->fsev_stream_ref);
|
||||
}
|
||||
#endif
|
||||
FSEventStreamScheduleWithRunLoop(watch->fsev_stream_ref, _dmon.cf_loop_ref, kCFRunLoopDefaultMode);
|
||||
FSEventStreamStart(watch->fsev_stream_ref);
|
||||
|
||||
watch->init = true;
|
||||
}
|
||||
@@ -1584,8 +1587,8 @@ _DMON_PRIVATE void _dmon_fsevent_callback(ConstFSEventStreamRef stream_ref, void
|
||||
_dmon_strcpy(abs_filepath, sizeof(abs_filepath), filepath);
|
||||
_dmon_unixpath(abs_filepath, sizeof(abs_filepath), abs_filepath);
|
||||
|
||||
// normalize path for case-insensitive volumes
|
||||
_dmon_tolower(abs_filepath_lower, sizeof(abs_filepath_lower), abs_filepath);
|
||||
// normalize path, so it would be the same on both MacOS file-system types (case/nocase)
|
||||
_dmon_tolower(abs_filepath_lower, sizeof(abs_filepath), abs_filepath);
|
||||
DMON_ASSERT(strstr(abs_filepath_lower, watch->rootdir) == abs_filepath_lower);
|
||||
|
||||
// strip the root dir from the beginning
|
||||
@@ -1605,7 +1608,7 @@ DMON_API_IMPL dmon_watch_id dmon_watch(const char* rootdir,
|
||||
const char* oldname, void* user),
|
||||
uint32_t flags, void* user_data)
|
||||
{
|
||||
DMON_ASSERT(_dmon_init);
|
||||
DMON_ASSERT(_dmon_init);
|
||||
DMON_ASSERT(watch_cb);
|
||||
DMON_ASSERT(rootdir && rootdir[0]);
|
||||
|
||||
@@ -1704,6 +1707,7 @@ DMON_API_IMPL dmon_watch_id dmon_watch(const char* rootdir,
|
||||
cf_dirarr, kFSEventStreamEventIdSinceNow, 0.25,
|
||||
kFSEventStreamCreateFlagFileEvents);
|
||||
|
||||
|
||||
CFRelease(cf_dirarr);
|
||||
CFRelease(cf_dir);
|
||||
|
||||
@@ -1714,7 +1718,7 @@ DMON_API_IMPL dmon_watch_id dmon_watch(const char* rootdir,
|
||||
|
||||
DMON_API_IMPL void dmon_unwatch(dmon_watch_id id)
|
||||
{
|
||||
DMON_ASSERT(_dmon_init);
|
||||
DMON_ASSERT(_dmon_init);
|
||||
DMON_ASSERT(id.id > 0);
|
||||
int index = id.id - 1;
|
||||
DMON_ASSERT(index < DMON_MAX_WATCHES);
|
||||
|
||||
7152
source/jsffi.c
@@ -1,80 +1,7 @@
|
||||
#ifndef FFI_H
|
||||
#define FFI_H
|
||||
|
||||
#include <quickjs.h>
|
||||
#include "HandmadeMath.h"
|
||||
#include "render.h"
|
||||
#include "stb_ds.h"
|
||||
|
||||
#define JS_SetProperty(js, tar, str, val) JS_SetPropertyStr(js, tar, #str, val)
|
||||
#define JS_GetProperty(js, tar, atom) JS_GetPropertyStr(js, tar, #atom)
|
||||
|
||||
extern SDL_Window *global_window;
|
||||
|
||||
// Core FFI functions
|
||||
void ffi_load(JSContext *js);
|
||||
void ffi_load(JSContext *js, int argc, char **argv);
|
||||
int js_print_exception(JSContext *js, JSValue v);
|
||||
|
||||
// Common type definitions - rect and colorf are defined in render.h
|
||||
|
||||
// Common conversion functions used across modules
|
||||
JSValue rect2js(JSContext *js, rect r);
|
||||
JSValue vec22js(JSContext *js, HMM_Vec2 v);
|
||||
JSValue vec32js(JSContext *js, HMM_Vec3 v);
|
||||
JSValue vec42js(JSContext *js, HMM_Vec4 v);
|
||||
JSValue quat2js(JSContext *js, HMM_Quat q);
|
||||
JSValue color2js(JSContext *js, colorf c);
|
||||
JSValue number2js(JSContext *js, double d);
|
||||
JSValue angle2js(JSContext *js, double a);
|
||||
|
||||
rect js2rect(JSContext *js, JSValue v);
|
||||
HMM_Vec2 js2vec2(JSContext *js, JSValue v);
|
||||
HMM_Vec3 js2vec3(JSContext *js, JSValue v);
|
||||
HMM_Vec4 js2vec4(JSContext *js, JSValue v);
|
||||
HMM_Quat js2quat(JSContext *js, JSValue v);
|
||||
colorf js2color(JSContext *js, JSValue v);
|
||||
double js2number(JSContext *js, JSValue v);
|
||||
double js2angle(JSContext *js, JSValue v);
|
||||
|
||||
// Forward declaration for MTRand from prosperon.h
|
||||
typedef struct tagMTRand MTRand;
|
||||
|
||||
// Random number generation functions
|
||||
double genRand(MTRand *mrand);
|
||||
uint32_t genRandLong(MTRand *mrand);
|
||||
void m_seedRand(MTRand *mrand, uint32_t seed);
|
||||
double rand_range(JSContext *js, double min, double max);
|
||||
|
||||
// Common data structures
|
||||
struct lrtb {
|
||||
float l;
|
||||
float r;
|
||||
float t;
|
||||
float b;
|
||||
};
|
||||
typedef struct lrtb lrtb;
|
||||
typedef struct text_vert text_vert;
|
||||
|
||||
// Common macros for property access
|
||||
#define JS_GETPROP(JS, TARGET, VALUE, PROP, TYPE) {\
|
||||
JSValue __##PROP##__v = JS_GetPropertyStr(JS,VALUE,#PROP); \
|
||||
TARGET = js2##TYPE(JS, __##PROP##__v); \
|
||||
JS_FreeValue(JS,__##PROP##__v); }\
|
||||
|
||||
#define JS_GETATOM(JS, TARGET, VALUE, ATOM, TYPE) {\
|
||||
JSValue __##PROP##__v = JS_GetPropertyStr(JS,VALUE,#ATOM); \
|
||||
TARGET = js2##TYPE(JS, __##PROP##__v); \
|
||||
JS_FreeValue(JS,__##PROP##__v); }\
|
||||
|
||||
// Common conversion functions
|
||||
lrtb js2lrtb(JSContext *js, JSValue v);
|
||||
int js2bool(JSContext *js, JSValue v);
|
||||
JSValue make_gpu_buffer(JSContext *js, void *data, size_t size, int type, int elements, int copy, int index);
|
||||
JSValue make_quad_indices_buffer(JSContext *js, int quads);
|
||||
JSValue quads_to_mesh(JSContext *js, text_vert *buffer);
|
||||
|
||||
// SDL type conversion functions
|
||||
SDL_Window *js2SDL_Window(JSContext *js, JSValue v);
|
||||
JSValue SDL_Window2js(JSContext *js, SDL_Window *w);
|
||||
|
||||
#endif
|
||||
|
||||
2956
source/monocypher.c
@@ -1,321 +0,0 @@
|
||||
// Monocypher version __git__
|
||||
//
|
||||
// This file is dual-licensed. Choose whichever licence you want from
|
||||
// the two licences listed below.
|
||||
//
|
||||
// The first licence is a regular 2-clause BSD licence. The second licence
|
||||
// is the CC-0 from Creative Commons. It is intended to release Monocypher
|
||||
// to the public domain. The BSD licence serves as a fallback option.
|
||||
//
|
||||
// SPDX-License-Identifier: BSD-2-Clause OR CC0-1.0
|
||||
//
|
||||
// ------------------------------------------------------------------------
|
||||
//
|
||||
// Copyright (c) 2017-2019, Loup Vaillant
|
||||
// All rights reserved.
|
||||
//
|
||||
//
|
||||
// Redistribution and use in source and binary forms, with or without
|
||||
// modification, are permitted provided that the following conditions are
|
||||
// met:
|
||||
//
|
||||
// 1. Redistributions of source code must retain the above copyright
|
||||
// notice, this list of conditions and the following disclaimer.
|
||||
//
|
||||
// 2. Redistributions in binary form must reproduce the above copyright
|
||||
// notice, this list of conditions and the following disclaimer in the
|
||||
// documentation and/or other materials provided with the
|
||||
// distribution.
|
||||
//
|
||||
// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
|
||||
// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
|
||||
// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
|
||||
// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
|
||||
// HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
|
||||
// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
|
||||
// LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
|
||||
// DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
|
||||
// THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
|
||||
// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
|
||||
// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
||||
//
|
||||
// ------------------------------------------------------------------------
|
||||
//
|
||||
// Written in 2017-2019 by Loup Vaillant
|
||||
//
|
||||
// To the extent possible under law, the author(s) have dedicated all copyright
|
||||
// and related neighboring rights to this software to the public domain
|
||||
// worldwide. This software is distributed without any warranty.
|
||||
//
|
||||
// You should have received a copy of the CC0 Public Domain Dedication along
|
||||
// with this software. If not, see
|
||||
// <https://creativecommons.org/publicdomain/zero/1.0/>
|
||||
|
||||
#ifndef MONOCYPHER_H
|
||||
#define MONOCYPHER_H
|
||||
|
||||
#include <stddef.h>
|
||||
#include <stdint.h>
|
||||
|
||||
#ifdef MONOCYPHER_CPP_NAMESPACE
|
||||
namespace MONOCYPHER_CPP_NAMESPACE {
|
||||
#elif defined(__cplusplus)
|
||||
extern "C" {
|
||||
#endif
|
||||
|
||||
// Constant time comparisons
|
||||
// -------------------------
|
||||
|
||||
// Return 0 if a and b are equal, -1 otherwise
|
||||
int crypto_verify16(const uint8_t a[16], const uint8_t b[16]);
|
||||
int crypto_verify32(const uint8_t a[32], const uint8_t b[32]);
|
||||
int crypto_verify64(const uint8_t a[64], const uint8_t b[64]);
|
||||
|
||||
|
||||
// Erase sensitive data
|
||||
// --------------------
|
||||
void crypto_wipe(void *secret, size_t size);
|
||||
|
||||
|
||||
// Authenticated encryption
|
||||
// ------------------------
|
||||
void crypto_aead_lock(uint8_t *cipher_text,
|
||||
uint8_t mac [16],
|
||||
const uint8_t key [32],
|
||||
const uint8_t nonce[24],
|
||||
const uint8_t *ad, size_t ad_size,
|
||||
const uint8_t *plain_text, size_t text_size);
|
||||
int crypto_aead_unlock(uint8_t *plain_text,
|
||||
const uint8_t mac [16],
|
||||
const uint8_t key [32],
|
||||
const uint8_t nonce[24],
|
||||
const uint8_t *ad, size_t ad_size,
|
||||
const uint8_t *cipher_text, size_t text_size);
|
||||
|
||||
// Authenticated stream
|
||||
// --------------------
|
||||
typedef struct {
|
||||
uint64_t counter;
|
||||
uint8_t key[32];
|
||||
uint8_t nonce[8];
|
||||
} crypto_aead_ctx;
|
||||
|
||||
void crypto_aead_init_x(crypto_aead_ctx *ctx,
|
||||
const uint8_t key[32], const uint8_t nonce[24]);
|
||||
void crypto_aead_init_djb(crypto_aead_ctx *ctx,
|
||||
const uint8_t key[32], const uint8_t nonce[8]);
|
||||
void crypto_aead_init_ietf(crypto_aead_ctx *ctx,
|
||||
const uint8_t key[32], const uint8_t nonce[12]);
|
||||
|
||||
void crypto_aead_write(crypto_aead_ctx *ctx,
|
||||
uint8_t *cipher_text,
|
||||
uint8_t mac[16],
|
||||
const uint8_t *ad , size_t ad_size,
|
||||
const uint8_t *plain_text, size_t text_size);
|
||||
int crypto_aead_read(crypto_aead_ctx *ctx,
|
||||
uint8_t *plain_text,
|
||||
const uint8_t mac[16],
|
||||
const uint8_t *ad , size_t ad_size,
|
||||
const uint8_t *cipher_text, size_t text_size);
|
||||
|
||||
|
||||
// General purpose hash (BLAKE2b)
|
||||
// ------------------------------
|
||||
|
||||
// Direct interface
|
||||
void crypto_blake2b(uint8_t *hash, size_t hash_size,
|
||||
const uint8_t *message, size_t message_size);
|
||||
|
||||
void crypto_blake2b_keyed(uint8_t *hash, size_t hash_size,
|
||||
const uint8_t *key, size_t key_size,
|
||||
const uint8_t *message, size_t message_size);
|
||||
|
||||
// Incremental interface
|
||||
typedef struct {
|
||||
// Do not rely on the size or contents of this type,
|
||||
// for they may change without notice.
|
||||
uint64_t hash[8];
|
||||
uint64_t input_offset[2];
|
||||
uint64_t input[16];
|
||||
size_t input_idx;
|
||||
size_t hash_size;
|
||||
} crypto_blake2b_ctx;
|
||||
|
||||
void crypto_blake2b_init(crypto_blake2b_ctx *ctx, size_t hash_size);
|
||||
void crypto_blake2b_keyed_init(crypto_blake2b_ctx *ctx, size_t hash_size,
|
||||
const uint8_t *key, size_t key_size);
|
||||
void crypto_blake2b_update(crypto_blake2b_ctx *ctx,
|
||||
const uint8_t *message, size_t message_size);
|
||||
void crypto_blake2b_final(crypto_blake2b_ctx *ctx, uint8_t *hash);
|
||||
|
||||
|
||||
// Password key derivation (Argon2)
|
||||
// --------------------------------
|
||||
#define CRYPTO_ARGON2_D 0
|
||||
#define CRYPTO_ARGON2_I 1
|
||||
#define CRYPTO_ARGON2_ID 2
|
||||
|
||||
typedef struct {
|
||||
uint32_t algorithm; // Argon2d, Argon2i, Argon2id
|
||||
uint32_t nb_blocks; // memory hardness, >= 8 * nb_lanes
|
||||
uint32_t nb_passes; // CPU hardness, >= 1 (>= 3 recommended for Argon2i)
|
||||
uint32_t nb_lanes; // parallelism level (single threaded anyway)
|
||||
} crypto_argon2_config;
|
||||
|
||||
typedef struct {
|
||||
const uint8_t *pass;
|
||||
const uint8_t *salt;
|
||||
uint32_t pass_size;
|
||||
uint32_t salt_size; // 16 bytes recommended
|
||||
} crypto_argon2_inputs;
|
||||
|
||||
typedef struct {
|
||||
const uint8_t *key; // may be NULL if no key
|
||||
const uint8_t *ad; // may be NULL if no additional data
|
||||
uint32_t key_size; // 0 if no key (32 bytes recommended otherwise)
|
||||
uint32_t ad_size; // 0 if no additional data
|
||||
} crypto_argon2_extras;
|
||||
|
||||
extern const crypto_argon2_extras crypto_argon2_no_extras;
|
||||
|
||||
void crypto_argon2(uint8_t *hash, uint32_t hash_size, void *work_area,
|
||||
crypto_argon2_config config,
|
||||
crypto_argon2_inputs inputs,
|
||||
crypto_argon2_extras extras);
|
||||
|
||||
|
||||
// Key exchange (X-25519)
|
||||
// ----------------------
|
||||
|
||||
// Shared secrets are not quite random.
|
||||
// Hash them to derive an actual shared key.
|
||||
void crypto_x25519_public_key(uint8_t public_key[32],
|
||||
const uint8_t secret_key[32]);
|
||||
void crypto_x25519(uint8_t raw_shared_secret[32],
|
||||
const uint8_t your_secret_key [32],
|
||||
const uint8_t their_public_key [32]);
|
||||
|
||||
// Conversion to EdDSA
|
||||
void crypto_x25519_to_eddsa(uint8_t eddsa[32], const uint8_t x25519[32]);
|
||||
|
||||
// scalar "division"
|
||||
// Used for OPRF. Be aware that exponential blinding is less secure
|
||||
// than Diffie-Hellman key exchange.
|
||||
void crypto_x25519_inverse(uint8_t blind_salt [32],
|
||||
const uint8_t private_key[32],
|
||||
const uint8_t curve_point[32]);
|
||||
|
||||
// "Dirty" versions of x25519_public_key().
|
||||
// Use with crypto_elligator_rev().
|
||||
// Leaks 3 bits of the private key.
|
||||
void crypto_x25519_dirty_small(uint8_t pk[32], const uint8_t sk[32]);
|
||||
void crypto_x25519_dirty_fast (uint8_t pk[32], const uint8_t sk[32]);
|
||||
|
||||
|
||||
// Signatures
|
||||
// ----------
|
||||
|
||||
// EdDSA with curve25519 + BLAKE2b
|
||||
void crypto_eddsa_key_pair(uint8_t secret_key[64],
|
||||
uint8_t public_key[32],
|
||||
uint8_t seed[32]);
|
||||
void crypto_eddsa_sign(uint8_t signature [64],
|
||||
const uint8_t secret_key[64],
|
||||
const uint8_t *message, size_t message_size);
|
||||
int crypto_eddsa_check(const uint8_t signature [64],
|
||||
const uint8_t public_key[32],
|
||||
const uint8_t *message, size_t message_size);
|
||||
|
||||
// Conversion to X25519
|
||||
void crypto_eddsa_to_x25519(uint8_t x25519[32], const uint8_t eddsa[32]);
|
||||
|
||||
// EdDSA building blocks
|
||||
void crypto_eddsa_trim_scalar(uint8_t out[32], const uint8_t in[32]);
|
||||
void crypto_eddsa_reduce(uint8_t reduced[32], const uint8_t expanded[64]);
|
||||
void crypto_eddsa_mul_add(uint8_t r[32],
|
||||
const uint8_t a[32],
|
||||
const uint8_t b[32],
|
||||
const uint8_t c[32]);
|
||||
void crypto_eddsa_scalarbase(uint8_t point[32], const uint8_t scalar[32]);
|
||||
int crypto_eddsa_check_equation(const uint8_t signature[64],
|
||||
const uint8_t public_key[32],
|
||||
const uint8_t h_ram[32]);
|
||||
|
||||
|
||||
// Chacha20
|
||||
// --------
|
||||
|
||||
// Specialised hash.
|
||||
// Used to hash X25519 shared secrets.
|
||||
void crypto_chacha20_h(uint8_t out[32],
|
||||
const uint8_t key[32],
|
||||
const uint8_t in [16]);
|
||||
|
||||
// Unauthenticated stream cipher.
|
||||
// Don't forget to add authentication.
|
||||
uint64_t crypto_chacha20_djb(uint8_t *cipher_text,
|
||||
const uint8_t *plain_text,
|
||||
size_t text_size,
|
||||
const uint8_t key[32],
|
||||
const uint8_t nonce[8],
|
||||
uint64_t ctr);
|
||||
uint32_t crypto_chacha20_ietf(uint8_t *cipher_text,
|
||||
const uint8_t *plain_text,
|
||||
size_t text_size,
|
||||
const uint8_t key[32],
|
||||
const uint8_t nonce[12],
|
||||
uint32_t ctr);
|
||||
uint64_t crypto_chacha20_x(uint8_t *cipher_text,
|
||||
const uint8_t *plain_text,
|
||||
size_t text_size,
|
||||
const uint8_t key[32],
|
||||
const uint8_t nonce[24],
|
||||
uint64_t ctr);
|
||||
|
||||
|
||||
// Poly 1305
|
||||
// ---------
|
||||
|
||||
// This is a *one time* authenticator.
|
||||
// Disclosing the mac reveals the key.
|
||||
// See crypto_lock() on how to use it properly.
|
||||
|
||||
// Direct interface
|
||||
void crypto_poly1305(uint8_t mac[16],
|
||||
const uint8_t *message, size_t message_size,
|
||||
const uint8_t key[32]);
|
||||
|
||||
// Incremental interface
|
||||
typedef struct {
|
||||
// Do not rely on the size or contents of this type,
|
||||
// for they may change without notice.
|
||||
uint8_t c[16]; // chunk of the message
|
||||
size_t c_idx; // How many bytes are there in the chunk.
|
||||
uint32_t r [4]; // constant multiplier (from the secret key)
|
||||
uint32_t pad[4]; // random number added at the end (from the secret key)
|
||||
uint32_t h [5]; // accumulated hash
|
||||
} crypto_poly1305_ctx;
|
||||
|
||||
void crypto_poly1305_init (crypto_poly1305_ctx *ctx, const uint8_t key[32]);
|
||||
void crypto_poly1305_update(crypto_poly1305_ctx *ctx,
|
||||
const uint8_t *message, size_t message_size);
|
||||
void crypto_poly1305_final (crypto_poly1305_ctx *ctx, uint8_t mac[16]);
|
||||
|
||||
|
||||
// Elligator 2
|
||||
// -----------
|
||||
|
||||
// Elligator mappings proper
|
||||
void crypto_elligator_map(uint8_t curve [32], const uint8_t hidden[32]);
|
||||
int crypto_elligator_rev(uint8_t hidden[32], const uint8_t curve [32],
|
||||
uint8_t tweak);
|
||||
|
||||
// Easy to use key pair generation
|
||||
void crypto_elligator_key_pair(uint8_t hidden[32], uint8_t secret_key[32],
|
||||
uint8_t seed[32]);
|
||||
|
||||
#ifdef __cplusplus
|
||||
}
|
||||
#endif
|
||||
|
||||
#endif // MONOCYPHER_H
|
||||
173
source/nota.h
@@ -29,7 +29,7 @@
|
||||
#define NOTA_EXP_SIGN(CHAR) (CHAR & (1<<4))
|
||||
#define NOTA_TYPE 0x70
|
||||
#define NOTA_HEAD_DATA 0x0f
|
||||
#define CONTINUE(CHAR) (CHAR>>7)
|
||||
#define CONTINUE(CHAR) ((CHAR)>>7)
|
||||
#define UTF8_DATA 0x3f
|
||||
|
||||
/* A helper to get the high-level Nota type nibble from a byte */
|
||||
@@ -362,177 +362,6 @@ void nota_write_record(NotaBuffer *nb, unsigned long long count)
|
||||
nb->size -= (10 - used);
|
||||
}
|
||||
|
||||
void nota_write_number_str(NotaBuffer *nb, const char *str)
|
||||
{
|
||||
/* -------------------------------------------
|
||||
1) Parse sign
|
||||
------------------------------------------- */
|
||||
int negative = 0;
|
||||
if (*str == '+') {
|
||||
str++;
|
||||
}
|
||||
else if (*str == '-') {
|
||||
negative = 1;
|
||||
str++;
|
||||
}
|
||||
|
||||
/* -------------------------------------------
|
||||
2) Parse integer part
|
||||
------------------------------------------- */
|
||||
long long coefficient = 0;
|
||||
int got_digits = 0;
|
||||
|
||||
while (*str >= '0' && *str <= '9') {
|
||||
got_digits = 1;
|
||||
int d = (*str - '0');
|
||||
str++;
|
||||
|
||||
// Basic overflow check (very naive):
|
||||
if (coefficient <= (LLONG_MAX - d) / 10) {
|
||||
coefficient = coefficient * 10 + d;
|
||||
} else {
|
||||
// If you want to handle overflow by switching to float, do that here.
|
||||
// For simplicity, let's just keep wrapping. In production, be careful!
|
||||
coefficient = coefficient * 10 + d;
|
||||
}
|
||||
}
|
||||
|
||||
/* -------------------------------------------
|
||||
3) Check for decimal part
|
||||
------------------------------------------- */
|
||||
int has_decimal_point = 0;
|
||||
int fraction_digits = 0;
|
||||
|
||||
if (*str == '.') {
|
||||
has_decimal_point = 1;
|
||||
str++;
|
||||
while (*str >= '0' && *str <= '9') {
|
||||
got_digits = 1;
|
||||
int d = (*str - '0');
|
||||
str++;
|
||||
fraction_digits++;
|
||||
if (coefficient <= (LLONG_MAX - d) / 10) {
|
||||
coefficient = coefficient * 10 + d;
|
||||
} else {
|
||||
// Same naive overflow comment
|
||||
coefficient = coefficient * 10 + d;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/* -------------------------------------------
|
||||
4) Check for exponent part
|
||||
------------------------------------------- */
|
||||
int exponent_negative = 0;
|
||||
long long exponent_val = 0;
|
||||
|
||||
if (*str == 'e' || *str == 'E') {
|
||||
str++;
|
||||
if (*str == '+') {
|
||||
str++;
|
||||
}
|
||||
else if (*str == '-') {
|
||||
exponent_negative = 1;
|
||||
str++;
|
||||
}
|
||||
while (*str >= '0' && *str <= '9') {
|
||||
int d = (*str - '0');
|
||||
str++;
|
||||
if (exponent_val <= (LLONG_MAX - d) / 10) {
|
||||
exponent_val = exponent_val * 10 + d;
|
||||
} else {
|
||||
// Again, naive overflow handling
|
||||
exponent_val = exponent_val * 10 + d;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/* -------------------------------------------
|
||||
5) If there were no valid digits at all,
|
||||
store 0 and return. (simple fallback)
|
||||
------------------------------------------- */
|
||||
if (!got_digits) {
|
||||
nota_write_int_buf(nb, 0);
|
||||
return;
|
||||
}
|
||||
|
||||
/* -------------------------------------------
|
||||
6) Combine fraction digits into exponent
|
||||
final_exponent = exponent_val - fraction_digits
|
||||
(apply exponent sign if any)
|
||||
------------------------------------------- */
|
||||
if (exponent_negative) {
|
||||
exponent_val = -exponent_val;
|
||||
}
|
||||
long long final_exponent = exponent_val - fraction_digits;
|
||||
|
||||
/* -------------------------------------------
|
||||
7) Decide if we are storing an integer
|
||||
or a float in Nota format.
|
||||
-------------------------------------------
|
||||
Rule used here:
|
||||
- If there's no decimal point AND final_exponent == 0,
|
||||
=> integer
|
||||
- If we do have a decimal point, but fraction_digits == 0
|
||||
and exponent_val == 0, then the user typed something
|
||||
like "123." or "100.0". That is effectively an integer,
|
||||
so store it as an integer if you want a purely numeric approach.
|
||||
- Otherwise store as float.
|
||||
------------------------------------------- */
|
||||
|
||||
// If "no decimal" => definitely integer:
|
||||
// or decimal present but fraction_digits=0 & exponent_val=0 => integer
|
||||
int treat_as_integer = 0;
|
||||
if (!has_decimal_point && final_exponent == 0) {
|
||||
treat_as_integer = 1;
|
||||
}
|
||||
else if (has_decimal_point && fraction_digits == 0 && exponent_val == 0) {
|
||||
// Means "123." or "123.0"
|
||||
treat_as_integer = 1;
|
||||
}
|
||||
|
||||
if (treat_as_integer) {
|
||||
// If negative => flip the sign in the stored value
|
||||
if (negative) {
|
||||
coefficient = -coefficient;
|
||||
}
|
||||
// Write the integer in Nota format (varint with sign bit)
|
||||
nota_write_int_buf(nb, coefficient);
|
||||
return;
|
||||
}
|
||||
|
||||
/* -------------------------------------------
|
||||
8) Write as float in Nota format
|
||||
We do basically the same approach as
|
||||
nota_write_float_buf does:
|
||||
- NOTA_FLOAT nibble
|
||||
- sign bit if negative
|
||||
- exponent sign bit if final_exponent < 0
|
||||
- varint of |final_exponent|
|
||||
- varint of |coefficient|
|
||||
------------------------------------------- */
|
||||
{
|
||||
char *p = nota_buffer_alloc(nb, 21); // Up to ~21 bytes worst-case
|
||||
p[0] = NOTA_FLOAT;
|
||||
if (negative) {
|
||||
p[0] |= (1 << 3); // Mantissa sign bit
|
||||
}
|
||||
if (final_exponent < 0) {
|
||||
p[0] |= (1 << 4); // Exponent sign bit
|
||||
final_exponent = -final_exponent;
|
||||
}
|
||||
// Write exponent as varint (with 3 bits used in the first byte)
|
||||
char *c = nota_continue_num(final_exponent, p, 3);
|
||||
// Write the absolute coefficient (7 bits used in the first byte)
|
||||
char *end = nota_continue_num(coefficient, c, 7);
|
||||
|
||||
// Adjust the buffer size to the actual used length
|
||||
size_t used = (size_t)(end - p);
|
||||
nb->size -= (21 - used);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
void nota_write_number(NotaBuffer *nb, double n)
|
||||
{
|
||||
nota_write_int_or_float_buf(nb, n);
|
||||
|
||||
1477
source/prosperon.c
@@ -1,91 +0,0 @@
|
||||
#ifndef PROSPERON_H
|
||||
#define PROSPERON_H
|
||||
|
||||
#include <SDL3/SDL.h>
|
||||
#include "quickjs.h"
|
||||
|
||||
#define STATE_VECTOR_LENGTH 624
|
||||
#define STATE_VECTOR_M 397
|
||||
|
||||
#define ACTOR_IDLE 0
|
||||
#define ACTOR_READY 1
|
||||
#define ACTOR_RUNNING 2
|
||||
#define ACTOR_EXHAUSTED 3
|
||||
#define ACTOR_RECLAIMING 4
|
||||
#define ACTOR_SLOW 5
|
||||
|
||||
typedef JSValue (*MODULEFN)(JSContext *js);
|
||||
|
||||
typedef struct tagMTRand {
|
||||
uint32_t mt[STATE_VECTOR_LENGTH];
|
||||
int32_t index;
|
||||
} MTRand;
|
||||
|
||||
typedef struct {
|
||||
const char *name;
|
||||
MODULEFN fn;
|
||||
} ModuleEntry;
|
||||
|
||||
typedef struct {
|
||||
int argc;
|
||||
char **argv;
|
||||
} cmdargs;
|
||||
|
||||
typedef struct prosperon_rt {
|
||||
cmdargs cmd;
|
||||
JSContext *context;
|
||||
JSValue cycle_fn;
|
||||
JSValue idx_buffer;
|
||||
JSValue on_exception;
|
||||
JSValue message_handle;
|
||||
JSValue unneeded;
|
||||
|
||||
ModuleEntry *module_registry;
|
||||
JSValue *js_swapchains;
|
||||
|
||||
/* Protects JSContext usage */
|
||||
SDL_Mutex *mutex;
|
||||
|
||||
SDL_Mutex *turn;
|
||||
|
||||
char *id;
|
||||
MTRand mrand;
|
||||
double unneeded_secs;
|
||||
int idx_count;
|
||||
|
||||
/* The “mailbox” for incoming messages + a dedicated lock for it: */
|
||||
void **messages;
|
||||
JSValue *events;
|
||||
SDL_Mutex *msg_mutex; /* For messages queue only */
|
||||
|
||||
/* CHANGED FOR EVENTS: a separate lock for the actor->events queue */
|
||||
struct { Uint32 key; JSValue value; } *timers;
|
||||
|
||||
int state;
|
||||
Uint32 ar;
|
||||
int need_stop;
|
||||
int main_thread_only;
|
||||
} prosperon_rt;
|
||||
|
||||
extern SDL_ThreadID main_thread;
|
||||
extern SDL_TLSID prosperon_id;
|
||||
|
||||
prosperon_rt *create_actor(int argc, char **argv);
|
||||
const char *register_actor(const char *id, prosperon_rt *actor, int mainthread);
|
||||
void actor_free(prosperon_rt *actor);
|
||||
const char *send_message(const char *id, void *msg);
|
||||
Uint32 actor_timer_cb(prosperon_rt *actor, SDL_TimerID id, Uint32 interval);
|
||||
JSValue js_actor_delay(JSContext *js, JSValue self, int argc, JSValue *argv);
|
||||
JSValue js_actor_removetimer(JSContext *js, JSValue self, int argc, JSValue *argv);
|
||||
void script_startup(prosperon_rt *rt);
|
||||
void script_evalf(JSContext *js, const char *format, ...);
|
||||
JSValue script_eval(JSContext *js, const char *file, const char *script);
|
||||
int uncaught_exception(JSContext *js, JSValue v);
|
||||
int actor_exists(const char *id);
|
||||
void set_actor_state(prosperon_rt *actor);
|
||||
|
||||
int prosperon_mount_core(void);
|
||||
|
||||
JSValue js_os_ioactor(JSContext *js, JSValue self, int argc, JSValue *argv);
|
||||
|
||||
#endif
|
||||
@@ -1,115 +0,0 @@
|
||||
#include "qjs_actor.h"
|
||||
#include "jsffi.h"
|
||||
#include "qjs_macros.h"
|
||||
#include "qjs_wota.h"
|
||||
#include "prosperon.h"
|
||||
|
||||
#include <SDL3/SDL.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
|
||||
// External variables
|
||||
extern prosperon_rt *io_actor;
|
||||
|
||||
// External function declarations
|
||||
JSValue js_actor_delay(JSContext *js, JSValue self, int argc, JSValue *argv);
|
||||
JSValue js_actor_removetimer(JSContext *js, JSValue self, int argc, JSValue *argv);
|
||||
JSValue js_os_ioactor(JSContext *js, JSValue self, int argc, JSValue *argv);
|
||||
|
||||
JSC_CCALL(os_createactor,
|
||||
int margc = JS_ArrayLength(js, argv[0]);
|
||||
|
||||
char **margv = malloc(margc*sizeof(char*));
|
||||
|
||||
for (int i = 0; i < margc; i++) {
|
||||
JSValue val = JS_GetPropertyUint32(js, argv[0], i);
|
||||
const char *cstr = JS_ToCString(js,val);
|
||||
margv[i] = strdup(cstr);
|
||||
JS_FreeCString(js,cstr);
|
||||
JS_FreeValue(js,val);
|
||||
}
|
||||
|
||||
create_actor(margc, margv);
|
||||
)
|
||||
|
||||
JSC_CCALL(os_mailbox_push,
|
||||
if (argc < 2) return JS_ThrowInternalError(js, "Need an actor and a message");
|
||||
if (!JS_IsObject(argv[1])) return JS_ThrowInternalError(js, "Object to push must be an object.");
|
||||
|
||||
const char *id = JS_ToCString(js, argv[0]);
|
||||
int exist = actor_exists(id);
|
||||
JS_FreeCString(js,id);
|
||||
if (!exist)
|
||||
return JS_ThrowInternalError(js, "No mailbox found for given ID");
|
||||
|
||||
void *data = value2wota(js, argv[1], JS_UNDEFINED);
|
||||
|
||||
const char *err = send_message(id, data);
|
||||
if (err) {
|
||||
free(data);
|
||||
return JS_ThrowInternalError(js, "Could not send message: %s", err);
|
||||
}
|
||||
)
|
||||
|
||||
JSC_CCALL(os_register_actor,
|
||||
prosperon_rt *rt = JS_GetContextOpaque(js);
|
||||
const char *id = JS_ToCString(js, argv[0]);
|
||||
const char *err = register_actor(id, rt, JS_ToBool(js, argv[2]));
|
||||
if (err) return JS_ThrowInternalError(js, "Could not register actor: %s", err);
|
||||
rt->message_handle = JS_DupValue(js, argv[1]);
|
||||
rt->context = js;
|
||||
JS_FreeCString(js, id);
|
||||
)
|
||||
|
||||
JSC_CCALL(os_mailbox_exist,
|
||||
const char *id = JS_ToCString(js, argv[0]);
|
||||
int exist = actor_exists(id);
|
||||
JS_FreeCString(js, id);
|
||||
|
||||
return JS_NewBool(js, exist);
|
||||
)
|
||||
|
||||
JSC_CCALL(os_unneeded,
|
||||
prosperon_rt *actor = JS_GetContextOpaque(js);
|
||||
SDL_LockMutex(actor->msg_mutex);
|
||||
JS_FreeValue(js, actor->unneeded);
|
||||
|
||||
if (!JS_IsFunction(js, argv[0])) {
|
||||
actor->unneeded = JS_UNDEFINED;
|
||||
goto END;
|
||||
}
|
||||
|
||||
actor->unneeded = JS_DupValue(js, argv[0]);
|
||||
JS_ToFloat64(js, &actor->unneeded_secs, argv[1]);
|
||||
|
||||
END:
|
||||
if (actor->ar) {
|
||||
SDL_RemoveTimer(actor->ar);
|
||||
actor->ar = 0;
|
||||
}
|
||||
set_actor_state(actor);
|
||||
SDL_UnlockMutex(actor->msg_mutex);
|
||||
)
|
||||
|
||||
JSC_CCALL(os_destroy,
|
||||
prosperon_rt *rt = JS_GetContextOpaque(js);
|
||||
rt->need_stop = 1;
|
||||
)
|
||||
|
||||
static const JSCFunctionListEntry js_actor_funcs[] = {
|
||||
MIST_FUNC_DEF(os, createactor, 1),
|
||||
MIST_FUNC_DEF(os, mailbox_push, 2),
|
||||
MIST_FUNC_DEF(os, mailbox_exist, 1),
|
||||
MIST_FUNC_DEF(actor, delay, 2),
|
||||
MIST_FUNC_DEF(actor, removetimer, 1),
|
||||
MIST_FUNC_DEF(os, register_actor, 2),
|
||||
MIST_FUNC_DEF(os, unneeded, 2),
|
||||
MIST_FUNC_DEF(os, destroy, 0),
|
||||
MIST_FUNC_DEF(os, ioactor, 0),
|
||||
};
|
||||
|
||||
JSValue js_actor_use(JSContext *js) {
|
||||
JSValue mod = JS_NewObject(js);
|
||||
JS_SetPropertyFunctionList(js,mod,js_actor_funcs,countof(js_actor_funcs));
|
||||
return mod;
|
||||
}
|
||||
@@ -1,8 +0,0 @@
|
||||
#ifndef QJS_ACTOR_H
|
||||
#define QJS_ACTOR_H
|
||||
|
||||
#include <quickjs.h>
|
||||
|
||||
JSValue js_actor_use(JSContext *js);
|
||||
|
||||
#endif
|
||||
@@ -1,460 +0,0 @@
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
#include <stdint.h>
|
||||
#include "quickjs.h"
|
||||
#include "qjs_blob.h"
|
||||
|
||||
// -----------------------------------------------------------------------------
|
||||
// A simple blob structure that can be in two states:
|
||||
// - antestone (mutable): writing is allowed
|
||||
// - stone (immutable): reading is allowed
|
||||
//
|
||||
// The blob is stored as an array of bits in memory, but for simplicity here,
|
||||
// we store them in a dynamic byte array with a bit_length and capacity in bits.
|
||||
//
|
||||
// This is a minimal demonstration. Real usage might require more sophisticated
|
||||
// memory or bit manipulation for performance.
|
||||
// -----------------------------------------------------------------------------
|
||||
|
||||
typedef struct {
|
||||
// The actual buffer holding the bits (in multiples of 8 bits).
|
||||
uint8_t *data;
|
||||
|
||||
// The total number of bits currently in use (the "length" of the blob).
|
||||
size_t bit_length;
|
||||
|
||||
// The total capacity in bits that 'data' can currently hold without realloc.
|
||||
size_t bit_capacity;
|
||||
|
||||
// 0 = antestone (mutable)
|
||||
// 1 = stone (immutable)
|
||||
int is_stone;
|
||||
} JSBlobData;
|
||||
|
||||
// Forward declaration of class ID and methods
|
||||
static JSClassID js_blob_class_id;
|
||||
|
||||
// Helper to ensure capacity for writing
|
||||
// new_bits is additional bits to be appended
|
||||
static int js_blob_ensure_capacity(JSContext *ctx, JSBlobData *bd, size_t new_bits) {
|
||||
size_t need_bits = bd->bit_length + new_bits;
|
||||
if (need_bits <= bd->bit_capacity) return 0;
|
||||
|
||||
// Increase capacity (in multiples of bytes).
|
||||
// We can pick a growth strategy. For demonstration, double it:
|
||||
size_t new_capacity = bd->bit_capacity == 0 ? 64 : bd->bit_capacity * 2;
|
||||
while (new_capacity < need_bits) new_capacity *= 2;
|
||||
|
||||
// Round up new_capacity to a multiple of 8 bits
|
||||
if (new_capacity % 8) {
|
||||
new_capacity += 8 - (new_capacity % 8);
|
||||
}
|
||||
|
||||
size_t new_size_bytes = new_capacity / 8;
|
||||
uint8_t *new_ptr = realloc(bd->data, new_size_bytes);
|
||||
if (!new_ptr) {
|
||||
return -1; // out of memory
|
||||
}
|
||||
// zero-fill the new area (only beyond the old capacity)
|
||||
size_t old_size_bytes = bd->bit_capacity / 8;
|
||||
if (new_size_bytes > old_size_bytes) {
|
||||
memset(new_ptr + old_size_bytes, 0, new_size_bytes - old_size_bytes);
|
||||
}
|
||||
bd->data = new_ptr;
|
||||
bd->bit_capacity = new_capacity;
|
||||
return 0;
|
||||
}
|
||||
|
||||
// Finalizer for JSBlobData
|
||||
static void js_blob_finalizer(JSRuntime *rt, JSValue val) {
|
||||
JSBlobData *bd = JS_GetOpaque(val, js_blob_class_id);
|
||||
if (bd) {
|
||||
free(bd->data);
|
||||
bd->data = NULL;
|
||||
bd->bit_length = 0;
|
||||
bd->bit_capacity = 0;
|
||||
bd->is_stone = 0;
|
||||
free(bd);
|
||||
}
|
||||
}
|
||||
|
||||
// Mark function: not used here, as we have no child JS objects in JSBlobData
|
||||
static void js_blob_mark(JSRuntime *rt, JSValueConst val, JS_MarkFunc *mark_func) {
|
||||
// No child JS references to mark
|
||||
}
|
||||
|
||||
// A helper to create a new JSBlobData object, returning a JSValue wrapping it.
|
||||
static JSValue js_blob_wrap(JSContext *ctx, JSBlobData *bd) {
|
||||
JSValue obj = JS_NewObjectClass(ctx, js_blob_class_id);
|
||||
if (JS_IsException(obj)) {
|
||||
free(bd->data);
|
||||
free(bd);
|
||||
return obj;
|
||||
}
|
||||
JS_SetOpaque(obj, bd);
|
||||
return obj;
|
||||
}
|
||||
|
||||
// -----------------------------------------------------------------------------
|
||||
// Helpers for reading/writing bits
|
||||
// -----------------------------------------------------------------------------
|
||||
|
||||
// Write one bit (0 or 1) at the end of the blob
|
||||
static int js_blob_write_bit_internal(JSContext *ctx, JSBlobData *bd, int bit_val) {
|
||||
if (bd->is_stone) {
|
||||
// Trying to write to an immutable blob -> throw
|
||||
return -1;
|
||||
}
|
||||
if (js_blob_ensure_capacity(ctx, bd, 1) < 0) {
|
||||
return -1;
|
||||
}
|
||||
// index in bits
|
||||
size_t bit_index = bd->bit_length;
|
||||
size_t byte_index = bit_index >> 3;
|
||||
size_t offset_in_byte = bit_index & 7;
|
||||
|
||||
// set or clear bit
|
||||
if (bit_val)
|
||||
bd->data[byte_index] |= (1 << offset_in_byte);
|
||||
else
|
||||
bd->data[byte_index] &= ~(1 << offset_in_byte);
|
||||
|
||||
bd->bit_length++;
|
||||
return 0;
|
||||
}
|
||||
|
||||
// Read one bit from a stone blob at position 'pos'
|
||||
static int js_blob_read_bit_internal(JSBlobData *bd, size_t pos, int *out_bit) {
|
||||
if (!bd->is_stone) {
|
||||
// It's not stone -> reading might be out of the specification
|
||||
// but we can allow or return error. Here we just return error.
|
||||
return -1;
|
||||
}
|
||||
if (pos >= bd->bit_length) {
|
||||
return -1; // out of range
|
||||
}
|
||||
size_t byte_index = pos >> 3;
|
||||
size_t offset_in_byte = pos & 7;
|
||||
*out_bit = (bd->data[byte_index] & (1 << offset_in_byte)) ? 1 : 0;
|
||||
return 0;
|
||||
}
|
||||
|
||||
// Turn a blob into the "stone" state. This discards any extra capacity.
|
||||
static void js_blob_make_stone(JSBlobData *bd) {
|
||||
bd->is_stone = 1;
|
||||
// Optionally shrink the buffer to exactly bit_length in size
|
||||
if (bd->bit_capacity > bd->bit_length) {
|
||||
size_t size_in_bytes = (bd->bit_length + 7) >> 3; // round up to full bytes
|
||||
uint8_t *new_ptr = NULL;
|
||||
if (size_in_bytes) {
|
||||
new_ptr = realloc(bd->data, size_in_bytes);
|
||||
if (new_ptr) {
|
||||
bd->data = new_ptr;
|
||||
}
|
||||
} else {
|
||||
// zero length
|
||||
free(bd->data);
|
||||
bd->data = NULL;
|
||||
}
|
||||
bd->bit_capacity = bd->bit_length; // capacity in bits now matches length
|
||||
}
|
||||
}
|
||||
|
||||
// -----------------------------------------------------------------------------
|
||||
// JS Functions (blob.make, blob.write_bit, blob.read_logical, etc.)
|
||||
// -----------------------------------------------------------------------------
|
||||
|
||||
// blob.make(...)
|
||||
static JSValue js_blob_make(JSContext *ctx, JSValueConst this_val,
|
||||
int argc, JSValueConst *argv) {
|
||||
// We'll implement a few typical signatures:
|
||||
// blob.make()
|
||||
// blob.make(capacity)
|
||||
// blob.make(length, logical_value)
|
||||
// blob.make(blob, from, to) (makes a copy)
|
||||
//
|
||||
// This is a simplified approach. The spec mentions random, partial copy, etc.
|
||||
// We'll handle just these forms enough to demonstrate the concept.
|
||||
|
||||
JSBlobData *bd = calloc(1, sizeof(*bd));
|
||||
if (!bd) return JS_ThrowOutOfMemory(ctx);
|
||||
|
||||
// default
|
||||
bd->data = NULL;
|
||||
bd->bit_length = 0;
|
||||
bd->bit_capacity = 0;
|
||||
bd->is_stone = 0; // initially antestone
|
||||
|
||||
// blob.make()
|
||||
if (argc == 0) {
|
||||
// empty antestone blob
|
||||
}
|
||||
// blob.make(capacity)
|
||||
else if (argc == 1 && JS_IsNumber(argv[0])) {
|
||||
int64_t capacity_bits;
|
||||
if (JS_ToInt64(ctx, &capacity_bits, argv[0]) < 0) {
|
||||
free(bd);
|
||||
return JS_EXCEPTION;
|
||||
}
|
||||
if (capacity_bits < 0) capacity_bits = 0;
|
||||
bd->bit_capacity = (size_t)capacity_bits;
|
||||
if (bd->bit_capacity % 8) {
|
||||
bd->bit_capacity += 8 - (bd->bit_capacity % 8);
|
||||
}
|
||||
if (bd->bit_capacity) {
|
||||
size_t bytes = bd->bit_capacity / 8;
|
||||
bd->data = calloc(bytes, 1);
|
||||
if (!bd->data) {
|
||||
free(bd);
|
||||
return JS_ThrowOutOfMemory(ctx);
|
||||
}
|
||||
}
|
||||
}
|
||||
// blob.make(length, logical)
|
||||
else if (argc == 2 && JS_IsNumber(argv[0]) && JS_IsBool(argv[1])) {
|
||||
int64_t length_bits;
|
||||
if (JS_ToInt64(ctx, &length_bits, argv[0]) < 0) {
|
||||
free(bd);
|
||||
return JS_EXCEPTION;
|
||||
}
|
||||
if (length_bits < 0) length_bits = 0;
|
||||
int is_one = JS_ToBool(ctx, argv[1]);
|
||||
bd->bit_length = (size_t)length_bits;
|
||||
bd->bit_capacity = bd->bit_length;
|
||||
if (bd->bit_capacity % 8) {
|
||||
bd->bit_capacity += 8 - (bd->bit_capacity % 8);
|
||||
}
|
||||
size_t bytes = bd->bit_capacity / 8;
|
||||
if (bytes) {
|
||||
bd->data = malloc(bytes);
|
||||
if (!bd->data) {
|
||||
free(bd);
|
||||
return JS_ThrowOutOfMemory(ctx);
|
||||
}
|
||||
memset(bd->data, is_one ? 0xff : 0x00, bytes);
|
||||
// if length_bits isn't a multiple of 8, we need to clear the unused bits
|
||||
size_t used_bits_in_last_byte = (size_t)length_bits & 7;
|
||||
if (used_bits_in_last_byte && is_one) {
|
||||
// clear top bits in the last byte
|
||||
uint8_t mask = (1 << used_bits_in_last_byte) - 1;
|
||||
bd->data[bytes - 1] &= mask;
|
||||
}
|
||||
}
|
||||
}
|
||||
// blob.make(blob, from, to)
|
||||
else if (argc >= 1 && JS_IsObject(argv[0])) {
|
||||
// we try copying from another blob if it's of the same class
|
||||
JSBlobData *src = JS_GetOpaque(argv[0], js_blob_class_id);
|
||||
if (!src) {
|
||||
free(bd);
|
||||
return JS_ThrowTypeError(ctx, "blob.make: argument 1 not a blob");
|
||||
}
|
||||
int64_t from = 0, to = (int64_t)src->bit_length;
|
||||
if (argc >= 2 && JS_IsNumber(argv[1])) {
|
||||
JS_ToInt64(ctx, &from, argv[1]);
|
||||
if (from < 0) from = 0;
|
||||
}
|
||||
if (argc >= 3 && JS_IsNumber(argv[2])) {
|
||||
JS_ToInt64(ctx, &to, argv[2]);
|
||||
if (to < from) to = from;
|
||||
if (to > (int64_t)src->bit_length) to = (int64_t)src->bit_length;
|
||||
}
|
||||
size_t copy_len = (size_t)(to - from);
|
||||
bd->bit_length = copy_len;
|
||||
bd->bit_capacity = copy_len;
|
||||
if (bd->bit_capacity % 8) {
|
||||
bd->bit_capacity += 8 - (bd->bit_capacity % 8);
|
||||
}
|
||||
size_t bytes = bd->bit_capacity / 8;
|
||||
if (bytes) {
|
||||
bd->data = calloc(bytes, 1);
|
||||
if (!bd->data) {
|
||||
free(bd);
|
||||
return JS_ThrowOutOfMemory(ctx);
|
||||
}
|
||||
}
|
||||
// Now copy the bits.
|
||||
// For simplicity, let's do a naive bit copy one by one:
|
||||
for (size_t i = 0; i < copy_len; i++) {
|
||||
size_t src_bit_index = from + i;
|
||||
size_t src_byte = src_bit_index >> 3;
|
||||
size_t src_off = src_bit_index & 7;
|
||||
int bit_val = (src->data[src_byte] >> src_off) & 1;
|
||||
size_t dst_byte = i >> 3;
|
||||
size_t dst_off = i & 7;
|
||||
if (bit_val) {
|
||||
bd->data[dst_byte] |= (1 << dst_off);
|
||||
} else {
|
||||
bd->data[dst_byte] &= ~(1 << dst_off);
|
||||
}
|
||||
}
|
||||
}
|
||||
// else fail
|
||||
else {
|
||||
free(bd);
|
||||
return JS_ThrowTypeError(ctx, "blob.make: invalid arguments");
|
||||
}
|
||||
|
||||
return js_blob_wrap(ctx, bd);
|
||||
}
|
||||
|
||||
// blob.write_bit(blob, logical)
|
||||
static JSValue js_blob_write_bit(JSContext *ctx, JSValueConst this_val,
|
||||
int argc, JSValueConst *argv) {
|
||||
if (argc < 2) {
|
||||
return JS_ThrowTypeError(ctx, "blob.write_bit(blob, logical) requires 2 arguments");
|
||||
}
|
||||
JSBlobData *bd = JS_GetOpaque(argv[0], js_blob_class_id);
|
||||
if (!bd) {
|
||||
return JS_ThrowTypeError(ctx, "blob.write_bit: argument 1 not a blob");
|
||||
}
|
||||
int bit_val = JS_ToBool(ctx, argv[1]); // interpret any truthy as 1, else 0
|
||||
if (js_blob_write_bit_internal(ctx, bd, bit_val) < 0) {
|
||||
return JS_ThrowTypeError(ctx, "blob.write_bit: cannot write (maybe stone or OOM)");
|
||||
}
|
||||
return JS_UNDEFINED;
|
||||
}
|
||||
|
||||
// blob.read_logical(blob, from)
|
||||
static JSValue js_blob_read_logical(JSContext *ctx, JSValueConst this_val,
|
||||
int argc, JSValueConst *argv) {
|
||||
if (argc < 2) {
|
||||
return JS_ThrowTypeError(ctx, "blob.read_logical(blob, from) requires 2 arguments");
|
||||
}
|
||||
JSBlobData *bd = JS_GetOpaque(argv[0], js_blob_class_id);
|
||||
if (!bd) {
|
||||
return JS_ThrowTypeError(ctx, "blob.read_logical: argument 1 not a blob");
|
||||
}
|
||||
int64_t pos;
|
||||
if (JS_ToInt64(ctx, &pos, argv[1]) < 0) {
|
||||
return JS_EXCEPTION;
|
||||
}
|
||||
if (pos < 0) {
|
||||
return JS_NULL; // out of range
|
||||
}
|
||||
int bit_val;
|
||||
if (js_blob_read_bit_internal(bd, (size_t)pos, &bit_val) < 0) {
|
||||
return JS_NULL; // error or out of range
|
||||
}
|
||||
return JS_NewBool(ctx, bit_val);
|
||||
}
|
||||
|
||||
// blob.stone(blob)
|
||||
static JSValue js_blob_stone(JSContext *ctx, JSValueConst this_val,
|
||||
int argc, JSValueConst *argv) {
|
||||
if (argc < 1) {
|
||||
return JS_ThrowTypeError(ctx, "blob.stone(blob) requires 1 argument");
|
||||
}
|
||||
JSBlobData *bd = JS_GetOpaque(argv[0], js_blob_class_id);
|
||||
if (!bd) {
|
||||
return JS_ThrowTypeError(ctx, "blob.stone: argument not a blob");
|
||||
}
|
||||
if (!bd->is_stone) {
|
||||
js_blob_make_stone(bd);
|
||||
}
|
||||
return JS_UNDEFINED;
|
||||
}
|
||||
|
||||
// blob.length(blob)
|
||||
// Return number of bits in the blob
|
||||
static JSValue js_blob_length(JSContext *ctx, JSValueConst this_val,
|
||||
int argc, JSValueConst *argv) {
|
||||
if (argc < 1) {
|
||||
return JS_ThrowTypeError(ctx, "blob.length(blob) requires 1 argument");
|
||||
}
|
||||
JSBlobData *bd = JS_GetOpaque(argv[0], js_blob_class_id);
|
||||
if (!bd) {
|
||||
return JS_ThrowTypeError(ctx, "blob.length: argument not a blob");
|
||||
}
|
||||
return JS_NewInt64(ctx, bd->bit_length);
|
||||
}
|
||||
|
||||
// blob.blob?(value)
|
||||
// Return true if the value is a blob object
|
||||
static JSValue js_blob_is_blob(JSContext *ctx, JSValueConst this_val,
|
||||
int argc, JSValueConst *argv) {
|
||||
if (argc < 1) return JS_FALSE;
|
||||
JSBlobData *bd = JS_GetOpaque(argv[0], js_blob_class_id);
|
||||
return JS_NewBool(ctx, bd != NULL);
|
||||
}
|
||||
|
||||
// -----------------------------------------------------------------------------
|
||||
// Exports list
|
||||
// -----------------------------------------------------------------------------
|
||||
|
||||
static const JSCFunctionListEntry js_blob_funcs[] = {
|
||||
// The "make" function.
|
||||
JS_CFUNC_DEF("make", 3, js_blob_make),
|
||||
// Some example read/write routines
|
||||
JS_CFUNC_DEF("write_bit", 2, js_blob_write_bit),
|
||||
JS_CFUNC_DEF("read_logical", 2, js_blob_read_logical),
|
||||
// Convert blob from antestone -> stone
|
||||
JS_CFUNC_DEF("stone", 1, js_blob_stone),
|
||||
// Return the length in bits
|
||||
JS_CFUNC_DEF("length", 1, js_blob_length),
|
||||
// Check if a value is a blob
|
||||
JS_CFUNC_DEF("isblob", 1, js_blob_is_blob),
|
||||
};
|
||||
|
||||
// -----------------------------------------------------------------------------
|
||||
// Class definition for the 'blob' objects
|
||||
// -----------------------------------------------------------------------------
|
||||
|
||||
static JSClassDef js_blob_class = {
|
||||
"BlobClass",
|
||||
.finalizer = js_blob_finalizer,
|
||||
.gc_mark = js_blob_mark,
|
||||
};
|
||||
|
||||
// Module init function
|
||||
static int js_blob_init(JSContext *ctx, JSModuleDef *m) {
|
||||
// Register the class if not already done
|
||||
if (JS_IsRegisteredClass(ctx, js_blob_class_id) == 0) {
|
||||
JS_NewClassID(&js_blob_class_id);
|
||||
JS_NewClass(JS_GetRuntime(ctx), js_blob_class_id, &js_blob_class);
|
||||
}
|
||||
|
||||
// Create a prototype object
|
||||
JSValue proto = JS_NewObject(ctx);
|
||||
JS_SetClassProto(ctx, js_blob_class_id, proto);
|
||||
|
||||
// Export our functions as named exports
|
||||
JS_SetModuleExportList(ctx, m, js_blob_funcs, sizeof(js_blob_funcs) / sizeof(js_blob_funcs[0]));
|
||||
return 0;
|
||||
}
|
||||
|
||||
// The module entry point
|
||||
#ifdef JS_SHARED_LIBRARY
|
||||
#define JS_INIT_MODULE js_init_module
|
||||
#else
|
||||
#define JS_INIT_MODULE js_init_module_blob
|
||||
#endif
|
||||
|
||||
JSModuleDef *JS_INIT_MODULE(JSContext *ctx, const char *module_name) {
|
||||
JSModuleDef *m = JS_NewCModule(ctx, module_name, js_blob_init);
|
||||
if (!m) return NULL;
|
||||
JS_AddModuleExportList(ctx, m, js_blob_funcs, sizeof(js_blob_funcs) / sizeof(js_blob_funcs[0]));
|
||||
return m;
|
||||
}
|
||||
|
||||
// -----------------------------------------------------------------------------
|
||||
// js_blob_use(ctx) for easy embedding: returns an object with the blob functions
|
||||
// -----------------------------------------------------------------------------
|
||||
|
||||
JSValue js_blob_use(JSContext *ctx) {
|
||||
// Ensure class is registered
|
||||
if (JS_IsRegisteredClass(ctx, js_blob_class_id) == 0) {
|
||||
JS_NewClassID(&js_blob_class_id);
|
||||
JS_NewClass(JS_GetRuntime(ctx), js_blob_class_id, &js_blob_class);
|
||||
|
||||
// Create a prototype object
|
||||
JSValue proto = JS_NewObject(ctx);
|
||||
JS_SetClassProto(ctx, js_blob_class_id, proto);
|
||||
}
|
||||
|
||||
// Create a plain object (the "exports") and add the funcs
|
||||
JSValue obj = JS_NewObject(ctx);
|
||||
JS_SetPropertyFunctionList(ctx, obj, js_blob_funcs,
|
||||
sizeof(js_blob_funcs) / sizeof(js_blob_funcs[0]));
|
||||
return obj;
|
||||
}
|
||||
@@ -1,8 +0,0 @@
|
||||
#ifndef QJS_BLOB_H
|
||||
#define QJS_BLOB_H
|
||||
|
||||
#include <quickjs.h>
|
||||
|
||||
JSValue js_blob_use(JSContext *ctx);
|
||||
|
||||
#endif
|
||||
2341
source/qjs_box2d.c
@@ -1,8 +0,0 @@
|
||||
#ifndef QJS_BOX2D_H
|
||||
#define QJS_BOX2D_H
|
||||
|
||||
#include "quickjs.h"
|
||||
|
||||
JSValue js_box2d_use(JSContext*);
|
||||
|
||||
#endif
|
||||
1278
source/qjs_chipmunk.c
Normal file
8
source/qjs_chipmunk.h
Normal file
@@ -0,0 +1,8 @@
|
||||
#ifndef QJS_CHIPMUNK_H
|
||||
#define QJS_CHIPMUNK_H
|
||||
|
||||
#include "quickjs.h"
|
||||
|
||||
JSValue js_chipmunk2d_use(JSContext*);
|
||||
|
||||
#endif
|
||||
@@ -1,213 +0,0 @@
|
||||
#include "qjs_crypto.h"
|
||||
#include "quickjs.h"
|
||||
#include <stdint.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
|
||||
#include "monocypher.h"
|
||||
|
||||
#include <stdint.h>
|
||||
#include <stddef.h>
|
||||
|
||||
#if defined(_WIN32)
|
||||
// ------- Windows: use BCryptGenRandom -------
|
||||
#include <windows.h>
|
||||
#include <bcrypt.h>
|
||||
|
||||
int randombytes(void *buf, size_t n) {
|
||||
NTSTATUS status = BCryptGenRandom(NULL, (PUCHAR)buf, (ULONG)n, BCRYPT_USE_SYSTEM_PREFERRED_RNG);
|
||||
return (status == 0) ? 0 : -1;
|
||||
}
|
||||
|
||||
#elif defined(__linux__)
|
||||
// ------- Linux: try getrandom, fall back to /dev/urandom -------
|
||||
#include <unistd.h>
|
||||
#include <sys/syscall.h>
|
||||
#include <fcntl.h>
|
||||
#include <errno.h>
|
||||
|
||||
// If we have a new enough libc and kernel, getrandom is available.
|
||||
// Otherwise, we’ll do a /dev/urandom fallback.
|
||||
#include <sys/stat.h>
|
||||
|
||||
static int randombytes_fallback(void *buf, size_t n) {
|
||||
int fd = open("/dev/urandom", O_RDONLY);
|
||||
if (fd < 0) return -1;
|
||||
ssize_t r = read(fd, buf, n);
|
||||
close(fd);
|
||||
return (r == (ssize_t)n) ? 0 : -1;
|
||||
}
|
||||
|
||||
int randombytes(void *buf, size_t n) {
|
||||
#ifdef SYS_getrandom
|
||||
// Try getrandom(2) if available
|
||||
ssize_t ret = syscall(SYS_getrandom, buf, n, 0);
|
||||
if (ret < 0) {
|
||||
// If getrandom is not supported or fails, fall back
|
||||
if (errno == ENOSYS) {
|
||||
return randombytes_fallback(buf, n);
|
||||
}
|
||||
return -1;
|
||||
}
|
||||
return (ret == (ssize_t)n) ? 0 : -1;
|
||||
#else
|
||||
// getrandom not available, just fallback
|
||||
return randombytes_fallback(buf, n);
|
||||
#endif
|
||||
}
|
||||
|
||||
#else
|
||||
// ------- Other Unix: read from /dev/urandom -------
|
||||
#include <fcntl.h>
|
||||
#include <unistd.h>
|
||||
|
||||
int randombytes(void *buf, size_t n) {
|
||||
int fd = open("/dev/urandom", O_RDONLY);
|
||||
if (fd < 0) return -1;
|
||||
ssize_t r = read(fd, buf, n);
|
||||
close(fd);
|
||||
return (r == (ssize_t)n) ? 0 : -1;
|
||||
}
|
||||
#endif
|
||||
|
||||
static inline void to_hex(const uint8_t *in, size_t in_len, char *out)
|
||||
{
|
||||
static const char hexchars[] = "0123456789abcdef";
|
||||
for (size_t i = 0; i < in_len; i++) {
|
||||
out[2*i ] = hexchars[(in[i] >> 4) & 0x0F];
|
||||
out[2*i + 1] = hexchars[ in[i] & 0x0F];
|
||||
}
|
||||
out[2 * in_len] = '\0'; // null-terminate
|
||||
}
|
||||
|
||||
static inline int nibble_from_char(char c, uint8_t *nibble)
|
||||
{
|
||||
if (c >= '0' && c <= '9') { *nibble = (uint8_t)(c - '0'); return 0; }
|
||||
if (c >= 'a' && c <= 'f') { *nibble = (uint8_t)(c - 'a' + 10); return 0; }
|
||||
if (c >= 'A' && c <= 'F') { *nibble = (uint8_t)(c - 'A' + 10); return 0; }
|
||||
return -1; // invalid char
|
||||
}
|
||||
|
||||
static inline int from_hex(const char *hex, uint8_t *out, size_t out_len)
|
||||
{
|
||||
for (size_t i = 0; i < out_len; i++) {
|
||||
uint8_t hi, lo;
|
||||
if (nibble_from_char(hex[2*i], &hi) < 0) return -1;
|
||||
if (nibble_from_char(hex[2*i + 1], &lo) < 0) return -1;
|
||||
out[i] = (uint8_t)((hi << 4) | lo);
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
// Convert a JSValue containing a 64-character hex string into a 32-byte array.
|
||||
static inline void js2crypto(JSContext *js, JSValue v, uint8_t *crypto)
|
||||
{
|
||||
size_t hex_len;
|
||||
const char *hex_str = JS_ToCStringLen(js, &hex_len, v);
|
||||
if (!hex_str)
|
||||
return;
|
||||
|
||||
if (hex_len != 64) {
|
||||
JS_FreeCString(js, hex_str);
|
||||
JS_ThrowTypeError(js, "js2crypto: expected 64-hex-char string");
|
||||
return;
|
||||
}
|
||||
|
||||
if (from_hex(hex_str, crypto, 32) < 0) {
|
||||
JS_FreeCString(js, hex_str);
|
||||
JS_ThrowTypeError(js, "js2crypto: invalid hex encoding");
|
||||
return;
|
||||
}
|
||||
|
||||
JS_FreeCString(js, hex_str);
|
||||
}
|
||||
|
||||
static inline JSValue crypto2js(JSContext *js, const uint8_t *crypto)
|
||||
{
|
||||
char hex[65]; // 32*2 + 1 for null terminator
|
||||
to_hex(crypto, 32, hex);
|
||||
return JS_NewString(js, hex);
|
||||
}
|
||||
|
||||
JSValue js_crypto_keypair(JSContext *js, JSValue self, int argc, JSValue *argv) {
|
||||
JSValue ret = JS_NewObject(js);
|
||||
|
||||
uint8_t public[32];
|
||||
uint8_t private[32];
|
||||
|
||||
randombytes(private,32);
|
||||
|
||||
private[0] &= 248;
|
||||
private[31] &= 127;
|
||||
private[31] |= 64;
|
||||
|
||||
crypto_x25519_public_key(public,private);
|
||||
|
||||
JS_SetPropertyStr(js, ret, "public", crypto2js(js, public));
|
||||
JS_SetPropertyStr(js, ret, "private", crypto2js(js,private));
|
||||
return ret;
|
||||
}
|
||||
|
||||
JSValue js_crypto_shared(JSContext *js, JSValue self, int argc, JSValue *argv)
|
||||
{
|
||||
if (argc < 1 || !JS_IsObject(argv[0])) {
|
||||
return JS_ThrowTypeError(js, "crypto.shared: expected an object argument");
|
||||
}
|
||||
|
||||
JSValue obj = argv[0];
|
||||
|
||||
JSValue val_pub = JS_GetPropertyStr(js, obj, "public");
|
||||
if (JS_IsException(val_pub)) {
|
||||
JS_FreeValue(js, val_pub);
|
||||
return JS_EXCEPTION;
|
||||
}
|
||||
|
||||
JSValue val_priv = JS_GetPropertyStr(js, obj, "private");
|
||||
if (JS_IsException(val_priv)) {
|
||||
JS_FreeValue(js, val_pub);
|
||||
JS_FreeValue(js, val_priv);
|
||||
return JS_EXCEPTION;
|
||||
}
|
||||
|
||||
uint8_t pub[32], priv[32];
|
||||
js2crypto(js, val_pub, pub);
|
||||
js2crypto(js, val_priv, priv);
|
||||
|
||||
JS_FreeValue(js, val_pub);
|
||||
JS_FreeValue(js, val_priv);
|
||||
|
||||
uint8_t shared[32];
|
||||
crypto_x25519(shared, priv, pub);
|
||||
|
||||
return crypto2js(js, shared);
|
||||
}
|
||||
|
||||
JSValue js_crypto_random(JSContext *js, JSValue self, int argc, JSValue *argv)
|
||||
{
|
||||
// 1) Pull 64 bits of cryptographically secure randomness
|
||||
uint64_t r;
|
||||
if (randombytes(&r, sizeof(r)) != 0) {
|
||||
// If something fails (extremely rare), throw an error
|
||||
return JS_ThrowInternalError(js, "crypto.random: unable to get random bytes");
|
||||
}
|
||||
|
||||
// 2) Convert r to a double in the range [0,1).
|
||||
// We divide by (UINT64_MAX + 1.0) to ensure we never produce exactly 1.0.
|
||||
double val = (double)r / ((double)UINT64_MAX + 1.0);
|
||||
|
||||
// 3) Return that as a JavaScript number
|
||||
return JS_NewFloat64(js, val);
|
||||
}
|
||||
|
||||
static const JSCFunctionListEntry js_crypto_funcs[] = {
|
||||
JS_CFUNC_DEF("keypair", 0, js_crypto_keypair),
|
||||
JS_CFUNC_DEF("shared", 1, js_crypto_shared),
|
||||
JS_CFUNC_DEF("random", 0, js_crypto_random),
|
||||
};
|
||||
|
||||
JSValue js_crypto_use(JSContext *js)
|
||||
{
|
||||
JSValue obj = JS_NewObject(js);
|
||||
JS_SetPropertyFunctionList(js, obj, js_crypto_funcs, sizeof(js_crypto_funcs)/sizeof(js_crypto_funcs[0]));
|
||||
return obj;
|
||||
}
|
||||
@@ -1,8 +0,0 @@
|
||||
#ifndef QJS_CRYPTO_H
|
||||
#define QJS_CRYPTO_H
|
||||
|
||||
#include "quickjs.h"
|
||||
|
||||
JSValue js_crypto_use(JSContext *ctx);
|
||||
|
||||
#endif
|
||||
@@ -1,29 +0,0 @@
|
||||
#include "qjs_debug.h"
|
||||
#include "qjs_macros.h"
|
||||
#include "jsffi.h"
|
||||
|
||||
// Debug function implementations
|
||||
|
||||
JSC_CCALL(debug_stack_depth, return number2js(js,js_debugger_stack_depth(js)))
|
||||
JSC_CCALL(debug_build_backtrace, return js_debugger_build_backtrace(js,NULL))
|
||||
JSC_CCALL(debug_closure_vars, return js_debugger_closure_variables(js,argv[0]))
|
||||
JSC_CCALL(debug_local_vars, return js_debugger_local_variables(js, js2number(js,argv[0])))
|
||||
JSC_CCALL(debug_fn_info, return js_debugger_fn_info(js, argv[0]))
|
||||
JSC_CCALL(debug_backtrace_fns, return js_debugger_backtrace_fns(js,NULL))
|
||||
JSC_CCALL(debug_dump_obj, return js_dump_value(js, argv[0]))
|
||||
|
||||
static const JSCFunctionListEntry js_debug_funcs[] = {
|
||||
MIST_FUNC_DEF(debug, stack_depth, 0),
|
||||
MIST_FUNC_DEF(debug, build_backtrace, 0),
|
||||
MIST_FUNC_DEF(debug, closure_vars, 1),
|
||||
MIST_FUNC_DEF(debug, local_vars, 1),
|
||||
MIST_FUNC_DEF(debug, fn_info, 1),
|
||||
MIST_FUNC_DEF(debug, backtrace_fns,0),
|
||||
MIST_FUNC_DEF(debug, dump_obj, 1),
|
||||
};
|
||||
|
||||
JSValue js_debug_use(JSContext *js) {
|
||||
JSValue mod = JS_NewObject(js);
|
||||
JS_SetPropertyFunctionList(js,mod,js_debug_funcs,countof(js_debug_funcs));
|
||||
return mod;
|
||||
}
|
||||