Adding modding folder with a hacked build.
This commit is contained in:
@@ -42,3 +42,6 @@
|
|||||||
# Signing keys
|
# Signing keys
|
||||||
*.jks
|
*.jks
|
||||||
*.keystore
|
*.keystore
|
||||||
|
|
||||||
|
# modding/hacked build output (see modding/README.md)
|
||||||
|
/modding/dist/
|
||||||
|
|||||||
@@ -76,6 +76,7 @@ These come from the maintainer. Follow them without being asked again.
|
|||||||
| `build-image/` | Docker build environment (`Dockerfile`, `VERSION`, `prepare-android-project.sh`, `build-apk.sh`, engine build scripts). |
|
| `build-image/` | Docker build environment (`Dockerfile`, `VERSION`, `prepare-android-project.sh`, `build-apk.sh`, engine build scripts). |
|
||||||
| `res/` | Bundled resources: `questshock.config.default.json`, `controller_profiles/` (JSON + PNG per controller), `splash.png` (startup splash), `logo.png` (1024x1024 logo, master of the launcher icons in `android/app/src/main/res/mipmap-*/`), `logo_small.png` (the README header), `screenshots/` (headset captures and videos named `questshock_v<version>_<nnn>.<ext>`, numbered per version), `store/` (store-listing artwork, generated from `logo.png`/`splash.png`/screenshots, not used by the app itself), `assets/` (where the user's own game data goes, git-ignored), `run.sh`/`run.bat`. |
|
| `res/` | Bundled resources: `questshock.config.default.json`, `controller_profiles/` (JSON + PNG per controller), `splash.png` (startup splash), `logo.png` (1024x1024 logo, master of the launcher icons in `android/app/src/main/res/mipmap-*/`), `logo_small.png` (the README header), `screenshots/` (headset captures and videos named `questshock_v<version>_<nnn>.<ext>`, numbered per version), `store/` (store-listing artwork, generated from `logo.png`/`splash.png`/screenshots, not used by the app itself), `assets/` (where the user's own game data goes, git-ignored), `run.sh`/`run.bat`. |
|
||||||
| `.gitea/workflows/build.yml`, `.github/workflows/release.yml` | CI. Gitea is the primary repository and builds on every push; GitHub is a mirror whose workflow builds and publishes a release when a `vX.Y.Z` tag is pushed. Each platform has its own file. |
|
| `.gitea/workflows/build.yml`, `.github/workflows/release.yml` | CI. Gitea is the primary repository and builds on every push; GitHub is a mirror whose workflow builds and publishes a release when a `vX.Y.Z` tag is pushed. Each platform has its own file. |
|
||||||
|
| `modding/` | Separate subproject: System Shock modding tools (InkyBlackness `hacked`, `ss-specs`), unrelated to the VR app. Own `README.md`, `Makefile` and Docker build-image; see that README rather than this file. |
|
||||||
| `Makefile` | `engine`, `dist`, `package`, `apk`, `android-studio`, `clean`, ... (see README section 2). |
|
| `Makefile` | `engine`, `dist`, `package`, `apk`, `android-studio`, `clean`, ... (see README section 2). |
|
||||||
| `README.md`, `CONFIG.md`, `NOTICE.txt` | User docs, config reference, license notice. |
|
| `README.md`, `CONFIG.md`, `NOTICE.txt` | User docs, config reference, license notice. |
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,30 @@
|
|||||||
|
# Builds the modding subproject - see README.md.
|
||||||
|
#
|
||||||
|
# Targets:
|
||||||
|
# all (default) - alias for `hacked`.
|
||||||
|
# build-image - builds the hacked-build-image Docker image
|
||||||
|
# (./hacked-build-image.sh). Only needed once, or after
|
||||||
|
# hacked-build-image/ or hacked/go.mod|go.sum changes.
|
||||||
|
# hacked - compiles hacked/ for Linux and Windows via that image,
|
||||||
|
# offline. Output lands in dist/.
|
||||||
|
# clean - removes dist/.
|
||||||
|
.PHONY: all build-image hacked clean
|
||||||
|
|
||||||
|
all: hacked
|
||||||
|
|
||||||
|
build-image:
|
||||||
|
./hacked-build-image.sh
|
||||||
|
|
||||||
|
# If we're already running inside the hacked-build-image
|
||||||
|
# (HACKED_BUILD_IMAGE, set by its Dockerfile), compile directly instead of
|
||||||
|
# trying to docker-run it again - e.g. a CI job already using this image as
|
||||||
|
# its container. Otherwise, run it via Docker as usual.
|
||||||
|
hacked:
|
||||||
|
@if [ -n "$$HACKED_BUILD_IMAGE" ]; then \
|
||||||
|
bash hacked-build-image/build-hacked.sh; \
|
||||||
|
else \
|
||||||
|
./hacked-build-run-image.sh; \
|
||||||
|
fi
|
||||||
|
|
||||||
|
clean:
|
||||||
|
rm -rf dist
|
||||||
@@ -0,0 +1,84 @@
|
|||||||
|
# Modding
|
||||||
|
|
||||||
|
Tools and reference material for modding System Shock (1994) itself - distinct
|
||||||
|
from the rest of this repository, which is the [QuestShock](../README.md) VR
|
||||||
|
port of the game engine. Everything modding-related lives under this folder.
|
||||||
|
|
||||||
|
Both pieces here come from the [InkyBlackness](https://inkyblackness.github.io)
|
||||||
|
project and are vendored unmodified, each under its own upstream license (see
|
||||||
|
below) - neither is relicensed by this repository's own MIT license.
|
||||||
|
|
||||||
|
## Layout
|
||||||
|
|
||||||
|
- `hacked/` - [InkyBlackness HackEd](https://github.com/inkyblackness/hacked),
|
||||||
|
the graphical mod editor. A vendored source snapshot (New BSD License),
|
||||||
|
taken from the `main` branch on 2026-09-26. To update it, replace this
|
||||||
|
directory with a fresh checkout and update that date.
|
||||||
|
- `ss-specs/` - [InkyBlackness ss-specs](https://github.com/inkyblackness/ss-specs),
|
||||||
|
the unofficial file-format specifications the editor (and any other modding
|
||||||
|
tool) is written against. Reference documentation only, not built. A
|
||||||
|
vendored snapshot (CC BY-NC-SA 4.0 - **non-commercial**, unlike the rest of
|
||||||
|
this repository), taken from the `main` branch on 2026-09-26.
|
||||||
|
- `hacked-build-image/` - the Dockerfile (and supporting scripts) for the
|
||||||
|
`hacked` build environment: the Go toolchain plus every dependency
|
||||||
|
`hacked/go.mod` pins, fetched once at image-build time so the actual build
|
||||||
|
needs no network access. Mirrors the main project's own `build-image/`
|
||||||
|
(see the repository root's [README.md](../README.md) section 4) one level
|
||||||
|
down, for this subproject.
|
||||||
|
- `hacked-build-image.sh` / `hacked-build-run-image.sh` /
|
||||||
|
`hacked-build-upload-image.sh` - build the image, run it to compile
|
||||||
|
`hacked/`, and push it to a registry, respectively. Same pattern as the
|
||||||
|
root project's own `build-image.sh`/`run-image.sh`/`upload-image.sh`.
|
||||||
|
- `Makefile` - `hacked` (default) builds `hacked/` via the image; `build-image`
|
||||||
|
(re)builds the image itself; `clean` removes `dist/`.
|
||||||
|
- `dist/` - build output (git-ignored), created by `make hacked`.
|
||||||
|
|
||||||
|
Shockolate (the engine `../engine/` vendors) already has its own modding
|
||||||
|
support (level/resource files, `res/data/`); this folder is about the
|
||||||
|
external tools that create and inspect that content, not about changing the
|
||||||
|
engine itself.
|
||||||
|
|
||||||
|
## Building `hacked`
|
||||||
|
|
||||||
|
Packages `dist/<version>.linux64.tgz` and `dist/<version>.win64.zip` (native
|
||||||
|
Linux build, Windows amd64 cross-compiled via mingw-w64 - both from the same
|
||||||
|
source, in one run).
|
||||||
|
|
||||||
|
**Prerequisites:** Docker - same as the root project, nothing modding-specific
|
||||||
|
to install locally.
|
||||||
|
|
||||||
|
```sh
|
||||||
|
cd modding
|
||||||
|
|
||||||
|
# 1. Build the hacked-build-image (once, or after hacked-build-image/ or
|
||||||
|
# hacked/go.mod|go.sum changes)
|
||||||
|
./hacked-build-image.sh
|
||||||
|
|
||||||
|
# 2. Compile hacked/ for Linux and Windows
|
||||||
|
make hacked
|
||||||
|
```
|
||||||
|
|
||||||
|
`<version>` is `hacked-<date>`, not an InkyBlackness release version:
|
||||||
|
`hacked/` is a vendored snapshot here, not its own git checkout, so there is
|
||||||
|
no tag to derive a real version from (see
|
||||||
|
`hacked-build-image/build-hacked.sh`'s own comment).
|
||||||
|
|
||||||
|
Run the result: `dist/linux/hacked/hacked` on Linux,
|
||||||
|
`dist\win\hacked\hacked.exe` on Windows. Needs OpenGL 3.2; usage is documented
|
||||||
|
on [hacked's own wiki](https://github.com/inkyblackness/hacked/wiki).
|
||||||
|
|
||||||
|
### Publishing the build-image
|
||||||
|
|
||||||
|
Same registry as the root project's own build-image (see the repository
|
||||||
|
root's `registry.env.example`), under a separate image name so the two don't
|
||||||
|
collide:
|
||||||
|
|
||||||
|
```sh
|
||||||
|
./hacked-build-upload-image.sh
|
||||||
|
```
|
||||||
|
|
||||||
|
Reads `REGISTRY`/`REGISTRY_USER`/`REGISTRY_PASSWORD` and (optionally)
|
||||||
|
`HACKED_REGISTRY_IMAGE` from `../registry.env`, same as the root scripts.
|
||||||
|
Don't bump `hacked-build-image/VERSION` just because the Dockerfile changed -
|
||||||
|
only once a build with the current version has actually succeeded, same
|
||||||
|
policy as the root project's own `build-image/VERSION`.
|
||||||
Executable
+35
@@ -0,0 +1,35 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
# Builds the hacked build-image (hacked-build-image/Dockerfile) locally, tagged with
|
||||||
|
# hacked-build-image/VERSION and :latest. Does not push - test it with
|
||||||
|
# ./hacked-build-run-image.sh first, then publish with
|
||||||
|
# ./hacked-build-upload-image.sh.
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
cd "$(dirname "${BASH_SOURCE[0]}")"
|
||||||
|
ROOT="$(pwd)"
|
||||||
|
|
||||||
|
fail() { echo "PREFLIGHT FAIL: $*" >&2; exit 1; }
|
||||||
|
|
||||||
|
command -v docker >/dev/null 2>&1 \
|
||||||
|
|| fail "docker not found in PATH"
|
||||||
|
|
||||||
|
IMAGE="${HACKED_REGISTRY_IMAGE:-questshock/hacked-builder}"
|
||||||
|
if [ -f "$ROOT/../registry.env" ]; then
|
||||||
|
# shellcheck disable=SC1091
|
||||||
|
source "$ROOT/../registry.env"
|
||||||
|
IMAGE="${HACKED_REGISTRY_IMAGE:-$IMAGE}"
|
||||||
|
fi
|
||||||
|
|
||||||
|
VERSION="$(<"$ROOT/hacked-build-image/VERSION")"
|
||||||
|
[ -n "$VERSION" ] || fail "hacked-build-image/VERSION is empty"
|
||||||
|
|
||||||
|
echo "== Building $IMAGE:$VERSION =="
|
||||||
|
docker build \
|
||||||
|
-f "$ROOT/hacked-build-image/Dockerfile" \
|
||||||
|
-t "$IMAGE:$VERSION" \
|
||||||
|
-t "$IMAGE:latest" \
|
||||||
|
"$ROOT"
|
||||||
|
|
||||||
|
echo "== Done =="
|
||||||
|
echo "Image: $IMAGE:$VERSION (and :latest)"
|
||||||
|
echo "Test it with ./hacked-build-run-image.sh, then publish with ./hacked-build-upload-image.sh"
|
||||||
@@ -0,0 +1,64 @@
|
|||||||
|
# Build environment for modding/hacked/ (vendored from
|
||||||
|
# https://github.com/inkyblackness/hacked, see ../README.md). Fetches the Go
|
||||||
|
# toolchain and this module's dependencies once, here, at image-build time, so
|
||||||
|
# ../hacked-build-run-image.sh can compile fully offline afterward. Rebuild
|
||||||
|
# with ../hacked-build-image.sh whenever a version below changes, or whenever
|
||||||
|
# ../hacked/go.mod or go.sum changes (its dependencies are baked into the
|
||||||
|
# image's module cache).
|
||||||
|
FROM ubuntu:22.04
|
||||||
|
|
||||||
|
# go.mod's own "go" directive is a minimum toolchain version, not a version to
|
||||||
|
# fetch - pin a stable release that already satisfies it so `go build` never
|
||||||
|
# needs to fetch a newer toolchain over the network at build time.
|
||||||
|
ARG GO_VERSION=1.27.1
|
||||||
|
|
||||||
|
# build-essential/pkg-config/curl/ca-certificates/git/gosu: base toolchain.
|
||||||
|
# mingw-w64: the Windows cross-build. libgl1-mesa-dev/xorg-dev: go-gl/glfw's
|
||||||
|
# cgo bindings (Linux build only - the Windows cross-build talks to the Win32
|
||||||
|
# API directly, via mingw-w64's own headers). libgtk-3-dev:
|
||||||
|
# github.com/sqweek/dialog's Linux backend uses GTK3 via cgo (`pkg-config
|
||||||
|
# gtk+-3.0`) for native file/message dialogs. zip: packages the Windows build
|
||||||
|
# (see build-hacked.sh) - tar, for the Linux .tgz, is already part of the
|
||||||
|
# base image.
|
||||||
|
RUN apt-get update \
|
||||||
|
&& DEBIAN_FRONTEND=noninteractive apt-get install -y --no-install-recommends \
|
||||||
|
build-essential pkg-config curl ca-certificates git gosu \
|
||||||
|
mingw-w64 \
|
||||||
|
libgl1-mesa-dev xorg-dev \
|
||||||
|
libgtk-3-dev \
|
||||||
|
zip \
|
||||||
|
&& rm -rf /var/lib/apt/lists/*
|
||||||
|
|
||||||
|
RUN curl -sSLO "https://go.dev/dl/go${GO_VERSION}.linux-amd64.tar.gz" \
|
||||||
|
&& tar -C /usr/local -xzf "go${GO_VERSION}.linux-amd64.tar.gz" \
|
||||||
|
&& rm "go${GO_VERSION}.linux-amd64.tar.gz"
|
||||||
|
ENV PATH=/usr/local/go/bin:${PATH}
|
||||||
|
ENV GOTOOLCHAIN=local
|
||||||
|
ENV GOPATH=/opt/go
|
||||||
|
|
||||||
|
# Populates /opt/go/pkg/mod with every dependency hacked/go.mod pins, so the
|
||||||
|
# container never needs network access to build it. Only go.mod/go.sum are
|
||||||
|
# copied in for this - not the rest of the source - so editing hacked's own
|
||||||
|
# .go files doesn't invalidate this layer's cache.
|
||||||
|
COPY hacked/go.mod hacked/go.sum /tmp/hacked-deps/
|
||||||
|
RUN cd /tmp/hacked-deps && go mod download && rm -rf /tmp/hacked-deps
|
||||||
|
|
||||||
|
COPY hacked-build-image/docker-entrypoint.sh /usr/local/bin/docker-entrypoint.sh
|
||||||
|
COPY hacked-build-image/build-hacked.sh /usr/local/bin/build-hacked.sh
|
||||||
|
# 755, not +x: the entrypoint drops to a `builder` user matching the *host's*
|
||||||
|
# UID/GID (see docker-entrypoint.sh), which is never in these root-owned
|
||||||
|
# files' group, so it needs the image's own explicit world-read+execute here -
|
||||||
|
# `chmod +x` alone only adds execute bits on top of whatever "other"
|
||||||
|
# permissions the source file happened to have (host umask-dependent).
|
||||||
|
RUN chmod 755 /usr/local/bin/docker-entrypoint.sh /usr/local/bin/build-hacked.sh
|
||||||
|
|
||||||
|
WORKDIR /workspace
|
||||||
|
ENTRYPOINT ["/usr/local/bin/docker-entrypoint.sh"]
|
||||||
|
CMD ["/usr/local/bin/build-hacked.sh"]
|
||||||
|
|
||||||
|
# Marks a shell as already running inside this image, same convention as the
|
||||||
|
# main project's own build-image (see ../../build-image/Dockerfile) - lets
|
||||||
|
# ../Makefile compile directly instead of trying to docker-run this image
|
||||||
|
# again from inside itself (e.g. a CI job already using this image as its
|
||||||
|
# container).
|
||||||
|
ENV HACKED_BUILD_IMAGE=1
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
1
|
||||||
Executable
+77
@@ -0,0 +1,77 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
# Compiles modding/hacked/ for Linux and Windows (amd64), entirely offline -
|
||||||
|
# every Go module dependency was already fetched into the build-image at
|
||||||
|
# image-build time (see Dockerfile). Baked into the image at
|
||||||
|
# /usr/local/bin/build-hacked.sh (see Dockerfile's CMD) - run via
|
||||||
|
# ../hacked-build-run-image.sh, which bind-mounts modding/ at /workspace, not
|
||||||
|
# directly.
|
||||||
|
#
|
||||||
|
# Does not follow hacked/build.sh (its own upstream build script): that script
|
||||||
|
# assumes hacked/ is its own git checkout (it derives its version from `git
|
||||||
|
# describe --tags`), which isn't true here - hacked/ is a vendored snapshot,
|
||||||
|
# not a git submodule, so `git describe` there would silently pick up
|
||||||
|
# *questshock's* tags instead. Version is a plain build-date string instead.
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
MODDING_ROOT="$(pwd)"
|
||||||
|
HACKED_SRC="$MODDING_ROOT/hacked"
|
||||||
|
OUT_DIR="$MODDING_ROOT/dist"
|
||||||
|
|
||||||
|
VERSION="hacked-$(date -u +%Y%m%d)"
|
||||||
|
echo "Version: $VERSION"
|
||||||
|
|
||||||
|
rm -rf "$OUT_DIR"
|
||||||
|
mkdir -p "$OUT_DIR/linux/hacked" "$OUT_DIR/win/hacked"
|
||||||
|
|
||||||
|
cd "$HACKED_SRC"
|
||||||
|
|
||||||
|
echo "== Building Linux amd64 =="
|
||||||
|
go build -gcflags "-d=checkptr=0" -ldflags "-X main.version=$VERSION" \
|
||||||
|
-trimpath -o "$OUT_DIR/linux/hacked/hacked" .
|
||||||
|
|
||||||
|
echo "== Preparing the Windows icon/version-info resource =="
|
||||||
|
# cmd/link automatically links in a *.syso file sitting in the main package
|
||||||
|
# directory during a Windows build - no explicit flag needed, but it does mean
|
||||||
|
# generating it (via windres, from hacked.rc/.ico/.exe.manifest) has to happen
|
||||||
|
# right here, before go build runs, and be cleaned up after regardless of
|
||||||
|
# outcome so it never leaks into a Linux build run from the same checkout.
|
||||||
|
WIN_RES_DIR="$(mktemp -d)"
|
||||||
|
trap 'rm -rf "$WIN_RES_DIR"; rm -f "$HACKED_SRC/main-res.syso"' EXIT
|
||||||
|
cp "$HACKED_SRC"/_resources/build/win/* "$WIN_RES_DIR/"
|
||||||
|
# No git tag to derive a real version from (hacked/ is a vendored snapshot,
|
||||||
|
# not its own checkout) - same date-based fallback hacked/build.sh itself
|
||||||
|
# uses when `git describe` fails.
|
||||||
|
sed -i "s/§MAJOR/$(date -u +%Y)/;s/§MINOR/$(date -u +%m)/;s/§PATCH/$(date -u +%d)/" \
|
||||||
|
"$WIN_RES_DIR/hacked.exe.manifest"
|
||||||
|
x86_64-w64-mingw32-windres -o "$HACKED_SRC/main-res.syso" "$WIN_RES_DIR/hacked.rc"
|
||||||
|
|
||||||
|
echo "== Building Windows amd64 (cross-compiled via mingw-w64) =="
|
||||||
|
# -extldflags -static-libgcc/-static-libstdc++: statically links the mingw
|
||||||
|
# C/C++ runtime into hacked.exe, the same technique the main project's own
|
||||||
|
# Windows cross-build uses (see ../../build-image/Dockerfile) - avoids having
|
||||||
|
# to locate and ship libgcc_s_seh-1.dll/libstdc++-6.dll (their exact path
|
||||||
|
# depends on the mingw-w64 package's gcc version). libwinpthread-1.dll still
|
||||||
|
# needs shipping alongside the .exe; it isn't part of that static link.
|
||||||
|
GOOS=windows GOARCH=amd64 CGO_ENABLED=1 \
|
||||||
|
CC=x86_64-w64-mingw32-gcc CXX=x86_64-w64-mingw32-g++ \
|
||||||
|
go build -gcflags "-d=checkptr=0" \
|
||||||
|
-ldflags "-X main.version=$VERSION -H=windowsgui -extldflags '-static-libgcc -static-libstdc++'" \
|
||||||
|
-trimpath -o "$OUT_DIR/win/hacked/hacked.exe" .
|
||||||
|
cp /usr/x86_64-w64-mingw32/lib/libwinpthread-1.dll "$OUT_DIR/win/hacked/"
|
||||||
|
|
||||||
|
echo "== Copying distribution resources =="
|
||||||
|
for os in linux win; do
|
||||||
|
packageDir="$OUT_DIR/$os/hacked"
|
||||||
|
cp "$HACKED_SRC/LICENSE" "$packageDir/"
|
||||||
|
cp -R "$HACKED_SRC/_resources/dist/." "$packageDir/"
|
||||||
|
done
|
||||||
|
|
||||||
|
echo "== Packaging =="
|
||||||
|
# $VERSION (hacked-<date>) is already a complete, self-describing name - no
|
||||||
|
# separate package-name prefix needed on top of it.
|
||||||
|
(cd "$OUT_DIR/linux" && tar -czf "$OUT_DIR/$VERSION.linux64.tgz" hacked)
|
||||||
|
(cd "$OUT_DIR/win" && zip -rq "$OUT_DIR/$VERSION.win64.zip" hacked)
|
||||||
|
|
||||||
|
echo "== Done =="
|
||||||
|
echo "Linux: $OUT_DIR/$VERSION.linux64.tgz"
|
||||||
|
echo "Windows: $OUT_DIR/$VERSION.win64.zip"
|
||||||
+35
@@ -0,0 +1,35 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
# Drops from root (needed to read the Go toolchain and module cache under
|
||||||
|
# /usr/local/go and /opt/go) to a user matching the host's UID/GID, so
|
||||||
|
# anything written into the bind-mounted repo (hacked/_build/, ...) is owned
|
||||||
|
# by the host user instead of root.
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
USER_UID="${HOST_UID:-1000}"
|
||||||
|
USER_GID="${HOST_GID:-1000}"
|
||||||
|
|
||||||
|
groupadd -g "$USER_GID" builder 2>/dev/null || true
|
||||||
|
useradd -u "$USER_UID" -g "$USER_GID" -m -s /bin/bash builder 2>/dev/null || true
|
||||||
|
|
||||||
|
# /workspace (the bind-mounted repo) may actually be owned by a group the
|
||||||
|
# host user only has via *supplementary* membership rather than their
|
||||||
|
# primary GID above - e.g. a VirtualBox vboxsf shared folder, which shows
|
||||||
|
# up as root:vboxsf on the host and would otherwise be inaccessible to
|
||||||
|
# `builder` here (falls through to "other", which vboxsf's default mode
|
||||||
|
# leaves with no permissions at all). Join whatever group actually owns
|
||||||
|
# /workspace too, if it differs.
|
||||||
|
WORKSPACE_GID="$(stat -c %g /workspace 2>/dev/null || true)"
|
||||||
|
if [ -n "$WORKSPACE_GID" ] && [ "$WORKSPACE_GID" != "$USER_GID" ]; then
|
||||||
|
groupadd -g "$WORKSPACE_GID" workspace 2>/dev/null || true
|
||||||
|
usermod -aG "$WORKSPACE_GID" builder 2>/dev/null || true
|
||||||
|
fi
|
||||||
|
|
||||||
|
# The Go module cache under /opt/go was populated as root at image-build
|
||||||
|
# time (see Dockerfile) - readable by anyone, but go build also wants to
|
||||||
|
# write GOCACHE (build cache) under there by default. Point it at a
|
||||||
|
# per-run tmpfs-friendly location inside the builder's own home instead of
|
||||||
|
# fighting permissions on the image's baked-in GOPATH.
|
||||||
|
export GOCACHE=/home/builder/.cache/go-build
|
||||||
|
export GOMODCACHE=/opt/go/pkg/mod
|
||||||
|
|
||||||
|
exec gosu builder "$@"
|
||||||
Executable
+46
@@ -0,0 +1,46 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
# Runs the hacked build-image against this folder to compile hacked/ (the
|
||||||
|
# vendored InkyBlackness HackEd snapshot, see README.md) for Linux and
|
||||||
|
# Windows, entirely offline - every Go module dependency it needs was already
|
||||||
|
# fetched into the image by ./hacked-build-image.sh.
|
||||||
|
#
|
||||||
|
# Output lands in dist/ (both platforms, packaged as .tgz/.zip).
|
||||||
|
#
|
||||||
|
# Usage: ./hacked-build-run-image.sh [command...]
|
||||||
|
# With no arguments, runs the image's baked-in build-hacked.sh (its default
|
||||||
|
# CMD). Pass a command (e.g. `bash`) to get a shell in the build environment
|
||||||
|
# instead, useful for debugging a failed build.
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
cd "$(dirname "${BASH_SOURCE[0]}")"
|
||||||
|
ROOT="$(pwd)"
|
||||||
|
|
||||||
|
fail() { echo "PREFLIGHT FAIL: $*" >&2; exit 1; }
|
||||||
|
|
||||||
|
command -v docker >/dev/null 2>&1 \
|
||||||
|
|| fail "docker not found in PATH"
|
||||||
|
|
||||||
|
IMAGE="${HACKED_REGISTRY_IMAGE:-questshock/hacked-builder}"
|
||||||
|
if [ -f "$ROOT/../registry.env" ]; then
|
||||||
|
# shellcheck disable=SC1091
|
||||||
|
source "$ROOT/../registry.env"
|
||||||
|
IMAGE="${HACKED_REGISTRY_IMAGE:-$IMAGE}"
|
||||||
|
fi
|
||||||
|
|
||||||
|
IMAGE_TAG="$(<"$ROOT/hacked-build-image/VERSION")"
|
||||||
|
[ -n "$IMAGE_TAG" ] || fail "hacked-build-image/VERSION is empty"
|
||||||
|
IMAGE="$IMAGE:$IMAGE_TAG"
|
||||||
|
|
||||||
|
docker image inspect "$IMAGE" >/dev/null 2>&1 \
|
||||||
|
|| fail "$IMAGE not found locally - run ./hacked-build-image.sh first"
|
||||||
|
|
||||||
|
TTY_FLAGS="-i"
|
||||||
|
[ -t 1 ] && TTY_FLAGS="-it"
|
||||||
|
|
||||||
|
# shellcheck disable=SC2086
|
||||||
|
docker run --rm $TTY_FLAGS \
|
||||||
|
-v "$ROOT:/workspace" \
|
||||||
|
-e HOST_UID="$(id -u)" \
|
||||||
|
-e HOST_GID="$(id -g)" \
|
||||||
|
"$IMAGE" \
|
||||||
|
"$@"
|
||||||
Executable
+41
@@ -0,0 +1,41 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
# Pushes the hacked build-image - already built locally with
|
||||||
|
# ./hacked-build-image.sh, and ideally verified with
|
||||||
|
# ./hacked-build-run-image.sh - to a container registry, so CI or other
|
||||||
|
# machines can reuse it without rebuilding the whole toolchain.
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
cd "$(dirname "${BASH_SOURCE[0]}")"
|
||||||
|
ROOT="$(pwd)"
|
||||||
|
|
||||||
|
fail() { echo "PREFLIGHT FAIL: $*" >&2; exit 1; }
|
||||||
|
|
||||||
|
command -v docker >/dev/null 2>&1 \
|
||||||
|
|| fail "docker not found in PATH"
|
||||||
|
|
||||||
|
[ -f "$ROOT/../registry.env" ] \
|
||||||
|
|| fail "../registry.env not found - copy ../registry.env.example to ../registry.env and fill in your registry credentials"
|
||||||
|
|
||||||
|
# shellcheck disable=SC1091
|
||||||
|
source "$ROOT/../registry.env"
|
||||||
|
|
||||||
|
: "${REGISTRY:?registry.env must set REGISTRY}"
|
||||||
|
: "${REGISTRY_USER:?registry.env must set REGISTRY_USER}"
|
||||||
|
: "${REGISTRY_PASSWORD:?registry.env must set REGISTRY_PASSWORD}"
|
||||||
|
HACKED_REGISTRY_IMAGE="${HACKED_REGISTRY_IMAGE:-questshock/hacked-builder}"
|
||||||
|
|
||||||
|
VERSION="$(<"$ROOT/hacked-build-image/VERSION")"
|
||||||
|
[ -n "$VERSION" ] || fail "hacked-build-image/VERSION is empty"
|
||||||
|
|
||||||
|
docker image inspect "$HACKED_REGISTRY_IMAGE:$VERSION" >/dev/null 2>&1 \
|
||||||
|
|| fail "$HACKED_REGISTRY_IMAGE:$VERSION not found locally - run ./hacked-build-image.sh first"
|
||||||
|
|
||||||
|
echo "== Logging in to $REGISTRY =="
|
||||||
|
echo "$REGISTRY_PASSWORD" | docker login "$REGISTRY" -u "$REGISTRY_USER" --password-stdin
|
||||||
|
|
||||||
|
echo "== Pushing $HACKED_REGISTRY_IMAGE:$VERSION and :latest =="
|
||||||
|
docker push "$HACKED_REGISTRY_IMAGE:$VERSION"
|
||||||
|
docker push "$HACKED_REGISTRY_IMAGE:latest"
|
||||||
|
|
||||||
|
echo "== Done =="
|
||||||
|
echo "Image: $HACKED_REGISTRY_IMAGE:$VERSION"
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
* text=auto eol=lf
|
||||||
|
*.{cmd,[cC][mM][dD]} text eol=crlf
|
||||||
|
*.{bat,[bB][aA][tT]} text eol=crlf
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
.idea/
|
||||||
|
_build/
|
||||||
|
|
||||||
|
imgui.ini
|
||||||
|
*.syso
|
||||||
@@ -0,0 +1,84 @@
|
|||||||
|
run:
|
||||||
|
deadline: 10m
|
||||||
|
|
||||||
|
linters:
|
||||||
|
enable:
|
||||||
|
- asciicheck
|
||||||
|
- bodyclose
|
||||||
|
- deadcode
|
||||||
|
- depguard
|
||||||
|
- dogsled
|
||||||
|
- dupl
|
||||||
|
- durationcheck
|
||||||
|
- errcheck
|
||||||
|
- errorlint
|
||||||
|
- exportloopref
|
||||||
|
- forbidigo
|
||||||
|
- goconst
|
||||||
|
- gocritic
|
||||||
|
- godot
|
||||||
|
- godox
|
||||||
|
- goerr113
|
||||||
|
- gofmt
|
||||||
|
- goheader
|
||||||
|
- gomoddirectives
|
||||||
|
- gomodguard
|
||||||
|
- goprintffuncname
|
||||||
|
- gosimple
|
||||||
|
- govet
|
||||||
|
- importas
|
||||||
|
- ineffassign
|
||||||
|
- lll
|
||||||
|
- makezero
|
||||||
|
- misspell
|
||||||
|
- nilerr
|
||||||
|
- noctx
|
||||||
|
- nolintlint
|
||||||
|
- predeclared
|
||||||
|
- promlinter
|
||||||
|
- revive
|
||||||
|
- rowserrcheck
|
||||||
|
- sqlclosecheck
|
||||||
|
- staticcheck
|
||||||
|
- structcheck
|
||||||
|
- tagliatelle
|
||||||
|
- testpackage
|
||||||
|
- thelper
|
||||||
|
- tparallel
|
||||||
|
- typecheck
|
||||||
|
- unconvert
|
||||||
|
- unused
|
||||||
|
- varcheck
|
||||||
|
- wastedassign
|
||||||
|
- whitespace
|
||||||
|
|
||||||
|
# linters are disabled if their majority of issues is considered false-positive (intended code)
|
||||||
|
# and the remaining issues (if existing) aren't worth it.
|
||||||
|
disable:
|
||||||
|
- gochecknoglobals
|
||||||
|
- gochecknoinits
|
||||||
|
- goimports # disabled because of so many false-positives with "imgui-go"
|
||||||
|
- gosec
|
||||||
|
- maligned
|
||||||
|
- nakedret
|
||||||
|
- prealloc
|
||||||
|
- unparam
|
||||||
|
- wsl
|
||||||
|
- funlen
|
||||||
|
- gocyclo # disabled for UI code
|
||||||
|
- nestif # disabled for UI code
|
||||||
|
- gocognit
|
||||||
|
- stylecheck # enable again for doing all the package documentation.
|
||||||
|
- gomnd # too many false positives with constants themselves.
|
||||||
|
|
||||||
|
issues:
|
||||||
|
exclude:
|
||||||
|
# too often for OpenGL constants
|
||||||
|
- don't use ALL_CAPS in Go names
|
||||||
|
exclude-use-default: false # disable filtering of defaults for better zero-issue policy
|
||||||
|
max-per-linter: 0 # disable limit; report all issues of a linter
|
||||||
|
max-same-issues: 0 # disable limit; report all issues of the same issue
|
||||||
|
|
||||||
|
linters-settings:
|
||||||
|
lll:
|
||||||
|
line-length: 160
|
||||||
@@ -0,0 +1,38 @@
|
|||||||
|
## Contributing Guidelines
|
||||||
|
|
||||||
|
Thank you for considering to contribute to this application!
|
||||||
|
|
||||||
|
The following text lists guidelines for contributions.
|
||||||
|
These guidelines don't have legal status, so use them as a reference and common sense - and feel free to update them as well!
|
||||||
|
|
||||||
|
|
||||||
|
### "I just want to know..."
|
||||||
|
|
||||||
|
For questions, or general usage-information about HackEd, please refer to the [systemshock.org](https://systemshock.org) forums (`Engineering` subforum for modding in general, `Deck 13` for source development, including engine code), or refer to the [Wiki](https://github.com/inkyblackness/hacked/wiki).
|
||||||
|
|
||||||
|
You might also find information on the data files in the [ss-specs](https://github.com/inkyblackness/ss-specs) project.
|
||||||
|
|
||||||
|
|
||||||
|
### Scope
|
||||||
|
|
||||||
|
This is an editor for mods and fan-missions of the video game System Shock (1994, and compatible), written in Go.
|
||||||
|
|
||||||
|
If there are extensions to the data files, please be sure that these were discussed and/or documented in `ss-specs`. Usability features are always welcome, I only ask to give them a second thought if there might be a more generic solution (i.e., don't immediately jump to the implementation of "I need X", rather think how a slightly different feature might both give you X and also Y in even a simpler form...)
|
||||||
|
|
||||||
|
Please also consider engine compatibility. If there are engine features not supported by all (especially the classic version), please make sure these are indicated to the user. Also refer to the documentation on the Wiki for such cases.
|
||||||
|
|
||||||
|
|
||||||
|
### Code Style
|
||||||
|
|
||||||
|
Please make sure code is formatted according to `go fmt`, and use the following linter: [golangci-lint](https://github.com/golangci/golangci-lint).
|
||||||
|
|
||||||
|
> If there are linter errors that you didn't introduce, you don't have to clean them up - I might have missed them and will be handling them separately.
|
||||||
|
|
||||||
|
|
||||||
|
### Testing Style
|
||||||
|
|
||||||
|
The code under `ss1` should be well tested, please try to keep up coverage. The code under `editor` is tricky to test - if you figure out how to test ImGui based stuff, I'd be happy for a pull request! At least utilities should have tests.
|
||||||
|
|
||||||
|
> I am aiming for a coverage of over 90% in there.
|
||||||
|
|
||||||
|
For testing use either the vanilla `testing` package, or augment it with the `github.com/stretchr/testify` framework. See existing examples.
|
||||||
@@ -0,0 +1,27 @@
|
|||||||
|
New BSD License
|
||||||
|
|
||||||
|
© 2018, Christian Haas
|
||||||
|
All rights reserved.
|
||||||
|
|
||||||
|
Redistribution and use in source and binary forms, with or without
|
||||||
|
modification, are permitted provided that the following conditions are met:
|
||||||
|
|
||||||
|
* Redistributions of source code must retain the above copyright
|
||||||
|
notice, this list of conditions and the following disclaimer.
|
||||||
|
* 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.
|
||||||
|
* Neither the name of "InkyBlackness" nor the names of its
|
||||||
|
contributors may be used to endorse or promote products derived from
|
||||||
|
this software without specific prior written permission.
|
||||||
|
|
||||||
|
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 LISTED COPYRIGHT HOLDERS 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.
|
||||||
@@ -0,0 +1,28 @@
|
|||||||
|
# InkyBlackness HackEd
|
||||||
|
|
||||||
|
HackEd is the main graphical editor application as part of the [InkyBlackness](https://inkyblackness.github.io) project, written in [Go](http://golang.org/). It allows editing of game files of the original System Shock (1994 and compatible) to create mods and fan-missions.
|
||||||
|
|
||||||
|
This application is the successor of the `deck` package of InkyBlackness, which provided the first public editor, even before the source release of the original engine. The rewrite was necessary due to several factors.
|
||||||
|
|
||||||
|
The [releases page](https://github.com/inkyblackness/hacked/releases) provides the latest binaries for both MS Windows and Linux.
|
||||||
|
|
||||||
|
## Documentation
|
||||||
|
|
||||||
|
Usage of the editor is documented on its [Wiki](https://github.com/inkyblackness/hacked/wiki) page.
|
||||||
|
Answers and further help about modding can be furthermore found on the [systemshock.org](https://systemshock.org) forums, particularly the `Engineering` subforum.
|
||||||
|
|
||||||
|
## Screenshots
|
||||||
|
|
||||||
|
Level editing details:
|
||||||
|

|
||||||
|
|
||||||
|
Media editing details:
|
||||||
|

|
||||||
|
|
||||||
|
4K glory showing all the windows:
|
||||||
|

|
||||||
|
|
||||||
|
|
||||||
|
## License
|
||||||
|
|
||||||
|
The project is available under the terms of the **New BSD License** (see LICENSE file).
|
||||||
@@ -0,0 +1,20 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
|
||||||
|
<assembly xmlns="urn:schemas-microsoft-com:asm.v1" manifestVersion="1.0">
|
||||||
|
<assemblyIdentity
|
||||||
|
type="win32"
|
||||||
|
name="InkyBlackness.hacked"
|
||||||
|
version="§MAJOR.§MINOR.§PATCH.0"
|
||||||
|
/>
|
||||||
|
<dependency>
|
||||||
|
<dependentAssembly>
|
||||||
|
<assemblyIdentity
|
||||||
|
type="win32"
|
||||||
|
name="Microsoft.Windows.Common-Controls"
|
||||||
|
version="6.0.0.0"
|
||||||
|
processorArchitecture="*"
|
||||||
|
publicKeyToken="6595b64144ccf1df"
|
||||||
|
language="*"
|
||||||
|
/>
|
||||||
|
</dependentAssembly>
|
||||||
|
</dependency>
|
||||||
|
</assembly>
|
||||||
BIN
Binary file not shown.
|
After Width: | Height: | Size: 58 KiB |
@@ -0,0 +1,3 @@
|
|||||||
|
100 ICON "hacked.ico"
|
||||||
|
100 24 "hacked.exe.manifest"
|
||||||
|
GLFW_ICON ICON "hacked.ico"
|
||||||
+17
@@ -0,0 +1,17 @@
|
|||||||
|
# InkyBlackness - HackEd
|
||||||
|
|
||||||
|
This package contains the public distribution of the [InkyBlackness](https://inkyblackness.github.io) HackEd binary.
|
||||||
|
|
||||||
|
The executable is a modding tool for the DOS game [System Shock (1994)](http://en.wikipedia.org/wiki/System_Shock) and compatibles.
|
||||||
|
|
||||||
|
## Disclaimer
|
||||||
|
A general warning: Keep backups of your files!
|
||||||
|
This is an executables downloaded from the internet. If you are unsure what they do or how they work, refer to the sources [on GitHub](https://github.com/inkyblackness).
|
||||||
|
|
||||||
|
|
||||||
|
## Requirements
|
||||||
|
|
||||||
|
HackEd is a GUI editor for the resources, including a map editor.
|
||||||
|
The tool uses OpenGL 3.2 .
|
||||||
|
|
||||||
|
User documentation is available online here: https://github.com/inkyblackness/hacked/wiki
|
||||||
Executable
+76
@@ -0,0 +1,76 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
|
||||||
|
pushd $(dirname "${0}") >/dev/null
|
||||||
|
HACKED_BASE=$(pwd -L)
|
||||||
|
FOLDER_NAME=inkyblackness-hacked
|
||||||
|
|
||||||
|
echo "Cleaning output directories..."
|
||||||
|
rm -rf _build
|
||||||
|
rm main-res.syso
|
||||||
|
|
||||||
|
mkdir -p $HACKED_BASE/_build/linux/$FOLDER_NAME
|
||||||
|
mkdir -p $HACKED_BASE/_build/win/$FOLDER_NAME
|
||||||
|
|
||||||
|
echo "Determining version"
|
||||||
|
|
||||||
|
MAJOR=$(date +%Y)
|
||||||
|
MINOR=$(date +%m)
|
||||||
|
PATCH=$(date +%d)
|
||||||
|
VERSION=$(git describe --tags)
|
||||||
|
if [ $? -ne 0 ]; then
|
||||||
|
echo "Could not determine tag, defaulting to revision for version"
|
||||||
|
REV=$(git rev-parse --short HEAD)
|
||||||
|
VERSION="rev$REV"
|
||||||
|
else
|
||||||
|
VERSION_RAW=$(echo "$VERSION" | cut -d'-' -f 1 | cut -d'v' -f 2)
|
||||||
|
fi
|
||||||
|
|
||||||
|
EXTRA=$(echo "$VERSION" | cut -d'-' -f 2)
|
||||||
|
if [[ "$VERSION" == "$EXTRA" ]]; then
|
||||||
|
MAJOR=$(echo "$VERSION_RAW" | cut -d'.' -f 1)
|
||||||
|
MINOR=$(echo "$VERSION_RAW" | cut -d'.' -f 2)
|
||||||
|
PATCH=$(echo "$VERSION_RAW" | cut -d'.' -f 3)
|
||||||
|
fi
|
||||||
|
echo "Determined version: $VERSION ($MAJOR.$MINOR.$PATCH)"
|
||||||
|
|
||||||
|
echo "Preparing build resources"
|
||||||
|
mkdir -p $HACKED_BASE/_build/win_temp
|
||||||
|
cp $HACKED_BASE/_resources/build/win/* $HACKED_BASE/_build/win_temp
|
||||||
|
sed -i "s/§MAJOR/$MAJOR/g" $HACKED_BASE/_build/win_temp/hacked.exe.manifest
|
||||||
|
sed -i "s/§MINOR/$MINOR/g" $HACKED_BASE/_build/win_temp/hacked.exe.manifest
|
||||||
|
sed -i "s/§PATCH/$PATCH/g" $HACKED_BASE/_build/win_temp/hacked.exe.manifest
|
||||||
|
x86_64-w64-mingw32-windres -o main-res.syso $HACKED_BASE/_build/win_temp/hacked.rc
|
||||||
|
|
||||||
|
echo "Building executables..."
|
||||||
|
go build -gcflags "-d=checkptr=0" -ldflags "-X main.version=$VERSION" -a -o $HACKED_BASE/_build/linux/$FOLDER_NAME/hacked -trimpath $(pwd) .
|
||||||
|
GOOS=windows GOARCH=amd64 CGO_ENABLED=1 CXX=x86_64-w64-mingw32-g++ CC=x86_64-w64-mingw32-gcc go build -gcflags "-d=checkptr=0" -ldflags "-X main.version=$VERSION -H=windowsgui" -a -o $HACKED_BASE/_build/win/$FOLDER_NAME/hacked.exe -trimpath $(pwd) .
|
||||||
|
|
||||||
|
echo "Copying distribution resources..."
|
||||||
|
|
||||||
|
for os in "linux" "win"; do
|
||||||
|
packageDir=$HACKED_BASE/_build/$os/$FOLDER_NAME
|
||||||
|
|
||||||
|
cp $HACKED_BASE/LICENSE $packageDir
|
||||||
|
cp -R $HACKED_BASE/_resources/dist/* $packageDir
|
||||||
|
done
|
||||||
|
|
||||||
|
for lib in "libgcc_s_seh-1.dll" "libstdc++-6.dll" "libwinpthread-1.dll"; do
|
||||||
|
for base in "/usr/x86_64-w64-mingw32/bin" "/usr/lib/gcc/x86_64-w64-mingw32/10-win32" "/usr/x86_64-w64-mingw32/lib"; do
|
||||||
|
if [ -e $base/$lib ]; then
|
||||||
|
cp $base/$lib $HACKED_BASE/_build/win/$FOLDER_NAME
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
if [ ! -f $HACKED_BASE/_build/win/$FOLDER_NAME/$lib ]; then
|
||||||
|
echo " FAIL: file $lib is nowhere found"
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
|
||||||
|
echo "Creating packages..."
|
||||||
|
|
||||||
|
cd $HACKED_BASE/_build/linux
|
||||||
|
tar -cvzf $HACKED_BASE/_build/$FOLDER_NAME-$VERSION.linux64.tgz ./$FOLDER_NAME
|
||||||
|
|
||||||
|
cd $HACKED_BASE/_build/win
|
||||||
|
zip -r $HACKED_BASE/_build/$FOLDER_NAME-$VERSION.win64.zip .
|
||||||
|
|
||||||
|
popd >/dev/null
|
||||||
@@ -0,0 +1,4 @@
|
|||||||
|
package crash
|
||||||
|
|
||||||
|
// Handler is a function that tries to preserve some panic/stack info to the user.
|
||||||
|
var Handler = func(versionInfo string) {}
|
||||||
@@ -0,0 +1,42 @@
|
|||||||
|
package crash
|
||||||
|
|
||||||
|
//#include <windows.h>
|
||||||
|
import "C"
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"runtime/debug"
|
||||||
|
"unsafe"
|
||||||
|
)
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
Handler = windowsHandler
|
||||||
|
}
|
||||||
|
|
||||||
|
func windowsHandler(versionInfo string) {
|
||||||
|
if e := recover(); e != nil {
|
||||||
|
text := `There was a crash in the application. This is bad and I am sorry.
|
||||||
|
|
||||||
|
Perhaps you can make something with the details of the error below.
|
||||||
|
If you can reproduce this, please make a screenshot of this box and report it with details on the http://www.systemshock.org forums.
|
||||||
|
Thank you!
|
||||||
|
|
||||||
|
`
|
||||||
|
text += fmt.Sprintf("%s:\n%s", e, debug.Stack())
|
||||||
|
messageBox(text, "Something unexpected happened - "+versionInfo)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func messageBox(text, caption string) {
|
||||||
|
textArg, textFin := wrapString(text)
|
||||||
|
defer textFin()
|
||||||
|
captionArg, captionFin := wrapString(caption)
|
||||||
|
defer captionFin()
|
||||||
|
var hwnd unsafe.Pointer
|
||||||
|
C.MessageBoxExA((*C.struct_HWND__)(hwnd), textArg, captionArg, C.UINT(0), C.WORD(0))
|
||||||
|
}
|
||||||
|
|
||||||
|
func wrapString(value string) (wrapped *C.char, finisher func()) {
|
||||||
|
wrapped = C.CString(value)
|
||||||
|
finisher = func() { C.free(unsafe.Pointer(wrapped)) } // nolint: gas
|
||||||
|
return
|
||||||
|
}
|
||||||
@@ -0,0 +1,945 @@
|
|||||||
|
package editor
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
"io/ioutil"
|
||||||
|
"path/filepath"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/inkyblackness/imgui-go/v3"
|
||||||
|
|
||||||
|
"github.com/inkyblackness/hacked/editor/about"
|
||||||
|
"github.com/inkyblackness/hacked/editor/animations"
|
||||||
|
"github.com/inkyblackness/hacked/editor/archives"
|
||||||
|
"github.com/inkyblackness/hacked/editor/bitmaps"
|
||||||
|
"github.com/inkyblackness/hacked/editor/external"
|
||||||
|
"github.com/inkyblackness/hacked/editor/graphics"
|
||||||
|
"github.com/inkyblackness/hacked/editor/levels"
|
||||||
|
"github.com/inkyblackness/hacked/editor/messages"
|
||||||
|
"github.com/inkyblackness/hacked/editor/movies"
|
||||||
|
"github.com/inkyblackness/hacked/editor/objects"
|
||||||
|
"github.com/inkyblackness/hacked/editor/project"
|
||||||
|
"github.com/inkyblackness/hacked/editor/render"
|
||||||
|
"github.com/inkyblackness/hacked/editor/sounds"
|
||||||
|
"github.com/inkyblackness/hacked/editor/texts"
|
||||||
|
"github.com/inkyblackness/hacked/editor/textures"
|
||||||
|
"github.com/inkyblackness/hacked/ss1"
|
||||||
|
"github.com/inkyblackness/hacked/ss1/content/archive/level"
|
||||||
|
"github.com/inkyblackness/hacked/ss1/content/bitmap"
|
||||||
|
"github.com/inkyblackness/hacked/ss1/content/movie"
|
||||||
|
"github.com/inkyblackness/hacked/ss1/content/sound"
|
||||||
|
"github.com/inkyblackness/hacked/ss1/content/text"
|
||||||
|
"github.com/inkyblackness/hacked/ss1/edit"
|
||||||
|
"github.com/inkyblackness/hacked/ss1/edit/media"
|
||||||
|
"github.com/inkyblackness/hacked/ss1/edit/undoable"
|
||||||
|
"github.com/inkyblackness/hacked/ss1/edit/undoable/cmd"
|
||||||
|
"github.com/inkyblackness/hacked/ss1/resource"
|
||||||
|
"github.com/inkyblackness/hacked/ss1/world"
|
||||||
|
"github.com/inkyblackness/hacked/ss1/world/ids"
|
||||||
|
"github.com/inkyblackness/hacked/ui/gui"
|
||||||
|
"github.com/inkyblackness/hacked/ui/input"
|
||||||
|
"github.com/inkyblackness/hacked/ui/opengl"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
errCouldNotOpenFile ss1.StringError = "could not open file"
|
||||||
|
errCouldNotReadFile ss1.StringError = "could not read file"
|
||||||
|
)
|
||||||
|
|
||||||
|
type projectState struct {
|
||||||
|
ProjectSettings *edit.ProjectSettings `json:",omitempty"`
|
||||||
|
GameStateSettings *edit.GameStateSettings `json:",omitempty"`
|
||||||
|
OpenWindows []string `json:",omitempty"`
|
||||||
|
ActiveLevelIndex *int `json:",omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func projectStateFromFile(filename string) (projectState, error) {
|
||||||
|
data, err := ioutil.ReadFile(filename)
|
||||||
|
if err != nil {
|
||||||
|
return projectState{}, errCouldNotOpenFile
|
||||||
|
}
|
||||||
|
var state projectState
|
||||||
|
err = json.Unmarshal(data, &state)
|
||||||
|
if err != nil {
|
||||||
|
return projectState{}, errCouldNotReadFile
|
||||||
|
}
|
||||||
|
return state, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// SaveTo stores the state in a file with given filename.
|
||||||
|
func (state projectState) SaveTo(filename string) error {
|
||||||
|
bytes, err := json.MarshalIndent(state, "", " ")
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return ioutil.WriteFile(filename, bytes, 0640)
|
||||||
|
}
|
||||||
|
|
||||||
|
type lastProjectState struct {
|
||||||
|
projectState
|
||||||
|
LastProject string `json:",omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func lastProjectStateFromFile(filename string) lastProjectState {
|
||||||
|
data, err := ioutil.ReadFile(filename)
|
||||||
|
if err != nil {
|
||||||
|
return lastProjectState{}
|
||||||
|
}
|
||||||
|
var state lastProjectState
|
||||||
|
err = json.Unmarshal(data, &state)
|
||||||
|
if err != nil {
|
||||||
|
return lastProjectState{}
|
||||||
|
}
|
||||||
|
return state
|
||||||
|
}
|
||||||
|
|
||||||
|
// SaveTo stores the state in a file with given filename.
|
||||||
|
func (state lastProjectState) SaveTo(filename string) error {
|
||||||
|
bytes, err := json.MarshalIndent(state, "", " ")
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return ioutil.WriteFile(filename, bytes, 0640)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Application is the root object of the graphical editor.
|
||||||
|
// It is set up by the main method.
|
||||||
|
type Application struct {
|
||||||
|
// Version identifies the build of the application.
|
||||||
|
Version string
|
||||||
|
|
||||||
|
// ConfigDir is the base path to store configuration in.
|
||||||
|
ConfigDir string
|
||||||
|
|
||||||
|
window opengl.Window
|
||||||
|
clipboard clipboardAdapter
|
||||||
|
gl opengl.OpenGL
|
||||||
|
|
||||||
|
// FontFile specifies the font to use.
|
||||||
|
FontFile string
|
||||||
|
// FontSize specifies the font size to use.
|
||||||
|
FontSize float32
|
||||||
|
// GuiScale is applied when the window is initialized.
|
||||||
|
GuiScale float32
|
||||||
|
guiContext *gui.Context
|
||||||
|
|
||||||
|
lastModifier input.Modifier
|
||||||
|
lastMouseX float32
|
||||||
|
lastMouseY float32
|
||||||
|
|
||||||
|
txnBuilder cmd.TransactionBuilder
|
||||||
|
cmdStack *cmd.Stack
|
||||||
|
mod *world.Mod
|
||||||
|
cp text.Codepage
|
||||||
|
textLineCache *text.Cache
|
||||||
|
textPageCache *text.Cache
|
||||||
|
messagesCache *text.ElectronicMessageCache
|
||||||
|
paletteCache *graphics.PaletteCache
|
||||||
|
textureCache *graphics.TextureCache
|
||||||
|
frameCache *graphics.FrameCache
|
||||||
|
animationCache *bitmap.AnimationCache
|
||||||
|
movieCache *movie.Cache
|
||||||
|
soundEffectCache *sound.EffectCache
|
||||||
|
|
||||||
|
mapDisplay *levels.MapDisplay
|
||||||
|
|
||||||
|
levels *edit.EditableLevels
|
||||||
|
levelSelection *edit.LevelSelectionService
|
||||||
|
levelEditorService *edit.LevelEditorService
|
||||||
|
|
||||||
|
projectService *edit.ProjectService
|
||||||
|
gameStateService *edit.GameStateService
|
||||||
|
gameObjectsService *edit.GameObjectsService
|
||||||
|
|
||||||
|
projectView *project.View
|
||||||
|
archiveView *archives.View
|
||||||
|
levelControlView *levels.ControlView
|
||||||
|
levelTilesView *levels.TilesView
|
||||||
|
levelObjectsView *levels.ObjectsView
|
||||||
|
messagesView *messages.View
|
||||||
|
textsView *texts.View
|
||||||
|
bitmapsView *bitmaps.View
|
||||||
|
texturesView *textures.View
|
||||||
|
animationsView *animations.View
|
||||||
|
moviesView *movies.View
|
||||||
|
soundEffectsView *sounds.View
|
||||||
|
objectsView *objects.View
|
||||||
|
aboutView *about.View
|
||||||
|
licensesView *about.LicensesView
|
||||||
|
|
||||||
|
modalState gui.ModalStateWrapper
|
||||||
|
|
||||||
|
failureMessage string
|
||||||
|
failurePending bool
|
||||||
|
}
|
||||||
|
|
||||||
|
// InitializeWindow takes the given window and attaches the callbacks.
|
||||||
|
func (app *Application) InitializeWindow(window opengl.Window) (err error) {
|
||||||
|
app.window = window
|
||||||
|
app.clipboard.window = window
|
||||||
|
app.gl = window.OpenGL()
|
||||||
|
|
||||||
|
app.window.RestoreState(opengl.WindowStateFromFile(app.windowStateConfigFilename(), app.window.StateSnapshot()))
|
||||||
|
|
||||||
|
app.initSignalling()
|
||||||
|
app.initWindowCallbacks()
|
||||||
|
app.initOpenGL()
|
||||||
|
|
||||||
|
app.initModel()
|
||||||
|
err = app.initGui()
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
app.initView()
|
||||||
|
app.restoreWorkspace()
|
||||||
|
|
||||||
|
app.onWindowResize(app.window.Size())
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
func (app *Application) onWindowClosed() {
|
||||||
|
if app.guiContext != nil {
|
||||||
|
app.guiContext.Destroy()
|
||||||
|
app.guiContext = nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (app *Application) initWindowCallbacks() {
|
||||||
|
app.window.OnClosing(app.onWindowClosing)
|
||||||
|
app.window.OnClosed(app.onWindowClosed)
|
||||||
|
app.window.OnFileDropCallback(app.onFilesDropped)
|
||||||
|
|
||||||
|
app.window.OnResize(app.onWindowResize)
|
||||||
|
|
||||||
|
app.window.OnKeyPress(app.onKeyPress)
|
||||||
|
app.window.OnKeyRelease(app.onKeyRelease)
|
||||||
|
app.window.OnCharCallback(app.onChar)
|
||||||
|
app.window.OnModifier(app.onModifier)
|
||||||
|
|
||||||
|
app.window.OnMouseMove(app.onMouseMove)
|
||||||
|
app.window.OnMouseScroll(app.onMouseScroll)
|
||||||
|
app.window.OnMouseButtonDown(app.onMouseButtonDown)
|
||||||
|
app.window.OnMouseButtonUp(app.onMouseButtonUp)
|
||||||
|
|
||||||
|
app.window.OnRender(app.render)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (app *Application) render() {
|
||||||
|
app.guiContext.NewFrame()
|
||||||
|
|
||||||
|
width, height := app.window.Size()
|
||||||
|
app.gl.Viewport(0, 0, int32(width), int32(height))
|
||||||
|
app.gl.ClearColor(0.0, 0.0, 0.0, 1.0)
|
||||||
|
app.gl.Clear(opengl.COLOR_BUFFER_BIT)
|
||||||
|
|
||||||
|
paletteTexture, _ := app.paletteCache.Palette(0)
|
||||||
|
app.mapDisplay.Render(
|
||||||
|
paletteTexture, app.textureCache.Texture,
|
||||||
|
app.levelTilesView.TextureDisplay(), app.levelTilesView.ColorDisplay())
|
||||||
|
|
||||||
|
app.handleFailure()
|
||||||
|
app.renderMainMenu()
|
||||||
|
|
||||||
|
app.projectView.Render()
|
||||||
|
app.updateAutoSave()
|
||||||
|
|
||||||
|
app.archiveView.Render()
|
||||||
|
app.levelControlView.Render()
|
||||||
|
app.levelTilesView.Render()
|
||||||
|
app.levelObjectsView.Render()
|
||||||
|
app.messagesView.Render()
|
||||||
|
app.textsView.Render()
|
||||||
|
app.bitmapsView.Render()
|
||||||
|
app.texturesView.Render()
|
||||||
|
app.animationsView.Render()
|
||||||
|
app.moviesView.Render()
|
||||||
|
app.soundEffectsView.Render()
|
||||||
|
app.objectsView.Render()
|
||||||
|
app.aboutView.Render()
|
||||||
|
app.licensesView.Render()
|
||||||
|
|
||||||
|
app.modalState.Render()
|
||||||
|
|
||||||
|
app.guiContext.Render(app.bitmapTextureForUI)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (app *Application) initOpenGL() {
|
||||||
|
app.gl.Disable(opengl.DEPTH_TEST)
|
||||||
|
app.gl.Enable(opengl.BLEND)
|
||||||
|
app.gl.BlendFunc(opengl.SRC_ALPHA, opengl.ONE_MINUS_SRC_ALPHA)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (app *Application) initGui() (err error) {
|
||||||
|
app.initGuiSizes()
|
||||||
|
param := gui.ContextParameters{
|
||||||
|
ConfigDir: app.ConfigDir,
|
||||||
|
FontFile: app.FontFile,
|
||||||
|
FontSize: app.FontSize,
|
||||||
|
}
|
||||||
|
app.guiContext, err = gui.NewContext(app.window, param)
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
imguiIO := imgui.CurrentIO()
|
||||||
|
imguiIO.KeyMap(imgui.KeyTab, int(input.KeyTab))
|
||||||
|
imguiIO.KeyMap(imgui.KeyLeftArrow, int(input.KeyLeft))
|
||||||
|
imguiIO.KeyMap(imgui.KeyRightArrow, int(input.KeyRight))
|
||||||
|
imguiIO.KeyMap(imgui.KeyUpArrow, int(input.KeyUp))
|
||||||
|
imguiIO.KeyMap(imgui.KeyDownArrow, int(input.KeyDown))
|
||||||
|
imguiIO.KeyMap(imgui.KeyPageUp, int(input.KeyPageUp))
|
||||||
|
imguiIO.KeyMap(imgui.KeyPageDown, int(input.KeyPageDown))
|
||||||
|
imguiIO.KeyMap(imgui.KeyHome, int(input.KeyHome))
|
||||||
|
imguiIO.KeyMap(imgui.KeyEnd, int(input.KeyEnd))
|
||||||
|
imguiIO.KeyMap(imgui.KeyInsert, int(input.KeyInsert))
|
||||||
|
imguiIO.KeyMap(imgui.KeyDelete, int(input.KeyDelete))
|
||||||
|
imguiIO.KeyMap(imgui.KeyBackspace, int(input.KeyBackspace))
|
||||||
|
imguiIO.KeyMap(imgui.KeyEnter, int(input.KeyEnter))
|
||||||
|
imguiIO.KeyMap(imgui.KeyEscape, int(input.KeyEscape))
|
||||||
|
imguiIO.KeyMap(imgui.KeyA, int(input.KeyA))
|
||||||
|
imguiIO.KeyMap(imgui.KeyC, int(input.KeyC))
|
||||||
|
imguiIO.KeyMap(imgui.KeyV, int(input.KeyV))
|
||||||
|
imguiIO.KeyMap(imgui.KeyX, int(input.KeyX))
|
||||||
|
|
||||||
|
app.initGuiStyle()
|
||||||
|
|
||||||
|
app.mapDisplay = levels.NewMapDisplay(app.gameObjectsService, app.levelSelection, app.levelEditorService,
|
||||||
|
app.gl, app.GuiScale,
|
||||||
|
app.gameTexture)
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
func (app *Application) gameTexture(index level.TextureIndex) (*graphics.BitmapTexture, error) {
|
||||||
|
key := resource.KeyOf(ids.LargeTextures.Plus(int(index)), resource.LangAny, 0)
|
||||||
|
return app.textureCache.Texture(key)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (app *Application) bitmapTextureForUI(textureID imgui.TextureID) (palette uint32, texture uint32) {
|
||||||
|
idType := byte(textureID >> 48)
|
||||||
|
switch idType {
|
||||||
|
case render.BitmapTextureTypeResource:
|
||||||
|
paletteTexture, _ := app.paletteCache.Palette(0)
|
||||||
|
if paletteTexture == nil {
|
||||||
|
return 0, 0
|
||||||
|
}
|
||||||
|
|
||||||
|
lang := resource.Language((textureID >> 32) & 0xFF)
|
||||||
|
resourceID := resource.ID((textureID >> 16) & 0xFFFF)
|
||||||
|
blockIndex := int(textureID & 0xFFFF)
|
||||||
|
key := resource.KeyOf(resourceID, lang, blockIndex)
|
||||||
|
tex, err := app.textureCache.Texture(key)
|
||||||
|
if err != nil {
|
||||||
|
return 0, 0
|
||||||
|
}
|
||||||
|
return paletteTexture.Handle(), tex.Handle()
|
||||||
|
case render.BitmapTextureTypeFrame:
|
||||||
|
key := graphics.FrameCacheKey(textureID & 0xFFFF)
|
||||||
|
return app.frameCache.HandlesForKey(key)
|
||||||
|
}
|
||||||
|
return 0, 0
|
||||||
|
}
|
||||||
|
|
||||||
|
func (app *Application) onWindowClosing() {
|
||||||
|
windowState := app.window.StateSnapshot()
|
||||||
|
_ = windowState.SaveTo(app.windowStateConfigFilename())
|
||||||
|
|
||||||
|
app.saveWorkspace()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (app *Application) onWindowResize(width int, height int) {
|
||||||
|
app.mapDisplay.WindowResized(width, height)
|
||||||
|
app.gl.Viewport(0, 0, int32(width), int32(height))
|
||||||
|
}
|
||||||
|
|
||||||
|
func (app *Application) onFilesDropped(names []string) {
|
||||||
|
app.modalState.HandleFiles(names)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (app *Application) onKeyPress(key input.Key, modifier input.Modifier) {
|
||||||
|
app.lastModifier = modifier
|
||||||
|
|
||||||
|
usedGlobalShortcut := true
|
||||||
|
switch {
|
||||||
|
case key == input.KeyEscape:
|
||||||
|
app.modalState.SetState(nil)
|
||||||
|
case key == input.KeyUndo:
|
||||||
|
app.tryUndo()
|
||||||
|
case key == input.KeyRedo:
|
||||||
|
app.tryRedo()
|
||||||
|
case key == input.KeyNew:
|
||||||
|
app.newProject()
|
||||||
|
case key == input.KeySave:
|
||||||
|
app.projectView.StartSavingMod()
|
||||||
|
case key == input.KeyF1 && modifier.IsClear():
|
||||||
|
*app.projectView.WindowOpen() = !*app.projectView.WindowOpen()
|
||||||
|
case key == input.KeyF2 && modifier.IsClear():
|
||||||
|
*app.levelControlView.WindowOpen() = !*app.levelControlView.WindowOpen()
|
||||||
|
case key == input.KeyF3 && modifier.IsClear():
|
||||||
|
*app.levelTilesView.WindowOpen() = !*app.levelTilesView.WindowOpen()
|
||||||
|
case key == input.KeyF4 && modifier.IsClear():
|
||||||
|
*app.levelObjectsView.WindowOpen() = !*app.levelObjectsView.WindowOpen()
|
||||||
|
case key == input.KeyF5 && modifier.IsClear():
|
||||||
|
*app.messagesView.WindowOpen() = !*app.messagesView.WindowOpen()
|
||||||
|
default:
|
||||||
|
usedGlobalShortcut = false
|
||||||
|
}
|
||||||
|
if usedGlobalShortcut {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if app.shouldReportKeyToImgui(key) {
|
||||||
|
imgui.CurrentIO().KeyPress(int(key))
|
||||||
|
app.updateKeyModifier()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (app *Application) onKeyRelease(key input.Key, modifier input.Modifier) {
|
||||||
|
app.lastModifier = modifier
|
||||||
|
|
||||||
|
if app.shouldReportKeyToImgui(key) {
|
||||||
|
imgui.CurrentIO().KeyRelease(int(key))
|
||||||
|
app.updateKeyModifier()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (app *Application) shouldReportKeyToImgui(key input.Key) bool {
|
||||||
|
keyIsModifier := key.AsModifier() != input.ModNone
|
||||||
|
return keyIsModifier || app.guiContext.IsUsingKeyboard()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (app *Application) onChar(char rune) {
|
||||||
|
if app.guiContext.IsUsingKeyboard() {
|
||||||
|
imgui.CurrentIO().AddInputCharacters(string(char))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
switch char {
|
||||||
|
case 'v':
|
||||||
|
_ = app.levelEditorService.PlaceObjectsOnFloor()
|
||||||
|
case 'f':
|
||||||
|
_ = app.levelEditorService.PlaceObjectsOnEyeLevel()
|
||||||
|
case 'r':
|
||||||
|
_ = app.levelEditorService.PlaceObjectsOnCeiling()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (app *Application) updateKeyModifier() {
|
||||||
|
imguiIO := imgui.CurrentIO()
|
||||||
|
imguiIO.KeyCtrl(int(input.KeyControl), int(input.KeyControl))
|
||||||
|
imguiIO.KeyShift(int(input.KeyShift), int(input.KeyShift))
|
||||||
|
imguiIO.KeyAlt(int(input.KeyAlt), int(input.KeyAlt))
|
||||||
|
imguiIO.KeySuper(int(input.KeySuper), int(input.KeySuper))
|
||||||
|
}
|
||||||
|
|
||||||
|
func (app *Application) onModifier(modifier input.Modifier) {
|
||||||
|
app.lastModifier = modifier
|
||||||
|
}
|
||||||
|
|
||||||
|
func (app *Application) modalActive() bool {
|
||||||
|
return (app.modalState.State != nil) || (len(app.failureMessage) > 0)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (app *Application) tryUndo() {
|
||||||
|
if !app.cmdStack.CanUndo() || app.modalActive() {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
imgui.ClearActiveID()
|
||||||
|
err := app.projectService.ModifyModWith(app.cmdStack.Undo)
|
||||||
|
if err != nil {
|
||||||
|
app.onFailure("Undo", "", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (app *Application) tryRedo() {
|
||||||
|
if !app.cmdStack.CanRedo() || app.modalActive() {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
imgui.ClearActiveID()
|
||||||
|
err := app.projectService.ModifyModWith(app.cmdStack.Redo)
|
||||||
|
if err != nil {
|
||||||
|
app.onFailure("Redo", "", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (app *Application) onMouseMove(x, y float32) {
|
||||||
|
app.lastMouseX = x
|
||||||
|
app.lastMouseY = y
|
||||||
|
app.guiContext.SetMousePosition(x, y)
|
||||||
|
if !app.guiContext.IsUsingMouse() {
|
||||||
|
app.mapDisplay.MouseMoved(x, y)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (app *Application) onMouseScroll(dx, dy float32) {
|
||||||
|
if !app.guiContext.IsUsingMouse() {
|
||||||
|
app.mapDisplay.MouseScrolled(app.lastMouseX, app.lastMouseY, dx, dy, app.lastModifier)
|
||||||
|
}
|
||||||
|
app.guiContext.MouseScroll(dx, dy)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (app *Application) onMouseButtonDown(buttonMask uint32, modifier input.Modifier) {
|
||||||
|
if !app.guiContext.IsUsingMouse() {
|
||||||
|
app.mapDisplay.MouseButtonDown(app.lastMouseX, app.lastMouseY, buttonMask)
|
||||||
|
}
|
||||||
|
app.reportButtonChange(buttonMask, true)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (app *Application) onMouseButtonUp(buttonMask uint32, modifier input.Modifier) {
|
||||||
|
if !app.guiContext.IsUsingMouse() {
|
||||||
|
app.mapDisplay.MouseButtonUp(app.lastMouseX, app.lastMouseY, buttonMask, modifier)
|
||||||
|
}
|
||||||
|
app.reportButtonChange(buttonMask, false)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (app *Application) reportButtonChange(buttonMask uint32, down bool) {
|
||||||
|
for buttonIndex := 0; buttonIndex < 32; buttonIndex++ {
|
||||||
|
if (buttonMask & (1 << uint32(buttonIndex))) != 0 {
|
||||||
|
app.guiContext.MouseButtonChanged(buttonIndex, down)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (app *Application) initGuiSizes() {
|
||||||
|
if app.GuiScale < 0.5 {
|
||||||
|
app.GuiScale = 1.0
|
||||||
|
} else if app.GuiScale > 10.0 {
|
||||||
|
app.GuiScale = 10.0
|
||||||
|
}
|
||||||
|
|
||||||
|
if app.FontSize <= 0 {
|
||||||
|
app.FontSize = 16.0
|
||||||
|
}
|
||||||
|
|
||||||
|
app.FontSize *= app.GuiScale
|
||||||
|
}
|
||||||
|
|
||||||
|
func (app *Application) initGuiStyle() {
|
||||||
|
if len(app.FontFile) == 0 {
|
||||||
|
imgui.CurrentIO().SetFontGlobalScale(app.GuiScale)
|
||||||
|
}
|
||||||
|
imgui.CurrentStyle().ScaleAllSizes(app.GuiScale)
|
||||||
|
|
||||||
|
color := func(r, g, b byte, alpha float32) imgui.Vec4 {
|
||||||
|
return imgui.Vec4{X: float32(r) / 255.0, Y: float32(g) / 255.0, Z: float32(b) / 255.0, W: alpha}
|
||||||
|
}
|
||||||
|
colorDoubleFull := func(alpha float32) imgui.Vec4 { return color(0xC4, 0x38, 0x9F, alpha) }
|
||||||
|
colorDoubleDark := func(alpha float32) imgui.Vec4 { return color(0x31, 0x01, 0x38, alpha) }
|
||||||
|
|
||||||
|
colorTripleFull := func(alpha float32) imgui.Vec4 { return color(0x21, 0xFF, 0x43, alpha) }
|
||||||
|
colorTripleDark := func(alpha float32) imgui.Vec4 { return color(0x06, 0xCC, 0x94, alpha) }
|
||||||
|
colorTripleLight := func(alpha float32) imgui.Vec4 { return color(0x51, 0x99, 0x58, alpha) }
|
||||||
|
|
||||||
|
style := imgui.CurrentStyle()
|
||||||
|
style.SetColor(imgui.StyleColorText, colorTripleFull(1.0))
|
||||||
|
style.SetColor(imgui.StyleColorTextDisabled, colorTripleDark(1.0))
|
||||||
|
|
||||||
|
style.SetColor(imgui.StyleColorWindowBg, colorDoubleDark(0.80))
|
||||||
|
style.SetColor(imgui.StyleColorPopupBg, colorDoubleDark(0.75))
|
||||||
|
|
||||||
|
style.SetColor(imgui.StyleColorTitleBgActive, colorTripleLight(1.0))
|
||||||
|
style.SetColor(imgui.StyleColorFrameBg, colorTripleLight(0.54))
|
||||||
|
|
||||||
|
style.SetColor(imgui.StyleColorFrameBgHovered, colorTripleDark(0.4))
|
||||||
|
style.SetColor(imgui.StyleColorFrameBgActive, colorTripleDark(0.67))
|
||||||
|
style.SetColor(imgui.StyleColorCheckMark, colorTripleDark(1.0))
|
||||||
|
style.SetColor(imgui.StyleColorSliderGrabActive, colorTripleDark(1.0))
|
||||||
|
style.SetColor(imgui.StyleColorButton, colorTripleDark(0.4))
|
||||||
|
style.SetColor(imgui.StyleColorButtonHovered, colorTripleDark(1.0))
|
||||||
|
style.SetColor(imgui.StyleColorHeader, colorTripleLight(0.70))
|
||||||
|
style.SetColor(imgui.StyleColorHeaderHovered, colorTripleDark(0.8))
|
||||||
|
style.SetColor(imgui.StyleColorHeaderActive, colorTripleDark(1.0))
|
||||||
|
style.SetColor(imgui.StyleColorResizeGrip, colorTripleDark(0.25))
|
||||||
|
style.SetColor(imgui.StyleColorResizeGripHovered, colorTripleDark(0.67))
|
||||||
|
style.SetColor(imgui.StyleColorResizeGripActive, colorTripleDark(0.95))
|
||||||
|
style.SetColor(imgui.StyleColorTextSelectedBg, colorTripleDark(0.35))
|
||||||
|
|
||||||
|
style.SetColor(imgui.StyleColorTab, colorTripleLight(0.54))
|
||||||
|
style.SetColor(imgui.StyleColorTabHovered, colorTripleLight(0.75))
|
||||||
|
style.SetColor(imgui.StyleColorTabActive, colorTripleLight(1.0))
|
||||||
|
|
||||||
|
style.SetColor(imgui.StyleColorSliderGrab, colorDoubleFull(1.0))
|
||||||
|
style.SetColor(imgui.StyleColorButtonActive, colorDoubleFull(1.0))
|
||||||
|
style.SetColor(imgui.StyleColorSeparatorHovered, colorDoubleFull(0.78))
|
||||||
|
style.SetColor(imgui.StyleColorSeparatorActive, colorTripleLight(1.0))
|
||||||
|
}
|
||||||
|
|
||||||
|
func (app *Application) initSignalling() {
|
||||||
|
app.cmdStack = new(cmd.Stack)
|
||||||
|
app.txnBuilder.Commander = app
|
||||||
|
}
|
||||||
|
|
||||||
|
func (app *Application) initModel() {
|
||||||
|
app.mod = world.NewMod(app.resourcesChanged, app.modReset)
|
||||||
|
|
||||||
|
app.cp = text.DefaultCodepage()
|
||||||
|
app.textLineCache = text.NewLineCache(app.cp, app.mod)
|
||||||
|
app.textPageCache = text.NewPageCache(app.cp, app.mod)
|
||||||
|
app.messagesCache = text.NewElectronicMessageCache(app.cp, app.mod)
|
||||||
|
app.movieCache = movie.NewCache(app.cp, app.mod)
|
||||||
|
app.soundEffectCache = sound.NewEffectCache(app.mod)
|
||||||
|
|
||||||
|
app.levels = edit.NewEditableLevels(&app.txnBuilder, app.mod)
|
||||||
|
app.levelSelection = edit.NewLevelSelectionService(app.levels)
|
||||||
|
|
||||||
|
app.gameObjectsService = edit.NewGameObjectsService(app.mod)
|
||||||
|
app.levelEditorService = edit.NewLevelEditorService(&app.txnBuilder, app.gameObjectsService, app.levels, app.levelSelection)
|
||||||
|
|
||||||
|
app.paletteCache = graphics.NewPaletteCache(app.gl, app.mod)
|
||||||
|
app.textureCache = graphics.NewTextureCache(app.gl, app.mod)
|
||||||
|
app.frameCache = graphics.NewFrameCache(app.gl)
|
||||||
|
app.animationCache = bitmap.NewAnimationCache(app.mod)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (app *Application) resourcesChanged(modifiedIDs []resource.ID, failedIDs []resource.ID) {
|
||||||
|
app.textLineCache.InvalidateResources(modifiedIDs)
|
||||||
|
app.textPageCache.InvalidateResources(modifiedIDs)
|
||||||
|
app.messagesCache.InvalidateResources(modifiedIDs)
|
||||||
|
app.movieCache.InvalidateResources(modifiedIDs)
|
||||||
|
app.soundEffectCache.InvalidateResources(modifiedIDs)
|
||||||
|
app.levels.InvalidateResources(modifiedIDs)
|
||||||
|
app.paletteCache.InvalidateResources(modifiedIDs)
|
||||||
|
app.textureCache.InvalidateResources(modifiedIDs)
|
||||||
|
app.animationCache.InvalidateResources(modifiedIDs)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (app *Application) modReset() {
|
||||||
|
app.cmdStack = new(cmd.Stack)
|
||||||
|
}
|
||||||
|
|
||||||
|
// nolint: lll
|
||||||
|
func (app *Application) initView() {
|
||||||
|
textViewer := media.NewTextViewerService(app.textLineCache, app.textPageCache, app.mod)
|
||||||
|
textSetter := media.NewTextSetterService(app.cp)
|
||||||
|
audioViewer := media.NewAudioViewerService(app.movieCache, app.mod)
|
||||||
|
audioSetter := media.NewAudioSetterService()
|
||||||
|
movieViewer := media.NewMovieViewerService(app.movieCache, app.mod)
|
||||||
|
movieSetter := media.NewMovieSetterService(app.cp)
|
||||||
|
soundEffectViewer := media.NewSoundViewerService(app.soundEffectCache, app.mod)
|
||||||
|
soundEffectSetter := media.NewSoundSetterService()
|
||||||
|
soundEffectService := undoable.NewSoundEffectService(edit.NewSoundEffectService(soundEffectViewer, soundEffectSetter), app)
|
||||||
|
augmentedTextService := undoable.NewAugmentedTextService(edit.NewAugmentedTextService(textViewer, textSetter, audioViewer, audioSetter), app)
|
||||||
|
movieService := undoable.NewMovieService(edit.NewMovieService(app.cp, movieViewer, movieSetter), app)
|
||||||
|
|
||||||
|
app.projectService = edit.NewProjectService(&app.txnBuilder, app.mod)
|
||||||
|
app.gameStateService = edit.NewGameStateService(&app.txnBuilder)
|
||||||
|
|
||||||
|
app.projectView = project.NewView(app.projectService, &app.modalState, app.GuiScale, &app.txnBuilder)
|
||||||
|
app.archiveView = archives.NewArchiveView(&app.txnBuilder, app.gameStateService, app.mod, app.textLineCache, app.cp, &app.modalState, app.GuiScale, app)
|
||||||
|
app.levelControlView = levels.NewControlView(app.levels, app.levelSelection, app.levelEditorService, app.GuiScale, app.textLineCache, app.textureCache, &app.txnBuilder)
|
||||||
|
app.levelTilesView = levels.NewTilesView(app.levelEditorService, app.GuiScale, app.textLineCache, app.textureCache, &app.txnBuilder)
|
||||||
|
app.levelObjectsView = levels.NewObjectsView(app.gameObjectsService, app.levelEditorService, app.levelSelection, app.gameStateService, app.GuiScale, app.textLineCache, app.textureCache, &app.txnBuilder, app.gl)
|
||||||
|
app.messagesView = messages.NewMessagesView(app.mod, app.messagesCache, app.cp, app.movieCache, app.textureCache, &app.modalState, app.clipboard, app.GuiScale, app)
|
||||||
|
app.textsView = texts.NewTextsView(augmentedTextService, &app.modalState, app.clipboard, app.GuiScale)
|
||||||
|
app.bitmapsView = bitmaps.NewBitmapsView(app.mod, app.textureCache, app.paletteCache, &app.modalState, app.clipboard, app.GuiScale, app)
|
||||||
|
app.texturesView = textures.NewTexturesView(app.mod, app.textLineCache, app.cp, app.textureCache, app.paletteCache, &app.modalState, app.clipboard, app.GuiScale, app)
|
||||||
|
app.animationsView = animations.NewAnimationsView(app.mod, app.textureCache, app.paletteCache, app.animationCache, &app.modalState, app.GuiScale, app)
|
||||||
|
app.moviesView = movies.NewMoviesView(app.mod, app.frameCache, movieService, &app.modalState, app.GuiScale, app)
|
||||||
|
app.soundEffectsView = sounds.NewSoundEffectsView(soundEffectService, &app.modalState, app.GuiScale)
|
||||||
|
app.objectsView = objects.NewView(app.mod, app.textLineCache, app.cp, app.textureCache, app.paletteCache, &app.modalState, app.clipboard, app.GuiScale, app)
|
||||||
|
app.aboutView = about.NewView(app.clipboard, app.GuiScale, app.Version)
|
||||||
|
app.licensesView = about.NewLicensesView(app.GuiScale)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Queue requests to perform the given command.
|
||||||
|
func (app *Application) Queue(command cmd.Command) {
|
||||||
|
err := app.projectService.ModifyModWith(func(modder world.Modder) error {
|
||||||
|
return app.cmdStack.Perform(command, modder)
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
app.onFailure("command", "", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (app *Application) onFailure(source string, details string, err error) {
|
||||||
|
app.failurePending = true
|
||||||
|
app.failureMessage = fmt.Sprintf("Source: %v\nDetails: %v\nError: %v", source, details, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (app *Application) renderMainMenu() {
|
||||||
|
windowEntry := func(name string, shortcut string, isOpen *bool) {
|
||||||
|
if imgui.MenuItemV(name, shortcut, *isOpen, true) {
|
||||||
|
*isOpen = !*isOpen
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if imgui.BeginMainMenuBar() {
|
||||||
|
if imgui.BeginMenu("File") {
|
||||||
|
if imgui.MenuItemV("New Project", "Ctrl+N", false, true) {
|
||||||
|
app.newProject()
|
||||||
|
}
|
||||||
|
if imgui.MenuItemV("Load Project", "", false, true) {
|
||||||
|
app.loadProject()
|
||||||
|
}
|
||||||
|
if imgui.MenuItemV("Save Project", "", false, true) {
|
||||||
|
app.saveProject()
|
||||||
|
}
|
||||||
|
imgui.Separator()
|
||||||
|
if imgui.MenuItemV("Save Mod", "Ctrl+S", false, true) {
|
||||||
|
app.projectView.StartSavingMod()
|
||||||
|
}
|
||||||
|
windowEntry("Project", "F1", app.projectView.WindowOpen())
|
||||||
|
imgui.Separator()
|
||||||
|
if imgui.MenuItem("Exit") {
|
||||||
|
app.window.SetCloseRequest(true)
|
||||||
|
}
|
||||||
|
imgui.EndMenu()
|
||||||
|
}
|
||||||
|
if imgui.BeginMenu("Edit") {
|
||||||
|
if imgui.MenuItemV("Undo", "Ctrl+Z", false, app.cmdStack.CanUndo()) {
|
||||||
|
app.tryUndo()
|
||||||
|
}
|
||||||
|
if imgui.MenuItemV("Redo", "Ctrl+Y / Ctrl+Shift+Z", false, app.cmdStack.CanRedo()) {
|
||||||
|
app.tryRedo()
|
||||||
|
}
|
||||||
|
imgui.EndMenu()
|
||||||
|
}
|
||||||
|
if imgui.BeginMenu("Window") {
|
||||||
|
windowEntry("Archive", "", app.archiveView.WindowOpen())
|
||||||
|
windowEntry("Level Control", "F2", app.levelControlView.WindowOpen())
|
||||||
|
windowEntry("Level Tiles", "F3", app.levelTilesView.WindowOpen())
|
||||||
|
windowEntry("Level Objects", "F4", app.levelObjectsView.WindowOpen())
|
||||||
|
windowEntry("Messages", "F5", app.messagesView.WindowOpen())
|
||||||
|
windowEntry("Texts", "", app.textsView.WindowOpen())
|
||||||
|
windowEntry("Bitmaps", "", app.bitmapsView.WindowOpen())
|
||||||
|
windowEntry("Textures", "", app.texturesView.WindowOpen())
|
||||||
|
windowEntry("Animations", "", app.animationsView.WindowOpen())
|
||||||
|
windowEntry("Movies", "", app.moviesView.WindowOpen())
|
||||||
|
windowEntry("Sound Effects", "", app.soundEffectsView.WindowOpen())
|
||||||
|
windowEntry("Game Objects", "", app.objectsView.WindowOpen())
|
||||||
|
imgui.EndMenu()
|
||||||
|
}
|
||||||
|
if imgui.BeginMenu("Help") {
|
||||||
|
if imgui.MenuItem("About...") {
|
||||||
|
app.aboutView.Show()
|
||||||
|
}
|
||||||
|
if imgui.MenuItem("Licenses") {
|
||||||
|
app.licensesView.Show()
|
||||||
|
}
|
||||||
|
imgui.EndMenu()
|
||||||
|
}
|
||||||
|
imgui.EndMainMenuBar()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (app *Application) handleFailure() {
|
||||||
|
if app.failurePending {
|
||||||
|
imgui.OpenPopup("Failure Message")
|
||||||
|
app.failurePending = false
|
||||||
|
}
|
||||||
|
if imgui.BeginPopupModal("Failure Message") {
|
||||||
|
imgui.Text(`Something went wrong. This is bad and I am sorry.
|
||||||
|
|
||||||
|
You have the option to "Ignore" this and hope for the best.
|
||||||
|
This action also clears the undo/redo buffer.
|
||||||
|
|
||||||
|
Or you can simply "Exit" the application and then restart it.
|
||||||
|
This action loses any pending changes.
|
||||||
|
|
||||||
|
Perhaps you can make something with the details of the error below.
|
||||||
|
If you can reproduce this, please make a screenshot and
|
||||||
|
report it with details on how to reproduce it on the
|
||||||
|
http://www.systemshock.org forums. Thank you!
|
||||||
|
`)
|
||||||
|
imgui.Text("Version: " + app.Version)
|
||||||
|
imgui.Separator()
|
||||||
|
imgui.Text(app.failureMessage)
|
||||||
|
imgui.Separator()
|
||||||
|
if imgui.Button("Ignore") {
|
||||||
|
app.failureMessage = ""
|
||||||
|
app.cmdStack = new(cmd.Stack)
|
||||||
|
imgui.CloseCurrentPopup()
|
||||||
|
}
|
||||||
|
imgui.SameLine()
|
||||||
|
if imgui.Button("Exit") {
|
||||||
|
app.window.SetCloseRequest(true)
|
||||||
|
}
|
||||||
|
imgui.EndPopup()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const settingsFileExtension = "hacked-project"
|
||||||
|
|
||||||
|
func projectFileTypes() []external.TypeInfo {
|
||||||
|
return []external.TypeInfo{
|
||||||
|
{
|
||||||
|
Title: "HackEd Project File (*." + settingsFileExtension + ")",
|
||||||
|
Extensions: []string{settingsFileExtension},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (app *Application) newProject() {
|
||||||
|
app.saveStoredProject()
|
||||||
|
app.restoreProjectState(projectState{}, "")
|
||||||
|
}
|
||||||
|
|
||||||
|
func (app *Application) loadProject() {
|
||||||
|
app.saveStoredProject()
|
||||||
|
|
||||||
|
external.LoadFile(&app.modalState, projectFileTypes(), func(filename string) error {
|
||||||
|
state, err := projectStateFromFile(filename)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
app.restoreProjectState(state, filename)
|
||||||
|
return nil
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func (app *Application) saveStoredProject() {
|
||||||
|
currentFilename := app.projectService.CurrentStateFilename()
|
||||||
|
if len(currentFilename) > 0 {
|
||||||
|
currentState := app.currentProjectState()
|
||||||
|
_ = currentState.SaveTo(currentFilename)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (app *Application) saveProject() {
|
||||||
|
currentState := app.currentProjectState()
|
||||||
|
currentFilename := app.projectService.CurrentStateFilename()
|
||||||
|
if (len(currentFilename) == 0) || (currentState.SaveTo(currentFilename) != nil) {
|
||||||
|
external.SaveFile(&app.modalState, projectFileTypes(), func(filename string) error {
|
||||||
|
completeFilename := filename
|
||||||
|
dotExtension := "." + settingsFileExtension
|
||||||
|
if !strings.HasSuffix(completeFilename, dotExtension) {
|
||||||
|
completeFilename += dotExtension
|
||||||
|
}
|
||||||
|
err := currentState.SaveTo(completeFilename)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
app.projectService.SetCurrentStateFilename(completeFilename)
|
||||||
|
app.window.SetTitleSuffix(completeFilename)
|
||||||
|
return nil
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (app *Application) currentProjectState() projectState {
|
||||||
|
projectSettings := app.projectService.CurrentSettings()
|
||||||
|
gameStateSettings := app.gameStateService.CurrentSettings()
|
||||||
|
|
||||||
|
windowOpenByName := app.windowOpenByName()
|
||||||
|
var openWindows []string
|
||||||
|
for key, open := range windowOpenByName {
|
||||||
|
if *open {
|
||||||
|
openWindows = append(openWindows, key)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
activeLevelIndex := app.levelSelection.CurrentLevelID()
|
||||||
|
return projectState{
|
||||||
|
ProjectSettings: &projectSettings,
|
||||||
|
GameStateSettings: &gameStateSettings,
|
||||||
|
OpenWindows: openWindows,
|
||||||
|
ActiveLevelIndex: &activeLevelIndex,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (app *Application) saveWorkspace() {
|
||||||
|
currentProjectState := app.currentProjectState()
|
||||||
|
currentProjectStateFilename := app.projectService.CurrentStateFilename()
|
||||||
|
if len(currentProjectStateFilename) > 0 {
|
||||||
|
err := currentProjectState.SaveTo(currentProjectStateFilename)
|
||||||
|
if err != nil {
|
||||||
|
currentProjectStateFilename = ""
|
||||||
|
} else {
|
||||||
|
currentProjectState = projectState{}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
lastProjectState := lastProjectState{
|
||||||
|
projectState: currentProjectState,
|
||||||
|
LastProject: currentProjectStateFilename,
|
||||||
|
}
|
||||||
|
_ = lastProjectState.SaveTo(app.lastProjectStateConfigFilename())
|
||||||
|
}
|
||||||
|
|
||||||
|
func (app *Application) restoreWorkspace() {
|
||||||
|
lastProjectState := lastProjectStateFromFile(app.lastProjectStateConfigFilename())
|
||||||
|
|
||||||
|
projectStateToRestore := lastProjectState.projectState
|
||||||
|
|
||||||
|
projectStateFilename := ""
|
||||||
|
if len(lastProjectState.LastProject) > 0 {
|
||||||
|
state, err := projectStateFromFile(lastProjectState.LastProject)
|
||||||
|
if err == nil {
|
||||||
|
projectStateToRestore = state
|
||||||
|
projectStateFilename = lastProjectState.LastProject
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
app.restoreProjectState(projectStateToRestore, projectStateFilename)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (app *Application) restoreProjectState(state projectState, filename string) {
|
||||||
|
var projectSettings edit.ProjectSettings
|
||||||
|
if state.ProjectSettings != nil {
|
||||||
|
projectSettings = *state.ProjectSettings
|
||||||
|
}
|
||||||
|
app.projectService.RestoreProject(projectSettings, filename)
|
||||||
|
var gameStateSettings edit.GameStateSettings
|
||||||
|
if state.GameStateSettings != nil {
|
||||||
|
gameStateSettings = *state.GameStateSettings
|
||||||
|
}
|
||||||
|
app.gameStateService.RestoreSettings(gameStateSettings)
|
||||||
|
|
||||||
|
windowOpenByName := app.windowOpenByName()
|
||||||
|
for _, open := range windowOpenByName {
|
||||||
|
*open = false
|
||||||
|
}
|
||||||
|
for _, key := range state.OpenWindows {
|
||||||
|
open := windowOpenByName[key]
|
||||||
|
if open != nil {
|
||||||
|
*open = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if len(state.OpenWindows) == 0 {
|
||||||
|
*app.projectView.WindowOpen() = true
|
||||||
|
}
|
||||||
|
|
||||||
|
activeLevel := world.StartingLevel
|
||||||
|
if (state.ActiveLevelIndex != nil) && app.levels.IsLevelAvailable(*state.ActiveLevelIndex) {
|
||||||
|
activeLevel = *state.ActiveLevelIndex
|
||||||
|
}
|
||||||
|
app.levelSelection.SetCurrentLevelID(activeLevel)
|
||||||
|
|
||||||
|
app.window.SetTitleSuffix(filename)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (app *Application) updateAutoSave() {
|
||||||
|
status := app.projectService.SaveStatus()
|
||||||
|
app.window.SetProjectModified(status.FilesModified > 0)
|
||||||
|
if status.SavePending && (status.SaveIn == 0) {
|
||||||
|
status.ConfirmPendingSave()
|
||||||
|
app.projectView.StartSavingMod()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (app *Application) windowStateConfigFilename() string {
|
||||||
|
return filepath.Join(app.ConfigDir, "WindowState.json")
|
||||||
|
}
|
||||||
|
|
||||||
|
func (app *Application) lastProjectStateConfigFilename() string {
|
||||||
|
return filepath.Join(app.ConfigDir, "LastProjectState.json")
|
||||||
|
}
|
||||||
|
|
||||||
|
func (app *Application) windowOpenByName() map[string]*bool {
|
||||||
|
return map[string]*bool{
|
||||||
|
"project": app.projectView.WindowOpen(),
|
||||||
|
"archive": app.archiveView.WindowOpen(),
|
||||||
|
"levelControl": app.levelControlView.WindowOpen(),
|
||||||
|
"levelTiles": app.levelTilesView.WindowOpen(),
|
||||||
|
"levelObjects": app.levelObjectsView.WindowOpen(),
|
||||||
|
"messages": app.messagesView.WindowOpen(),
|
||||||
|
"texts": app.textsView.WindowOpen(),
|
||||||
|
"bitmaps": app.bitmapsView.WindowOpen(),
|
||||||
|
"textures": app.texturesView.WindowOpen(),
|
||||||
|
"animations": app.animationsView.WindowOpen(),
|
||||||
|
"movies": app.moviesView.WindowOpen(),
|
||||||
|
"soundEffects": app.soundEffectsView.WindowOpen(),
|
||||||
|
"gameObjects": app.objectsView.WindowOpen(),
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,15 @@
|
|||||||
|
package editor
|
||||||
|
|
||||||
|
import "github.com/inkyblackness/hacked/ui/opengl"
|
||||||
|
|
||||||
|
type clipboardAdapter struct {
|
||||||
|
window opengl.Window
|
||||||
|
}
|
||||||
|
|
||||||
|
func (adapter clipboardAdapter) String() (string, error) {
|
||||||
|
return adapter.window.ClipboardString()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (adapter clipboardAdapter) SetString(value string) {
|
||||||
|
adapter.window.SetClipboardString(value)
|
||||||
|
}
|
||||||
@@ -0,0 +1,66 @@
|
|||||||
|
package about
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/inkyblackness/imgui-go/v3"
|
||||||
|
)
|
||||||
|
|
||||||
|
// LicensesView handles the about display.
|
||||||
|
type LicensesView struct {
|
||||||
|
guiScale float32
|
||||||
|
|
||||||
|
model viewModel
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewLicensesView creates a new instance for the about display.
|
||||||
|
func NewLicensesView(guiScale float32) *LicensesView {
|
||||||
|
return &LicensesView{
|
||||||
|
guiScale: guiScale,
|
||||||
|
|
||||||
|
model: freshViewModel(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Show requests to show the licenses view.
|
||||||
|
func (view *LicensesView) Show() {
|
||||||
|
view.model.windowOpen = true
|
||||||
|
}
|
||||||
|
|
||||||
|
// Render requests to render the view.
|
||||||
|
func (view *LicensesView) Render() {
|
||||||
|
if view.model.windowOpen {
|
||||||
|
imgui.OpenPopup("Licenses")
|
||||||
|
view.model.windowOpen = false
|
||||||
|
imgui.SetNextWindowSize(imgui.Vec2{X: 640 * view.guiScale, Y: 480 * view.guiScale})
|
||||||
|
}
|
||||||
|
if imgui.BeginPopupModalV("Licenses", nil, imgui.WindowFlagsHorizontalScrollbar|imgui.WindowFlagsNoSavedSettings) {
|
||||||
|
view.renderContent()
|
||||||
|
imgui.EndPopup()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (view *LicensesView) renderContent() {
|
||||||
|
if imgui.TreeNodeV("System Shock", imgui.TreeNodeFlagsDefaultOpen) {
|
||||||
|
imgui.Text("System Shock by Night Dive Studios, LLC.\nOriginally by Looking Glass Technologies")
|
||||||
|
imgui.TreePop()
|
||||||
|
}
|
||||||
|
add := func(title, license string) {
|
||||||
|
if imgui.TreeNode(title) {
|
||||||
|
imgui.Text(license)
|
||||||
|
imgui.TreePop()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
add("InkyBlackness - HackEd", inkyblacknessHackedLicense)
|
||||||
|
add("InkyBlackness - imgui", inkyblacknessImGuiLicense)
|
||||||
|
add("go-gl/glfw - Go bindings for GLFW 3", goglGlfwLicense)
|
||||||
|
add("go-gl/gl", goglOpenGLLicense)
|
||||||
|
add("go-gl/mathgl", goglMathGLLicense)
|
||||||
|
add("Dear ImGui", dearImGuiLicense)
|
||||||
|
add("GLFW", glfwLicense)
|
||||||
|
add("MinGW-w64", mingwLicense)
|
||||||
|
|
||||||
|
imgui.Separator()
|
||||||
|
if imgui.Button("OK") {
|
||||||
|
imgui.CloseCurrentPopup()
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,70 @@
|
|||||||
|
package about
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/inkyblackness/imgui-go/v3"
|
||||||
|
|
||||||
|
"github.com/inkyblackness/hacked/editor/external"
|
||||||
|
)
|
||||||
|
|
||||||
|
// View handles the about display.
|
||||||
|
type View struct {
|
||||||
|
version string
|
||||||
|
guiScale float32
|
||||||
|
clipboard external.Clipboard
|
||||||
|
|
||||||
|
model viewModel
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewView creates a new instance for the about display.
|
||||||
|
func NewView(clipboard external.Clipboard, guiScale float32, version string) *View {
|
||||||
|
return &View{
|
||||||
|
version: version,
|
||||||
|
guiScale: guiScale,
|
||||||
|
clipboard: clipboard,
|
||||||
|
|
||||||
|
model: freshViewModel(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Show requests to show the about view.
|
||||||
|
func (view *View) Show() {
|
||||||
|
view.model.windowOpen = true
|
||||||
|
}
|
||||||
|
|
||||||
|
// Render requests to render the view.
|
||||||
|
func (view *View) Render() {
|
||||||
|
if view.model.windowOpen {
|
||||||
|
imgui.OpenPopup("About")
|
||||||
|
view.model.windowOpen = false
|
||||||
|
}
|
||||||
|
if imgui.BeginPopupModalV("About", nil, imgui.WindowFlagsAlwaysAutoResize|imgui.WindowFlagsNoMove|imgui.WindowFlagsHorizontalScrollbar) {
|
||||||
|
view.renderContent()
|
||||||
|
imgui.EndPopup()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (view *View) renderContent() {
|
||||||
|
projectURL := "https://inkyblackness.github.io"
|
||||||
|
communityURL := "https://www.systemshock.org"
|
||||||
|
userguideURL := "https://github.com/inkyblackness/hacked/wiki"
|
||||||
|
urlLine := func(title, url string) {
|
||||||
|
imgui.Text(title + ": " + url)
|
||||||
|
imgui.SameLine()
|
||||||
|
if imgui.Button("-> Clip##" + title) {
|
||||||
|
view.clipboard.SetString(url)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
imgui.Text("InkyBlackness - HackEd - " + view.version)
|
||||||
|
imgui.Separator()
|
||||||
|
urlLine("User guide", userguideURL)
|
||||||
|
urlLine("Community", communityURL)
|
||||||
|
urlLine("Project", projectURL)
|
||||||
|
imgui.Separator()
|
||||||
|
imgui.Text("Thanks to: 3RDPlayer: testing, ToxicFrog: name idea.")
|
||||||
|
imgui.Text("Written in Go. Because I can.")
|
||||||
|
imgui.Separator()
|
||||||
|
if imgui.Button("OK") {
|
||||||
|
imgui.CloseCurrentPopup()
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
package about
|
||||||
|
|
||||||
|
type viewModel struct {
|
||||||
|
windowOpen bool
|
||||||
|
}
|
||||||
|
|
||||||
|
func freshViewModel() viewModel {
|
||||||
|
return viewModel{}
|
||||||
|
}
|
||||||
@@ -0,0 +1,227 @@
|
|||||||
|
package about
|
||||||
|
|
||||||
|
var inkyblacknessHackedLicense = `New BSD License
|
||||||
|
|
||||||
|
© 2018, Christian Haas
|
||||||
|
All rights reserved.
|
||||||
|
|
||||||
|
Redistribution and use in source and binary forms, with or without
|
||||||
|
modification, are permitted provided that the following conditions are met:
|
||||||
|
|
||||||
|
* Redistributions of source code must retain the above copyright
|
||||||
|
notice, this list of conditions and the following disclaimer.
|
||||||
|
* 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.
|
||||||
|
* Neither the name of "InkyBlackness" nor the names of its
|
||||||
|
contributors may be used to endorse or promote products derived from
|
||||||
|
this software without specific prior written permission.
|
||||||
|
|
||||||
|
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 LISTED COPYRIGHT HOLDERS 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.
|
||||||
|
`
|
||||||
|
|
||||||
|
var inkyblacknessImGuiLicense = `New BSD License
|
||||||
|
|
||||||
|
© 2018, Christian Haas
|
||||||
|
All rights reserved.
|
||||||
|
|
||||||
|
Redistribution and use in source and binary forms, with or without
|
||||||
|
modification, are permitted provided that the following conditions are met:
|
||||||
|
|
||||||
|
* Redistributions of source code must retain the above copyright
|
||||||
|
notice, this list of conditions and the following disclaimer.
|
||||||
|
* 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.
|
||||||
|
* Neither the name of "InkyBlackness" nor the names of its
|
||||||
|
contributors may be used to endorse or promote products derived from
|
||||||
|
this software without specific prior written permission.
|
||||||
|
|
||||||
|
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 LISTED COPYRIGHT HOLDERS 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.
|
||||||
|
`
|
||||||
|
|
||||||
|
var dearImGuiLicense = `The MIT License (MIT)
|
||||||
|
|
||||||
|
Copyright (c) 2014-2018 Omar Cornut
|
||||||
|
|
||||||
|
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||||
|
of this software and associated documentation files (the "Software"), to deal
|
||||||
|
in the Software without restriction, including without limitation the rights
|
||||||
|
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||||
|
copies of the Software, and to permit persons to whom the Software is
|
||||||
|
furnished to do so, subject to the following conditions:
|
||||||
|
|
||||||
|
The above copyright notice and this permission notice shall be included in all
|
||||||
|
copies or substantial portions of the Software.
|
||||||
|
|
||||||
|
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||||
|
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||||
|
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||||
|
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||||
|
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||||
|
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||||
|
SOFTWARE.
|
||||||
|
`
|
||||||
|
|
||||||
|
var mingwLicense = `With exception of certain parts that are prominently marked as being
|
||||||
|
in the Public Domain, BSD, or LGPL this Software is provided under the
|
||||||
|
Zope Public License (ZPL) Version 2.1.
|
||||||
|
|
||||||
|
Copyright (c) 2009 - 2013 by the mingw-w64 project
|
||||||
|
|
||||||
|
See the AUTHORS file for the list of contributors to the mingw-w64 project.
|
||||||
|
|
||||||
|
This license has been certified as open source. It has also been designated
|
||||||
|
as GPL compatible by the Free Software Foundation (FSF).
|
||||||
|
|
||||||
|
Redistribution and use in source and binary forms, with or without
|
||||||
|
modification, are permitted provided that the following conditions are met:
|
||||||
|
|
||||||
|
1. Redistributions in source code must retain the accompanying copyright
|
||||||
|
notice, this list of conditions, and the following disclaimer.
|
||||||
|
2. Redistributions in binary form must reproduce the accompanying
|
||||||
|
copyright notice, this list of conditions, and the following disclaimer
|
||||||
|
in the documentation and/or other materials provided with the
|
||||||
|
distribution.
|
||||||
|
3. Names of the copyright holders must not be used to endorse or promote
|
||||||
|
products derived from this software without prior written permission
|
||||||
|
from the copyright holders.
|
||||||
|
4. The right to distribute this software or to use it for any purpose does
|
||||||
|
not give you the right to use Servicemarks (sm) or Trademarks (tm) of
|
||||||
|
the copyright holders. Use of them is covered by separate agreement
|
||||||
|
with the copyright holders.
|
||||||
|
5. If any files are modified, you must cause the modified files to carry
|
||||||
|
prominent notices stating that you changed the files and the date of
|
||||||
|
any change.
|
||||||
|
|
||||||
|
Disclaimer
|
||||||
|
|
||||||
|
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS ''AS IS'' AND ANY EXPRESSED
|
||||||
|
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 HOLDERS 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.
|
||||||
|
`
|
||||||
|
|
||||||
|
var goglGlfwLicense = `Copyright (c) 2012 The glfw3-go Authors. All rights reserved.
|
||||||
|
|
||||||
|
Redistribution and use in source and binary forms, with or without
|
||||||
|
modification, are permitted provided that the following conditions are
|
||||||
|
met:
|
||||||
|
|
||||||
|
* Redistributions of source code must retain the above copyright
|
||||||
|
notice, this list of conditions and the following disclaimer.
|
||||||
|
* 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.
|
||||||
|
* Neither the name of Google Inc. nor the names of its
|
||||||
|
contributors may be used to endorse or promote products derived from
|
||||||
|
this software without specific prior written permission.
|
||||||
|
|
||||||
|
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
|
||||||
|
OWNER 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.
|
||||||
|
`
|
||||||
|
|
||||||
|
var goglOpenGLLicense = `The MIT License (MIT)
|
||||||
|
|
||||||
|
Copyright (c) 2014 Eric Woroshow
|
||||||
|
|
||||||
|
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||||
|
of this software and associated documentation files (the "Software"), to deal
|
||||||
|
in the Software without restriction, including without limitation the rights
|
||||||
|
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||||
|
copies of the Software, and to permit persons to whom the Software is
|
||||||
|
furnished to do so, subject to the following conditions:
|
||||||
|
|
||||||
|
The above copyright notice and this permission notice shall be included in all
|
||||||
|
copies or substantial portions of the Software.
|
||||||
|
|
||||||
|
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||||
|
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||||
|
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||||
|
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||||
|
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||||
|
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||||
|
SOFTWARE.
|
||||||
|
`
|
||||||
|
|
||||||
|
var goglMathGLLicense = `Copyright ©2013 The go-gl Authors. All rights reserved.
|
||||||
|
|
||||||
|
Redistribution and use in source and binary forms, with or without
|
||||||
|
modification, are permitted provided that the following conditions are met:
|
||||||
|
* Redistributions of source code must retain the above copyright
|
||||||
|
notice, this list of conditions and the following disclaimer.
|
||||||
|
* 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.
|
||||||
|
* Neither the name of the gonum project nor the names of its authors and
|
||||||
|
contributors may be used to endorse or promote products derived from this
|
||||||
|
software without specific prior written permission.
|
||||||
|
|
||||||
|
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.
|
||||||
|
`
|
||||||
|
|
||||||
|
var glfwLicense = `Copyright (c) 2002-2006 Marcus Geelnard
|
||||||
|
Copyright (c) 2006-2016 Camilla Löwy <elmindreda@glfw.org>
|
||||||
|
|
||||||
|
This software is provided 'as-is', without any express or implied
|
||||||
|
warranty. In no event will the authors be held liable for any damages
|
||||||
|
arising from the use of this software.
|
||||||
|
|
||||||
|
Permission is granted to anyone to use this software for any purpose,
|
||||||
|
including commercial applications, and to alter it and redistribute it
|
||||||
|
freely, subject to the following restrictions:
|
||||||
|
|
||||||
|
1. The origin of this software must not be misrepresented; you must not
|
||||||
|
claim that you wrote the original software. If you use this software
|
||||||
|
in a product, an acknowledgment in the product documentation would
|
||||||
|
be appreciated but is not required.
|
||||||
|
|
||||||
|
2. Altered source versions must be plainly marked as such, and must not
|
||||||
|
be misrepresented as being the original software.
|
||||||
|
|
||||||
|
3. This notice may not be removed or altered from any source
|
||||||
|
distribution.
|
||||||
|
`
|
||||||
@@ -0,0 +1,41 @@
|
|||||||
|
package animations
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/inkyblackness/hacked/ss1/resource"
|
||||||
|
"github.com/inkyblackness/hacked/ss1/world"
|
||||||
|
)
|
||||||
|
|
||||||
|
type setAnimationCommand struct {
|
||||||
|
model *viewModel
|
||||||
|
|
||||||
|
animationKey resource.Key
|
||||||
|
framesID resource.ID
|
||||||
|
|
||||||
|
oldAnimation []byte
|
||||||
|
newAnimation []byte
|
||||||
|
|
||||||
|
oldFrames [][]byte
|
||||||
|
newFrames [][]byte
|
||||||
|
}
|
||||||
|
|
||||||
|
func (command setAnimationCommand) Do(modder world.Modder) error {
|
||||||
|
return command.perform(modder, command.newAnimation, command.newFrames)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (command setAnimationCommand) Undo(modder world.Modder) error {
|
||||||
|
return command.perform(modder, command.oldAnimation, command.oldFrames)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (command setAnimationCommand) perform(modder world.Modder, animData []byte, frames [][]byte) error {
|
||||||
|
if len(frames) == 0 {
|
||||||
|
modder.DelResource(command.animationKey.Lang, command.animationKey.ID.Plus(command.animationKey.Index))
|
||||||
|
modder.DelResource(command.animationKey.Lang, command.framesID)
|
||||||
|
} else {
|
||||||
|
modder.SetResourceBlock(command.animationKey.Lang, command.animationKey.ID.Plus(command.animationKey.Index), 0, animData)
|
||||||
|
modder.SetResourceBlocks(command.animationKey.Lang, command.framesID, frames)
|
||||||
|
}
|
||||||
|
|
||||||
|
command.model.restoreFocus = true
|
||||||
|
command.model.currentKey = command.animationKey
|
||||||
|
return nil
|
||||||
|
}
|
||||||
@@ -0,0 +1,338 @@
|
|||||||
|
package animations
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"encoding/binary"
|
||||||
|
"fmt"
|
||||||
|
"image"
|
||||||
|
"image/gif"
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
|
||||||
|
"github.com/inkyblackness/imgui-go/v3"
|
||||||
|
|
||||||
|
"github.com/inkyblackness/hacked/editor/external"
|
||||||
|
"github.com/inkyblackness/hacked/editor/graphics"
|
||||||
|
"github.com/inkyblackness/hacked/editor/render"
|
||||||
|
"github.com/inkyblackness/hacked/ss1/content/bitmap"
|
||||||
|
"github.com/inkyblackness/hacked/ss1/edit/undoable/cmd"
|
||||||
|
"github.com/inkyblackness/hacked/ss1/resource"
|
||||||
|
"github.com/inkyblackness/hacked/ss1/serial/rle"
|
||||||
|
"github.com/inkyblackness/hacked/ss1/world"
|
||||||
|
"github.com/inkyblackness/hacked/ss1/world/ids"
|
||||||
|
"github.com/inkyblackness/hacked/ui/gui"
|
||||||
|
)
|
||||||
|
|
||||||
|
type animationInfo struct {
|
||||||
|
title string
|
||||||
|
}
|
||||||
|
|
||||||
|
var knownAnimationTypes = map[resource.ID]animationInfo{
|
||||||
|
ids.VideoMailAnimationsStart: {title: "Video Mail Parts"},
|
||||||
|
}
|
||||||
|
|
||||||
|
var knownAnimationTypesOrder = []resource.ID{
|
||||||
|
ids.VideoMailAnimationsStart,
|
||||||
|
}
|
||||||
|
|
||||||
|
// View provides edit controls for animations.
|
||||||
|
type View struct {
|
||||||
|
mod *world.Mod
|
||||||
|
imageCache *graphics.TextureCache
|
||||||
|
paletteCache *graphics.PaletteCache
|
||||||
|
animationCache *bitmap.AnimationCache
|
||||||
|
|
||||||
|
modalStateMachine gui.ModalStateMachine
|
||||||
|
guiScale float32
|
||||||
|
commander cmd.Commander
|
||||||
|
|
||||||
|
model viewModel
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewAnimationsView returns a new instance.
|
||||||
|
func NewAnimationsView(mod *world.Mod, imageCache *graphics.TextureCache, paletteCache *graphics.PaletteCache,
|
||||||
|
animationCache *bitmap.AnimationCache,
|
||||||
|
modalStateMachine gui.ModalStateMachine, guiScale float32, commander cmd.Commander) *View {
|
||||||
|
view := &View{
|
||||||
|
mod: mod,
|
||||||
|
imageCache: imageCache,
|
||||||
|
paletteCache: paletteCache,
|
||||||
|
animationCache: animationCache,
|
||||||
|
|
||||||
|
modalStateMachine: modalStateMachine,
|
||||||
|
guiScale: guiScale,
|
||||||
|
commander: commander,
|
||||||
|
|
||||||
|
model: freshViewModel(),
|
||||||
|
}
|
||||||
|
return view
|
||||||
|
}
|
||||||
|
|
||||||
|
// WindowOpen returns the flag address, to be used with the main menu.
|
||||||
|
func (view *View) WindowOpen() *bool {
|
||||||
|
return &view.model.windowOpen
|
||||||
|
}
|
||||||
|
|
||||||
|
// Render renders the view.
|
||||||
|
func (view *View) Render() {
|
||||||
|
if view.model.restoreFocus {
|
||||||
|
imgui.SetNextWindowFocus()
|
||||||
|
view.model.restoreFocus = false
|
||||||
|
view.model.windowOpen = true
|
||||||
|
}
|
||||||
|
if view.model.windowOpen {
|
||||||
|
imgui.SetNextWindowSizeV(imgui.Vec2{X: 800 * view.guiScale, Y: 300 * view.guiScale}, imgui.ConditionFirstUseEver)
|
||||||
|
if imgui.BeginV("Animations", view.WindowOpen(), imgui.WindowFlagsNoCollapse|imgui.WindowFlagsHorizontalScrollbar) {
|
||||||
|
view.renderContent()
|
||||||
|
}
|
||||||
|
imgui.End()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (view *View) renderContent() {
|
||||||
|
if imgui.BeginChildV("Properties", imgui.Vec2{X: 350 * view.guiScale, Y: 0}, false, 0) {
|
||||||
|
imgui.PushItemWidth(-150 * view.guiScale)
|
||||||
|
if imgui.BeginCombo("Animation Type", knownAnimationTypes[view.model.currentKey.ID].title) {
|
||||||
|
for _, id := range knownAnimationTypesOrder {
|
||||||
|
if imgui.SelectableV(knownAnimationTypes[id].title, id == view.model.currentKey.ID, 0, imgui.Vec2{}) {
|
||||||
|
view.model.currentKey.ID = id
|
||||||
|
view.model.currentKey.Index = 0
|
||||||
|
view.model.currentFrame = 0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
imgui.EndCombo()
|
||||||
|
}
|
||||||
|
|
||||||
|
info, _ := ids.Info(view.model.currentKey.ID)
|
||||||
|
|
||||||
|
if gui.StepSliderInt("Index", &view.model.currentKey.Index, 0, info.MaxCount-1) {
|
||||||
|
view.model.currentFrame = 0
|
||||||
|
}
|
||||||
|
|
||||||
|
imgui.Separator()
|
||||||
|
|
||||||
|
view.renderProperties()
|
||||||
|
|
||||||
|
imgui.PopItemWidth()
|
||||||
|
}
|
||||||
|
imgui.EndChild()
|
||||||
|
imgui.SameLine()
|
||||||
|
|
||||||
|
if imgui.BeginChildV("Frames", imgui.Vec2{X: -1, Y: 0}, false, 0) {
|
||||||
|
anim, hasAnim, _ := view.currentAnimation()
|
||||||
|
|
||||||
|
if hasAnim {
|
||||||
|
frameKey := resource.KeyOf(anim.ResourceID, resource.LangAny, view.model.currentFrame)
|
||||||
|
if view.cacheFrame(frameKey) {
|
||||||
|
render.TextureImage("Frame", view.imageCache, frameKey,
|
||||||
|
imgui.Vec2{X: float32(anim.Width) * view.guiScale, Y: float32(anim.Height) * view.guiScale})
|
||||||
|
if imgui.Button("Export") {
|
||||||
|
view.requestExport()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
imgui.SameLine()
|
||||||
|
if imgui.Button("Import") {
|
||||||
|
view.requestImport()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
imgui.EndChild()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (view *View) cacheFrame(key resource.Key) bool {
|
||||||
|
var err error
|
||||||
|
var lastKey *resource.Key
|
||||||
|
for index := 0; (index <= key.Index) && (err == nil); index++ {
|
||||||
|
nextKey := resource.KeyOf(key.ID, key.Lang, index)
|
||||||
|
_, err = view.imageCache.TextureReferenced(nextKey, lastKey)
|
||||||
|
lastKey = &nextKey
|
||||||
|
}
|
||||||
|
return err == nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (view *View) renderProperties() {
|
||||||
|
anim, hasAnim, _ := view.currentAnimation()
|
||||||
|
widthString := ""
|
||||||
|
heightString := ""
|
||||||
|
lastFrame := 0
|
||||||
|
|
||||||
|
if hasAnim {
|
||||||
|
widthString = fmt.Sprintf("%d", anim.Width)
|
||||||
|
heightString = fmt.Sprintf("%d", anim.Height)
|
||||||
|
for _, entry := range anim.Entries {
|
||||||
|
lastFrame = int(entry.LastFrame)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
imgui.LabelText("Width", widthString)
|
||||||
|
imgui.LabelText("Height", heightString)
|
||||||
|
|
||||||
|
gui.StepSliderInt("Frame Index", &view.model.currentFrame, 0, lastFrame)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (view *View) currentAnimation() (bitmap.Animation, bool, bool) {
|
||||||
|
key := resource.KeyOf(view.model.currentKey.ID.Plus(view.model.currentKey.Index), resource.LangAny, 0)
|
||||||
|
anim, err := view.animationCache.Animation(key)
|
||||||
|
if err != nil {
|
||||||
|
return anim, false, true
|
||||||
|
}
|
||||||
|
readOnly := len(view.mod.ModifiedBlocks(resource.LangAny, key.ID)) == 0
|
||||||
|
return anim, true, readOnly
|
||||||
|
}
|
||||||
|
|
||||||
|
func (view *View) requestImport() {
|
||||||
|
info := "File must be an animated GIF file.\nIdeally, it matches the game palette 1:1,\nothers are mapped closest fitting."
|
||||||
|
types := []external.TypeInfo{{Title: "Animation files (*.gif)", Extensions: []string{"gif"}}}
|
||||||
|
var fileHandler func(string)
|
||||||
|
|
||||||
|
fileHandler = func(filename string) {
|
||||||
|
reader, err := os.Open(filename)
|
||||||
|
if err != nil {
|
||||||
|
external.Import(view.modalStateMachine, "Could not open file.\n"+info, types, fileHandler, true)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
defer func() { _ = reader.Close() }()
|
||||||
|
data, err := gif.DecodeAll(reader)
|
||||||
|
if err != nil {
|
||||||
|
external.Import(view.modalStateMachine, "File not recognized as GIF.\n"+info, types, fileHandler, true)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
palette, err := view.paletteCache.Palette(0)
|
||||||
|
if err != nil {
|
||||||
|
external.Import(view.modalStateMachine, "Can not import image without having a palette loaded.\n"+info, types, fileHandler, true)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
anim := bitmap.Animation{
|
||||||
|
Width: int16(data.Config.Width),
|
||||||
|
Height: int16(data.Config.Height),
|
||||||
|
ResourceID: view.model.currentKey.ID.Plus(view.model.currentKey.Index).Plus(-12),
|
||||||
|
IntroFlag: 0,
|
||||||
|
}
|
||||||
|
var frames [][]byte
|
||||||
|
rawPalette := palette.Palette()
|
||||||
|
|
||||||
|
highestBitShift := func(value int16) (result byte) {
|
||||||
|
if value != 0 {
|
||||||
|
for (value >> result) != 1 {
|
||||||
|
result++
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
var prevFrame []byte
|
||||||
|
for index, img := range data.Image {
|
||||||
|
if (img.Bounds().Max.X == data.Config.Width) && (img.Bounds().Max.Y == data.Config.Height) {
|
||||||
|
bitmapper := bitmap.NewBitmapper(&rawPalette)
|
||||||
|
bmp := bitmapper.Map(img)
|
||||||
|
entry := bitmap.AnimationEntry{
|
||||||
|
FirstFrame: byte(index),
|
||||||
|
LastFrame: byte(index),
|
||||||
|
FrameTime: int16(data.Delay[index] * 10),
|
||||||
|
}
|
||||||
|
anim.Entries = append(anim.Entries, entry)
|
||||||
|
bmp.Header.Type = bitmap.TypeCompressed8Bit
|
||||||
|
bmp.Header.WidthFactor = highestBitShift(bmp.Header.Width)
|
||||||
|
bmp.Header.HeightFactor = highestBitShift(bmp.Header.Height)
|
||||||
|
bmp.Header.Area = [4]int16{0, 0, anim.Width, anim.Height}
|
||||||
|
bmp.Header.Stride = uint16(bmp.Header.Width)
|
||||||
|
|
||||||
|
buf := bytes.NewBuffer(nil)
|
||||||
|
_ = binary.Write(buf, binary.LittleEndian, &bmp.Header)
|
||||||
|
_ = rle.Compress(buf, bmp.Pixels, prevFrame)
|
||||||
|
prevFrame = bmp.Pixels
|
||||||
|
frames = append(frames, buf.Bytes())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
view.requestSetAnimation(anim, frames)
|
||||||
|
}
|
||||||
|
|
||||||
|
external.Import(view.modalStateMachine, info, types, fileHandler, false)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (view *View) requestExport() {
|
||||||
|
anim, hasAnim, _ := view.currentAnimation()
|
||||||
|
if hasAnim {
|
||||||
|
filename := fmt.Sprintf("Anim%04X.gif", int(view.model.currentKey.ID.Plus(view.model.currentKey.Index)))
|
||||||
|
view.exportTo(filename, anim)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (view *View) exportTo(filename string, anim bitmap.Animation) {
|
||||||
|
info := "File to be written: " + filename
|
||||||
|
var exportTo func(string)
|
||||||
|
|
||||||
|
exportTo = func(dirname string) {
|
||||||
|
writer, err := os.Create(filepath.Join(dirname, filename))
|
||||||
|
if err != nil {
|
||||||
|
external.Export(view.modalStateMachine, "Could not create file.\n"+info, exportTo, true)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
defer func() { _ = writer.Close() }()
|
||||||
|
|
||||||
|
palTex, err := view.paletteCache.Palette(0)
|
||||||
|
if err != nil {
|
||||||
|
external.Export(view.modalStateMachine, "Could not create file. No palette loaded.\n"+info, exportTo, true)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
colorPalette := palTex.Palette().ColorPalette(false)
|
||||||
|
data := gif.GIF{
|
||||||
|
Config: image.Config{
|
||||||
|
Width: int(anim.Width),
|
||||||
|
Height: int(anim.Height),
|
||||||
|
ColorModel: colorPalette,
|
||||||
|
},
|
||||||
|
LoopCount: -1,
|
||||||
|
}
|
||||||
|
|
||||||
|
imageRect := image.Rect(0, 0, data.Config.Width, data.Config.Height)
|
||||||
|
frameIndex := 0
|
||||||
|
for _, entry := range anim.Entries {
|
||||||
|
for frameIndex <= int(entry.LastFrame) {
|
||||||
|
frameKey := resource.KeyOf(anim.ResourceID, resource.LangAny, frameIndex)
|
||||||
|
if !view.cacheFrame(frameKey) {
|
||||||
|
external.Export(view.modalStateMachine, "Failed to cache frame.\n"+info, exportTo, true)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
frameTex, _ := view.imageCache.Texture(frameKey)
|
||||||
|
frameImg := image.NewPaletted(imageRect, colorPalette)
|
||||||
|
frameImg.Pix = frameTex.PixelData()
|
||||||
|
data.Image = append(data.Image, frameImg)
|
||||||
|
data.Delay = append(data.Delay, int(entry.FrameTime)/10)
|
||||||
|
frameIndex++
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
err = gif.EncodeAll(writer, &data)
|
||||||
|
if err != nil {
|
||||||
|
external.Export(view.modalStateMachine, info, exportTo, true)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
external.Export(view.modalStateMachine, info, exportTo, false)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (view *View) requestSetAnimation(newAnim bitmap.Animation, newFrames [][]byte) {
|
||||||
|
encodeAnim := func(anim bitmap.Animation) []byte {
|
||||||
|
buf := bytes.NewBuffer(nil)
|
||||||
|
_ = bitmap.WriteAnimation(buf, anim)
|
||||||
|
return buf.Bytes()
|
||||||
|
}
|
||||||
|
command := setAnimationCommand{
|
||||||
|
model: &view.model,
|
||||||
|
animationKey: view.model.currentKey,
|
||||||
|
newAnimation: encodeAnim(newAnim),
|
||||||
|
newFrames: newFrames,
|
||||||
|
framesID: newAnim.ResourceID,
|
||||||
|
}
|
||||||
|
oldAnim, oldExisting, _ := view.currentAnimation()
|
||||||
|
if oldExisting {
|
||||||
|
command.oldAnimation = encodeAnim(oldAnim)
|
||||||
|
}
|
||||||
|
command.oldFrames = view.mod.ModifiedBlocks(view.model.currentKey.Lang, newAnim.ResourceID)
|
||||||
|
view.commander.Queue(command)
|
||||||
|
}
|
||||||
@@ -0,0 +1,20 @@
|
|||||||
|
package animations
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/inkyblackness/hacked/ss1/resource"
|
||||||
|
"github.com/inkyblackness/hacked/ss1/world/ids"
|
||||||
|
)
|
||||||
|
|
||||||
|
type viewModel struct {
|
||||||
|
windowOpen bool
|
||||||
|
restoreFocus bool
|
||||||
|
|
||||||
|
currentKey resource.Key
|
||||||
|
currentFrame int
|
||||||
|
}
|
||||||
|
|
||||||
|
func freshViewModel() viewModel {
|
||||||
|
return viewModel{
|
||||||
|
currentKey: resource.KeyOf(ids.VideoMailAnimationsStart, resource.LangAny, 0),
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,125 @@
|
|||||||
|
package archives
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/inkyblackness/imgui-go/v3"
|
||||||
|
|
||||||
|
"github.com/inkyblackness/hacked/editor/values"
|
||||||
|
"github.com/inkyblackness/hacked/ss1/content/archive"
|
||||||
|
"github.com/inkyblackness/hacked/ss1/edit"
|
||||||
|
"github.com/inkyblackness/hacked/ui/gui"
|
||||||
|
)
|
||||||
|
|
||||||
|
type editBooleanVariableDialog struct {
|
||||||
|
machine gui.ModalStateMachine
|
||||||
|
view *View
|
||||||
|
|
||||||
|
service *edit.GameStateService
|
||||||
|
index int
|
||||||
|
varOverride bool
|
||||||
|
info archive.GameVariableInfo
|
||||||
|
|
||||||
|
opened bool
|
||||||
|
}
|
||||||
|
|
||||||
|
func (dialog *editBooleanVariableDialog) Render() {
|
||||||
|
if !dialog.opened {
|
||||||
|
dialog.opened = true
|
||||||
|
dialog.varOverride = dialog.service.BooleanVariableOverride(dialog.index)
|
||||||
|
dialog.info = dialog.service.BooleanVariable(dialog.index)
|
||||||
|
imgui.OpenPopup("Edit boolean variable")
|
||||||
|
}
|
||||||
|
|
||||||
|
imgui.SetNextWindowSize(imgui.Vec2{X: 600, Y: 0}.Times(dialog.view.guiScale))
|
||||||
|
if imgui.BeginPopupModalV("Edit boolean variable", nil,
|
||||||
|
imgui.WindowFlagsNoSavedSettings|imgui.WindowFlagsAlwaysAutoResize) {
|
||||||
|
dialog.renderControls()
|
||||||
|
imgui.Separator()
|
||||||
|
if imgui.Button("OK") {
|
||||||
|
dialog.applyChanges()
|
||||||
|
dialog.close()
|
||||||
|
}
|
||||||
|
imgui.SameLine()
|
||||||
|
if imgui.Button("Cancel") {
|
||||||
|
dialog.close()
|
||||||
|
}
|
||||||
|
imgui.EndPopup()
|
||||||
|
} else {
|
||||||
|
dialog.machine.SetState(nil)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (dialog *editBooleanVariableDialog) applyChanges() {
|
||||||
|
if dialog.varOverride {
|
||||||
|
_ = dialog.service.SetBooleanVariable(dialog.index, dialog.info)
|
||||||
|
} else {
|
||||||
|
_ = dialog.service.DefaultBooleanVariable(dialog.index)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (dialog *editBooleanVariableDialog) close() {
|
||||||
|
dialog.machine.SetState(nil)
|
||||||
|
imgui.CloseCurrentPopup()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (dialog *editBooleanVariableDialog) renderControls() {
|
||||||
|
imgui.PushItemWidth(-150 * dialog.view.guiScale)
|
||||||
|
values.RenderUnifiedCheckboxCombo(false, "Override", values.UnifierFor(dialog.varOverride), func(newValue bool) {
|
||||||
|
dialog.varOverride = newValue
|
||||||
|
})
|
||||||
|
|
||||||
|
textFlags := imgui.InputTextFlagsNoUndoRedo
|
||||||
|
if !dialog.varOverride {
|
||||||
|
textFlags |= imgui.InputTextFlagsReadOnly
|
||||||
|
}
|
||||||
|
imgui.InputTextV("Name", &dialog.info.Name, textFlags, nil)
|
||||||
|
imgui.InputTextMultilineV("Description", &dialog.info.Description, imgui.Vec2{X: 0, Y: 100 * dialog.view.guiScale},
|
||||||
|
textFlags, nil)
|
||||||
|
|
||||||
|
imgui.Separator()
|
||||||
|
defaultValues := map[int]string{0: "False", 1: "True"}
|
||||||
|
valueLabels := map[bool]string{false: "Default", true: "Special"}
|
||||||
|
hasSpecialValues := dialog.info.ValueNames != nil
|
||||||
|
if dialog.varOverride {
|
||||||
|
if imgui.BeginCombo("Values", valueLabels[hasSpecialValues]) {
|
||||||
|
if imgui.SelectableV(valueLabels[false], !hasSpecialValues, 0, imgui.Vec2{}) {
|
||||||
|
dialog.info.ValueNames = nil
|
||||||
|
}
|
||||||
|
if imgui.SelectableV(valueLabels[true], hasSpecialValues, 0, imgui.Vec2{}) {
|
||||||
|
dialog.info.ValueNames = map[int16]string{0: defaultValues[0], 1: defaultValues[1]}
|
||||||
|
}
|
||||||
|
imgui.EndCombo()
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
imgui.LabelText("Values", valueLabels[hasSpecialValues])
|
||||||
|
}
|
||||||
|
hasSpecialValues = dialog.info.ValueNames != nil
|
||||||
|
if hasSpecialValues {
|
||||||
|
for i := int16(0); i < 2; i++ {
|
||||||
|
text := dialog.info.ValueNames[i]
|
||||||
|
if imgui.InputTextV(defaultValues[int(i)], &text, textFlags, nil) {
|
||||||
|
dialog.info.ValueNames[i] = text
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
imgui.LabelText(defaultValues[0], defaultValues[0])
|
||||||
|
imgui.LabelText(defaultValues[1], defaultValues[1])
|
||||||
|
}
|
||||||
|
|
||||||
|
initUnifier := values.UnifierFor(dialog.info.ResetValueInt())
|
||||||
|
values.RenderUnifiedCombo(dialog.info.Hardcoded || !dialog.varOverride, "Reset Value", initUnifier,
|
||||||
|
func(u values.Unifier) int { return int(u.Unified().(int16)) },
|
||||||
|
func(value int) string {
|
||||||
|
name, found := dialog.info.ValueNames[int16(value)]
|
||||||
|
if found {
|
||||||
|
return name
|
||||||
|
}
|
||||||
|
return defaultValues[value]
|
||||||
|
},
|
||||||
|
2,
|
||||||
|
func(newValue int) {
|
||||||
|
value16 := int16(newValue)
|
||||||
|
dialog.info.InitValue = &value16
|
||||||
|
})
|
||||||
|
|
||||||
|
imgui.PopItemWidth()
|
||||||
|
}
|
||||||
@@ -0,0 +1,214 @@
|
|||||||
|
package archives
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"math"
|
||||||
|
"sort"
|
||||||
|
|
||||||
|
"github.com/inkyblackness/imgui-go/v3"
|
||||||
|
|
||||||
|
"github.com/inkyblackness/hacked/editor/values"
|
||||||
|
"github.com/inkyblackness/hacked/ss1/content/archive"
|
||||||
|
"github.com/inkyblackness/hacked/ss1/edit"
|
||||||
|
"github.com/inkyblackness/hacked/ui/gui"
|
||||||
|
)
|
||||||
|
|
||||||
|
type editIntegerVariableDialog struct {
|
||||||
|
machine gui.ModalStateMachine
|
||||||
|
view *View
|
||||||
|
|
||||||
|
service *edit.GameStateService
|
||||||
|
index int
|
||||||
|
varOverride bool
|
||||||
|
info archive.GameVariableInfo
|
||||||
|
|
||||||
|
selectedEnum int
|
||||||
|
opened bool
|
||||||
|
}
|
||||||
|
|
||||||
|
func (dialog *editIntegerVariableDialog) Render() {
|
||||||
|
if !dialog.opened {
|
||||||
|
dialog.opened = true
|
||||||
|
dialog.varOverride = dialog.service.IntegerVariableOverride(dialog.index)
|
||||||
|
dialog.info = dialog.service.IntegerVariable(dialog.index)
|
||||||
|
imgui.OpenPopup("Edit integer variable")
|
||||||
|
}
|
||||||
|
|
||||||
|
imgui.SetNextWindowSize(imgui.Vec2{X: 600, Y: 0}.Times(dialog.view.guiScale))
|
||||||
|
if imgui.BeginPopupModalV("Edit integer variable", nil,
|
||||||
|
imgui.WindowFlagsNoSavedSettings|imgui.WindowFlagsAlwaysAutoResize) {
|
||||||
|
dialog.renderControls()
|
||||||
|
imgui.Separator()
|
||||||
|
if imgui.Button("OK") {
|
||||||
|
dialog.applyChanges()
|
||||||
|
dialog.close()
|
||||||
|
}
|
||||||
|
imgui.SameLine()
|
||||||
|
if imgui.Button("Cancel") {
|
||||||
|
dialog.close()
|
||||||
|
}
|
||||||
|
imgui.EndPopup()
|
||||||
|
} else {
|
||||||
|
dialog.machine.SetState(nil)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (dialog *editIntegerVariableDialog) applyChanges() {
|
||||||
|
if dialog.varOverride {
|
||||||
|
_ = dialog.service.SetIntegerVariable(dialog.index, dialog.info)
|
||||||
|
} else {
|
||||||
|
_ = dialog.service.DefaultIntegerVariable(dialog.index)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (dialog *editIntegerVariableDialog) close() {
|
||||||
|
dialog.machine.SetState(nil)
|
||||||
|
imgui.CloseCurrentPopup()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (dialog *editIntegerVariableDialog) renderControls() {
|
||||||
|
imgui.PushItemWidth(-150 * dialog.view.guiScale)
|
||||||
|
values.RenderUnifiedCheckboxCombo(false, "Override", values.UnifierFor(dialog.varOverride), func(newValue bool) {
|
||||||
|
dialog.varOverride = newValue
|
||||||
|
})
|
||||||
|
|
||||||
|
textFlags := imgui.InputTextFlagsNoUndoRedo
|
||||||
|
if !dialog.varOverride {
|
||||||
|
textFlags |= imgui.InputTextFlagsReadOnly
|
||||||
|
}
|
||||||
|
imgui.InputTextV("Name", &dialog.info.Name, textFlags, nil)
|
||||||
|
imgui.InputTextMultilineV("Description", &dialog.info.Description, imgui.Vec2{X: 0, Y: 100 * dialog.view.guiScale},
|
||||||
|
textFlags, nil)
|
||||||
|
|
||||||
|
imgui.Separator()
|
||||||
|
|
||||||
|
valueLabels := map[bool]string{false: "Numeric", true: "Enumerated"}
|
||||||
|
isEnumerated := len(dialog.info.ValueNames) > 0
|
||||||
|
if imgui.BeginCombo("Values", valueLabels[isEnumerated]) {
|
||||||
|
if imgui.SelectableV(valueLabels[false], !isEnumerated, 0, imgui.Vec2{}) {
|
||||||
|
dialog.info.ValueNames = nil
|
||||||
|
}
|
||||||
|
if imgui.SelectableV(valueLabels[true], isEnumerated, 0, imgui.Vec2{}) {
|
||||||
|
dialog.info.ValueNames = map[int16]string{0: "Zero"}
|
||||||
|
dialog.selectedEnum = 0
|
||||||
|
}
|
||||||
|
imgui.EndCombo()
|
||||||
|
}
|
||||||
|
if len(dialog.info.ValueNames) > 0 {
|
||||||
|
gui.StepSliderInt("Index", &dialog.selectedEnum, math.MinInt16, math.MaxInt16)
|
||||||
|
if dialog.varOverride {
|
||||||
|
text := dialog.info.ValueNames[int16(dialog.selectedEnum)]
|
||||||
|
if imgui.InputTextV("Value", &text, textFlags, nil) {
|
||||||
|
if len(text) > 0 {
|
||||||
|
dialog.info.ValueNames[int16(dialog.selectedEnum)] = text
|
||||||
|
} else {
|
||||||
|
delete(dialog.info.ValueNames, int16(dialog.selectedEnum))
|
||||||
|
if len(dialog.info.ValueNames) == 0 {
|
||||||
|
dialog.info.ValueNames[0] = "Zero"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
imgui.LabelText("Value", dialog.info.ValueNames[int16(dialog.selectedEnum)])
|
||||||
|
}
|
||||||
|
|
||||||
|
resetValue := dialog.info.ResetValueInt()
|
||||||
|
enumEntries := toEnumEntries(dialog.info.ValueNames)
|
||||||
|
linearInitIndex := linearIndex(enumEntries, resetValue)
|
||||||
|
initUnifier := values.UnifierFor(linearInitIndex)
|
||||||
|
values.RenderUnifiedCombo(dialog.info.Hardcoded || !dialog.varOverride, "Reset Value", initUnifier,
|
||||||
|
func(u values.Unifier) int { return u.Unified().(int) },
|
||||||
|
func(value int) string {
|
||||||
|
if value < 0 {
|
||||||
|
return fmt.Sprintf("%d: (unmapped)", resetValue)
|
||||||
|
}
|
||||||
|
entry := enumEntries[value]
|
||||||
|
return fmt.Sprintf("%d: '%s'", entry.index, entry.value)
|
||||||
|
},
|
||||||
|
len(enumEntries),
|
||||||
|
func(newValue int) {
|
||||||
|
dialog.info.InitValue = &enumEntries[newValue].index
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
minValue := math.MinInt16
|
||||||
|
maxValue := math.MaxInt16
|
||||||
|
|
||||||
|
if dialog.info.Limits != nil {
|
||||||
|
minValue = int(dialog.info.Limits.Minimum)
|
||||||
|
maxValue = int(dialog.info.Limits.Maximum)
|
||||||
|
}
|
||||||
|
if minValue > maxValue {
|
||||||
|
maxValue = minValue
|
||||||
|
}
|
||||||
|
if dialog.varOverride {
|
||||||
|
if gui.StepSliderInt("Minimum", &minValue, math.MinInt16, math.MaxInt16) {
|
||||||
|
if minValue > maxValue {
|
||||||
|
maxValue = minValue
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if gui.StepSliderInt("Maximum", &maxValue, math.MinInt16, math.MaxInt16) {
|
||||||
|
if maxValue < minValue {
|
||||||
|
minValue = maxValue
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (minValue == math.MinInt16) && (maxValue == math.MaxInt16) {
|
||||||
|
dialog.info.Limits = nil
|
||||||
|
} else {
|
||||||
|
if dialog.info.Limits == nil {
|
||||||
|
dialog.info.Limits = &archive.GameVariableLimits{}
|
||||||
|
}
|
||||||
|
dialog.info.Limits.Minimum = int16(minValue)
|
||||||
|
dialog.info.Limits.Maximum = int16(maxValue)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
imgui.LabelText("Minimum", fmt.Sprintf("%d", minValue))
|
||||||
|
imgui.LabelText("Maximum", fmt.Sprintf("%d", maxValue))
|
||||||
|
}
|
||||||
|
|
||||||
|
resetValue := int(dialog.info.ResetValueInt())
|
||||||
|
if dialog.info.Hardcoded || !dialog.varOverride {
|
||||||
|
imgui.LabelText("Reset Value", fmt.Sprintf("%d", resetValue))
|
||||||
|
} else if gui.StepSliderInt("Reset Value", &resetValue, minValue, maxValue) {
|
||||||
|
resetValue16 := int16(resetValue)
|
||||||
|
dialog.info.InitValue = &resetValue16
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
imgui.PopItemWidth()
|
||||||
|
}
|
||||||
|
|
||||||
|
type enumEntry struct {
|
||||||
|
index int16
|
||||||
|
value string
|
||||||
|
}
|
||||||
|
|
||||||
|
func toEnumEntries(mapped map[int16]string) []enumEntry {
|
||||||
|
minIndex := math.MaxInt16
|
||||||
|
maxIndex := math.MinInt16
|
||||||
|
for index := range mapped {
|
||||||
|
if int(index) < minIndex {
|
||||||
|
minIndex = int(index)
|
||||||
|
}
|
||||||
|
if int(index) > maxIndex {
|
||||||
|
maxIndex = int(index)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
orderedEntries := make([]enumEntry, 0, len(mapped))
|
||||||
|
for index, value := range mapped {
|
||||||
|
orderedEntries = append(orderedEntries, enumEntry{
|
||||||
|
index: index,
|
||||||
|
value: value,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
sort.Slice(orderedEntries, func(a, b int) bool { return orderedEntries[a].index < orderedEntries[b].index })
|
||||||
|
return orderedEntries
|
||||||
|
}
|
||||||
|
|
||||||
|
func linearIndex(entries []enumEntry, index int16) int {
|
||||||
|
for linear, entry := range entries {
|
||||||
|
if entry.index == index {
|
||||||
|
return linear
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return -1
|
||||||
|
}
|
||||||
@@ -0,0 +1,45 @@
|
|||||||
|
package archives
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/inkyblackness/hacked/ss1/resource"
|
||||||
|
"github.com/inkyblackness/hacked/ss1/world"
|
||||||
|
)
|
||||||
|
|
||||||
|
type setArchiveDataCommand struct {
|
||||||
|
model *viewModel
|
||||||
|
|
||||||
|
selectedLevel int
|
||||||
|
|
||||||
|
oldData map[resource.ID][]byte
|
||||||
|
newData map[resource.ID][]byte
|
||||||
|
}
|
||||||
|
|
||||||
|
func (command setArchiveDataCommand) Do(modder world.Modder) error {
|
||||||
|
command.delResources(modder, command.oldData)
|
||||||
|
return command.perform(modder, command.newData)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (command setArchiveDataCommand) Undo(modder world.Modder) error {
|
||||||
|
command.delResources(modder, command.newData)
|
||||||
|
return command.perform(modder, command.oldData)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (command setArchiveDataCommand) delResources(modder world.Modder, data map[resource.ID][]byte) {
|
||||||
|
for id := range data {
|
||||||
|
modder.DelResource(resource.LangAny, id)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// nolint: interfacer
|
||||||
|
func (command setArchiveDataCommand) perform(modder world.Modder, data map[resource.ID][]byte) error {
|
||||||
|
for id, blockData := range data {
|
||||||
|
if len(blockData) > 0 {
|
||||||
|
modder.SetResourceBlocks(resource.LangAny, id, [][]byte{blockData})
|
||||||
|
} else {
|
||||||
|
modder.DelResource(resource.LangAny, id)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
command.model.restoreFocus = true
|
||||||
|
command.model.selectedLevel = command.selectedLevel
|
||||||
|
return nil
|
||||||
|
}
|
||||||
@@ -0,0 +1,934 @@
|
|||||||
|
package archives
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"math"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/inkyblackness/imgui-go/v3"
|
||||||
|
|
||||||
|
"github.com/inkyblackness/hacked/editor/values"
|
||||||
|
"github.com/inkyblackness/hacked/ss1/content/archive"
|
||||||
|
"github.com/inkyblackness/hacked/ss1/content/archive/level"
|
||||||
|
"github.com/inkyblackness/hacked/ss1/content/archive/level/lvlids"
|
||||||
|
"github.com/inkyblackness/hacked/ss1/content/archive/level/lvlobj"
|
||||||
|
"github.com/inkyblackness/hacked/ss1/content/interpreters"
|
||||||
|
"github.com/inkyblackness/hacked/ss1/content/numbers"
|
||||||
|
"github.com/inkyblackness/hacked/ss1/content/object"
|
||||||
|
"github.com/inkyblackness/hacked/ss1/content/text"
|
||||||
|
"github.com/inkyblackness/hacked/ss1/edit"
|
||||||
|
"github.com/inkyblackness/hacked/ss1/edit/undoable/cmd"
|
||||||
|
"github.com/inkyblackness/hacked/ss1/resource"
|
||||||
|
"github.com/inkyblackness/hacked/ss1/world"
|
||||||
|
"github.com/inkyblackness/hacked/ss1/world/citadel"
|
||||||
|
"github.com/inkyblackness/hacked/ss1/world/ids"
|
||||||
|
"github.com/inkyblackness/hacked/ui/gui"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
hintUnknown = "???"
|
||||||
|
)
|
||||||
|
|
||||||
|
var variableContextList = map[edit.VariableBaseContextIdentifier]struct {
|
||||||
|
title string
|
||||||
|
info string
|
||||||
|
}{
|
||||||
|
edit.VariableContextCitadel: {
|
||||||
|
title: "Citadel Mission",
|
||||||
|
info: "Variables are listed based on use on Citadel.\n" +
|
||||||
|
"Useful for Citadel-based savegames and mods, such as 'New Game+' variants.",
|
||||||
|
},
|
||||||
|
edit.VariableContextEngine: {
|
||||||
|
title: "Engine",
|
||||||
|
info: "Variables are listed based on the hardcoded engine behaviour.\n" +
|
||||||
|
"Useful for dedicated missions.",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
// View provides edit controls for the archive.
|
||||||
|
type View struct {
|
||||||
|
registry cmd.Registry
|
||||||
|
gameStateService *edit.GameStateService
|
||||||
|
mod *world.Mod
|
||||||
|
textCache *text.Cache
|
||||||
|
cp text.Codepage
|
||||||
|
|
||||||
|
modalStateMachine gui.ModalStateMachine
|
||||||
|
guiScale float32
|
||||||
|
commander cmd.Commander
|
||||||
|
|
||||||
|
model viewModel
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewArchiveView returns a new instance.
|
||||||
|
func NewArchiveView(registry cmd.Registry,
|
||||||
|
gameStateService *edit.GameStateService, mod *world.Mod,
|
||||||
|
textCache *text.Cache, cp text.Codepage,
|
||||||
|
modalStateMachine gui.ModalStateMachine, guiScale float32, commander cmd.Commander) *View {
|
||||||
|
view := &View{
|
||||||
|
registry: registry,
|
||||||
|
gameStateService: gameStateService,
|
||||||
|
mod: mod,
|
||||||
|
textCache: textCache,
|
||||||
|
cp: cp,
|
||||||
|
|
||||||
|
modalStateMachine: modalStateMachine,
|
||||||
|
guiScale: guiScale,
|
||||||
|
commander: commander,
|
||||||
|
|
||||||
|
model: freshViewModel(),
|
||||||
|
}
|
||||||
|
return view
|
||||||
|
}
|
||||||
|
|
||||||
|
// WindowOpen returns the flag address, to be used with the main menu.
|
||||||
|
func (view *View) WindowOpen() *bool {
|
||||||
|
return &view.model.windowOpen
|
||||||
|
}
|
||||||
|
|
||||||
|
// Render renders the view.
|
||||||
|
func (view *View) Render() {
|
||||||
|
if view.model.restoreFocus {
|
||||||
|
imgui.SetNextWindowFocus()
|
||||||
|
view.model.restoreFocus = false
|
||||||
|
view.model.windowOpen = true
|
||||||
|
}
|
||||||
|
if view.model.windowOpen {
|
||||||
|
imgui.SetNextWindowSizeV(imgui.Vec2{X: 650 * view.guiScale, Y: 400 * view.guiScale}, imgui.ConditionFirstUseEver)
|
||||||
|
if imgui.BeginV("Archive", view.WindowOpen(), imgui.WindowFlagsNoCollapse) {
|
||||||
|
view.renderContent()
|
||||||
|
}
|
||||||
|
imgui.End()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (view *View) renderContent() {
|
||||||
|
imgui.BeginTabBar("archive-tab")
|
||||||
|
|
||||||
|
if imgui.BeginTabItem("Levels") {
|
||||||
|
view.renderLevelsContent()
|
||||||
|
imgui.EndTabItem()
|
||||||
|
}
|
||||||
|
if imgui.BeginTabItem("Game State") {
|
||||||
|
view.renderGameStateContent()
|
||||||
|
imgui.EndTabItem()
|
||||||
|
}
|
||||||
|
|
||||||
|
imgui.EndTabBar()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (view *View) renderLevelsContent() {
|
||||||
|
imgui.BeginChildV("Levels", imgui.Vec2{X: -100 * view.guiScale, Y: 0}, true, 0)
|
||||||
|
for id := 0; id < archive.MaxLevels; id++ {
|
||||||
|
inMod := view.hasLevelInMod(id)
|
||||||
|
info := fmt.Sprintf("%d", id)
|
||||||
|
if !inMod {
|
||||||
|
if view.hasGameStateInMod() && (id == view.effectiveGameState().CurrentLevel()) {
|
||||||
|
// current level should really be in the archive.
|
||||||
|
imgui.PushStyleColor(imgui.StyleColorText, imgui.Vec4{X: 1.0, Y: 0.0, Z: 0.0, W: 1.0})
|
||||||
|
} else {
|
||||||
|
imgui.PushStyleColor(imgui.StyleColorText, imgui.Vec4{X: 1.0, Y: 1.0, Z: 1.0, W: 0.8})
|
||||||
|
}
|
||||||
|
info += " (not in mod, read-only)"
|
||||||
|
}
|
||||||
|
if imgui.SelectableV(info, id == view.model.selectedLevel, 0, imgui.Vec2{}) {
|
||||||
|
view.model.selectedLevel = id
|
||||||
|
}
|
||||||
|
if !inMod {
|
||||||
|
imgui.PopStyleColor()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
imgui.EndChild()
|
||||||
|
imgui.SameLine()
|
||||||
|
imgui.BeginGroup()
|
||||||
|
if imgui.ButtonV("Clear", imgui.Vec2{X: -1, Y: 0}) {
|
||||||
|
view.requestClearLevel(view.model.selectedLevel)
|
||||||
|
}
|
||||||
|
if imgui.ButtonV("Remove", imgui.Vec2{X: -1, Y: 0}) {
|
||||||
|
view.requestRemoveLevel(view.model.selectedLevel)
|
||||||
|
}
|
||||||
|
imgui.EndGroup()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (view *View) renderGameStateContent() {
|
||||||
|
data := view.gameStateData()
|
||||||
|
modStateIsDefaulting := false
|
||||||
|
|
||||||
|
imgui.PushItemWidth(-350 * view.guiScale)
|
||||||
|
if data.isPresent() {
|
||||||
|
modState := data.toInstance()
|
||||||
|
if !modState.IsSavegame() {
|
||||||
|
modStateIsDefaulting = modState.IsDefaulting()
|
||||||
|
if modStateIsDefaulting {
|
||||||
|
if imgui.Button("Override") {
|
||||||
|
view.requestSetGameState(citadel.DefaultGameState().Raw())
|
||||||
|
}
|
||||||
|
if imgui.IsItemHovered() {
|
||||||
|
imgui.SetTooltip("Values of new game archives are only considered by special engines.\n" +
|
||||||
|
"Overriding them will only work in those.")
|
||||||
|
}
|
||||||
|
} else if imgui.Button("Remove") {
|
||||||
|
view.requestSetGameState(archive.ZeroGameStateData())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
readOnly := false
|
||||||
|
var editState *archive.GameState
|
||||||
|
if !data.isPresent() || modStateIsDefaulting {
|
||||||
|
editState = citadel.DefaultGameState()
|
||||||
|
readOnly = true
|
||||||
|
} else {
|
||||||
|
editState = data.imported().toInstance()
|
||||||
|
}
|
||||||
|
|
||||||
|
onChange := func() {
|
||||||
|
view.requestSetGameState(editState.Raw())
|
||||||
|
}
|
||||||
|
if imgui.TreeNodeV("Game State: General", imgui.TreeNodeFlagsDefaultOpen|imgui.TreeNodeFlagsFramed) {
|
||||||
|
imgui.LabelText("Hacker Name", "\""+editState.HackerName(view.cp)+"\"")
|
||||||
|
view.createPropertyControls(readOnly, editState.Instance, func(key string, modifier func(uint32) uint32) {
|
||||||
|
view.setInterpreterValueKeyed(editState.Instance, key, modifier)
|
||||||
|
onChange()
|
||||||
|
})
|
||||||
|
imgui.TreePop()
|
||||||
|
}
|
||||||
|
if imgui.TreeNodeV("Inventory", imgui.TreeNodeFlagsFramed) {
|
||||||
|
view.createInventoryControls(readOnly, editState, onChange)
|
||||||
|
imgui.TreePop()
|
||||||
|
}
|
||||||
|
if imgui.TreeNodeV("Hard-/Software", imgui.TreeNodeFlagsFramed) {
|
||||||
|
view.createWareControls(readOnly, editState, onChange)
|
||||||
|
imgui.TreePop()
|
||||||
|
}
|
||||||
|
if imgui.TreeNodeV("Game State: Variables", imgui.TreeNodeFlagsFramed) {
|
||||||
|
view.createVariableControls(readOnly, editState, onChange)
|
||||||
|
imgui.TreePop()
|
||||||
|
}
|
||||||
|
if imgui.TreeNodeV("Game State: Messages", imgui.TreeNodeFlagsFramed) {
|
||||||
|
view.createMessageControls(readOnly, editState, onChange)
|
||||||
|
imgui.TreePop()
|
||||||
|
}
|
||||||
|
|
||||||
|
imgui.PopItemWidth()
|
||||||
|
}
|
||||||
|
|
||||||
|
type gameStateData struct{ raw []byte }
|
||||||
|
|
||||||
|
func (data gameStateData) isPresent() bool {
|
||||||
|
return len(data.raw) > 0
|
||||||
|
}
|
||||||
|
|
||||||
|
func (data gameStateData) toInstance() *archive.GameState {
|
||||||
|
return archive.NewGameState(data.raw)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (data gameStateData) snapshot() gameStateData {
|
||||||
|
copied := make([]byte, len(data.raw))
|
||||||
|
copy(copied, data.raw)
|
||||||
|
return gameStateData{raw: copied}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (data gameStateData) imported() gameStateData {
|
||||||
|
// Avoid truncating data when working with savegames.
|
||||||
|
if len(data.raw) >= archive.GameStateSize {
|
||||||
|
return data.snapshot()
|
||||||
|
}
|
||||||
|
copied := archive.ZeroGameStateData()
|
||||||
|
copy(copied, data.raw)
|
||||||
|
return gameStateData{raw: copied}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (view *View) gameStateData() gameStateData {
|
||||||
|
raw := view.mod.ModifiedBlock(resource.LangAny, ids.GameState, 0)
|
||||||
|
return gameStateData{raw: raw}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (view *View) effectiveGameState() *archive.GameState {
|
||||||
|
state := view.gameStateData().toInstance()
|
||||||
|
if state.IsDefaulting() {
|
||||||
|
return citadel.DefaultGameState()
|
||||||
|
}
|
||||||
|
return state
|
||||||
|
}
|
||||||
|
|
||||||
|
func (view *View) hasGameStateInMod() bool {
|
||||||
|
return len(view.mod.ModifiedBlocks(resource.LangAny, ids.GameState)) > 0
|
||||||
|
}
|
||||||
|
|
||||||
|
func (view *View) hasLevelInMod(id int) bool {
|
||||||
|
return len(view.mod.ModifiedBlocks(resource.LangAny, ids.LevelResourcesStart.Plus(lvlids.PerLevel*id+lvlids.FirstUsed))) > 0
|
||||||
|
}
|
||||||
|
|
||||||
|
func (view *View) requestClearLevel(id int) {
|
||||||
|
if (id >= 0) && (id < archive.MaxLevels) {
|
||||||
|
command := setArchiveDataCommand{
|
||||||
|
model: &view.model,
|
||||||
|
selectedLevel: id,
|
||||||
|
newData: make(map[resource.ID][]byte),
|
||||||
|
oldData: make(map[resource.ID][]byte),
|
||||||
|
}
|
||||||
|
|
||||||
|
if !view.hasGameStateInMod() {
|
||||||
|
command.newData[ids.ArchiveName] = text.DefaultCodepage().Encode("Starting Game | by InkyBlackness HackEd")
|
||||||
|
command.newData[ids.GameState] = archive.ZeroGameStateData()
|
||||||
|
}
|
||||||
|
|
||||||
|
param := level.EmptyLevelParameters{
|
||||||
|
Cyberspace: world.IsConsideredCyberspaceByDefault(id),
|
||||||
|
}
|
||||||
|
effectiveGameState := view.effectiveGameState()
|
||||||
|
if id == effectiveGameState.CurrentLevel() {
|
||||||
|
posX, posY := effectiveGameState.HackerMapPosition()
|
||||||
|
param.MapModifier = func(x, y int, entry *level.TileMapEntry) {
|
||||||
|
if (x == int(posX.Tile())) && (y == int(posY.Tile())) {
|
||||||
|
entry.Type = level.TileTypeOpen
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
levelData := level.EmptyLevelData(param)
|
||||||
|
levelIDBegin := ids.LevelResourcesStart.Plus(lvlids.PerLevel * id)
|
||||||
|
for offset, newData := range &levelData {
|
||||||
|
if (offset < lvlids.FirstUsed) || (offset >= lvlids.FirstUnused) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
resourceID := levelIDBegin.Plus(offset)
|
||||||
|
oldData := view.mod.ModifiedBlock(resource.LangAny, resourceID, 0)
|
||||||
|
if len(oldData) > 0 {
|
||||||
|
command.oldData[resourceID] = oldData
|
||||||
|
}
|
||||||
|
if len(newData) > 0 {
|
||||||
|
command.newData[resourceID] = newData
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
view.commander.Queue(command)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (view *View) requestRemoveLevel(id int) {
|
||||||
|
if (id >= 0) && (id < archive.MaxLevels) && view.hasLevelInMod(id) {
|
||||||
|
command := setArchiveDataCommand{
|
||||||
|
model: &view.model,
|
||||||
|
selectedLevel: id,
|
||||||
|
newData: make(map[resource.ID][]byte),
|
||||||
|
oldData: make(map[resource.ID][]byte),
|
||||||
|
}
|
||||||
|
|
||||||
|
levelIDBegin := ids.LevelResourcesStart.Plus(lvlids.PerLevel * id)
|
||||||
|
for offset := lvlids.FirstUsed; offset < lvlids.PerLevel; offset++ {
|
||||||
|
resourceID := levelIDBegin.Plus(offset)
|
||||||
|
oldData := view.mod.ModifiedBlock(resource.LangAny, resourceID, 0)
|
||||||
|
if len(oldData) > 0 {
|
||||||
|
command.oldData[resourceID] = oldData
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
view.commander.Queue(command)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (view *View) requestSetGameState(newData []byte) {
|
||||||
|
command := setArchiveDataCommand{
|
||||||
|
model: &view.model,
|
||||||
|
selectedLevel: view.model.selectedLevel,
|
||||||
|
newData: make(map[resource.ID][]byte),
|
||||||
|
oldData: make(map[resource.ID][]byte),
|
||||||
|
}
|
||||||
|
|
||||||
|
command.oldData[ids.GameState] = view.gameStateData().snapshot().raw
|
||||||
|
command.newData[ids.GameState] = newData
|
||||||
|
|
||||||
|
view.commander.Queue(command)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (view *View) createPropertyControls(readOnly bool, rootInterpreter *interpreters.Instance,
|
||||||
|
updater func(string, func(uint32) uint32)) {
|
||||||
|
objTypeRenderer := values.ObjectTypeControlRenderer{
|
||||||
|
Meta: view.mod.ObjectProperties(),
|
||||||
|
TextCache: view.textCache,
|
||||||
|
}
|
||||||
|
|
||||||
|
var processInterpreter func(string, *interpreters.Instance)
|
||||||
|
processInterpreter = func(path string, interpreter *interpreters.Instance) {
|
||||||
|
for _, key := range interpreter.Keys() {
|
||||||
|
fullKey := path + key
|
||||||
|
unifier := values.NewUnifier()
|
||||||
|
unifier.Add(int32(interpreter.Get(key)))
|
||||||
|
simplifier := values.StandardSimplifier(readOnly, fullKey, unifier,
|
||||||
|
func(modifier func(uint32) uint32) {
|
||||||
|
updater(fullKey, modifier)
|
||||||
|
}, objTypeRenderer)
|
||||||
|
|
||||||
|
interpreter.Describe(key, simplifier)
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, key := range interpreter.ActiveRefinements() {
|
||||||
|
fullKey := path + key
|
||||||
|
if len(fullKey) > 0 {
|
||||||
|
imgui.Separator()
|
||||||
|
imgui.Text(fullKey + ":")
|
||||||
|
}
|
||||||
|
processInterpreter(fullKey+".", interpreter.Refined(key))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
processInterpreter("", rootInterpreter)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (view *View) setInterpreterValueKeyed(instance *interpreters.Instance, key string, modifier func(uint32) uint32) {
|
||||||
|
resolvedInterpreter := instance
|
||||||
|
keys := strings.Split(key, ".")
|
||||||
|
keyCount := len(keys)
|
||||||
|
if len(keys) > 1 {
|
||||||
|
for _, subKey := range keys[:keyCount-1] {
|
||||||
|
resolvedInterpreter = resolvedInterpreter.Refined(subKey)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
resolvedInterpreter.Set(keys[keyCount-1], modifier(resolvedInterpreter.Get(keys[keyCount-1])))
|
||||||
|
}
|
||||||
|
|
||||||
|
func (view *View) createInventoryControls(readOnly bool, gameState *archive.GameState, onChange func()) {
|
||||||
|
if imgui.TreeNodeV("Weapons", imgui.TreeNodeFlagsFramed) {
|
||||||
|
for i := 0; i < archive.InventoryWeaponSlots; i++ {
|
||||||
|
if i > 0 {
|
||||||
|
imgui.Separator()
|
||||||
|
}
|
||||||
|
imgui.PushID(fmt.Sprintf("weapon%d", i))
|
||||||
|
view.createInventoryWeaponSlotControls(readOnly, gameState.InventoryWeaponSlot(i), onChange)
|
||||||
|
imgui.PopID()
|
||||||
|
}
|
||||||
|
imgui.TreePop()
|
||||||
|
}
|
||||||
|
if imgui.TreeNodeV("Ammo", imgui.TreeNodeFlagsFramed) {
|
||||||
|
for i := 0; i < archive.AmmoTypeCount; i++ {
|
||||||
|
imgui.Separator()
|
||||||
|
imgui.PushID(fmt.Sprintf("ammo%d", i))
|
||||||
|
view.createInventoryAmmoControls(readOnly, gameState.InventoryAmmo(i), onChange)
|
||||||
|
imgui.PopID()
|
||||||
|
}
|
||||||
|
imgui.TreePop()
|
||||||
|
}
|
||||||
|
if imgui.TreeNodeV("Grenades", imgui.TreeNodeFlagsFramed) {
|
||||||
|
for i := 0; i < archive.GrenadeTypeCount; i++ {
|
||||||
|
imgui.Separator()
|
||||||
|
imgui.PushID(fmt.Sprintf("grenade%d", i))
|
||||||
|
view.createInventoryGrenadeControls(readOnly, gameState.InventoryGrenade(i), onChange)
|
||||||
|
imgui.PopID()
|
||||||
|
}
|
||||||
|
imgui.TreePop()
|
||||||
|
}
|
||||||
|
if imgui.TreeNodeV("Patches", imgui.TreeNodeFlagsFramed) {
|
||||||
|
for i := 0; i < archive.GrenadeTypeCount; i++ {
|
||||||
|
imgui.PushID(fmt.Sprintf("patch%d", i))
|
||||||
|
view.createPatchStateControls(readOnly, gameState.PatchState(i), onChange)
|
||||||
|
imgui.PopID()
|
||||||
|
}
|
||||||
|
imgui.TreePop()
|
||||||
|
}
|
||||||
|
if imgui.TreeNodeV("General", imgui.TreeNodeFlagsFramed) {
|
||||||
|
for i := 0; i < archive.GeneralInventorySlotCount; i++ {
|
||||||
|
imgui.PushID(fmt.Sprintf("general%d", i))
|
||||||
|
view.createGeneralInventoryControls(readOnly, gameState.GeneralInventorySlot(i), onChange)
|
||||||
|
imgui.PopID()
|
||||||
|
}
|
||||||
|
imgui.TreePop()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (view *View) createWareControls(readOnly bool, gameState *archive.GameState, onChange func()) {
|
||||||
|
if imgui.TreeNodeV("Hardware", imgui.TreeNodeFlagsFramed) {
|
||||||
|
for i := 0; i < archive.HardwareTypeCount; i++ {
|
||||||
|
imgui.PushID(fmt.Sprintf("hw%d", i))
|
||||||
|
view.createHardwareControls(readOnly, gameState.HardwareState(i), onChange)
|
||||||
|
imgui.PopID()
|
||||||
|
}
|
||||||
|
imgui.TreePop()
|
||||||
|
}
|
||||||
|
|
||||||
|
if imgui.TreeNodeV("Software", imgui.TreeNodeFlagsFramed) {
|
||||||
|
for i := 0; i < archive.SoftwareOffenseTypeCount; i++ {
|
||||||
|
imgui.PushID(fmt.Sprintf("swoffense%d", i))
|
||||||
|
view.createVersionedSoftwareControls(readOnly, gameState.VersionedOffenseSoftwareState(i), 0, onChange)
|
||||||
|
imgui.PopID()
|
||||||
|
}
|
||||||
|
for i := 0; i < archive.SoftwareDefenseTypeCount; i++ {
|
||||||
|
imgui.PushID(fmt.Sprintf("swdefense%d", i))
|
||||||
|
view.createVersionedSoftwareControls(readOnly, gameState.VersionedDefenseSoftwareState(i), 1, onChange)
|
||||||
|
imgui.PopID()
|
||||||
|
}
|
||||||
|
for i := 0; i < archive.SoftwareOneshotTypeCount; i++ {
|
||||||
|
imgui.PushID(fmt.Sprintf("swoneshot%d", i))
|
||||||
|
view.createCountedSoftwareControls(readOnly, gameState.OneshotSoftwareState(i), 2, onChange)
|
||||||
|
imgui.PopID()
|
||||||
|
}
|
||||||
|
imgui.TreePop()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (view *View) createHardwareControls(readOnly bool, state archive.HardwareState, onChange func()) {
|
||||||
|
values.RenderUnifiedSliderInt(readOnly,
|
||||||
|
fmt.Sprintf("Version (%s)", view.indexedName(object.ClassHardware, state.Index)),
|
||||||
|
values.UnifierFor(state.Version()),
|
||||||
|
func(u values.Unifier) int {
|
||||||
|
return u.Unified().(int)
|
||||||
|
}, func(value int) string {
|
||||||
|
return "%d"
|
||||||
|
}, 0, 4,
|
||||||
|
func(newValue int) {
|
||||||
|
state.SetVersion(newValue)
|
||||||
|
onChange()
|
||||||
|
})
|
||||||
|
|
||||||
|
values.RenderUnifiedCheckboxCombo(readOnly, "Is Active", values.UnifierFor(state.IsActive()), func(newActive bool) {
|
||||||
|
state.SetActive(newActive)
|
||||||
|
onChange()
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func (view *View) createVersionedSoftwareControls(readOnly bool, sw archive.VersionedSoftwareState, subclass int, onChange func()) {
|
||||||
|
values.RenderUnifiedSliderInt(readOnly,
|
||||||
|
fmt.Sprintf("Version (%s)", view.tripleName(object.TripleFrom(int(object.ClassSoftware), subclass, sw.Index))),
|
||||||
|
values.UnifierFor(sw.Version()),
|
||||||
|
func(u values.Unifier) int {
|
||||||
|
return u.Unified().(int)
|
||||||
|
}, func(value int) string {
|
||||||
|
return "%d"
|
||||||
|
}, 0, 0xFF,
|
||||||
|
func(newValue int) {
|
||||||
|
sw.SetVersion(newValue)
|
||||||
|
onChange()
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func (view *View) createCountedSoftwareControls(readOnly bool, sw archive.CountedSoftwareState, subclass int, onChange func()) {
|
||||||
|
values.RenderUnifiedSliderInt(readOnly,
|
||||||
|
fmt.Sprintf("Count (%s)", view.tripleName(object.TripleFrom(int(object.ClassSoftware), subclass, sw.Index))),
|
||||||
|
values.UnifierFor(sw.Count()),
|
||||||
|
func(u values.Unifier) int {
|
||||||
|
return u.Unified().(int)
|
||||||
|
}, func(value int) string {
|
||||||
|
return "%d"
|
||||||
|
}, 0, 0xFF,
|
||||||
|
func(newValue int) {
|
||||||
|
sw.SetCount(newValue)
|
||||||
|
onChange()
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func (view *View) createGeneralInventoryControls(readOnly bool, slot archive.GeneralInventorySlot, onChange func()) {
|
||||||
|
values.RenderUnifiedSliderInt(readOnly,
|
||||||
|
fmt.Sprintf("ObjectIndex (Slot %d)", slot.Index+1),
|
||||||
|
values.UnifierFor(slot.ObjectID()),
|
||||||
|
func(u values.Unifier) int {
|
||||||
|
return int(u.Unified().(level.ObjectID))
|
||||||
|
}, func(value int) string {
|
||||||
|
return "%d"
|
||||||
|
}, 0, 871, // TODO: get ID limit of current level
|
||||||
|
func(newValue int) {
|
||||||
|
slot.SetObjectID(level.ObjectID(newValue))
|
||||||
|
onChange()
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func (view *View) createPatchStateControls(readOnly bool, patch archive.PatchState, onChange func()) {
|
||||||
|
values.RenderUnifiedSliderInt(readOnly,
|
||||||
|
fmt.Sprintf("Patch Count (%s)", view.indexedName(object.ClassDrug, patch.Index)),
|
||||||
|
values.UnifierFor(patch.Count()),
|
||||||
|
func(u values.Unifier) int {
|
||||||
|
return u.Unified().(int)
|
||||||
|
}, func(value int) string {
|
||||||
|
return "%d"
|
||||||
|
}, 0, 0xFF,
|
||||||
|
func(newValue int) {
|
||||||
|
patch.SetCount(newValue)
|
||||||
|
onChange()
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func (view *View) createInventoryGrenadeControls(readOnly bool, grenade archive.InventoryGrenade, onChange func()) {
|
||||||
|
values.RenderUnifiedSliderInt(readOnly,
|
||||||
|
fmt.Sprintf("Grenade Count (%s)", view.indexedName(object.ClassGrenade, grenade.Index)),
|
||||||
|
values.UnifierFor(grenade.Count()),
|
||||||
|
func(u values.Unifier) int {
|
||||||
|
return u.Unified().(int)
|
||||||
|
}, func(value int) string {
|
||||||
|
return "%d"
|
||||||
|
}, 0, 0xFF,
|
||||||
|
func(newValue int) {
|
||||||
|
grenade.SetCount(newValue)
|
||||||
|
onChange()
|
||||||
|
})
|
||||||
|
|
||||||
|
values.RenderUnifiedSliderInt(readOnly, "Timer", values.UnifierFor(grenade.TimerSetting()),
|
||||||
|
func(u values.Unifier) int {
|
||||||
|
return int(u.Unified().(archive.GrenadeTimerSetting))
|
||||||
|
}, func(value int) string {
|
||||||
|
setting := archive.GrenadeTimerSetting(value)
|
||||||
|
return fmt.Sprintf("%.01fs -- raw: %%d", setting.InSeconds())
|
||||||
|
}, 0, archive.GrenadeTimerMaximum,
|
||||||
|
func(newValue int) {
|
||||||
|
grenade.SetTimerSetting(archive.GrenadeTimerSetting(newValue))
|
||||||
|
onChange()
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func (view *View) createInventoryAmmoControls(readOnly bool, ammo archive.InventoryAmmo, onChange func()) {
|
||||||
|
values.RenderUnifiedSliderInt(readOnly,
|
||||||
|
fmt.Sprintf("Clip Count (%s)", view.indexedName(object.ClassAmmo, ammo.Index)),
|
||||||
|
values.UnifierFor(ammo.FullClipCount()),
|
||||||
|
func(u values.Unifier) int {
|
||||||
|
return u.Unified().(int)
|
||||||
|
}, func(value int) string {
|
||||||
|
return "%d"
|
||||||
|
}, 0, 0xFF,
|
||||||
|
func(newValue int) {
|
||||||
|
ammo.SetFullClipCount(newValue)
|
||||||
|
onChange()
|
||||||
|
})
|
||||||
|
values.RenderUnifiedSliderInt(readOnly,
|
||||||
|
"Extra rounds",
|
||||||
|
values.UnifierFor(ammo.ExtraRoundsCount()),
|
||||||
|
func(u values.Unifier) int {
|
||||||
|
return u.Unified().(int)
|
||||||
|
}, func(value int) string {
|
||||||
|
return "%d"
|
||||||
|
}, 0, 0xFF,
|
||||||
|
func(newValue int) {
|
||||||
|
ammo.SetExtraRoundsCount(newValue)
|
||||||
|
onChange()
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func (view *View) createInventoryWeaponSlotControls(readOnly bool, slot archive.InventoryWeaponSlot, onChange func()) {
|
||||||
|
isInUse := slot.IsInUse()
|
||||||
|
inUseName := fmt.Sprintf("Weapon %d In Use", slot.Index+1)
|
||||||
|
|
||||||
|
values.RenderUnifiedCheckboxCombo(readOnly, inUseName, values.UnifierFor(isInUse), func(newUse bool) {
|
||||||
|
if newUse {
|
||||||
|
slot.SetInUse(0, 0)
|
||||||
|
} else {
|
||||||
|
slot.SetFree()
|
||||||
|
}
|
||||||
|
onChange()
|
||||||
|
})
|
||||||
|
if !isInUse {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
currentWeaponTriple := slot.Triple()
|
||||||
|
if imgui.BeginCombo("Type", view.tripleName(currentWeaponTriple)) {
|
||||||
|
allTypes := view.mod.ObjectProperties().TriplesInClass(currentWeaponTriple.Class)
|
||||||
|
for _, triple := range allTypes {
|
||||||
|
if imgui.SelectableV(view.tripleName(triple), triple == currentWeaponTriple, 0, imgui.Vec2{}) {
|
||||||
|
slot.SetInUse(triple.Subclass, triple.Type)
|
||||||
|
onChange()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
imgui.EndCombo()
|
||||||
|
}
|
||||||
|
isEnergyWeapon := currentWeaponTriple.Subclass >= 4
|
||||||
|
if isEnergyWeapon {
|
||||||
|
temperature, chargeAndOverload := slot.WeaponState()
|
||||||
|
values.RenderUnifiedSliderInt(readOnly, "Temperature", values.UnifierFor(int(temperature)),
|
||||||
|
func(u values.Unifier) int {
|
||||||
|
return u.Unified().(int)
|
||||||
|
}, func(value int) string {
|
||||||
|
return fmt.Sprintf("%.02f%%%% -- raw: %%d", (float32(value)/float32(lvlobj.EnergyWeaponMaxTemperature))*100)
|
||||||
|
}, 0x00, lvlobj.EnergyWeaponMaxTemperature,
|
||||||
|
func(newValue int) {
|
||||||
|
slot.SetWeaponState(byte(newValue), chargeAndOverload)
|
||||||
|
onChange()
|
||||||
|
})
|
||||||
|
|
||||||
|
overloadState := chargeAndOverload & 0x80
|
||||||
|
charge := chargeAndOverload & 0x7F
|
||||||
|
values.RenderUnifiedCheckboxCombo(readOnly, "Overload", values.UnifierFor(overloadState != 0), func(newOverload bool) {
|
||||||
|
if newOverload {
|
||||||
|
slot.SetWeaponState(temperature, 0x80|charge)
|
||||||
|
} else {
|
||||||
|
slot.SetWeaponState(temperature, charge)
|
||||||
|
}
|
||||||
|
onChange()
|
||||||
|
})
|
||||||
|
values.RenderUnifiedSliderInt(readOnly, "Charge", values.UnifierFor(int(charge)),
|
||||||
|
func(u values.Unifier) int {
|
||||||
|
return u.Unified().(int)
|
||||||
|
}, func(value int) string {
|
||||||
|
return fmt.Sprintf("%.02f%%%% -- raw: %%d", (float32(value)/float32(lvlobj.EnergyWeaponMaxEnergy))*100)
|
||||||
|
}, 0x00, 0x7F,
|
||||||
|
func(newValue int) {
|
||||||
|
slot.SetWeaponState(temperature, overloadState|byte(newValue))
|
||||||
|
onChange()
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
rounds, ammoType := slot.WeaponState()
|
||||||
|
values.RenderUnifiedSliderInt(readOnly, "Rounds", values.UnifierFor(int(rounds)),
|
||||||
|
func(u values.Unifier) int {
|
||||||
|
return u.Unified().(int)
|
||||||
|
}, func(value int) string {
|
||||||
|
return fmt.Sprintf("%d", value)
|
||||||
|
}, 0x00, 0xFF,
|
||||||
|
func(newValue int) {
|
||||||
|
slot.SetWeaponState(byte(newValue), ammoType)
|
||||||
|
onChange()
|
||||||
|
})
|
||||||
|
values.RenderUnifiedCombo(readOnly, "Ammo Type", values.UnifierFor(int(ammoType)),
|
||||||
|
func(u values.Unifier) int {
|
||||||
|
return u.Unified().(int)
|
||||||
|
},
|
||||||
|
func(value int) string {
|
||||||
|
switch value {
|
||||||
|
case 0:
|
||||||
|
return "Standard"
|
||||||
|
case 1:
|
||||||
|
return "Special"
|
||||||
|
default:
|
||||||
|
return fmt.Sprintf("Unknown %d", ammoType)
|
||||||
|
}
|
||||||
|
}, 2,
|
||||||
|
func(newValue int) {
|
||||||
|
slot.SetWeaponState(rounds, byte(newValue))
|
||||||
|
onChange()
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (view *View) createVariableControls(readOnly bool, gameState *archive.GameState, onChange func()) {
|
||||||
|
currentContextIdentifier := view.gameStateService.VariableBaseContext()
|
||||||
|
if imgui.BeginCombo("Base Context", variableContextList[currentContextIdentifier].title) {
|
||||||
|
contextIdentifiers := edit.VariableContextIdentifiers()
|
||||||
|
for _, identifier := range contextIdentifiers {
|
||||||
|
contextInfo := variableContextList[identifier]
|
||||||
|
if imgui.SelectableV(contextInfo.title, identifier == currentContextIdentifier, 0, imgui.Vec2{}) {
|
||||||
|
view.gameStateService.SetVariableBaseContext(identifier)
|
||||||
|
}
|
||||||
|
if imgui.IsItemHovered() {
|
||||||
|
imgui.SetTooltip(contextInfo.info)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
imgui.EndCombo()
|
||||||
|
}
|
||||||
|
if imgui.IsItemHovered() {
|
||||||
|
imgui.SetTooltip("The base context defines to what variables default to\n" +
|
||||||
|
"in case they are not overridden by the project.")
|
||||||
|
}
|
||||||
|
|
||||||
|
if imgui.Button("Remove All Overrides") {
|
||||||
|
_ = view.gameStateService.DefaultAllVariables()
|
||||||
|
}
|
||||||
|
if imgui.IsItemHovered() {
|
||||||
|
imgui.SetTooltip("Removes all overrides and their parameters.")
|
||||||
|
}
|
||||||
|
if !readOnly {
|
||||||
|
imgui.SameLine()
|
||||||
|
if imgui.Button("Reset All") {
|
||||||
|
for i := 0; i < archive.BooleanVarCount; i++ {
|
||||||
|
gameState.SetBooleanVar(i, view.gameStateService.BooleanVariable(i).ResetValueBool())
|
||||||
|
}
|
||||||
|
for i := 0; i < archive.IntegerVarCount; i++ {
|
||||||
|
gameState.SetIntegerVar(i, view.gameStateService.IntegerVariable(i).ResetValueInt())
|
||||||
|
}
|
||||||
|
onChange()
|
||||||
|
}
|
||||||
|
if imgui.IsItemHovered() {
|
||||||
|
imgui.SetTooltip("Resets all variables to their configured initial value.\n" +
|
||||||
|
"To reset a single variable, hover over the specific variable and use the context menu.")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
isSavegame := gameState.IsSavegame()
|
||||||
|
if imgui.TreeNodeV("Boolean Variables", imgui.TreeNodeFlagsFramed) {
|
||||||
|
intConverter := func(u values.Unifier) int {
|
||||||
|
bValue := u.Unified().(bool)
|
||||||
|
if bValue {
|
||||||
|
return 1
|
||||||
|
}
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
defaultNames := map[int]string{0: "False", 1: "True"}
|
||||||
|
|
||||||
|
for i := 0; i < archive.BooleanVarCount; i++ {
|
||||||
|
varIndex := i
|
||||||
|
info := view.gameStateService.BooleanVariable(varIndex)
|
||||||
|
|
||||||
|
varReadOnly := readOnly || (info.Hardcoded && !isSavegame)
|
||||||
|
varLabel := fmt.Sprintf("Var%03d", varIndex)
|
||||||
|
varName := fmt.Sprintf("Var%03d: %s", varIndex, info.Name)
|
||||||
|
varUnifier := values.UnifierFor(gameState.BooleanVar(varIndex))
|
||||||
|
|
||||||
|
values.RenderUnifiedCombo(varReadOnly, varName, varUnifier, intConverter,
|
||||||
|
func(value int) string {
|
||||||
|
name, found := info.ValueNames[int16(value)]
|
||||||
|
if found {
|
||||||
|
return name
|
||||||
|
}
|
||||||
|
return defaultNames[value]
|
||||||
|
},
|
||||||
|
2,
|
||||||
|
func(newValue int) {
|
||||||
|
gameState.SetBooleanVar(varIndex, newValue != 0)
|
||||||
|
onChange()
|
||||||
|
})
|
||||||
|
|
||||||
|
if imgui.BeginPopupContextItemV(varLabel+"-Popup", 1) {
|
||||||
|
if imgui.Selectable("Edit info...") {
|
||||||
|
view.modalStateMachine.SetState(&editBooleanVariableDialog{
|
||||||
|
machine: view.modalStateMachine,
|
||||||
|
view: view,
|
||||||
|
service: view.gameStateService,
|
||||||
|
index: varIndex,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
if !varReadOnly {
|
||||||
|
imgui.Separator()
|
||||||
|
if imgui.Selectable("Reset") {
|
||||||
|
gameState.SetBooleanVar(varIndex, info.ResetValueBool())
|
||||||
|
onChange()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
imgui.EndPopup()
|
||||||
|
} else if imgui.IsItemHovered() && (len(info.Description) > 0) {
|
||||||
|
imgui.SetTooltip(info.Description)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
imgui.TreePop()
|
||||||
|
}
|
||||||
|
if imgui.TreeNodeV("Integer Variables", imgui.TreeNodeFlagsFramed) {
|
||||||
|
intConverter := func(u values.Unifier) int {
|
||||||
|
return int(u.Unified().(int16))
|
||||||
|
}
|
||||||
|
for i := 0; i < archive.IntegerVarCount; i++ {
|
||||||
|
varIndex := i
|
||||||
|
info := view.gameStateService.IntegerVariable(varIndex)
|
||||||
|
|
||||||
|
varReadOnly := readOnly || (info.Hardcoded && !isSavegame)
|
||||||
|
varLabel := fmt.Sprintf("Var%02d", varIndex)
|
||||||
|
varName := fmt.Sprintf("Var%02d: %s", varIndex, info.Name)
|
||||||
|
varUnifier := values.UnifierFor(gameState.IntegerVar(varIndex))
|
||||||
|
changeHandler := func(newValue int) {
|
||||||
|
gameState.SetIntegerVar(varIndex, int16(newValue))
|
||||||
|
onChange()
|
||||||
|
}
|
||||||
|
|
||||||
|
switch {
|
||||||
|
case archive.IsRandomIntegerVariable(varIndex):
|
||||||
|
values.RenderUnifiedSliderInt(readOnly, varName, varUnifier,
|
||||||
|
func(u values.Unifier) int { return int(numbers.FromBinaryCodedDecimal(uint16(intConverter(u)))) },
|
||||||
|
func(value int) string { return "%03d" },
|
||||||
|
0, 999,
|
||||||
|
func(newValue int) {
|
||||||
|
changeHandler(int(numbers.ToBinaryCodedDecimal(uint16(newValue))))
|
||||||
|
})
|
||||||
|
case len(info.ValueNames) > 0:
|
||||||
|
var max int16
|
||||||
|
for key := range info.ValueNames {
|
||||||
|
if key > max {
|
||||||
|
max = key
|
||||||
|
}
|
||||||
|
}
|
||||||
|
values.RenderUnifiedCombo(varReadOnly, varName, varUnifier, intConverter,
|
||||||
|
func(value int) string {
|
||||||
|
name, found := info.ValueNames[int16(value)]
|
||||||
|
if found {
|
||||||
|
return name
|
||||||
|
}
|
||||||
|
return fmt.Sprintf("Unknown %d", value)
|
||||||
|
},
|
||||||
|
int(max+1),
|
||||||
|
changeHandler)
|
||||||
|
default:
|
||||||
|
min := math.MinInt16
|
||||||
|
max := math.MaxInt16
|
||||||
|
if info.Limits != nil {
|
||||||
|
min = int(info.Limits.Minimum)
|
||||||
|
max = int(info.Limits.Maximum)
|
||||||
|
}
|
||||||
|
|
||||||
|
values.RenderUnifiedSliderInt(varReadOnly, varName, varUnifier, intConverter,
|
||||||
|
func(value int) string {
|
||||||
|
return "%d"
|
||||||
|
},
|
||||||
|
min, max,
|
||||||
|
changeHandler)
|
||||||
|
}
|
||||||
|
if imgui.BeginPopupContextItemV(varLabel+"-Popup", 1) {
|
||||||
|
if imgui.Selectable("Edit info...") {
|
||||||
|
view.modalStateMachine.SetState(&editIntegerVariableDialog{
|
||||||
|
machine: view.modalStateMachine,
|
||||||
|
view: view,
|
||||||
|
service: view.gameStateService,
|
||||||
|
index: varIndex,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
if !varReadOnly {
|
||||||
|
imgui.Separator()
|
||||||
|
if imgui.Selectable("Reset") {
|
||||||
|
gameState.SetIntegerVar(varIndex, info.ResetValueInt())
|
||||||
|
onChange()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
imgui.EndPopup()
|
||||||
|
} else if imgui.IsItemHovered() && (len(info.Description) > 0) {
|
||||||
|
imgui.SetTooltip(info.Description)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
imgui.TreePop()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (view *View) createMessageControls(readOnly bool, gameState *archive.GameState, onChange func()) {
|
||||||
|
view.createMessageControlsFor(readOnly, "EMail",
|
||||||
|
&view.model.emailIndex, archive.EMailCount, gameState.EMailState, onChange)
|
||||||
|
view.createMessageControlsFor(readOnly, "Log",
|
||||||
|
&view.model.logIndex, archive.LogCount, gameState.LogState, onChange)
|
||||||
|
view.createMessageControlsFor(readOnly, "Fragment",
|
||||||
|
&view.model.fragmentIndex, archive.FragmentCount, gameState.FragmentState, onChange)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (view *View) createMessageControlsFor(readOnly bool, name string, index *int, limit int,
|
||||||
|
retriever func(int) archive.MessageState, onChange func()) {
|
||||||
|
imgui.PushID(name)
|
||||||
|
gui.StepSliderInt(name+" Index", index, 0, limit-1)
|
||||||
|
messageState := retriever(*index)
|
||||||
|
values.RenderUnifiedCheckboxCombo(readOnly, "Received", values.UnifierFor(messageState.Received()),
|
||||||
|
func(newState bool) {
|
||||||
|
messageState.SetReceived(newState)
|
||||||
|
onChange()
|
||||||
|
})
|
||||||
|
values.RenderUnifiedCheckboxCombo(readOnly, "Read", values.UnifierFor(messageState.Read()),
|
||||||
|
func(newState bool) {
|
||||||
|
messageState.SetRead(newState)
|
||||||
|
onChange()
|
||||||
|
})
|
||||||
|
imgui.PopID()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (view *View) tripleName(triple object.Triple) string {
|
||||||
|
suffix := hintUnknown
|
||||||
|
linearIndex := view.mod.ObjectProperties().TripleIndex(triple)
|
||||||
|
if linearIndex >= 0 {
|
||||||
|
key := resource.KeyOf(ids.ObjectLongNames, resource.LangDefault, linearIndex)
|
||||||
|
objName, err := view.textCache.Text(key)
|
||||||
|
if err == nil {
|
||||||
|
suffix = singleLineString(objName)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return triple.String() + ": " + suffix
|
||||||
|
}
|
||||||
|
|
||||||
|
func (view *View) indexedName(class object.Class, index int) string {
|
||||||
|
startIndex := view.mod.ObjectProperties().TripleIndex(object.TripleFrom(int(class), 0, 0))
|
||||||
|
if startIndex >= 0 {
|
||||||
|
key := resource.KeyOf(ids.ObjectLongNames, resource.LangDefault, startIndex+index)
|
||||||
|
objName, err := view.textCache.Text(key)
|
||||||
|
if err == nil {
|
||||||
|
return singleLineString(objName)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return hintUnknown
|
||||||
|
}
|
||||||
|
|
||||||
|
func singleLineString(str string) string {
|
||||||
|
return strings.ReplaceAll(strings.ReplaceAll(str, "\r", ""), "\n", "")
|
||||||
|
}
|
||||||
@@ -0,0 +1,16 @@
|
|||||||
|
package archives
|
||||||
|
|
||||||
|
type viewModel struct {
|
||||||
|
windowOpen bool
|
||||||
|
restoreFocus bool
|
||||||
|
|
||||||
|
selectedLevel int
|
||||||
|
|
||||||
|
emailIndex int
|
||||||
|
logIndex int
|
||||||
|
fragmentIndex int
|
||||||
|
}
|
||||||
|
|
||||||
|
func freshViewModel() viewModel {
|
||||||
|
return viewModel{}
|
||||||
|
}
|
||||||
@@ -0,0 +1,32 @@
|
|||||||
|
package bitmaps
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/inkyblackness/hacked/ss1/resource"
|
||||||
|
"github.com/inkyblackness/hacked/ss1/world"
|
||||||
|
)
|
||||||
|
|
||||||
|
type setBitmapCommand struct {
|
||||||
|
model *viewModel
|
||||||
|
|
||||||
|
displayKey resource.Key
|
||||||
|
|
||||||
|
resourceKey resource.Key
|
||||||
|
oldData []byte
|
||||||
|
newData []byte
|
||||||
|
}
|
||||||
|
|
||||||
|
func (cmd setBitmapCommand) Do(modder world.Modder) error {
|
||||||
|
return cmd.perform(modder, cmd.newData)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (cmd setBitmapCommand) Undo(modder world.Modder) error {
|
||||||
|
return cmd.perform(modder, cmd.oldData)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (cmd setBitmapCommand) perform(modder world.Modder, data []byte) error {
|
||||||
|
modder.SetResourceBlock(cmd.resourceKey.Lang, cmd.resourceKey.ID, cmd.resourceKey.Index, data)
|
||||||
|
|
||||||
|
cmd.model.restoreFocus = true
|
||||||
|
cmd.model.currentKey = cmd.displayKey
|
||||||
|
return nil
|
||||||
|
}
|
||||||
@@ -0,0 +1,271 @@
|
|||||||
|
package bitmaps
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
|
||||||
|
"github.com/inkyblackness/imgui-go/v3"
|
||||||
|
|
||||||
|
"github.com/inkyblackness/hacked/editor/external"
|
||||||
|
"github.com/inkyblackness/hacked/editor/graphics"
|
||||||
|
"github.com/inkyblackness/hacked/editor/render"
|
||||||
|
"github.com/inkyblackness/hacked/ss1/content/bitmap"
|
||||||
|
"github.com/inkyblackness/hacked/ss1/edit/undoable/cmd"
|
||||||
|
"github.com/inkyblackness/hacked/ss1/resource"
|
||||||
|
"github.com/inkyblackness/hacked/ss1/world"
|
||||||
|
"github.com/inkyblackness/hacked/ss1/world/ids"
|
||||||
|
"github.com/inkyblackness/hacked/ui/gui"
|
||||||
|
)
|
||||||
|
|
||||||
|
type bitmapInfo struct {
|
||||||
|
title string
|
||||||
|
languageSpecific bool
|
||||||
|
bitmapType bitmap.Type
|
||||||
|
bitmapFlags bitmap.Flag
|
||||||
|
}
|
||||||
|
|
||||||
|
var knownBitmapTypes = map[resource.ID]bitmapInfo{
|
||||||
|
ids.MfdDataBitmaps: {title: "MFD Data Images", languageSpecific: true, bitmapType: bitmap.TypeCompressed8Bit, bitmapFlags: bitmap.FlagTransparent},
|
||||||
|
ids.ObjectMaterialBitmaps: {title: "Object Materials", languageSpecific: false, bitmapType: bitmap.TypeFlat8Bit, bitmapFlags: 0},
|
||||||
|
ids.ObjectTextureBitmaps: {title: "Object Textures", languageSpecific: false, bitmapType: bitmap.TypeFlat8Bit, bitmapFlags: 0},
|
||||||
|
ids.IconBitmaps: {title: "Wall Icons", languageSpecific: false, bitmapType: bitmap.TypeCompressed8Bit, bitmapFlags: bitmap.FlagTransparent},
|
||||||
|
ids.GraffitiBitmaps: {title: "Graffiti", languageSpecific: false, bitmapType: bitmap.TypeCompressed8Bit, bitmapFlags: bitmap.FlagTransparent},
|
||||||
|
ids.ScreenTextures: {title: "Screens", languageSpecific: false, bitmapType: bitmap.TypeCompressed8Bit, bitmapFlags: 0},
|
||||||
|
}
|
||||||
|
|
||||||
|
var knownBitmapTypesOrder = []resource.ID{
|
||||||
|
ids.MfdDataBitmaps,
|
||||||
|
ids.ObjectMaterialBitmaps,
|
||||||
|
ids.ObjectTextureBitmaps,
|
||||||
|
ids.IconBitmaps,
|
||||||
|
ids.GraffitiBitmaps,
|
||||||
|
ids.ScreenTextures,
|
||||||
|
}
|
||||||
|
|
||||||
|
// View provides edit controls for bitmaps.
|
||||||
|
type View struct {
|
||||||
|
mod *world.Mod
|
||||||
|
imageCache *graphics.TextureCache
|
||||||
|
paletteCache *graphics.PaletteCache
|
||||||
|
|
||||||
|
modalStateMachine gui.ModalStateMachine
|
||||||
|
clipboard external.Clipboard
|
||||||
|
guiScale float32
|
||||||
|
commander cmd.Commander
|
||||||
|
|
||||||
|
model viewModel
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewBitmapsView returns a new instance.
|
||||||
|
func NewBitmapsView(mod *world.Mod, imageCache *graphics.TextureCache, paletteCache *graphics.PaletteCache,
|
||||||
|
modalStateMachine gui.ModalStateMachine, clipboard external.Clipboard,
|
||||||
|
guiScale float32, commander cmd.Commander) *View {
|
||||||
|
view := &View{
|
||||||
|
mod: mod,
|
||||||
|
imageCache: imageCache,
|
||||||
|
paletteCache: paletteCache,
|
||||||
|
|
||||||
|
modalStateMachine: modalStateMachine,
|
||||||
|
clipboard: clipboard,
|
||||||
|
guiScale: guiScale,
|
||||||
|
commander: commander,
|
||||||
|
|
||||||
|
model: freshViewModel(),
|
||||||
|
}
|
||||||
|
return view
|
||||||
|
}
|
||||||
|
|
||||||
|
// WindowOpen returns the flag address, to be used with the main menu.
|
||||||
|
func (view *View) WindowOpen() *bool {
|
||||||
|
return &view.model.windowOpen
|
||||||
|
}
|
||||||
|
|
||||||
|
// Render renders the view.
|
||||||
|
func (view *View) Render() {
|
||||||
|
if view.model.restoreFocus {
|
||||||
|
imgui.SetNextWindowFocus()
|
||||||
|
view.model.restoreFocus = false
|
||||||
|
view.model.windowOpen = true
|
||||||
|
}
|
||||||
|
if view.model.windowOpen {
|
||||||
|
imgui.SetNextWindowSizeV(imgui.Vec2{X: 800 * view.guiScale, Y: 300 * view.guiScale}, imgui.ConditionFirstUseEver)
|
||||||
|
if imgui.BeginV("Bitmaps", view.WindowOpen(), imgui.WindowFlagsNoCollapse|imgui.WindowFlagsHorizontalScrollbar) {
|
||||||
|
view.renderContent()
|
||||||
|
}
|
||||||
|
imgui.End()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (view *View) renderContent() {
|
||||||
|
if imgui.BeginChildV("Properties", imgui.Vec2{X: 350 * view.guiScale, Y: 0}, false, 0) {
|
||||||
|
imgui.PushItemWidth(-150 * view.guiScale)
|
||||||
|
if imgui.BeginCombo("Bitmap Type", knownBitmapTypes[view.model.currentKey.ID].title) {
|
||||||
|
for _, id := range knownBitmapTypesOrder {
|
||||||
|
if imgui.SelectableV(knownBitmapTypes[id].title, id == view.model.currentKey.ID, 0, imgui.Vec2{}) {
|
||||||
|
view.model.currentKey.ID = id
|
||||||
|
view.model.currentKey.Index = 0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
imgui.EndCombo()
|
||||||
|
}
|
||||||
|
selectedType := knownBitmapTypes[view.model.currentKey.ID]
|
||||||
|
if selectedType.languageSpecific {
|
||||||
|
if imgui.BeginCombo("Language", view.model.currentKey.Lang.String()) {
|
||||||
|
languages := resource.Languages()
|
||||||
|
for _, lang := range languages {
|
||||||
|
if imgui.SelectableV(lang.String(), lang == view.model.currentKey.Lang, 0, imgui.Vec2{}) {
|
||||||
|
view.model.currentKey.Lang = lang
|
||||||
|
}
|
||||||
|
}
|
||||||
|
imgui.EndCombo()
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
view.model.currentKey.Lang = resource.LangAny
|
||||||
|
}
|
||||||
|
|
||||||
|
info, _ := ids.Info(view.model.currentKey.ID)
|
||||||
|
|
||||||
|
gui.StepSliderInt("Index", &view.model.currentKey.Index, 0, info.MaxCount-1)
|
||||||
|
|
||||||
|
render.TextureSelector("###"+"IndexBitmap", -1, view.guiScale, info.MaxCount,
|
||||||
|
view.model.currentKey.Index, view.imageCache,
|
||||||
|
view.indexedResourceKey,
|
||||||
|
func(index int) string { return fmt.Sprintf("%d", index) },
|
||||||
|
func(newValue int) {
|
||||||
|
view.model.currentKey.Index = newValue
|
||||||
|
})
|
||||||
|
|
||||||
|
tex, err := view.imageCache.Texture(view.currentResourceKey())
|
||||||
|
|
||||||
|
if imgui.Button("Clear") {
|
||||||
|
view.requestClear(selectedType)
|
||||||
|
}
|
||||||
|
imgui.SameLine()
|
||||||
|
if imgui.Button("Import") {
|
||||||
|
view.requestImport(selectedType)
|
||||||
|
}
|
||||||
|
if err == nil {
|
||||||
|
imgui.SameLine()
|
||||||
|
if imgui.Button("Export") {
|
||||||
|
view.requestExport()
|
||||||
|
}
|
||||||
|
if view.hasModCurrentBitmap() {
|
||||||
|
imgui.SameLine()
|
||||||
|
if imgui.Button("Remove") {
|
||||||
|
view.requestSetBitmapData(nil)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
width, height := tex.Size()
|
||||||
|
imgui.LabelText("Width", fmt.Sprintf("%d", int(width)))
|
||||||
|
imgui.LabelText("Height", fmt.Sprintf("%d", int(height)))
|
||||||
|
}
|
||||||
|
|
||||||
|
imgui.PopItemWidth()
|
||||||
|
}
|
||||||
|
imgui.EndChild()
|
||||||
|
imgui.SameLine()
|
||||||
|
render.TextureImage("Big texture", view.imageCache, view.currentResourceKey(), imgui.Vec2{X: 320 * view.guiScale, Y: 240 * view.guiScale})
|
||||||
|
}
|
||||||
|
|
||||||
|
func (view *View) currentResourceKey() resource.Key {
|
||||||
|
return view.indexedResourceKey(view.model.currentKey.Index)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (view *View) indexedResourceKey(index int) resource.Key {
|
||||||
|
key := view.model.currentKey
|
||||||
|
info, _ := ids.Info(view.model.currentKey.ID)
|
||||||
|
if !info.List {
|
||||||
|
key.ID = key.ID.Plus(index)
|
||||||
|
key.Index = 0
|
||||||
|
} else {
|
||||||
|
key.Index = index
|
||||||
|
}
|
||||||
|
return key
|
||||||
|
}
|
||||||
|
|
||||||
|
func (view *View) hasModCurrentBitmap() bool {
|
||||||
|
key := view.currentResourceKey()
|
||||||
|
return len(view.mod.ModifiedBlock(key.Lang, key.ID, key.Index)) > 0
|
||||||
|
}
|
||||||
|
|
||||||
|
func (view *View) requestExport() {
|
||||||
|
key := view.currentResourceKey()
|
||||||
|
texture, err := view.imageCache.Texture(key)
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
palette, err := view.paletteCache.Palette(0)
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
rawPalette := palette.Palette()
|
||||||
|
filename := fmt.Sprintf("%05d_%03d_%s.png", key.ID.Value(), key.Index, key.Lang.String())
|
||||||
|
width, height := texture.Size()
|
||||||
|
bmp := bitmap.Bitmap{
|
||||||
|
Header: bitmap.Header{
|
||||||
|
Width: int16(width),
|
||||||
|
Height: int16(height),
|
||||||
|
},
|
||||||
|
Pixels: texture.PixelData(),
|
||||||
|
Palette: &rawPalette,
|
||||||
|
}
|
||||||
|
|
||||||
|
external.ExportImage(view.modalStateMachine, filename, bmp)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (view *View) requestImport(bmpInfo bitmapInfo) {
|
||||||
|
paletteRetriever := func() (bitmap.Palette, error) {
|
||||||
|
palette, err := view.paletteCache.Palette(0)
|
||||||
|
if err != nil {
|
||||||
|
return bitmap.Palette{}, err
|
||||||
|
}
|
||||||
|
return palette.Palette(), nil
|
||||||
|
}
|
||||||
|
external.ImportImage(view.modalStateMachine, paletteRetriever, func(bmp bitmap.Bitmap) {
|
||||||
|
view.requestSetBitmap(bmp, bmpInfo)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func (view *View) requestClear(bmpInfo bitmapInfo) {
|
||||||
|
bmp := bitmap.Bitmap{
|
||||||
|
Header: bitmap.Header{
|
||||||
|
Width: 1,
|
||||||
|
Height: 1,
|
||||||
|
},
|
||||||
|
Pixels: []byte{0x00},
|
||||||
|
}
|
||||||
|
view.requestSetBitmap(bmp, bmpInfo)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (view *View) requestSetBitmap(bmp bitmap.Bitmap, bmpInfo bitmapInfo) {
|
||||||
|
highestBitShift := func(value int16) (result byte) {
|
||||||
|
if value != 0 {
|
||||||
|
for (value >> result) != 1 {
|
||||||
|
result++
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
bmp.Header.Flags = bmpInfo.bitmapFlags
|
||||||
|
bmp.Header.Type = bmpInfo.bitmapType
|
||||||
|
bmp.Header.WidthFactor = highestBitShift(bmp.Header.Width)
|
||||||
|
bmp.Header.HeightFactor = highestBitShift(bmp.Header.Height)
|
||||||
|
bmp.Header.Stride = uint16(bmp.Header.Width)
|
||||||
|
data := bitmap.Encode(&bmp, 0)
|
||||||
|
view.requestSetBitmapData(data)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (view *View) requestSetBitmapData(newData []byte) {
|
||||||
|
resourceKey := view.currentResourceKey()
|
||||||
|
|
||||||
|
command := setBitmapCommand{
|
||||||
|
displayKey: view.model.currentKey,
|
||||||
|
model: &view.model,
|
||||||
|
|
||||||
|
resourceKey: resourceKey,
|
||||||
|
oldData: view.mod.ModifiedBlock(resourceKey.Lang, resourceKey.ID, resourceKey.Index),
|
||||||
|
newData: newData,
|
||||||
|
}
|
||||||
|
view.commander.Queue(command)
|
||||||
|
}
|
||||||
@@ -0,0 +1,19 @@
|
|||||||
|
package bitmaps
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/inkyblackness/hacked/ss1/resource"
|
||||||
|
"github.com/inkyblackness/hacked/ss1/world/ids"
|
||||||
|
)
|
||||||
|
|
||||||
|
type viewModel struct {
|
||||||
|
windowOpen bool
|
||||||
|
restoreFocus bool
|
||||||
|
|
||||||
|
currentKey resource.Key
|
||||||
|
}
|
||||||
|
|
||||||
|
func freshViewModel() viewModel {
|
||||||
|
return viewModel{
|
||||||
|
currentKey: resource.KeyOf(ids.MfdDataBitmaps, resource.LangDefault, 0),
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,4 @@
|
|||||||
|
/*
|
||||||
|
Package editor and its sub-packages contain the editor-specifics of hacked.
|
||||||
|
*/
|
||||||
|
package editor
|
||||||
+9
@@ -0,0 +1,9 @@
|
|||||||
|
package external
|
||||||
|
|
||||||
|
// Clipboard represents a temporary storage.
|
||||||
|
type Clipboard interface {
|
||||||
|
// String returns the current value of the clipboard, if it is compatible with UTF-8.
|
||||||
|
String() (string, error)
|
||||||
|
// SetString sets the current value of the clipboard as UTF-8 string.
|
||||||
|
SetString(value string)
|
||||||
|
}
|
||||||
+71
@@ -0,0 +1,71 @@
|
|||||||
|
package external
|
||||||
|
|
||||||
|
import (
|
||||||
|
"image"
|
||||||
|
"image/png"
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
|
||||||
|
"github.com/inkyblackness/hacked/ss1/content/audio"
|
||||||
|
"github.com/inkyblackness/hacked/ss1/content/audio/wav"
|
||||||
|
"github.com/inkyblackness/hacked/ss1/content/bitmap"
|
||||||
|
"github.com/inkyblackness/hacked/ui/gui"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Export starts an export dialog series, calling the given callback with a folder name.
|
||||||
|
func Export(machine gui.ModalStateMachine, info string, callback func(string), lastFailed bool) {
|
||||||
|
machine.SetState(&exportStartState{
|
||||||
|
machine: machine,
|
||||||
|
callback: callback,
|
||||||
|
info: info,
|
||||||
|
withError: lastFailed,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// ExportAudio is a helper wrapper for exporting audio.
|
||||||
|
func ExportAudio(machine gui.ModalStateMachine, filename string, sound audio.L8) {
|
||||||
|
info := "File to be written: " + filename
|
||||||
|
var dirHandler func(string)
|
||||||
|
|
||||||
|
dirHandler = func(dirname string) {
|
||||||
|
writer, err := os.Create(filepath.Join(dirname, filename))
|
||||||
|
if err != nil {
|
||||||
|
Export(machine, info, dirHandler, true)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
defer func() { _ = writer.Close() }()
|
||||||
|
err = wav.Save(writer, sound.SampleRate, sound.Samples)
|
||||||
|
if err != nil {
|
||||||
|
Export(machine, info, dirHandler, true)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Export(machine, info, dirHandler, false)
|
||||||
|
}
|
||||||
|
|
||||||
|
// ExportImage is a helper wrapper for exporting single images.
|
||||||
|
func ExportImage(machine gui.ModalStateMachine, filename string, bmp bitmap.Bitmap) {
|
||||||
|
info := "File to be written: " + filename
|
||||||
|
var exportTo func(string)
|
||||||
|
|
||||||
|
exportTo = func(dirname string) {
|
||||||
|
writer, err := os.Create(filepath.Join(dirname, filename))
|
||||||
|
if err != nil {
|
||||||
|
Export(machine, "Could not create file.\n"+info, exportTo, true)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
defer func() { _ = writer.Close() }()
|
||||||
|
|
||||||
|
imageRect := image.Rect(0, 0, int(bmp.Header.Width), int(bmp.Header.Height))
|
||||||
|
imagePal := bmp.Palette.ColorPalette(false)
|
||||||
|
paletted := image.NewPaletted(imageRect, imagePal)
|
||||||
|
paletted.Pix = bmp.Pixels
|
||||||
|
err = png.Encode(writer, paletted)
|
||||||
|
if err != nil {
|
||||||
|
Export(machine, info, exportTo, true)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Export(machine, info, exportTo, false)
|
||||||
|
}
|
||||||
@@ -0,0 +1,32 @@
|
|||||||
|
package external
|
||||||
|
|
||||||
|
import (
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/inkyblackness/imgui-go/v3"
|
||||||
|
|
||||||
|
"github.com/inkyblackness/hacked/ui/gui"
|
||||||
|
)
|
||||||
|
|
||||||
|
type exportStartState struct {
|
||||||
|
machine gui.ModalStateMachine
|
||||||
|
callback func(string)
|
||||||
|
info string
|
||||||
|
withError bool
|
||||||
|
}
|
||||||
|
|
||||||
|
func (state exportStartState) Render() {
|
||||||
|
imgui.OpenPopup("Export")
|
||||||
|
nextState := &exportWaitingState{
|
||||||
|
machine: state.machine,
|
||||||
|
callback: state.callback,
|
||||||
|
info: state.info,
|
||||||
|
}
|
||||||
|
if state.withError {
|
||||||
|
nextState.failureTime = time.Now()
|
||||||
|
}
|
||||||
|
state.machine.SetState(nextState)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (state exportStartState) HandleFiles(names []string) {
|
||||||
|
}
|
||||||
@@ -0,0 +1,81 @@
|
|||||||
|
package external
|
||||||
|
|
||||||
|
import (
|
||||||
|
"os"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/inkyblackness/imgui-go/v3"
|
||||||
|
"github.com/sqweek/dialog"
|
||||||
|
|
||||||
|
"github.com/inkyblackness/hacked/ui/gui"
|
||||||
|
)
|
||||||
|
|
||||||
|
type exportWaitingState struct {
|
||||||
|
machine gui.ModalStateMachine
|
||||||
|
callback func(string)
|
||||||
|
info string
|
||||||
|
|
||||||
|
failureTime time.Time
|
||||||
|
}
|
||||||
|
|
||||||
|
func (state *exportWaitingState) Render() {
|
||||||
|
if imgui.BeginPopupModalV("Export", nil,
|
||||||
|
imgui.WindowFlagsNoResize|imgui.WindowFlagsNoMove|imgui.WindowFlagsNoSavedSettings|imgui.WindowFlagsAlwaysAutoResize) {
|
||||||
|
imgui.Text("Waiting for folder.")
|
||||||
|
if !state.failureTime.IsZero() {
|
||||||
|
imgui.PushStyleColor(imgui.StyleColorText, imgui.Vec4{X: 1, Y: 0, Z: 0, W: 1})
|
||||||
|
imgui.Text("Previous attempt failed, could not save file.\nPlease check and try again.")
|
||||||
|
imgui.PopStyleColor()
|
||||||
|
if time.Since(state.failureTime).Seconds() > 5 {
|
||||||
|
state.failureTime = time.Time{}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
imgui.Text(`From your file browser drag'n'drop the folder
|
||||||
|
where to save the data into the editor window.
|
||||||
|
`)
|
||||||
|
imgui.PushStyleColor(imgui.StyleColorText, imgui.Vec4{X: 1, Y: 0, Z: 0, W: 1})
|
||||||
|
imgui.Text("Saving will potentially overwrite existing files in the folder.")
|
||||||
|
imgui.PopStyleColor()
|
||||||
|
imgui.Text(state.info)
|
||||||
|
imgui.Separator()
|
||||||
|
if imgui.Button("Browse...") {
|
||||||
|
dlgBuilder := dialog.Directory()
|
||||||
|
filename, err := dlgBuilder.Browse()
|
||||||
|
if err == nil {
|
||||||
|
state.HandleFiles([]string{filename})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
imgui.SameLine()
|
||||||
|
if imgui.Button("Cancel") {
|
||||||
|
state.machine.SetState(nil)
|
||||||
|
imgui.CloseCurrentPopup()
|
||||||
|
}
|
||||||
|
imgui.EndPopup()
|
||||||
|
} else {
|
||||||
|
state.machine.SetState(nil)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (state *exportWaitingState) HandleFiles(names []string) {
|
||||||
|
dirPath, ok := state.verifyDir(names)
|
||||||
|
if ok {
|
||||||
|
state.machine.SetState(nil)
|
||||||
|
state.callback(dirPath)
|
||||||
|
} else {
|
||||||
|
state.failureTime = time.Now()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (state exportWaitingState) verifyDir(names []string) (string, bool) {
|
||||||
|
if len(names) != 1 {
|
||||||
|
return "", false
|
||||||
|
}
|
||||||
|
fileInfo, err := os.Stat(names[0])
|
||||||
|
if err != nil {
|
||||||
|
return "", false
|
||||||
|
}
|
||||||
|
if !fileInfo.IsDir() {
|
||||||
|
return "", false
|
||||||
|
}
|
||||||
|
return names[0], true
|
||||||
|
}
|
||||||
+137
@@ -0,0 +1,137 @@
|
|||||||
|
package external
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/inkyblackness/imgui-go/v3"
|
||||||
|
"github.com/sqweek/dialog"
|
||||||
|
|
||||||
|
"github.com/inkyblackness/hacked/ui/gui"
|
||||||
|
)
|
||||||
|
|
||||||
|
// SingleFileHandler is a callback for a single selected file.
|
||||||
|
type SingleFileHandler func(string) error
|
||||||
|
|
||||||
|
type singleFileRunner func(*dialog.FileBuilder) (string, error)
|
||||||
|
|
||||||
|
// SaveFile starts a dialog series to save a file.
|
||||||
|
func SaveFile(machine gui.ModalStateMachine, types []TypeInfo, callback SingleFileHandler) {
|
||||||
|
machine.SetState(&fileStartState{
|
||||||
|
machine: machine,
|
||||||
|
title: "Save",
|
||||||
|
typeInfo: types,
|
||||||
|
runner: func(builder *dialog.FileBuilder) (string, error) { return builder.Save() },
|
||||||
|
callback: callback,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// LoadFile starts a dialog series to load a file.
|
||||||
|
func LoadFile(machine gui.ModalStateMachine, types []TypeInfo, callback SingleFileHandler) {
|
||||||
|
machine.SetState(&fileStartState{
|
||||||
|
machine: machine,
|
||||||
|
title: "Load",
|
||||||
|
typeInfo: types,
|
||||||
|
runner: func(builder *dialog.FileBuilder) (string, error) { return builder.Load() },
|
||||||
|
callback: callback,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
type fileStartState struct {
|
||||||
|
machine gui.ModalStateMachine
|
||||||
|
title string
|
||||||
|
typeInfo []TypeInfo
|
||||||
|
runner singleFileRunner
|
||||||
|
callback SingleFileHandler
|
||||||
|
}
|
||||||
|
|
||||||
|
func (state fileStartState) HandleFiles(names []string) {
|
||||||
|
}
|
||||||
|
|
||||||
|
func (state fileStartState) Render() {
|
||||||
|
imgui.OpenPopup(state.title)
|
||||||
|
nextState := &fileWaitingState{
|
||||||
|
machine: state.machine,
|
||||||
|
title: state.title,
|
||||||
|
typeInfo: state.typeInfo,
|
||||||
|
runner: state.runner,
|
||||||
|
callback: state.callback,
|
||||||
|
}
|
||||||
|
state.machine.SetState(nextState)
|
||||||
|
}
|
||||||
|
|
||||||
|
type fileWaitingState struct {
|
||||||
|
machine gui.ModalStateMachine
|
||||||
|
title string
|
||||||
|
typeInfo []TypeInfo
|
||||||
|
runner singleFileRunner
|
||||||
|
callback SingleFileHandler
|
||||||
|
|
||||||
|
renderCount int
|
||||||
|
shouldOpenDialog bool
|
||||||
|
problem string
|
||||||
|
}
|
||||||
|
|
||||||
|
func (state *fileWaitingState) HandleFiles(names []string) {
|
||||||
|
}
|
||||||
|
|
||||||
|
func (state *fileWaitingState) Render() {
|
||||||
|
if imgui.BeginPopupModalV(state.title, nil,
|
||||||
|
imgui.WindowFlagsNoResize|imgui.WindowFlagsNoMove|imgui.WindowFlagsNoSavedSettings|imgui.WindowFlagsAlwaysAutoResize) {
|
||||||
|
if len(state.problem) == 0 {
|
||||||
|
imgui.Text("Please use popup dialog...")
|
||||||
|
} else {
|
||||||
|
imgui.PushStyleColor(imgui.StyleColorText, imgui.Vec4{X: 1, Y: 0, Z: 0, W: 1})
|
||||||
|
imgui.Text(state.problem)
|
||||||
|
imgui.PopStyleColor()
|
||||||
|
}
|
||||||
|
state.renderControls()
|
||||||
|
imgui.EndPopup()
|
||||||
|
|
||||||
|
state.renderCount++
|
||||||
|
if state.renderCount == 3 {
|
||||||
|
state.shouldOpenDialog = true
|
||||||
|
}
|
||||||
|
|
||||||
|
if state.shouldOpenDialog {
|
||||||
|
state.shouldOpenDialog = false
|
||||||
|
state.openDialog()
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
state.machine.SetState(nil)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (state *fileWaitingState) renderControls() {
|
||||||
|
imgui.Separator()
|
||||||
|
if imgui.Button("Browse...") {
|
||||||
|
state.shouldOpenDialog = true
|
||||||
|
}
|
||||||
|
imgui.SameLine()
|
||||||
|
if imgui.Button("Cancel") {
|
||||||
|
state.machine.SetState(nil)
|
||||||
|
imgui.CloseCurrentPopup()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (state *fileWaitingState) openDialog() {
|
||||||
|
dlgBuilder := dialog.File()
|
||||||
|
for _, info := range state.typeInfo {
|
||||||
|
dlgBuilder = dlgBuilder.Filter(info.Title, info.Extensions...)
|
||||||
|
}
|
||||||
|
dlgBuilder = dlgBuilder.Filter("All files (*.*)", "*")
|
||||||
|
|
||||||
|
filename, err := state.runner(dlgBuilder)
|
||||||
|
state.machine.SetState(nil)
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
state.reportFile(filename)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (state *fileWaitingState) reportFile(filename string) {
|
||||||
|
state.problem = ""
|
||||||
|
|
||||||
|
err := state.callback(filename)
|
||||||
|
if err != nil {
|
||||||
|
state.problem = "Previous attempt failed.\nPlease check and try again.\nReason:\n" + err.Error()
|
||||||
|
state.machine.SetState(state)
|
||||||
|
}
|
||||||
|
}
|
||||||
+117
@@ -0,0 +1,117 @@
|
|||||||
|
package external
|
||||||
|
|
||||||
|
import (
|
||||||
|
"image"
|
||||||
|
"image/color"
|
||||||
|
"math"
|
||||||
|
"os"
|
||||||
|
|
||||||
|
"github.com/inkyblackness/hacked/ss1/content/audio"
|
||||||
|
"github.com/inkyblackness/hacked/ss1/content/audio/wav"
|
||||||
|
"github.com/inkyblackness/hacked/ss1/content/bitmap"
|
||||||
|
"github.com/inkyblackness/hacked/ui/gui"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Import starts an import dialog series, calling the given callback with a file name.
|
||||||
|
func Import(machine gui.ModalStateMachine, info string, types []TypeInfo, callback func(string), lastFailed bool) {
|
||||||
|
machine.SetState(&importStartState{
|
||||||
|
machine: machine,
|
||||||
|
callback: callback,
|
||||||
|
info: info,
|
||||||
|
typeInfo: types,
|
||||||
|
withError: lastFailed,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// ImportAudio is a helper to handle audio file import. The callback is called with the loaded audio.
|
||||||
|
func ImportAudio(machine gui.ModalStateMachine, callback func(l8 audio.L8)) {
|
||||||
|
info := "File must be a WAV file, 22050 Hz, 8-bit or 16-bit, uncompressed."
|
||||||
|
types := []TypeInfo{{Title: "Audio files (*.wav)", Extensions: []string{"wav"}}}
|
||||||
|
var fileHandler func(string)
|
||||||
|
|
||||||
|
fileHandler = func(filename string) {
|
||||||
|
reader, err := os.Open(filename)
|
||||||
|
if err != nil {
|
||||||
|
Import(machine, info, types, fileHandler, true)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
defer func() { _ = reader.Close() }()
|
||||||
|
sound, err := wav.Load(reader)
|
||||||
|
if err != nil {
|
||||||
|
Import(machine, info, types, fileHandler, true)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
callback(sound)
|
||||||
|
}
|
||||||
|
|
||||||
|
Import(machine, info, types, fileHandler, false)
|
||||||
|
}
|
||||||
|
|
||||||
|
// ImportImage is a helper to handle image file import. The callback is called with the loaded image.
|
||||||
|
func ImportImage(machine gui.ModalStateMachine, paletteRetriever func() (bitmap.Palette, error), callback func(bitmap.Bitmap)) {
|
||||||
|
info := "File should be either a BMP, GIF, or a PNG file.\nPaletted images matching game palette are taken 1:1,\nothers are mapped closest fitting."
|
||||||
|
types := []TypeInfo{{Title: "Image files (*.bmp, *.gif, *.png)", Extensions: []string{"bmp", "gif", "png"}}}
|
||||||
|
var fileHandler func(string)
|
||||||
|
|
||||||
|
fileHandler = func(filename string) {
|
||||||
|
reader, err := os.Open(filename)
|
||||||
|
if err != nil {
|
||||||
|
Import(machine, "Could not open file.\n"+info, types, fileHandler, true)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
defer func() { _ = reader.Close() }()
|
||||||
|
img, _, err := image.Decode(reader)
|
||||||
|
if err != nil {
|
||||||
|
Import(machine, "File not recognized as image.\n"+info, types, fileHandler, true)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
var bmp bitmap.Bitmap
|
||||||
|
importMapped := true
|
||||||
|
rawPalette, err := paletteRetriever()
|
||||||
|
if err != nil {
|
||||||
|
Import(machine, "Can not import image without having a palette loaded.\n"+info, types, fileHandler, true)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if palettedImg, isPaletted := img.(image.PalettedImage); isPaletted {
|
||||||
|
imgPalette, hasPalette := palettedImg.ColorModel().(color.Palette)
|
||||||
|
if hasPalette && paletteMatches(imgPalette, rawPalette.ColorPalette(false)) {
|
||||||
|
bounds := img.Bounds()
|
||||||
|
|
||||||
|
bmp.Header.Width = int16(math.Max(0, math.Min(float64(bounds.Dx()), math.MaxInt16)))
|
||||||
|
bmp.Header.Height = int16(math.Max(0, math.Min(float64(bounds.Dy()), math.MaxInt16)))
|
||||||
|
bmp.Pixels = make([]byte, int(bmp.Header.Width)*int(bmp.Header.Height))
|
||||||
|
for row := 0; row < int(bmp.Header.Height); row++ {
|
||||||
|
for column := 0; column < int(bmp.Header.Width); column++ {
|
||||||
|
bmp.Pixels[row*int(bmp.Header.Width)+column] = palettedImg.ColorIndexAt(column, row)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
importMapped = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if importMapped {
|
||||||
|
bitmapper := bitmap.NewBitmapper(&rawPalette)
|
||||||
|
bmp = bitmapper.Map(img)
|
||||||
|
}
|
||||||
|
callback(bmp)
|
||||||
|
}
|
||||||
|
|
||||||
|
Import(machine, info, types, fileHandler, false)
|
||||||
|
}
|
||||||
|
|
||||||
|
func paletteMatches(imgPalette color.Palette, rawPalette color.Palette) bool {
|
||||||
|
if len(imgPalette) > len(rawPalette) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
for index, clr := range imgPalette {
|
||||||
|
imgR, imgG, imgB, _ := clr.RGBA()
|
||||||
|
rawR, rawG, rawB, _ := rawPalette[index].RGBA()
|
||||||
|
|
||||||
|
if (imgR != rawR) || (imgG != rawG) || (imgB != rawB) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return true
|
||||||
|
}
|
||||||
@@ -0,0 +1,34 @@
|
|||||||
|
package external
|
||||||
|
|
||||||
|
import (
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/inkyblackness/imgui-go/v3"
|
||||||
|
|
||||||
|
"github.com/inkyblackness/hacked/ui/gui"
|
||||||
|
)
|
||||||
|
|
||||||
|
type importStartState struct {
|
||||||
|
machine gui.ModalStateMachine
|
||||||
|
callback func(string)
|
||||||
|
info string
|
||||||
|
typeInfo []TypeInfo
|
||||||
|
withError bool
|
||||||
|
}
|
||||||
|
|
||||||
|
func (state importStartState) Render() {
|
||||||
|
imgui.OpenPopup("Import")
|
||||||
|
nextState := &importWaitingState{
|
||||||
|
machine: state.machine,
|
||||||
|
callback: state.callback,
|
||||||
|
info: state.info,
|
||||||
|
typeInfo: state.typeInfo,
|
||||||
|
}
|
||||||
|
if state.withError {
|
||||||
|
nextState.failureTime = time.Now()
|
||||||
|
}
|
||||||
|
state.machine.SetState(nextState)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (state importStartState) HandleFiles(names []string) {
|
||||||
|
}
|
||||||
@@ -0,0 +1,83 @@
|
|||||||
|
package external
|
||||||
|
|
||||||
|
import (
|
||||||
|
"os"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/inkyblackness/imgui-go/v3"
|
||||||
|
"github.com/sqweek/dialog"
|
||||||
|
|
||||||
|
"github.com/inkyblackness/hacked/ui/gui"
|
||||||
|
)
|
||||||
|
|
||||||
|
type importWaitingState struct {
|
||||||
|
machine gui.ModalStateMachine
|
||||||
|
callback func(string)
|
||||||
|
info string
|
||||||
|
typeInfo []TypeInfo
|
||||||
|
|
||||||
|
failureTime time.Time
|
||||||
|
}
|
||||||
|
|
||||||
|
func (state *importWaitingState) Render() {
|
||||||
|
if imgui.BeginPopupModalV("Import", nil,
|
||||||
|
imgui.WindowFlagsNoResize|imgui.WindowFlagsNoMove|imgui.WindowFlagsNoSavedSettings|imgui.WindowFlagsAlwaysAutoResize) {
|
||||||
|
imgui.Text("Waiting for file.")
|
||||||
|
if !state.failureTime.IsZero() {
|
||||||
|
imgui.PushStyleColor(imgui.StyleColorText, imgui.Vec4{X: 1, Y: 0, Z: 0, W: 1})
|
||||||
|
imgui.Text("Previous attempt failed, could not load file.\nPlease check and try again.")
|
||||||
|
imgui.PopStyleColor()
|
||||||
|
if time.Since(state.failureTime).Seconds() > 5 {
|
||||||
|
state.failureTime = time.Time{}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
imgui.Text(`From your file browser drag'n'drop the file
|
||||||
|
that shall be loaded into the editor window.
|
||||||
|
`)
|
||||||
|
imgui.Text(state.info)
|
||||||
|
imgui.Separator()
|
||||||
|
if imgui.Button("Browse...") {
|
||||||
|
dlgBuilder := dialog.File()
|
||||||
|
for _, info := range state.typeInfo {
|
||||||
|
dlgBuilder = dlgBuilder.Filter(info.Title, info.Extensions...)
|
||||||
|
}
|
||||||
|
dlgBuilder = dlgBuilder.Filter("All files (*.*)", "*")
|
||||||
|
filename, err := dlgBuilder.Load()
|
||||||
|
if err == nil {
|
||||||
|
state.HandleFiles([]string{filename})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
imgui.SameLine()
|
||||||
|
if imgui.Button("Cancel") {
|
||||||
|
state.machine.SetState(nil)
|
||||||
|
imgui.CloseCurrentPopup()
|
||||||
|
}
|
||||||
|
imgui.EndPopup()
|
||||||
|
} else {
|
||||||
|
state.machine.SetState(nil)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (state *importWaitingState) HandleFiles(names []string) {
|
||||||
|
filename, ok := state.verifyFile(names)
|
||||||
|
if ok {
|
||||||
|
state.machine.SetState(nil)
|
||||||
|
state.callback(filename)
|
||||||
|
} else {
|
||||||
|
state.failureTime = time.Now()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (state importWaitingState) verifyFile(names []string) (string, bool) {
|
||||||
|
if len(names) != 1 {
|
||||||
|
return "", false
|
||||||
|
}
|
||||||
|
fileInfo, err := os.Stat(names[0])
|
||||||
|
if err != nil {
|
||||||
|
return "", false
|
||||||
|
}
|
||||||
|
if fileInfo.IsDir() {
|
||||||
|
return "", false
|
||||||
|
}
|
||||||
|
return names[0], true
|
||||||
|
}
|
||||||
+7
@@ -0,0 +1,7 @@
|
|||||||
|
package external
|
||||||
|
|
||||||
|
// TypeInfo describes a file.
|
||||||
|
type TypeInfo struct {
|
||||||
|
Title string
|
||||||
|
Extensions []string
|
||||||
|
}
|
||||||
@@ -0,0 +1,91 @@
|
|||||||
|
package graphics
|
||||||
|
|
||||||
|
import "github.com/inkyblackness/hacked/ui/opengl"
|
||||||
|
|
||||||
|
// BitmapTexture wraps an OpenGL handle for a downloaded image.
|
||||||
|
type BitmapTexture struct {
|
||||||
|
gl opengl.OpenGL
|
||||||
|
handle uint32
|
||||||
|
|
||||||
|
width, height float32
|
||||||
|
u, v float32
|
||||||
|
|
||||||
|
pixelData []byte
|
||||||
|
}
|
||||||
|
|
||||||
|
func powerOfTwo(value int) int {
|
||||||
|
result := 2
|
||||||
|
|
||||||
|
for (result < value) && (result < 0x1000) {
|
||||||
|
result *= 2
|
||||||
|
}
|
||||||
|
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewBitmapTexture downloads the provided raw data to OpenGL and returns a BitmapTexture instance.
|
||||||
|
func NewBitmapTexture(gl opengl.OpenGL, width, height int, pixelData []byte) *BitmapTexture {
|
||||||
|
textureWidth := powerOfTwo(width)
|
||||||
|
textureHeight := powerOfTwo(height)
|
||||||
|
tex := &BitmapTexture{
|
||||||
|
gl: gl,
|
||||||
|
handle: gl.GenTextures(1)[0],
|
||||||
|
|
||||||
|
width: float32(width),
|
||||||
|
height: float32(height),
|
||||||
|
|
||||||
|
pixelData: pixelData,
|
||||||
|
}
|
||||||
|
tex.u = tex.width / float32(textureWidth)
|
||||||
|
tex.v = tex.height / float32(textureHeight)
|
||||||
|
|
||||||
|
paddedData := make([]byte, textureWidth*textureHeight)
|
||||||
|
for y := 0; y < height; y++ {
|
||||||
|
inStart := y * width
|
||||||
|
outOffset := y * textureWidth
|
||||||
|
for x := 0; x < width; x++ {
|
||||||
|
value := pixelData[inStart+x]
|
||||||
|
paddedData[outOffset] = value
|
||||||
|
outOffset++
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
gl.BindTexture(opengl.TEXTURE_2D, tex.handle)
|
||||||
|
gl.TexImage2D(opengl.TEXTURE_2D, 0, opengl.RED, int32(textureWidth), int32(textureHeight),
|
||||||
|
0, opengl.RED, opengl.UNSIGNED_BYTE, paddedData)
|
||||||
|
gl.TexParameteri(opengl.TEXTURE_2D, opengl.TEXTURE_MAG_FILTER, opengl.NEAREST)
|
||||||
|
gl.TexParameteri(opengl.TEXTURE_2D, opengl.TEXTURE_MIN_FILTER, opengl.NEAREST)
|
||||||
|
gl.GenerateMipmap(opengl.TEXTURE_2D)
|
||||||
|
gl.BindTexture(opengl.TEXTURE_2D, 0)
|
||||||
|
|
||||||
|
return tex
|
||||||
|
}
|
||||||
|
|
||||||
|
// Dispose releases the OpenGL texture.
|
||||||
|
func (tex *BitmapTexture) Dispose() {
|
||||||
|
if tex.handle != 0 {
|
||||||
|
tex.gl.DeleteTextures([]uint32{tex.handle})
|
||||||
|
tex.handle = 0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Handle returns the texture handle.
|
||||||
|
func (tex *BitmapTexture) Handle() uint32 {
|
||||||
|
return tex.handle
|
||||||
|
}
|
||||||
|
|
||||||
|
// PixelData returns the original paletted pixel data.
|
||||||
|
func (tex *BitmapTexture) PixelData() []byte {
|
||||||
|
return tex.pixelData
|
||||||
|
}
|
||||||
|
|
||||||
|
// Size returns the dimensions of the bitmap, in pixels.
|
||||||
|
func (tex *BitmapTexture) Size() (width, height float32) {
|
||||||
|
return tex.width, tex.height
|
||||||
|
}
|
||||||
|
|
||||||
|
// UV returns the maximum U and V values for the bitmap. The bitmap will be
|
||||||
|
// stored in a power-of-two texture, which may be larger than the bitmap.
|
||||||
|
func (tex *BitmapTexture) UV() (u, v float32) {
|
||||||
|
return tex.u, tex.v
|
||||||
|
}
|
||||||
@@ -0,0 +1,76 @@
|
|||||||
|
package graphics
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/inkyblackness/hacked/ss1/content/bitmap"
|
||||||
|
"github.com/inkyblackness/hacked/ui/opengl"
|
||||||
|
)
|
||||||
|
|
||||||
|
// FrameCacheKey is used to identify a cached frame.
|
||||||
|
type FrameCacheKey uint16
|
||||||
|
|
||||||
|
// FrameCache loads bitmaps with their palettes and provides OpenGL textures.
|
||||||
|
type FrameCache struct {
|
||||||
|
gl opengl.OpenGL
|
||||||
|
|
||||||
|
textures map[FrameCacheKey]*BitmapTexture
|
||||||
|
palettes map[FrameCacheKey]*PaletteTexture
|
||||||
|
|
||||||
|
keyCounter uint16
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewFrameCache returns a new instance.
|
||||||
|
func NewFrameCache(gl opengl.OpenGL) *FrameCache {
|
||||||
|
cache := &FrameCache{
|
||||||
|
gl: gl,
|
||||||
|
textures: make(map[FrameCacheKey]*BitmapTexture),
|
||||||
|
palettes: make(map[FrameCacheKey]*PaletteTexture),
|
||||||
|
}
|
||||||
|
return cache
|
||||||
|
}
|
||||||
|
|
||||||
|
// AllocateKey returns a new key to be used with uploading a new frame.
|
||||||
|
func (cache *FrameCache) AllocateKey() FrameCacheKey {
|
||||||
|
cache.keyCounter++
|
||||||
|
return FrameCacheKey(cache.keyCounter)
|
||||||
|
}
|
||||||
|
|
||||||
|
// SetTexture registers a texture based on given bitmap under given key.
|
||||||
|
// The bitmap should contain a palette, otherwise it will most likely not be displayed.
|
||||||
|
func (cache *FrameCache) SetTexture(key FrameCacheKey, width, height uint16, pixels []byte, palette *bitmap.Palette) {
|
||||||
|
cache.DropTextureForKey(key)
|
||||||
|
tex := NewBitmapTexture(cache.gl, int(width), int(height), pixels)
|
||||||
|
cache.textures[key] = tex
|
||||||
|
if palette == nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
pal := NewPaletteTexture(cache.gl, palette)
|
||||||
|
cache.palettes[key] = pal
|
||||||
|
}
|
||||||
|
|
||||||
|
// DropTextureForKey removes the currently cached frame.
|
||||||
|
func (cache *FrameCache) DropTextureForKey(key FrameCacheKey) {
|
||||||
|
if tex, existing := cache.textures[key]; existing {
|
||||||
|
tex.Dispose()
|
||||||
|
delete(cache.textures, key)
|
||||||
|
}
|
||||||
|
if pal, existing := cache.palettes[key]; existing {
|
||||||
|
pal.Dispose()
|
||||||
|
delete(cache.palettes, key)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// HandlesForKey returns the OpenGL handles for both palette and texture for given key.
|
||||||
|
func (cache FrameCache) HandlesForKey(key FrameCacheKey) (palette uint32, texture uint32) {
|
||||||
|
if tex, existing := cache.textures[key]; existing {
|
||||||
|
texture = tex.Handle()
|
||||||
|
}
|
||||||
|
if pal, existing := cache.palettes[key]; existing {
|
||||||
|
palette = pal.Handle()
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Texture returns the buffered texture for given key.
|
||||||
|
func (cache *FrameCache) Texture(key FrameCacheKey) *BitmapTexture {
|
||||||
|
return cache.textures[key]
|
||||||
|
}
|
||||||
@@ -0,0 +1,71 @@
|
|||||||
|
package graphics
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/binary"
|
||||||
|
|
||||||
|
"github.com/inkyblackness/hacked/ss1/content/bitmap"
|
||||||
|
"github.com/inkyblackness/hacked/ss1/resource"
|
||||||
|
"github.com/inkyblackness/hacked/ss1/world/ids"
|
||||||
|
"github.com/inkyblackness/hacked/ui/opengl"
|
||||||
|
)
|
||||||
|
|
||||||
|
// PaletteCache loads palettes and provides OpenGL textures.
|
||||||
|
type PaletteCache struct {
|
||||||
|
gl opengl.OpenGL
|
||||||
|
localizer resource.Localizer
|
||||||
|
|
||||||
|
palettes map[resource.Key]*PaletteTexture
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewPaletteCache returns a new instance.
|
||||||
|
func NewPaletteCache(gl opengl.OpenGL, localizer resource.Localizer) *PaletteCache {
|
||||||
|
cache := &PaletteCache{
|
||||||
|
gl: gl,
|
||||||
|
localizer: localizer,
|
||||||
|
palettes: make(map[resource.Key]*PaletteTexture),
|
||||||
|
}
|
||||||
|
return cache
|
||||||
|
}
|
||||||
|
|
||||||
|
// InvalidateResources lets the cache remove any palette from resources that are specified in the given slice.
|
||||||
|
func (cache *PaletteCache) InvalidateResources(ids []resource.ID) {
|
||||||
|
for _, id := range ids {
|
||||||
|
for key, texture := range cache.palettes {
|
||||||
|
if key.ID == id {
|
||||||
|
texture.Dispose()
|
||||||
|
delete(cache.palettes, key)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Palette returns the palette with given index - if available.
|
||||||
|
func (cache *PaletteCache) Palette(index int) (*PaletteTexture, error) {
|
||||||
|
key := resource.KeyOf(ids.GamePalettesStart.Plus(index), resource.LangAny, 0)
|
||||||
|
pal, existing := cache.palettes[key]
|
||||||
|
if existing {
|
||||||
|
return pal, nil
|
||||||
|
}
|
||||||
|
selector := cache.localizer.LocalizedResources(key.Lang)
|
||||||
|
view, err := selector.Select(key.ID)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if view.ContentType() != resource.Palette {
|
||||||
|
return nil, resource.ErrWrongType(key, resource.Palette)
|
||||||
|
}
|
||||||
|
reader, err := view.Block(key.Index)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
var palette bitmap.Palette
|
||||||
|
err = binary.Read(reader, binary.LittleEndian, &palette)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
pal = NewPaletteTexture(cache.gl, &palette)
|
||||||
|
cache.palettes[key] = pal
|
||||||
|
|
||||||
|
return pal, nil
|
||||||
|
}
|
||||||
@@ -0,0 +1,70 @@
|
|||||||
|
package graphics
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/inkyblackness/hacked/ss1/content/bitmap"
|
||||||
|
"github.com/inkyblackness/hacked/ui/opengl"
|
||||||
|
)
|
||||||
|
|
||||||
|
// PaletteTexture contains a palette stored as OpenGL texture.
|
||||||
|
type PaletteTexture struct {
|
||||||
|
gl opengl.OpenGL
|
||||||
|
|
||||||
|
handle uint32
|
||||||
|
palette bitmap.Palette
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewPaletteTexture creates a new PaletteTexture instance.
|
||||||
|
func NewPaletteTexture(gl opengl.OpenGL, palette *bitmap.Palette) *PaletteTexture {
|
||||||
|
tex := &PaletteTexture{
|
||||||
|
gl: gl,
|
||||||
|
handle: gl.GenTextures(1)[0],
|
||||||
|
}
|
||||||
|
|
||||||
|
tex.Update(palette)
|
||||||
|
|
||||||
|
return tex
|
||||||
|
}
|
||||||
|
|
||||||
|
// Dispose releases the OpenGL texture.
|
||||||
|
func (tex *PaletteTexture) Dispose() {
|
||||||
|
if tex.handle != 0 {
|
||||||
|
tex.gl.DeleteTextures([]uint32{tex.handle})
|
||||||
|
tex.handle = 0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Palette returns the original palette.
|
||||||
|
func (tex *PaletteTexture) Palette() bitmap.Palette {
|
||||||
|
return tex.palette
|
||||||
|
}
|
||||||
|
|
||||||
|
// Handle returns the texture handle.
|
||||||
|
func (tex *PaletteTexture) Handle() uint32 {
|
||||||
|
return tex.handle
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update reloads the palette.
|
||||||
|
func (tex *PaletteTexture) Update(palette *bitmap.Palette) {
|
||||||
|
gl := tex.gl
|
||||||
|
const bytesPerRGBA = 4
|
||||||
|
const colors = 256
|
||||||
|
var data [colors * bytesPerRGBA]byte
|
||||||
|
|
||||||
|
for i := 0; i < colors; i++ {
|
||||||
|
entry := palette[i]
|
||||||
|
data[i*bytesPerRGBA+0] = entry.Red
|
||||||
|
data[i*bytesPerRGBA+1] = entry.Green
|
||||||
|
data[i*bytesPerRGBA+2] = entry.Blue
|
||||||
|
data[i*bytesPerRGBA+3] = 0xFF
|
||||||
|
}
|
||||||
|
data[3] = 0x00
|
||||||
|
|
||||||
|
gl.BindTexture(opengl.TEXTURE_2D, tex.handle)
|
||||||
|
gl.TexImage2D(opengl.TEXTURE_2D, 0, opengl.RGBA, int32(colors), 1, 0, opengl.RGBA, opengl.UNSIGNED_BYTE, data[:])
|
||||||
|
gl.TexParameteri(opengl.TEXTURE_2D, opengl.TEXTURE_MAG_FILTER, opengl.NEAREST)
|
||||||
|
gl.TexParameteri(opengl.TEXTURE_2D, opengl.TEXTURE_MIN_FILTER, opengl.NEAREST)
|
||||||
|
gl.GenerateMipmap(opengl.TEXTURE_2D)
|
||||||
|
gl.BindTexture(opengl.TEXTURE_2D, 0)
|
||||||
|
|
||||||
|
tex.palette = *palette
|
||||||
|
}
|
||||||
@@ -0,0 +1,94 @@
|
|||||||
|
package graphics
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/inkyblackness/hacked/ss1"
|
||||||
|
"github.com/inkyblackness/hacked/ss1/content/bitmap"
|
||||||
|
"github.com/inkyblackness/hacked/ss1/resource"
|
||||||
|
"github.com/inkyblackness/hacked/ui/opengl"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
errReferenceNotFound ss1.StringError = "reference not existing"
|
||||||
|
errReferenceWrongDimensions ss1.StringError = "reference has wrong dimensions"
|
||||||
|
)
|
||||||
|
|
||||||
|
// TextureCache loads bitmaps and provides OpenGL textures.
|
||||||
|
type TextureCache struct {
|
||||||
|
gl opengl.OpenGL
|
||||||
|
localizer resource.Localizer
|
||||||
|
|
||||||
|
textures map[resource.Key]*BitmapTexture
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewTextureCache returns a new instance.
|
||||||
|
func NewTextureCache(gl opengl.OpenGL, localizer resource.Localizer) *TextureCache {
|
||||||
|
cache := &TextureCache{
|
||||||
|
gl: gl,
|
||||||
|
localizer: localizer,
|
||||||
|
textures: make(map[resource.Key]*BitmapTexture),
|
||||||
|
}
|
||||||
|
return cache
|
||||||
|
}
|
||||||
|
|
||||||
|
// InvalidateResources lets the cache remove any textures from resources that are specified in the given slice.
|
||||||
|
func (cache *TextureCache) InvalidateResources(ids []resource.ID) {
|
||||||
|
for _, id := range ids {
|
||||||
|
for key, texture := range cache.textures {
|
||||||
|
if key.ID == id {
|
||||||
|
texture.Dispose()
|
||||||
|
delete(cache.textures, key)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Texture returns the texture with given key - if available.
|
||||||
|
func (cache *TextureCache) Texture(key resource.Key) (*BitmapTexture, error) {
|
||||||
|
return cache.TextureReferenced(key, nil)
|
||||||
|
}
|
||||||
|
|
||||||
|
// TextureReferenced returns the texture with given key - if available.
|
||||||
|
// Should the underlying bitmap have to be loaded, then the given reference is taken as a basis for compressed bitmaps.
|
||||||
|
// If reference is nil, then no reference is used.
|
||||||
|
func (cache *TextureCache) TextureReferenced(key resource.Key, reference *resource.Key) (*BitmapTexture, error) {
|
||||||
|
tex, existing := cache.textures[key]
|
||||||
|
if existing {
|
||||||
|
return tex, nil
|
||||||
|
}
|
||||||
|
selector := cache.localizer.LocalizedResources(key.Lang)
|
||||||
|
view, err := selector.Select(key.ID)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if view.ContentType() != resource.Bitmap {
|
||||||
|
return nil, resource.ErrWrongType(key, resource.Bitmap)
|
||||||
|
}
|
||||||
|
reader, err := view.Block(key.Index)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
bmp, err := bitmap.DecodeReferenced(reader, func(width, height int16) ([]byte, error) {
|
||||||
|
buf := make([]byte, int(width)*int(height))
|
||||||
|
if reference == nil {
|
||||||
|
return buf, nil
|
||||||
|
}
|
||||||
|
refTex, refExisting := cache.textures[*reference]
|
||||||
|
if !refExisting {
|
||||||
|
return nil, errReferenceNotFound
|
||||||
|
}
|
||||||
|
refWidth, refHeight := refTex.Size()
|
||||||
|
if (int16(refWidth) != width) || (int16(refHeight) != height) {
|
||||||
|
return nil, errReferenceWrongDimensions
|
||||||
|
}
|
||||||
|
copy(buf, refTex.PixelData())
|
||||||
|
return buf, nil
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
tex = NewBitmapTexture(cache.gl, int(bmp.Header.Width), int(bmp.Header.Height), bmp.Pixels)
|
||||||
|
cache.textures[key] = tex
|
||||||
|
|
||||||
|
return tex, nil
|
||||||
|
}
|
||||||
@@ -0,0 +1,162 @@
|
|||||||
|
package levels
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/inkyblackness/hacked/editor/render"
|
||||||
|
"github.com/inkyblackness/hacked/ss1/content/archive/level"
|
||||||
|
"github.com/inkyblackness/hacked/ui/opengl"
|
||||||
|
)
|
||||||
|
|
||||||
|
var gridVertexShaderSource = `
|
||||||
|
#version 150
|
||||||
|
precision mediump float;
|
||||||
|
|
||||||
|
in vec3 vertexPosition;
|
||||||
|
|
||||||
|
uniform mat4 viewMatrix;
|
||||||
|
uniform mat4 projectionMatrix;
|
||||||
|
|
||||||
|
out vec4 gridColor;
|
||||||
|
out vec3 originalPosition;
|
||||||
|
|
||||||
|
void main(void) {
|
||||||
|
gl_Position = projectionMatrix * viewMatrix * vec4(vertexPosition, 1.0);
|
||||||
|
|
||||||
|
gridColor = vec4(0.0, 0.1, 0.0, 0.6);
|
||||||
|
originalPosition = vertexPosition;
|
||||||
|
}
|
||||||
|
`
|
||||||
|
|
||||||
|
var gridFragmentShaderSource = `
|
||||||
|
#version 150
|
||||||
|
precision mediump float;
|
||||||
|
|
||||||
|
uniform vec4 gridSize;
|
||||||
|
|
||||||
|
in vec4 gridColor;
|
||||||
|
in vec3 originalPosition;
|
||||||
|
|
||||||
|
out vec4 fragColor;
|
||||||
|
|
||||||
|
float modulo(float x, float y) {
|
||||||
|
return x - y * floor(x/y);
|
||||||
|
}
|
||||||
|
|
||||||
|
float nearGrid(float stepSize, float value) {
|
||||||
|
float remainder = modulo(value - (stepSize / 2.0), stepSize) * 2.0;
|
||||||
|
|
||||||
|
if (remainder >= stepSize) {
|
||||||
|
remainder = (stepSize * 2.0) - remainder;
|
||||||
|
}
|
||||||
|
|
||||||
|
return remainder / stepSize;
|
||||||
|
}
|
||||||
|
|
||||||
|
void main(void) {
|
||||||
|
float alphaX = nearGrid(256.0, originalPosition.x);
|
||||||
|
float alphaY = nearGrid(256.0, originalPosition.y);
|
||||||
|
bool beyondX = (originalPosition.x / 256.0) >= gridSize.x || (originalPosition.x < 0.0);
|
||||||
|
bool beyondY = (originalPosition.y / 256.0) >= gridSize.y || (originalPosition.y < 0.0);
|
||||||
|
float alpha = 0.0;
|
||||||
|
|
||||||
|
if (!beyondX && !beyondY) {
|
||||||
|
alpha = max(alphaX, alphaY);
|
||||||
|
} else if (beyondX && !beyondY) {
|
||||||
|
alpha = alphaX;
|
||||||
|
} else if (beyondY && !beyondX) {
|
||||||
|
alpha = alphaY;
|
||||||
|
} else {
|
||||||
|
alpha = min(alphaX, alphaY);
|
||||||
|
}
|
||||||
|
|
||||||
|
alpha = pow(2.0, 10.0 * (alpha - 1.0));
|
||||||
|
|
||||||
|
fragColor = vec4(gridColor.rgb, gridColor.a * alpha);
|
||||||
|
}
|
||||||
|
`
|
||||||
|
|
||||||
|
// BackgroundGrid renders a grid with transparent holes.
|
||||||
|
type BackgroundGrid struct {
|
||||||
|
context *render.Context
|
||||||
|
|
||||||
|
program uint32
|
||||||
|
vao *opengl.VertexArrayObject
|
||||||
|
vertexPositionBuffer uint32
|
||||||
|
vertexPositionAttrib int32
|
||||||
|
gridSizeUniform opengl.Vector4Uniform
|
||||||
|
viewMatrixUniform opengl.Matrix4Uniform
|
||||||
|
projectionMatrixUniform opengl.Matrix4Uniform
|
||||||
|
|
||||||
|
lastReportedColumns int
|
||||||
|
lastReportedRows int
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewBackgroundGrid returns a new instance of BackgroundGrid.
|
||||||
|
func NewBackgroundGrid(context *render.Context) *BackgroundGrid {
|
||||||
|
gl := context.OpenGL
|
||||||
|
program, programErr := opengl.LinkNewStandardProgram(gl, gridVertexShaderSource, gridFragmentShaderSource)
|
||||||
|
if programErr != nil {
|
||||||
|
panic(opengl.NamedShaderError{Name: "BackgroundGridShader", Nested: programErr})
|
||||||
|
}
|
||||||
|
grid := &BackgroundGrid{
|
||||||
|
context: context,
|
||||||
|
program: program,
|
||||||
|
vao: opengl.NewVertexArrayObject(gl, program),
|
||||||
|
vertexPositionBuffer: gl.GenBuffers(1)[0],
|
||||||
|
vertexPositionAttrib: gl.GetAttribLocation(program, "vertexPosition"),
|
||||||
|
viewMatrixUniform: opengl.Matrix4Uniform(gl.GetUniformLocation(program, "viewMatrix")),
|
||||||
|
projectionMatrixUniform: opengl.Matrix4Uniform(gl.GetUniformLocation(program, "projectionMatrix")),
|
||||||
|
gridSizeUniform: opengl.Vector4Uniform(gl.GetUniformLocation(program, "gridSize")),
|
||||||
|
}
|
||||||
|
|
||||||
|
return grid
|
||||||
|
}
|
||||||
|
|
||||||
|
// Render renders the grid.
|
||||||
|
func (grid *BackgroundGrid) Render(columns, rows int) {
|
||||||
|
gl := grid.context.OpenGL
|
||||||
|
|
||||||
|
grid.setGridSize(columns, rows)
|
||||||
|
|
||||||
|
grid.vao.OnShader(func() {
|
||||||
|
grid.viewMatrixUniform.Set(gl, grid.context.ViewMatrix)
|
||||||
|
grid.projectionMatrixUniform.Set(gl, &grid.context.ProjectionMatrix)
|
||||||
|
grid.gridSizeUniform.Set(gl, &[4]float32{float32(columns), float32(rows), 0, 0})
|
||||||
|
|
||||||
|
gl.DrawArrays(opengl.TRIANGLES, 0, 6)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func (grid *BackgroundGrid) setGridSize(columns, rows int) {
|
||||||
|
if (grid.lastReportedColumns == columns) && (grid.lastReportedRows == rows) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
grid.lastReportedColumns = columns
|
||||||
|
grid.lastReportedRows = rows
|
||||||
|
|
||||||
|
gl := grid.context.OpenGL
|
||||||
|
{
|
||||||
|
hTiles := float32(columns)
|
||||||
|
vTiles := float32(rows)
|
||||||
|
|
||||||
|
gl.BindBuffer(opengl.ARRAY_BUFFER, grid.vertexPositionBuffer)
|
||||||
|
fineHalf := level.FineCoordinatesPerTileSide / float32(2.0)
|
||||||
|
hLimit := level.FineCoordinatesPerTileSide*hTiles + fineHalf
|
||||||
|
vLimit := level.FineCoordinatesPerTileSide*vTiles + fineHalf
|
||||||
|
var vertices = []float32{
|
||||||
|
-fineHalf, -fineHalf, 0.0,
|
||||||
|
hLimit, -fineHalf, 0.0,
|
||||||
|
hLimit, vLimit, 0.0,
|
||||||
|
|
||||||
|
hLimit, vLimit, 0.0,
|
||||||
|
-fineHalf, vLimit, 0.0,
|
||||||
|
-fineHalf, -fineHalf, 0.0}
|
||||||
|
gl.BufferData(opengl.ARRAY_BUFFER, len(vertices)*4, vertices, opengl.STATIC_DRAW)
|
||||||
|
gl.BindBuffer(opengl.ARRAY_BUFFER, 0)
|
||||||
|
}
|
||||||
|
grid.vao.WithSetter(func(gl opengl.OpenGL) {
|
||||||
|
gl.EnableVertexAttribArray(uint32(grid.vertexPositionAttrib))
|
||||||
|
gl.BindBuffer(opengl.ARRAY_BUFFER, grid.vertexPositionBuffer)
|
||||||
|
gl.VertexAttribOffset(uint32(grid.vertexPositionAttrib), 3, opengl.FLOAT, false, 0, 0)
|
||||||
|
gl.BindBuffer(opengl.ARRAY_BUFFER, 0)
|
||||||
|
})
|
||||||
|
}
|
||||||
@@ -0,0 +1,34 @@
|
|||||||
|
package levels
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
)
|
||||||
|
|
||||||
|
// ColorDisplay is an enumeration to how a tile should be colored.
|
||||||
|
type ColorDisplay int
|
||||||
|
|
||||||
|
// ColorDisplay constants are listed below.
|
||||||
|
const (
|
||||||
|
ColorDisplayNone ColorDisplay = 0
|
||||||
|
ColorDisplayFloor ColorDisplay = 1
|
||||||
|
ColorDisplayCeiling ColorDisplay = 2
|
||||||
|
)
|
||||||
|
|
||||||
|
// String returns a textual representation.
|
||||||
|
func (display ColorDisplay) String() string {
|
||||||
|
switch display {
|
||||||
|
case ColorDisplayNone:
|
||||||
|
return "None"
|
||||||
|
case ColorDisplayFloor:
|
||||||
|
return "Floor"
|
||||||
|
case ColorDisplayCeiling:
|
||||||
|
return "Ceiling"
|
||||||
|
default:
|
||||||
|
return fmt.Sprintf("Unknown%d", int(display))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ColorDisplays returns all ColorDisplay constants.
|
||||||
|
func ColorDisplays() []ColorDisplay {
|
||||||
|
return []ColorDisplay{ColorDisplayNone, ColorDisplayFloor, ColorDisplayCeiling}
|
||||||
|
}
|
||||||
@@ -0,0 +1,6 @@
|
|||||||
|
package levels
|
||||||
|
|
||||||
|
const (
|
||||||
|
hintUnknown = "???"
|
||||||
|
hintReadOnly = " (read-only)"
|
||||||
|
)
|
||||||
@@ -0,0 +1,409 @@
|
|||||||
|
package levels
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
|
||||||
|
"github.com/inkyblackness/imgui-go/v3"
|
||||||
|
|
||||||
|
"github.com/inkyblackness/hacked/editor/graphics"
|
||||||
|
"github.com/inkyblackness/hacked/editor/render"
|
||||||
|
"github.com/inkyblackness/hacked/ss1/content/archive"
|
||||||
|
"github.com/inkyblackness/hacked/ss1/content/archive/level"
|
||||||
|
"github.com/inkyblackness/hacked/ss1/content/text"
|
||||||
|
"github.com/inkyblackness/hacked/ss1/edit"
|
||||||
|
"github.com/inkyblackness/hacked/ss1/edit/undoable/cmd"
|
||||||
|
"github.com/inkyblackness/hacked/ss1/resource"
|
||||||
|
"github.com/inkyblackness/hacked/ss1/world"
|
||||||
|
"github.com/inkyblackness/hacked/ss1/world/ids"
|
||||||
|
"github.com/inkyblackness/hacked/ui/gui"
|
||||||
|
)
|
||||||
|
|
||||||
|
// ControlView is the core view for level editing.
|
||||||
|
type ControlView struct {
|
||||||
|
levels *edit.EditableLevels
|
||||||
|
levelSelection *edit.LevelSelectionService
|
||||||
|
editor *edit.LevelEditorService
|
||||||
|
|
||||||
|
guiScale float32
|
||||||
|
registry cmd.Registry
|
||||||
|
|
||||||
|
textCache *text.Cache
|
||||||
|
textureCache *graphics.TextureCache
|
||||||
|
|
||||||
|
model controlViewModel
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewControlView returns a new instance.
|
||||||
|
func NewControlView(levels *edit.EditableLevels, levelSelection *edit.LevelSelectionService, editor *edit.LevelEditorService,
|
||||||
|
guiScale float32, textCache *text.Cache, textureCache *graphics.TextureCache,
|
||||||
|
registry cmd.Registry) *ControlView {
|
||||||
|
view := &ControlView{
|
||||||
|
levels: levels,
|
||||||
|
levelSelection: levelSelection,
|
||||||
|
editor: editor,
|
||||||
|
|
||||||
|
guiScale: guiScale,
|
||||||
|
registry: registry,
|
||||||
|
textCache: textCache,
|
||||||
|
textureCache: textureCache,
|
||||||
|
model: freshControlViewModel(),
|
||||||
|
}
|
||||||
|
return view
|
||||||
|
}
|
||||||
|
|
||||||
|
// WindowOpen returns the flag address, to be used with the main menu.
|
||||||
|
func (view *ControlView) WindowOpen() *bool {
|
||||||
|
return &view.model.windowOpen
|
||||||
|
}
|
||||||
|
|
||||||
|
// Render renders the view.
|
||||||
|
func (view *ControlView) Render() {
|
||||||
|
if view.model.restoreFocus {
|
||||||
|
imgui.SetNextWindowFocus()
|
||||||
|
view.model.restoreFocus = false
|
||||||
|
view.model.windowOpen = true
|
||||||
|
}
|
||||||
|
if view.model.windowOpen {
|
||||||
|
imgui.SetNextWindowSizeV(imgui.Vec2{X: 400 * view.guiScale, Y: 300 * view.guiScale}, imgui.ConditionFirstUseEver)
|
||||||
|
title := "Level Control"
|
||||||
|
readOnly := view.editor.IsReadOnly()
|
||||||
|
if readOnly {
|
||||||
|
title += hintReadOnly
|
||||||
|
}
|
||||||
|
if imgui.BeginV(title+"###Level Control", view.WindowOpen(), imgui.WindowFlagsNoCollapse) {
|
||||||
|
view.renderContent(view.editor.Level(), readOnly)
|
||||||
|
}
|
||||||
|
imgui.End()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var levelHeights = []string{
|
||||||
|
"32 Tiles",
|
||||||
|
"16 Tiles",
|
||||||
|
"8 Tiles",
|
||||||
|
"4 Tiles",
|
||||||
|
"2 Tiles",
|
||||||
|
"1 Tile",
|
||||||
|
"1/2 Tile",
|
||||||
|
"1/4 Tile",
|
||||||
|
}
|
||||||
|
|
||||||
|
func (view *ControlView) renderContent(lvl *level.Level, readOnly bool) {
|
||||||
|
imgui.PushItemWidth(-200 * view.guiScale)
|
||||||
|
selectedLevel := view.levelSelection.CurrentLevelID()
|
||||||
|
if gui.StepSliderInt("Active Level", &selectedLevel, 0, archive.MaxLevels-1) {
|
||||||
|
view.levelSelection.SetCurrentLevelID(selectedLevel)
|
||||||
|
}
|
||||||
|
imgui.Separator()
|
||||||
|
levelType := "Real World"
|
||||||
|
if lvl.IsCyberspace() {
|
||||||
|
levelType = "Cyberspace"
|
||||||
|
}
|
||||||
|
imgui.LabelText("Type", levelType)
|
||||||
|
view.renderLevelHeight(lvl, readOnly)
|
||||||
|
|
||||||
|
if !lvl.IsCyberspace() {
|
||||||
|
view.renderTextureAtlas(lvl, readOnly)
|
||||||
|
view.renderSurveillanceObjects(lvl, readOnly)
|
||||||
|
view.renderHazards(lvl, readOnly)
|
||||||
|
view.renderTextureAnimations(lvl, readOnly)
|
||||||
|
}
|
||||||
|
|
||||||
|
imgui.PopItemWidth()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (view *ControlView) renderLevelHeight(lvl *level.Level, readOnly bool) {
|
||||||
|
_, _, currentShift := lvl.Size()
|
||||||
|
if readOnly {
|
||||||
|
imgui.LabelText("Level Height", levelHeights[currentShift])
|
||||||
|
} else if imgui.BeginCombo("Level Height", levelHeights[currentShift]) {
|
||||||
|
for shift, height := range levelHeights {
|
||||||
|
if imgui.SelectableV(height, shift == int(currentShift), 0, imgui.Vec2{}) {
|
||||||
|
view.requestSetZShift(lvl, shift)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
imgui.EndCombo()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (view *ControlView) textureName(index int) string {
|
||||||
|
key := resource.KeyOf(ids.TextureNames, resource.LangDefault, index)
|
||||||
|
name, err := view.textCache.Text(key)
|
||||||
|
suffix := ""
|
||||||
|
if err == nil {
|
||||||
|
suffix = ": " + name
|
||||||
|
}
|
||||||
|
return fmt.Sprintf("%3d", index) + suffix
|
||||||
|
}
|
||||||
|
|
||||||
|
func (view *ControlView) renderTextureAtlas(lvl *level.Level, readOnly bool) {
|
||||||
|
imgui.Separator()
|
||||||
|
|
||||||
|
atlas := lvl.TextureAtlas()
|
||||||
|
{
|
||||||
|
render.TextureSelector("Level Textures", -200, view.guiScale,
|
||||||
|
len(atlas), view.model.selectedAtlasIndex,
|
||||||
|
view.textureCache,
|
||||||
|
func(index int) resource.Key {
|
||||||
|
return resource.KeyOf(ids.LargeTextures.Plus(int(atlas[index])), resource.LangAny, 0)
|
||||||
|
},
|
||||||
|
func(index int) string {
|
||||||
|
textureType := "(F/C/W)"
|
||||||
|
if index >= level.FloorCeilingTextureLimit {
|
||||||
|
textureType = "(walls only)"
|
||||||
|
}
|
||||||
|
return fmt.Sprintf("Atlas Index %2d ", index) + textureType + "\n" + view.textureName(int(atlas[index]))
|
||||||
|
}, func(newIndex int) {
|
||||||
|
view.model.selectedAtlasIndex = newIndex
|
||||||
|
})
|
||||||
|
imgui.SameLine()
|
||||||
|
imgui.Text("Level Textures")
|
||||||
|
}
|
||||||
|
gameTextureIndex := -1
|
||||||
|
if (view.model.selectedAtlasIndex >= 0) && (view.model.selectedAtlasIndex < len(atlas)) {
|
||||||
|
gameTextureIndex = int(atlas[view.model.selectedAtlasIndex])
|
||||||
|
}
|
||||||
|
if !readOnly {
|
||||||
|
render.TextureSelector("Game Textures", -200, view.guiScale,
|
||||||
|
world.MaxWorldTextures, gameTextureIndex,
|
||||||
|
view.textureCache,
|
||||||
|
func(index int) resource.Key {
|
||||||
|
return resource.KeyOf(ids.LargeTextures.Plus(index), resource.LangAny, 0)
|
||||||
|
},
|
||||||
|
view.textureName,
|
||||||
|
func(newIndex int) {
|
||||||
|
view.requestSetLevelTexture(lvl, view.model.selectedAtlasIndex, newIndex)
|
||||||
|
})
|
||||||
|
imgui.SameLine()
|
||||||
|
imgui.Text("Game Textures")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (view *ControlView) renderSurveillanceObjects(lvl *level.Level, readOnly bool) {
|
||||||
|
imgui.Separator()
|
||||||
|
|
||||||
|
if imgui.BeginCombo("Surveillance Object", fmt.Sprintf("Object %d", view.model.selectedSurveillanceObjectIndex)) {
|
||||||
|
for i := 0; i < level.SurveillanceObjectCount; i++ {
|
||||||
|
if imgui.SelectableV(fmt.Sprintf("Object %d", i), i == view.model.selectedSurveillanceObjectIndex, 0, imgui.Vec2{}) {
|
||||||
|
view.model.selectedSurveillanceObjectIndex = i
|
||||||
|
}
|
||||||
|
}
|
||||||
|
imgui.EndCombo()
|
||||||
|
}
|
||||||
|
sources := lvl.SurveillanceSources()
|
||||||
|
surrogates := lvl.SurveillanceSurrogates()
|
||||||
|
capacity := lvl.ObjectCapacity()
|
||||||
|
|
||||||
|
view.renderSliderInt(readOnly, "Surveillance Source", int(sources[view.model.selectedSurveillanceObjectIndex]),
|
||||||
|
func(int) string { return "%d" },
|
||||||
|
0, capacity,
|
||||||
|
func(newValue int) {
|
||||||
|
view.requestSetSurveillanceSource(lvl, view.model.selectedSurveillanceObjectIndex, level.ObjectID(newValue))
|
||||||
|
})
|
||||||
|
view.renderSliderInt(readOnly, "Surveillance Surrogate", int(surrogates[view.model.selectedSurveillanceObjectIndex]),
|
||||||
|
func(int) string { return "%d" },
|
||||||
|
0, capacity,
|
||||||
|
func(newValue int) {
|
||||||
|
view.requestSetSurveillanceSurrogate(lvl, view.model.selectedSurveillanceObjectIndex, level.ObjectID(newValue))
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func (view *ControlView) renderHazards(lvl *level.Level, readOnly bool) {
|
||||||
|
imgui.Separator()
|
||||||
|
|
||||||
|
parameters := lvl.Parameters()
|
||||||
|
currentCeiling := currentCeilingHazard(parameters)
|
||||||
|
if readOnly {
|
||||||
|
imgui.LabelText("Ceiling Hazard", currentCeiling.title)
|
||||||
|
} else if imgui.BeginCombo("Ceiling Hazard", currentCeiling.title) {
|
||||||
|
for _, info := range ceilingHazards {
|
||||||
|
if imgui.SelectableV(info.title, info.title == currentCeiling.title, 0, imgui.Vec2{}) {
|
||||||
|
view.requestSetCeilingHazard(lvl, info)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
imgui.EndCombo()
|
||||||
|
}
|
||||||
|
view.renderSliderInt(readOnly, "Ceiling Hazard Level", int(parameters.CeilingHazardLevel),
|
||||||
|
currentCeiling.formatter, 0, 255,
|
||||||
|
func(newValue int) {
|
||||||
|
view.requestSetCeilingHazardLevel(lvl, byte(newValue))
|
||||||
|
})
|
||||||
|
|
||||||
|
currentFloor := currentFloorHazard(parameters)
|
||||||
|
if readOnly {
|
||||||
|
imgui.LabelText("Floor Hazard", currentFloor.title)
|
||||||
|
} else if imgui.BeginCombo("Floor Hazard", currentFloor.title) {
|
||||||
|
for _, info := range floorHazards {
|
||||||
|
if imgui.SelectableV(info.title, info.title == currentFloor.title, 0, imgui.Vec2{}) {
|
||||||
|
view.requestSetFloorHazard(lvl, info)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
imgui.EndCombo()
|
||||||
|
}
|
||||||
|
view.renderSliderInt(readOnly, "Floor Hazard Level", int(parameters.FloorHazardLevel),
|
||||||
|
currentFloor.formatter, 0, 255,
|
||||||
|
func(newValue int) {
|
||||||
|
view.requestSetFloorHazardLevel(lvl, byte(newValue))
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func (view *ControlView) renderTextureAnimations(lvl *level.Level, readOnly bool) {
|
||||||
|
imgui.Separator()
|
||||||
|
|
||||||
|
animations := lvl.TextureAnimations()
|
||||||
|
selectedText := ""
|
||||||
|
if (view.model.selectedTextureAnimationIndex >= 1) && (view.model.selectedTextureAnimationIndex < len(animations)) {
|
||||||
|
selectedText = fmt.Sprintf("%d", view.model.selectedTextureAnimationIndex)
|
||||||
|
}
|
||||||
|
if imgui.BeginCombo("Texture Animation Group", selectedText) {
|
||||||
|
for i := 1; i < len(animations); i++ {
|
||||||
|
if imgui.SelectableV(fmt.Sprintf("%2d", i), view.model.selectedTextureAnimationIndex == i, 0, imgui.Vec2{}) {
|
||||||
|
view.model.selectedTextureAnimationIndex = i
|
||||||
|
}
|
||||||
|
}
|
||||||
|
imgui.EndCombo()
|
||||||
|
}
|
||||||
|
|
||||||
|
if (view.model.selectedTextureAnimationIndex >= 1) && (view.model.selectedTextureAnimationIndex < len(animations)) {
|
||||||
|
animation := animations[view.model.selectedTextureAnimationIndex]
|
||||||
|
view.renderSliderInt(readOnly, "Texture Animation Time", int(animation.FrameTime),
|
||||||
|
func(int) string { return "%d msec" },
|
||||||
|
0, 1000,
|
||||||
|
func(newValue int) {
|
||||||
|
view.requestSetTextureAnimationTime(lvl, view.model.selectedTextureAnimationIndex, uint16(newValue))
|
||||||
|
})
|
||||||
|
view.renderSliderInt(readOnly, "Texture Animation Frame Count", int(animation.FrameCount),
|
||||||
|
func(int) string { return "%d" },
|
||||||
|
0, 10,
|
||||||
|
func(newValue int) {
|
||||||
|
view.requestSetTextureAnimationFrameCount(lvl, view.model.selectedTextureAnimationIndex, byte(newValue))
|
||||||
|
})
|
||||||
|
if readOnly {
|
||||||
|
imgui.LabelText("Texture Animation Loop Type", animation.LoopType.String())
|
||||||
|
} else if imgui.BeginCombo("Texture Animation Loop Type", animation.LoopType.String()) {
|
||||||
|
loopTypes := level.TextureAnimationLoopTypes()
|
||||||
|
for _, loopType := range loopTypes {
|
||||||
|
if imgui.SelectableV(loopType.String(), loopType == animation.LoopType, 0, imgui.Vec2{}) {
|
||||||
|
view.requestSetTextureAnimationType(lvl, view.model.selectedTextureAnimationIndex, loopType)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
imgui.EndCombo()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (view *ControlView) renderSliderInt(readOnly bool, label string, selectedValue int,
|
||||||
|
formatter func(int) string, min, max int, changeHandler func(int)) {
|
||||||
|
selectedString := formatter(selectedValue)
|
||||||
|
labelValue := fmt.Sprintf(selectedString, selectedValue)
|
||||||
|
if readOnly {
|
||||||
|
imgui.LabelText(label, labelValue)
|
||||||
|
} else if gui.StepSliderIntV(label, &selectedValue, min, max, selectedString) {
|
||||||
|
changeHandler(selectedValue)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (view *ControlView) requestSetZShift(lvl *level.Level, newValue int) {
|
||||||
|
lvl.SetHeightShift(level.HeightShift(newValue))
|
||||||
|
view.patchLevelResources(lvl, func() {})
|
||||||
|
}
|
||||||
|
|
||||||
|
func (view *ControlView) requestSetLevelTexture(lvl *level.Level, atlasIndex, worldTextureIndex int) {
|
||||||
|
lvl.SetTextureAtlasEntry(atlasIndex, level.TextureIndex(worldTextureIndex))
|
||||||
|
view.patchLevelResources(lvl, func() {
|
||||||
|
view.model.selectedAtlasIndex = atlasIndex
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func (view *ControlView) requestSetSurveillanceSource(lvl *level.Level, objectIndex int, objectID level.ObjectID) {
|
||||||
|
lvl.SetSurveillanceSource(objectIndex, objectID)
|
||||||
|
view.patchLevelResources(lvl, func() {
|
||||||
|
view.model.selectedSurveillanceObjectIndex = objectIndex
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func (view *ControlView) requestSetSurveillanceSurrogate(lvl *level.Level, objectIndex int, objectID level.ObjectID) {
|
||||||
|
lvl.SetSurveillanceSurrogate(objectIndex, objectID)
|
||||||
|
view.patchLevelResources(lvl, func() {
|
||||||
|
view.model.selectedSurveillanceObjectIndex = objectIndex
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func (view *ControlView) requestSetCeilingHazard(lvl *level.Level, info ceilingHazardInfo) {
|
||||||
|
parameters := lvl.Parameters()
|
||||||
|
parameters.RadiationRegister = 0
|
||||||
|
if info.radiationRegister {
|
||||||
|
parameters.RadiationRegister = 2
|
||||||
|
}
|
||||||
|
view.patchLevelResources(lvl, func() {})
|
||||||
|
}
|
||||||
|
|
||||||
|
func (view *ControlView) requestSetCeilingHazardLevel(lvl *level.Level, value byte) {
|
||||||
|
lvl.Parameters().CeilingHazardLevel = value
|
||||||
|
view.patchLevelResources(lvl, func() {})
|
||||||
|
}
|
||||||
|
|
||||||
|
func (view *ControlView) requestSetFloorHazard(lvl *level.Level, info floorHazardInfo) {
|
||||||
|
parameters := lvl.Parameters()
|
||||||
|
parameters.BiohazardRegister = 0
|
||||||
|
parameters.FloorHazardIsGravity = 0
|
||||||
|
if info.isGravity {
|
||||||
|
parameters.FloorHazardIsGravity = 1
|
||||||
|
} else if info.biohazardRegister {
|
||||||
|
parameters.BiohazardRegister = 2
|
||||||
|
}
|
||||||
|
view.patchLevelResources(lvl, func() {})
|
||||||
|
}
|
||||||
|
|
||||||
|
func (view *ControlView) requestSetFloorHazardLevel(lvl *level.Level, value byte) {
|
||||||
|
lvl.Parameters().FloorHazardLevel = value
|
||||||
|
view.patchLevelResources(lvl, func() {})
|
||||||
|
}
|
||||||
|
|
||||||
|
func (view *ControlView) requestSetTextureAnimationTime(lvl *level.Level, index int, value uint16) {
|
||||||
|
lvl.TextureAnimations()[index].FrameTime = value
|
||||||
|
view.patchLevelResources(lvl, func() {
|
||||||
|
view.model.selectedTextureAnimationIndex = index
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func (view *ControlView) requestSetTextureAnimationFrameCount(lvl *level.Level, index int, value byte) {
|
||||||
|
lvl.TextureAnimations()[index].FrameCount = value
|
||||||
|
view.patchLevelResources(lvl, func() {
|
||||||
|
view.model.selectedTextureAnimationIndex = index
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func (view *ControlView) requestSetTextureAnimationType(lvl *level.Level, index int, value level.TextureAnimationLoopType) {
|
||||||
|
lvl.TextureAnimations()[index].LoopType = value
|
||||||
|
view.patchLevelResources(lvl, func() {
|
||||||
|
view.model.selectedTextureAnimationIndex = index
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func (view *ControlView) patchLevelResources(lvl *level.Level, extraRestoreState func()) {
|
||||||
|
oldLevelID := view.levelSelection.CurrentLevelID()
|
||||||
|
extraTask := func(world.Modder) error {
|
||||||
|
extraRestoreState()
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
err := view.registry.Register(cmd.Named("PatchLevel"),
|
||||||
|
cmd.Forward(view.restoreFocusTask()),
|
||||||
|
cmd.Forward(view.levelSelection.SetCurrentLevelIDTask(lvl.ID())),
|
||||||
|
cmd.Forward(extraTask),
|
||||||
|
cmd.Nested(func() error { return view.levels.CommitLevelChanges(lvl.ID()) }),
|
||||||
|
cmd.Reverse(extraTask),
|
||||||
|
cmd.Reverse(view.levelSelection.SetCurrentLevelIDTask(oldLevelID)),
|
||||||
|
cmd.Reverse(view.restoreFocusTask()))
|
||||||
|
if err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (view *ControlView) restoreFocusTask() cmd.Task {
|
||||||
|
return func(modder world.Modder) error {
|
||||||
|
view.model.restoreFocus = true
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,16 @@
|
|||||||
|
package levels
|
||||||
|
|
||||||
|
type controlViewModel struct {
|
||||||
|
selectedAtlasIndex int
|
||||||
|
selectedSurveillanceObjectIndex int
|
||||||
|
selectedTextureAnimationIndex int
|
||||||
|
|
||||||
|
restoreFocus bool
|
||||||
|
windowOpen bool
|
||||||
|
}
|
||||||
|
|
||||||
|
func freshControlViewModel() controlViewModel {
|
||||||
|
return controlViewModel{
|
||||||
|
selectedTextureAnimationIndex: 1,
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,39 @@
|
|||||||
|
package levels
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
|
||||||
|
"github.com/inkyblackness/hacked/ss1/content/archive/level"
|
||||||
|
)
|
||||||
|
|
||||||
|
func tileHeightFormatterFor(levelHeight level.HeightShift) func(int) string {
|
||||||
|
return func(value int) string {
|
||||||
|
tileHeight, err := levelHeight.ValueFromTileHeight(level.TileHeightUnit(value))
|
||||||
|
tileHeightString := hintUnknown
|
||||||
|
if err == nil {
|
||||||
|
tileHeightString = fmt.Sprintf("%2.3f", tileHeight)
|
||||||
|
}
|
||||||
|
return tileHeightString + " tile(s) - raw: %d"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func objectHeightFormatterFor(levelHeight level.HeightShift) func(int) string {
|
||||||
|
return func(value int) string {
|
||||||
|
tileHeight, err := levelHeight.ValueFromObjectHeight(level.HeightUnit(value))
|
||||||
|
tileHeightString := hintUnknown
|
||||||
|
if err == nil {
|
||||||
|
tileHeightString = fmt.Sprintf("%2.3f", tileHeight)
|
||||||
|
}
|
||||||
|
return tileHeightString + " tile(s) - raw: %d"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func moveTileHeightFormatterFor(levelHeight level.HeightShift) func(int) string {
|
||||||
|
normalFormatter := tileHeightFormatterFor(levelHeight)
|
||||||
|
return func(value int) string {
|
||||||
|
if (value >= 0) && (value <= int(level.TileHeightUnitMax)) {
|
||||||
|
return normalFormatter(value)
|
||||||
|
}
|
||||||
|
return "Don't change - raw: %d"
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,124 @@
|
|||||||
|
package levels
|
||||||
|
|
||||||
|
import (
|
||||||
|
mgl "github.com/go-gl/mathgl/mgl32"
|
||||||
|
|
||||||
|
"github.com/inkyblackness/hacked/editor/render"
|
||||||
|
"github.com/inkyblackness/hacked/ui/opengl"
|
||||||
|
)
|
||||||
|
|
||||||
|
var highlighterVertexShaderSource = `
|
||||||
|
#version 150
|
||||||
|
precision mediump float;
|
||||||
|
|
||||||
|
in vec3 vertexPosition;
|
||||||
|
|
||||||
|
uniform mat4 modelMatrix;
|
||||||
|
uniform mat4 viewMatrix;
|
||||||
|
uniform mat4 projectionMatrix;
|
||||||
|
|
||||||
|
void main(void) {
|
||||||
|
gl_Position = projectionMatrix * viewMatrix * modelMatrix * vec4(vertexPosition, 1.0);
|
||||||
|
}
|
||||||
|
`
|
||||||
|
|
||||||
|
var highlighterFragmentShaderSource = `
|
||||||
|
#version 150
|
||||||
|
precision mediump float;
|
||||||
|
|
||||||
|
uniform vec4 inColor;
|
||||||
|
out vec4 fragColor;
|
||||||
|
|
||||||
|
void main(void) {
|
||||||
|
fragColor = inColor;
|
||||||
|
}
|
||||||
|
`
|
||||||
|
|
||||||
|
// Highlighter draws a simple highlighting of a rectangular area.
|
||||||
|
type Highlighter struct {
|
||||||
|
context *render.Context
|
||||||
|
|
||||||
|
program uint32
|
||||||
|
vao *opengl.VertexArrayObject
|
||||||
|
vertexPositionBuffer uint32
|
||||||
|
vertexPositionAttrib int32
|
||||||
|
modelMatrixUniform opengl.Matrix4Uniform
|
||||||
|
viewMatrixUniform opengl.Matrix4Uniform
|
||||||
|
projectionMatrixUniform opengl.Matrix4Uniform
|
||||||
|
inColorUniform opengl.Vector4Uniform
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewHighlighter returns a new instance of Highlighter.
|
||||||
|
func NewHighlighter(context *render.Context) *Highlighter {
|
||||||
|
gl := context.OpenGL
|
||||||
|
program, programErr := opengl.LinkNewStandardProgram(gl, highlighterVertexShaderSource, highlighterFragmentShaderSource)
|
||||||
|
|
||||||
|
if programErr != nil {
|
||||||
|
panic(opengl.NamedShaderError{Name: "HighlighterShader", Nested: programErr})
|
||||||
|
}
|
||||||
|
highlighter := &Highlighter{
|
||||||
|
context: context,
|
||||||
|
program: program,
|
||||||
|
|
||||||
|
vao: opengl.NewVertexArrayObject(gl, program),
|
||||||
|
vertexPositionBuffer: gl.GenBuffers(1)[0],
|
||||||
|
vertexPositionAttrib: gl.GetAttribLocation(program, "vertexPosition"),
|
||||||
|
modelMatrixUniform: opengl.Matrix4Uniform(gl.GetUniformLocation(program, "modelMatrix")),
|
||||||
|
viewMatrixUniform: opengl.Matrix4Uniform(gl.GetUniformLocation(program, "viewMatrix")),
|
||||||
|
projectionMatrixUniform: opengl.Matrix4Uniform(gl.GetUniformLocation(program, "projectionMatrix")),
|
||||||
|
inColorUniform: opengl.Vector4Uniform(gl.GetUniformLocation(program, "inColor"))}
|
||||||
|
|
||||||
|
{
|
||||||
|
gl.BindBuffer(opengl.ARRAY_BUFFER, highlighter.vertexPositionBuffer)
|
||||||
|
half := float32(0.5)
|
||||||
|
var vertices = []float32{
|
||||||
|
-half, -half, 0.0,
|
||||||
|
half, -half, 0.0,
|
||||||
|
half, half, 0.0,
|
||||||
|
|
||||||
|
half, half, 0.0,
|
||||||
|
-half, half, 0.0,
|
||||||
|
-half, -half, 0.0}
|
||||||
|
gl.BufferData(opengl.ARRAY_BUFFER, len(vertices)*4, vertices, opengl.STATIC_DRAW)
|
||||||
|
gl.BindBuffer(opengl.ARRAY_BUFFER, 0)
|
||||||
|
}
|
||||||
|
|
||||||
|
highlighter.vao.OnShader(func() {
|
||||||
|
gl.EnableVertexAttribArray(uint32(highlighter.vertexPositionAttrib))
|
||||||
|
gl.BindBuffer(opengl.ARRAY_BUFFER, highlighter.vertexPositionBuffer)
|
||||||
|
gl.VertexAttribOffset(uint32(highlighter.vertexPositionAttrib), 3, opengl.FLOAT, false, 0, 0)
|
||||||
|
gl.BindBuffer(opengl.ARRAY_BUFFER, 0)
|
||||||
|
})
|
||||||
|
|
||||||
|
return highlighter
|
||||||
|
}
|
||||||
|
|
||||||
|
// Dispose releases all resources.
|
||||||
|
func (highlighter *Highlighter) Dispose() {
|
||||||
|
gl := highlighter.context.OpenGL
|
||||||
|
|
||||||
|
highlighter.vao.Dispose()
|
||||||
|
gl.DeleteBuffers([]uint32{highlighter.vertexPositionBuffer})
|
||||||
|
gl.DeleteShader(highlighter.program)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Render renders the highlights.
|
||||||
|
func (highlighter *Highlighter) Render(positions []MapPosition, sideLength float32, color [4]float32) {
|
||||||
|
gl := highlighter.context.OpenGL
|
||||||
|
|
||||||
|
highlighter.vao.OnShader(func() {
|
||||||
|
highlighter.viewMatrixUniform.Set(gl, highlighter.context.ViewMatrix)
|
||||||
|
highlighter.projectionMatrixUniform.Set(gl, &highlighter.context.ProjectionMatrix)
|
||||||
|
highlighter.inColorUniform.Set(gl, &color)
|
||||||
|
|
||||||
|
for _, pos := range positions {
|
||||||
|
modelMatrix := mgl.Ident4().
|
||||||
|
Mul4(mgl.Translate3D(float32(pos.X), float32(pos.Y), 0.0)).
|
||||||
|
Mul4(mgl.Scale3D(sideLength, sideLength, 1.0))
|
||||||
|
|
||||||
|
highlighter.modelMatrixUniform.Set(gl, &modelMatrix)
|
||||||
|
|
||||||
|
gl.DrawArrays(opengl.TRIANGLES, 0, 6)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
@@ -0,0 +1,135 @@
|
|||||||
|
package levels
|
||||||
|
|
||||||
|
import (
|
||||||
|
"sort"
|
||||||
|
|
||||||
|
mgl "github.com/go-gl/mathgl/mgl32"
|
||||||
|
|
||||||
|
"github.com/inkyblackness/hacked/ss1/content/archive/level"
|
||||||
|
)
|
||||||
|
|
||||||
|
type hoverItem interface {
|
||||||
|
Pos() MapPosition
|
||||||
|
Size() float32
|
||||||
|
IsIn(lvl *level.Level) bool
|
||||||
|
}
|
||||||
|
|
||||||
|
type tileHoverItem struct {
|
||||||
|
pos MapPosition
|
||||||
|
}
|
||||||
|
|
||||||
|
func (item tileHoverItem) Pos() MapPosition {
|
||||||
|
return item.pos
|
||||||
|
}
|
||||||
|
|
||||||
|
func (item tileHoverItem) Size() float32 {
|
||||||
|
return level.FineCoordinatesPerTileSide
|
||||||
|
}
|
||||||
|
|
||||||
|
func (item tileHoverItem) IsIn(lvl *level.Level) bool {
|
||||||
|
return lvl.Tile(item.pos.Tile()) != nil
|
||||||
|
}
|
||||||
|
|
||||||
|
type objectHoverItem struct {
|
||||||
|
id level.ObjectID
|
||||||
|
pos MapPosition
|
||||||
|
}
|
||||||
|
|
||||||
|
func (item objectHoverItem) Pos() MapPosition {
|
||||||
|
return item.pos
|
||||||
|
}
|
||||||
|
|
||||||
|
func (item objectHoverItem) Size() float32 {
|
||||||
|
return level.FineCoordinatesPerTileSide / 4
|
||||||
|
}
|
||||||
|
|
||||||
|
func (item objectHoverItem) IsIn(lvl *level.Level) bool {
|
||||||
|
obj := lvl.Object(item.id)
|
||||||
|
return (obj != nil) && (obj.InUse != 0) && (item.pos.X == obj.X) && (item.pos.Y == obj.Y)
|
||||||
|
}
|
||||||
|
|
||||||
|
type hoverItems struct {
|
||||||
|
available []hoverItem
|
||||||
|
activeIndex int
|
||||||
|
activeItem hoverItem
|
||||||
|
}
|
||||||
|
|
||||||
|
func (items *hoverItems) reset() {
|
||||||
|
items.available = nil
|
||||||
|
items.activeIndex = 0
|
||||||
|
items.activeItem = nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (items *hoverItems) scroll(forward bool) {
|
||||||
|
count := len(items.available)
|
||||||
|
if count > 0 {
|
||||||
|
diff := 1
|
||||||
|
if !forward {
|
||||||
|
diff = -1
|
||||||
|
}
|
||||||
|
items.activeIndex = (count + (items.activeIndex + diff)) % count
|
||||||
|
items.activeItem = items.available[items.activeIndex]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (items *hoverItems) find(lvl *level.Level, pos MapPosition) {
|
||||||
|
refVec := mgl.Vec2{float32(pos.X), float32(pos.Y)}
|
||||||
|
var distances []float32
|
||||||
|
|
||||||
|
items.available = nil
|
||||||
|
lvl.ForEachObject(func(id level.ObjectID, entry level.ObjectMainEntry) {
|
||||||
|
entryVec := mgl.Vec2{float32(entry.X), float32(entry.Y)}
|
||||||
|
distance := refVec.Sub(entryVec).Len()
|
||||||
|
if distance < level.FineCoordinatesPerTileSide/4 {
|
||||||
|
items.available = append(items.available,
|
||||||
|
objectHoverItem{
|
||||||
|
id: id,
|
||||||
|
pos: MapPosition{X: entry.X, Y: entry.Y},
|
||||||
|
})
|
||||||
|
distances = append(distances, distance)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
items.available = append(items.available,
|
||||||
|
tileHoverItem{
|
||||||
|
pos: MapPosition{
|
||||||
|
X: level.CoordinateAt(pos.X.Tile(), level.FineCoordinatesPerTileSide/2),
|
||||||
|
Y: level.CoordinateAt(pos.Y.Tile(), level.FineCoordinatesPerTileSide/2),
|
||||||
|
},
|
||||||
|
})
|
||||||
|
distances = append(distances, level.FineCoordinatesPerTileSide)
|
||||||
|
sort.Slice(items.available, func(a, b int) bool { return distances[a] < distances[b] })
|
||||||
|
|
||||||
|
items.activeIndex = 0
|
||||||
|
items.activeItem = items.available[items.activeIndex]
|
||||||
|
}
|
||||||
|
|
||||||
|
func (items *hoverItems) validate(lvl *level.Level) {
|
||||||
|
allValid := true
|
||||||
|
for _, item := range items.available {
|
||||||
|
if !item.IsIn(lvl) {
|
||||||
|
allValid = false
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if allValid {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
newItems := make([]hoverItem, 0, len(items.available))
|
||||||
|
for index, item := range items.available {
|
||||||
|
isActive := index == items.activeIndex
|
||||||
|
if item.IsIn(lvl) {
|
||||||
|
if isActive {
|
||||||
|
items.activeIndex = len(newItems)
|
||||||
|
}
|
||||||
|
newItems = append(newItems, item)
|
||||||
|
} else if isActive {
|
||||||
|
items.activeIndex = 0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
items.available = newItems
|
||||||
|
items.activeItem = nil
|
||||||
|
if items.activeIndex < len(items.available) {
|
||||||
|
items.activeItem = items.available[items.activeIndex]
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,98 @@
|
|||||||
|
package levels
|
||||||
|
|
||||||
|
import (
|
||||||
|
"math"
|
||||||
|
|
||||||
|
mgl "github.com/go-gl/mathgl/mgl32"
|
||||||
|
)
|
||||||
|
|
||||||
|
// LimitedCamera is a camera implementation with ranged controls for zooming and moving.
|
||||||
|
type LimitedCamera struct {
|
||||||
|
viewportWidth, viewportHeight float32
|
||||||
|
minZoom, maxZoom float32
|
||||||
|
minPos, maxPos float32
|
||||||
|
|
||||||
|
requestedZoomLevel float32
|
||||||
|
viewOffsetX, viewOffsetY float32
|
||||||
|
|
||||||
|
viewMatrix mgl.Mat4
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewLimitedCamera returns a new instance of a LimitedCamera.
|
||||||
|
func NewLimitedCamera(minZoom, maxZoom float32, minPos, maxPos float32) *LimitedCamera {
|
||||||
|
cam := &LimitedCamera{
|
||||||
|
viewportWidth: 1.0,
|
||||||
|
viewportHeight: 1.0,
|
||||||
|
minZoom: minZoom,
|
||||||
|
maxZoom: maxZoom,
|
||||||
|
minPos: minPos,
|
||||||
|
maxPos: maxPos,
|
||||||
|
viewMatrix: mgl.Ident4()}
|
||||||
|
|
||||||
|
return cam
|
||||||
|
}
|
||||||
|
|
||||||
|
// SetViewportSize notifies the camera how big the view is.
|
||||||
|
func (cam *LimitedCamera) SetViewportSize(width, height float32) {
|
||||||
|
if (cam.viewportWidth != width) || (cam.viewportHeight != height) {
|
||||||
|
cam.viewportWidth, cam.viewportHeight = width, height
|
||||||
|
cam.updateViewMatrix()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ViewMatrix implements the Viewer interface.
|
||||||
|
func (cam *LimitedCamera) ViewMatrix() *mgl.Mat4 {
|
||||||
|
return &cam.viewMatrix
|
||||||
|
}
|
||||||
|
|
||||||
|
// MoveBy adjusts the requested view offset by given delta values in world coordinates.
|
||||||
|
func (cam *LimitedCamera) MoveBy(dx, dy float32) {
|
||||||
|
cam.MoveTo(cam.viewOffsetX+dx, cam.viewOffsetY+dy)
|
||||||
|
}
|
||||||
|
|
||||||
|
// MoveTo sets the requested view offset to the given world coordinates.
|
||||||
|
func (cam *LimitedCamera) MoveTo(worldX, worldY float32) {
|
||||||
|
cam.viewOffsetX = cam.limitValue(worldX, -cam.maxPos, cam.minPos)
|
||||||
|
cam.viewOffsetY = cam.limitValue(worldY, -cam.maxPos, cam.minPos)
|
||||||
|
cam.updateViewMatrix()
|
||||||
|
}
|
||||||
|
|
||||||
|
// ZoomAt adjusts the requested zoom level by given delta, centered around given world position.
|
||||||
|
// Positive values zoom in.
|
||||||
|
func (cam *LimitedCamera) ZoomAt(levelDelta float32, x, y float32) {
|
||||||
|
cam.requestedZoomLevel = cam.limitValue(cam.requestedZoomLevel+levelDelta, cam.minZoom, cam.maxZoom)
|
||||||
|
|
||||||
|
focusPoint := mgl.Vec4{x, y, 0.0, 1.0}
|
||||||
|
oldPixel := cam.viewMatrix.Mul4x1(focusPoint)
|
||||||
|
|
||||||
|
cam.updateViewMatrix()
|
||||||
|
|
||||||
|
newPixel := cam.viewMatrix.Mul4x1(focusPoint)
|
||||||
|
scaleFactor := cam.scaleFactor()
|
||||||
|
cam.MoveBy(-(newPixel[0]-oldPixel[0])/scaleFactor, +(newPixel[1]-oldPixel[1])/scaleFactor)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (cam *LimitedCamera) limitValue(value float32, min, max float32) float32 {
|
||||||
|
result := value
|
||||||
|
|
||||||
|
if result < min {
|
||||||
|
result = min
|
||||||
|
}
|
||||||
|
if result > max {
|
||||||
|
result = max
|
||||||
|
}
|
||||||
|
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
func (cam *LimitedCamera) scaleFactor() float32 {
|
||||||
|
return float32(math.Pow(2.0, float64(cam.requestedZoomLevel)))
|
||||||
|
}
|
||||||
|
|
||||||
|
func (cam *LimitedCamera) updateViewMatrix() {
|
||||||
|
scaleFactor := cam.scaleFactor()
|
||||||
|
cam.viewMatrix = mgl.Ident4().
|
||||||
|
Mul4(mgl.Translate3D(cam.viewportWidth/2.0, cam.viewportHeight/2.0, 0)).
|
||||||
|
Mul4(mgl.Scale3D(scaleFactor, -scaleFactor, 1.0)).
|
||||||
|
Mul4(mgl.Translate3D(cam.viewOffsetX, cam.viewOffsetY, 0))
|
||||||
|
}
|
||||||
@@ -0,0 +1,179 @@
|
|||||||
|
package levels
|
||||||
|
|
||||||
|
import (
|
||||||
|
mgl "github.com/go-gl/mathgl/mgl32"
|
||||||
|
|
||||||
|
"github.com/inkyblackness/hacked/editor/render"
|
||||||
|
"github.com/inkyblackness/hacked/ss1/content/archive/level"
|
||||||
|
"github.com/inkyblackness/hacked/ui/opengl"
|
||||||
|
)
|
||||||
|
|
||||||
|
var mapColorsVertexShaderSource = `
|
||||||
|
#version 150
|
||||||
|
precision mediump float;
|
||||||
|
|
||||||
|
in vec4 vertexColor;
|
||||||
|
in vec2 vertexPosition;
|
||||||
|
|
||||||
|
uniform mat4 modelMatrix;
|
||||||
|
uniform mat4 viewMatrix;
|
||||||
|
uniform mat4 projectionMatrix;
|
||||||
|
|
||||||
|
out vec4 color;
|
||||||
|
|
||||||
|
void main(void) {
|
||||||
|
gl_Position = projectionMatrix * viewMatrix * modelMatrix * vec4(vertexPosition.xy, 0.0, 1.0);
|
||||||
|
color = vertexColor;
|
||||||
|
}
|
||||||
|
`
|
||||||
|
|
||||||
|
var mapColorsFragmentShaderSource = `
|
||||||
|
#version 150
|
||||||
|
precision mediump float;
|
||||||
|
|
||||||
|
in vec4 color;
|
||||||
|
|
||||||
|
out vec4 fragColor;
|
||||||
|
|
||||||
|
void main(void) {
|
||||||
|
fragColor = color;
|
||||||
|
}
|
||||||
|
`
|
||||||
|
|
||||||
|
// ColorQuery returns the color for the specified tile.
|
||||||
|
type ColorQuery func(level.TilePosition) [4]float32
|
||||||
|
|
||||||
|
// MapColors is a renderable for the tile colorings.
|
||||||
|
type MapColors struct {
|
||||||
|
context *render.Context
|
||||||
|
|
||||||
|
program uint32
|
||||||
|
vao *opengl.VertexArrayObject
|
||||||
|
vertexPositionBuffer uint32
|
||||||
|
vertexPositionAttrib int32
|
||||||
|
vertexColorBuffer uint32
|
||||||
|
vertexColorAttrib int32
|
||||||
|
modelMatrixUniform opengl.Matrix4Uniform
|
||||||
|
viewMatrixUniform opengl.Matrix4Uniform
|
||||||
|
projectionMatrixUniform opengl.Matrix4Uniform
|
||||||
|
|
||||||
|
lastColorGrid [][][4]float32
|
||||||
|
lastColorGridColumns int
|
||||||
|
lastColorGridRows int
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewMapColors returns a new instance of a renderable for tile colorings.
|
||||||
|
func NewMapColors(context *render.Context) *MapColors {
|
||||||
|
gl := context.OpenGL
|
||||||
|
program, programErr := opengl.LinkNewStandardProgram(gl, mapColorsVertexShaderSource, mapColorsFragmentShaderSource)
|
||||||
|
|
||||||
|
if programErr != nil {
|
||||||
|
panic(opengl.NamedShaderError{Name: "MapColorsShader", Nested: programErr})
|
||||||
|
}
|
||||||
|
renderable := &MapColors{
|
||||||
|
context: context,
|
||||||
|
program: program,
|
||||||
|
vao: opengl.NewVertexArrayObject(gl, program),
|
||||||
|
vertexPositionBuffer: gl.GenBuffers(1)[0],
|
||||||
|
vertexPositionAttrib: gl.GetAttribLocation(program, "vertexPosition"),
|
||||||
|
vertexColorBuffer: gl.GenBuffers(1)[0],
|
||||||
|
vertexColorAttrib: gl.GetAttribLocation(program, "vertexColor"),
|
||||||
|
modelMatrixUniform: opengl.Matrix4Uniform(gl.GetUniformLocation(program, "modelMatrix")),
|
||||||
|
viewMatrixUniform: opengl.Matrix4Uniform(gl.GetUniformLocation(program, "viewMatrix")),
|
||||||
|
projectionMatrixUniform: opengl.Matrix4Uniform(gl.GetUniformLocation(program, "projectionMatrix")),
|
||||||
|
}
|
||||||
|
|
||||||
|
{
|
||||||
|
top := float32(level.FineCoordinatesPerTileSide)
|
||||||
|
left := float32(0.0)
|
||||||
|
right := float32(level.FineCoordinatesPerTileSide)
|
||||||
|
bottom := float32(0.0)
|
||||||
|
|
||||||
|
vertices := []float32{
|
||||||
|
left, bottom,
|
||||||
|
left, top,
|
||||||
|
right, top,
|
||||||
|
|
||||||
|
right, top,
|
||||||
|
right, bottom,
|
||||||
|
left, bottom}
|
||||||
|
|
||||||
|
gl.BindBuffer(opengl.ARRAY_BUFFER, renderable.vertexPositionBuffer)
|
||||||
|
gl.BufferData(opengl.ARRAY_BUFFER, len(vertices)*4, vertices, opengl.STATIC_DRAW)
|
||||||
|
gl.BindBuffer(opengl.ARRAY_BUFFER, 0)
|
||||||
|
}
|
||||||
|
|
||||||
|
renderable.vao.WithSetter(func(gl opengl.OpenGL) {
|
||||||
|
gl.EnableVertexAttribArray(uint32(renderable.vertexPositionAttrib))
|
||||||
|
gl.EnableVertexAttribArray(uint32(renderable.vertexColorAttrib))
|
||||||
|
gl.BindBuffer(opengl.ARRAY_BUFFER, renderable.vertexPositionBuffer)
|
||||||
|
gl.VertexAttribOffset(uint32(renderable.vertexPositionAttrib), 2, opengl.FLOAT, false, 0, 0)
|
||||||
|
gl.BindBuffer(opengl.ARRAY_BUFFER, renderable.vertexColorBuffer)
|
||||||
|
gl.VertexAttribOffset(uint32(renderable.vertexColorAttrib), 4, opengl.FLOAT, false, 0, 0)
|
||||||
|
gl.BindBuffer(opengl.ARRAY_BUFFER, 0)
|
||||||
|
})
|
||||||
|
|
||||||
|
return renderable
|
||||||
|
}
|
||||||
|
|
||||||
|
// Dispose releases any internal resources.
|
||||||
|
func (renderable *MapColors) Dispose() {
|
||||||
|
gl := renderable.context.OpenGL
|
||||||
|
gl.DeleteProgram(renderable.program)
|
||||||
|
gl.DeleteBuffers([]uint32{renderable.vertexPositionBuffer})
|
||||||
|
renderable.vao.Dispose()
|
||||||
|
}
|
||||||
|
|
||||||
|
// Render renders the renderable.
|
||||||
|
func (renderable *MapColors) Render(columnCount, rowCount int, query ColorQuery) {
|
||||||
|
gl := renderable.context.OpenGL
|
||||||
|
|
||||||
|
if (renderable.lastColorGridColumns != columnCount) || (renderable.lastColorGridRows != rowCount) {
|
||||||
|
renderable.lastColorGrid = make([][][4]float32, rowCount+1)
|
||||||
|
for y := 0; y < rowCount; y++ {
|
||||||
|
renderable.lastColorGrid[y] = make([][4]float32, columnCount+1)
|
||||||
|
}
|
||||||
|
renderable.lastColorGrid[rowCount] = make([][4]float32, columnCount+1)
|
||||||
|
renderable.lastColorGridColumns = columnCount
|
||||||
|
renderable.lastColorGridRows = rowCount
|
||||||
|
}
|
||||||
|
for y, row := range renderable.lastColorGrid {
|
||||||
|
for x := 0; x < columnCount; x++ {
|
||||||
|
row[x] = query(level.TilePosition{X: byte(x), Y: byte(y)})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
renderable.vao.OnShader(func() {
|
||||||
|
var colors [24]float32
|
||||||
|
|
||||||
|
renderable.viewMatrixUniform.Set(gl, renderable.context.ViewMatrix)
|
||||||
|
renderable.projectionMatrixUniform.Set(gl, &renderable.context.ProjectionMatrix)
|
||||||
|
|
||||||
|
gl.BindBuffer(opengl.ARRAY_BUFFER, renderable.vertexColorBuffer)
|
||||||
|
|
||||||
|
for y := 0; y < rowCount; y++ {
|
||||||
|
for x := 0; x < columnCount; x++ {
|
||||||
|
colorBottomLeft := renderable.lastColorGrid[y][x]
|
||||||
|
colorBottomRight := renderable.lastColorGrid[y][x+1]
|
||||||
|
colorTopLeft := renderable.lastColorGrid[y+1][x]
|
||||||
|
colorTopRight := renderable.lastColorGrid[y+1][x+1]
|
||||||
|
|
||||||
|
modelMatrix := mgl.Ident4().
|
||||||
|
Mul4(mgl.Translate3D((float32(x))*level.FineCoordinatesPerTileSide, (float32(y))*level.FineCoordinatesPerTileSide, 0.0))
|
||||||
|
renderable.modelMatrixUniform.Set(gl, &modelMatrix)
|
||||||
|
|
||||||
|
copy(colors[0:4], colorBottomLeft[:])
|
||||||
|
copy(colors[4:8], colorTopLeft[:])
|
||||||
|
copy(colors[8:12], colorTopRight[:])
|
||||||
|
copy(colors[12:16], colorTopRight[:])
|
||||||
|
copy(colors[16:20], colorBottomRight[:])
|
||||||
|
copy(colors[20:24], colorBottomLeft[:])
|
||||||
|
|
||||||
|
gl.BufferData(opengl.ARRAY_BUFFER, len(colors)*4, colors[:], opengl.STATIC_DRAW)
|
||||||
|
gl.DrawArrays(opengl.TRIANGLES, 0, 6)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
gl.BindBuffer(opengl.ARRAY_BUFFER, 0)
|
||||||
|
})
|
||||||
|
}
|
||||||
@@ -0,0 +1,587 @@
|
|||||||
|
package levels
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
|
||||||
|
mgl "github.com/go-gl/mathgl/mgl32"
|
||||||
|
"github.com/inkyblackness/imgui-go/v3"
|
||||||
|
|
||||||
|
"github.com/inkyblackness/hacked/editor/graphics"
|
||||||
|
"github.com/inkyblackness/hacked/editor/render"
|
||||||
|
"github.com/inkyblackness/hacked/ss1/content/archive/level"
|
||||||
|
"github.com/inkyblackness/hacked/ss1/content/object"
|
||||||
|
"github.com/inkyblackness/hacked/ss1/edit"
|
||||||
|
"github.com/inkyblackness/hacked/ss1/resource"
|
||||||
|
"github.com/inkyblackness/hacked/ss1/world/ids"
|
||||||
|
"github.com/inkyblackness/hacked/ui/input"
|
||||||
|
"github.com/inkyblackness/hacked/ui/opengl"
|
||||||
|
)
|
||||||
|
|
||||||
|
const contextMenuName = "MapContextMenu"
|
||||||
|
|
||||||
|
// MapDisplay renders a level map.
|
||||||
|
type MapDisplay struct {
|
||||||
|
gameObjects *edit.GameObjectsService
|
||||||
|
levelSelection *edit.LevelSelectionService
|
||||||
|
editor *edit.LevelEditorService
|
||||||
|
|
||||||
|
context render.Context
|
||||||
|
camera *LimitedCamera
|
||||||
|
guiScale float32
|
||||||
|
|
||||||
|
background *BackgroundGrid
|
||||||
|
textures *MapTextures
|
||||||
|
colors *MapColors
|
||||||
|
mapGrid *MapGrid
|
||||||
|
highlighter *Highlighter
|
||||||
|
icons *MapIcons
|
||||||
|
|
||||||
|
moveCapture func(pixelX, pixelY float32)
|
||||||
|
mouseMoved bool
|
||||||
|
|
||||||
|
positionPopupPos imgui.Vec2
|
||||||
|
positionValid bool
|
||||||
|
position MapPosition
|
||||||
|
contextMenuRequested bool
|
||||||
|
|
||||||
|
selectionReference *level.TilePosition
|
||||||
|
|
||||||
|
hoverItems hoverItems
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewMapDisplay returns a new instance.
|
||||||
|
func NewMapDisplay(gameObjects *edit.GameObjectsService, levelSelection *edit.LevelSelectionService, editor *edit.LevelEditorService,
|
||||||
|
gl opengl.OpenGL, guiScale float32,
|
||||||
|
textureQuery TextureQuery) *MapDisplay {
|
||||||
|
tilesPerMapSide := float32(64)
|
||||||
|
|
||||||
|
tileBaseLength := float32(level.FineCoordinatesPerTileSide)
|
||||||
|
tileBaseHalf := tileBaseLength / 2.0
|
||||||
|
camLimit := tilesPerMapSide*tileBaseLength - tileBaseHalf
|
||||||
|
zoomShift := guiScale - 1.0
|
||||||
|
zoomLevelMin := float32(-5) + zoomShift
|
||||||
|
zoomLevelMax := float32(1) + zoomShift
|
||||||
|
|
||||||
|
display := &MapDisplay{
|
||||||
|
gameObjects: gameObjects,
|
||||||
|
levelSelection: levelSelection,
|
||||||
|
editor: editor,
|
||||||
|
context: render.Context{
|
||||||
|
OpenGL: gl,
|
||||||
|
ProjectionMatrix: mgl.Ident4(),
|
||||||
|
},
|
||||||
|
camera: NewLimitedCamera(zoomLevelMin, zoomLevelMax, -tileBaseHalf, camLimit),
|
||||||
|
guiScale: guiScale,
|
||||||
|
moveCapture: func(float32, float32) {},
|
||||||
|
}
|
||||||
|
display.context.ViewMatrix = display.camera.ViewMatrix()
|
||||||
|
display.background = NewBackgroundGrid(&display.context)
|
||||||
|
display.textures = NewMapTextures(&display.context, textureQuery)
|
||||||
|
display.colors = NewMapColors(&display.context)
|
||||||
|
display.mapGrid = NewMapGrid(&display.context)
|
||||||
|
display.highlighter = NewHighlighter(&display.context)
|
||||||
|
display.icons = NewMapIcons(&display.context)
|
||||||
|
|
||||||
|
centerX, centerY := (tilesPerMapSide*tileBaseLength)/-2.0, (tilesPerMapSide*tileBaseLength)/-2.0
|
||||||
|
display.camera.ZoomAt(-3+zoomShift, centerX, centerY)
|
||||||
|
display.camera.MoveTo(centerX, centerY)
|
||||||
|
|
||||||
|
return display
|
||||||
|
}
|
||||||
|
|
||||||
|
// Render renders the whole map display.
|
||||||
|
func (display *MapDisplay) Render(
|
||||||
|
paletteTexture *graphics.PaletteTexture, textureRetriever func(resource.Key) (*graphics.BitmapTexture, error),
|
||||||
|
textureDisplay TextureDisplay, colorDisplay ColorDisplay) {
|
||||||
|
lvl := display.editor.Level()
|
||||||
|
columns, rows, _ := lvl.Size()
|
||||||
|
|
||||||
|
display.hoverItems.validate(lvl)
|
||||||
|
|
||||||
|
display.background.Render(columns, rows)
|
||||||
|
if lvl.IsCyberspace() {
|
||||||
|
display.renderCyberspaceColors(lvl, columns, rows, paletteTexture, colorDisplay)
|
||||||
|
} else {
|
||||||
|
display.renderRealWorldTextures(lvl, columns, rows, paletteTexture, textureDisplay)
|
||||||
|
display.renderRealWorldShadows(lvl, columns, rows, colorDisplay)
|
||||||
|
}
|
||||||
|
display.mapGrid.Render(columns, rows, lvl)
|
||||||
|
display.renderTileSelection()
|
||||||
|
display.renderObjectBackgrounds(lvl)
|
||||||
|
display.renderObjectIcons(lvl, paletteTexture, textureRetriever)
|
||||||
|
display.renderObjectSelection()
|
||||||
|
display.renderActiveHoverItem()
|
||||||
|
display.renderPositionOverlay(lvl)
|
||||||
|
display.renderContextMenu()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (display *MapDisplay) renderCyberspaceColors(lvl *level.Level, columns int, rows int, paletteTexture *graphics.PaletteTexture, colorDisplay ColorDisplay) {
|
||||||
|
if paletteTexture == nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
var colorQuery ColorQuery
|
||||||
|
palette := paletteTexture.Palette()
|
||||||
|
if colorDisplay == ColorDisplayFloor {
|
||||||
|
colorQuery = display.colorQueryFor(lvl, func(tile *level.TileMapEntry) [4]float32 {
|
||||||
|
rgb := palette[tile.TextureInfo.FloorPaletteIndex()]
|
||||||
|
return [4]float32{float32(rgb.Red) / 255, float32(rgb.Green) / 255, float32(rgb.Blue) / 255, 0.8}
|
||||||
|
})
|
||||||
|
} else if colorDisplay == ColorDisplayCeiling {
|
||||||
|
colorQuery = display.colorQueryFor(lvl, func(tile *level.TileMapEntry) [4]float32 {
|
||||||
|
rgb := palette[tile.TextureInfo.CeilingPaletteIndex()]
|
||||||
|
return [4]float32{float32(rgb.Red) / 255, float32(rgb.Green) / 255, float32(rgb.Blue) / 255, 0.8}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
if colorQuery != nil {
|
||||||
|
display.colors.Render(columns, rows, colorQuery)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (display *MapDisplay) renderRealWorldTextures(lvl *level.Level, columns int, rows int,
|
||||||
|
paletteTexture *graphics.PaletteTexture, textureDisplay TextureDisplay) {
|
||||||
|
if paletteTexture == nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
display.textures.Render(columns, rows, func(pos level.TilePosition) (level.TileType, level.TextureIndex, int) {
|
||||||
|
tile := lvl.Tile(pos)
|
||||||
|
if tile == nil {
|
||||||
|
return level.TileTypeSolid, 0, 0
|
||||||
|
}
|
||||||
|
atlasIndex, textureRotations := textureDisplay.Func()(tile)
|
||||||
|
atlas := lvl.TextureAtlas()
|
||||||
|
textureIndex := level.TextureIndex(-1)
|
||||||
|
if (int(atlasIndex) >= 0) && (int(atlasIndex) < len(atlas)) {
|
||||||
|
textureIndex = atlas[atlasIndex]
|
||||||
|
}
|
||||||
|
return tile.Type, textureIndex, textureRotations
|
||||||
|
}, paletteTexture)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (display *MapDisplay) renderRealWorldShadows(lvl *level.Level, columns int, rows int, colorDisplay ColorDisplay) {
|
||||||
|
var colorQuery ColorQuery
|
||||||
|
if colorDisplay == ColorDisplayFloor {
|
||||||
|
colorQuery = display.colorQueryFor(lvl, func(tile *level.TileMapEntry) [4]float32 {
|
||||||
|
return [4]float32{0.0, 0.0, 0.0, float32(tile.Flags.ForRealWorld().FloorShadow()) / 15.0}
|
||||||
|
})
|
||||||
|
} else if colorDisplay == ColorDisplayCeiling {
|
||||||
|
colorQuery = display.colorQueryFor(lvl, func(tile *level.TileMapEntry) [4]float32 {
|
||||||
|
return [4]float32{0.0, 0.0, 0.0, float32(tile.Flags.ForRealWorld().CeilingShadow()) / 15.0}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
if colorQuery != nil {
|
||||||
|
display.colors.Render(columns, rows, colorQuery)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (display *MapDisplay) renderTileSelection() {
|
||||||
|
selectedTiles := display.levelSelection.CurrentSelectedTiles()
|
||||||
|
tileMapPositions := make([]MapPosition, 0, len(selectedTiles))
|
||||||
|
for _, pos := range selectedTiles {
|
||||||
|
tileMapPositions = append(tileMapPositions, MapPosition{
|
||||||
|
X: level.CoordinateAt(pos.X, level.FineCoordinatesPerTileSide/2),
|
||||||
|
Y: level.CoordinateAt(pos.Y, level.FineCoordinatesPerTileSide/2),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
display.highlighter.Render(tileMapPositions, level.FineCoordinatesPerTileSide, [4]float32{0.0, 0.8, 0.2, 0.5})
|
||||||
|
}
|
||||||
|
|
||||||
|
func (display *MapDisplay) renderObjectBackgrounds(lvl *level.Level) {
|
||||||
|
var objects []MapPosition
|
||||||
|
lvl.ForEachObject(func(id level.ObjectID, entry level.ObjectMainEntry) {
|
||||||
|
objects = append(objects, MapPosition{X: entry.X, Y: entry.Y})
|
||||||
|
})
|
||||||
|
display.highlighter.Render(objects, level.FineCoordinatesPerTileSide/4, [4]float32{1.0, 1.0, 1.0, 0.3})
|
||||||
|
}
|
||||||
|
|
||||||
|
func (display *MapDisplay) renderObjectIcons(lvl *level.Level, paletteTexture *graphics.PaletteTexture,
|
||||||
|
textureRetriever func(resource.Key) (*graphics.BitmapTexture, error)) {
|
||||||
|
if paletteTexture == nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
tripleInfo := display.gameObjects.BitmapInfo()
|
||||||
|
var icons []iconData
|
||||||
|
var highlightIcon iconData
|
||||||
|
var highlightID level.ObjectID
|
||||||
|
|
||||||
|
if display.hoverItems.activeItem != nil {
|
||||||
|
objectItem, isObject := display.hoverItems.activeItem.(objectHoverItem)
|
||||||
|
if isObject {
|
||||||
|
highlightID = objectItem.id
|
||||||
|
}
|
||||||
|
}
|
||||||
|
lvl.ForEachObject(func(id level.ObjectID, entry level.ObjectMainEntry) {
|
||||||
|
triple := entry.Triple()
|
||||||
|
info, cached := tripleInfo[triple]
|
||||||
|
if !cached {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
key := resource.KeyOf(ids.ObjectBitmaps, resource.LangAny, info.Start+info.IconRecommendation)
|
||||||
|
texture, err := textureRetriever(key)
|
||||||
|
if err == nil {
|
||||||
|
icon := iconData{pos: MapPosition{X: entry.X, Y: entry.Y}, texture: texture}
|
||||||
|
if highlightID == id {
|
||||||
|
highlightIcon = icon
|
||||||
|
} else {
|
||||||
|
icons = append(icons, icon)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
if (highlightID != 0) && (highlightIcon.texture != nil) {
|
||||||
|
icons = append(icons, highlightIcon)
|
||||||
|
}
|
||||||
|
display.icons.Render(paletteTexture, level.FineCoordinatesPerTileSide/4, icons)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (display *MapDisplay) renderObjectSelection() {
|
||||||
|
selectedObjects := display.editor.Objects()
|
||||||
|
selectedObjectHighlights := make([]MapPosition, 0, len(selectedObjects))
|
||||||
|
for _, obj := range selectedObjects {
|
||||||
|
objPos := MapPosition{X: obj.X, Y: obj.Y}
|
||||||
|
selectedObjectHighlights = append(selectedObjectHighlights, objPos)
|
||||||
|
}
|
||||||
|
display.highlighter.Render(selectedObjectHighlights, level.FineCoordinatesPerTileSide/4, [4]float32{0.0, 0.8, 0.2, 0.5})
|
||||||
|
}
|
||||||
|
|
||||||
|
func (display *MapDisplay) renderActiveHoverItem() {
|
||||||
|
if display.hoverItems.activeItem == nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
display.highlighter.Render(
|
||||||
|
[]MapPosition{display.hoverItems.activeItem.Pos()},
|
||||||
|
display.hoverItems.activeItem.Size(),
|
||||||
|
[4]float32{0.0, 0.2, 0.8, 0.3})
|
||||||
|
}
|
||||||
|
|
||||||
|
func (display *MapDisplay) colorQueryFor(lvl *level.Level, tileToColor func(*level.TileMapEntry) [4]float32) func(level.TilePosition) [4]float32 {
|
||||||
|
return func(pos level.TilePosition) [4]float32 {
|
||||||
|
tile := lvl.Tile(pos)
|
||||||
|
if tile == nil {
|
||||||
|
return [4]float32{}
|
||||||
|
}
|
||||||
|
return tileToColor(tile)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (display *MapDisplay) renderPositionOverlay(lvl *level.Level) {
|
||||||
|
imgui.SetNextWindowPosV(display.positionPopupPos, imgui.ConditionAlways, imgui.Vec2{X: 1.0, Y: 1.0})
|
||||||
|
imgui.SetNextWindowSize(imgui.Vec2{X: 140 * display.guiScale, Y: 0})
|
||||||
|
imgui.SetNextWindowBgAlpha(0.3)
|
||||||
|
if imgui.BeginV("Position", nil, imgui.WindowFlagsNoMove|imgui.WindowFlagsNoTitleBar|imgui.WindowFlagsNoResize|imgui.WindowFlagsAlwaysAutoResize|
|
||||||
|
imgui.WindowFlagsNoSavedSettings|imgui.WindowFlagsNoFocusOnAppearing|imgui.WindowFlagsNoNav) {
|
||||||
|
typeString := "---"
|
||||||
|
hasPos := false
|
||||||
|
var pos MapPosition
|
||||||
|
hasFloor := false
|
||||||
|
var floorRaw int
|
||||||
|
floorString := hintUnknown
|
||||||
|
hasCeiling := false
|
||||||
|
var ceilingRaw int
|
||||||
|
ceilingString := hintUnknown
|
||||||
|
|
||||||
|
if display.hoverItems.activeItem != nil {
|
||||||
|
pos = display.hoverItems.activeItem.Pos()
|
||||||
|
hasPos = true
|
||||||
|
|
||||||
|
if _, isTileItem := display.hoverItems.activeItem.(tileHoverItem); isTileItem {
|
||||||
|
pos = display.position // use raw cursor position for this display
|
||||||
|
typeString = "Tile"
|
||||||
|
tile := lvl.Tile(pos.Tile())
|
||||||
|
if (tile != nil) && (tile.Type != level.TileTypeSolid) {
|
||||||
|
_, _, heightShift := lvl.Size()
|
||||||
|
floorHeight := tile.Floor.AbsoluteHeight()
|
||||||
|
floorHeightInTiles, err := heightShift.ValueFromTileHeight(floorHeight)
|
||||||
|
if err == nil {
|
||||||
|
floorString = fmt.Sprintf("%2.3f", floorHeightInTiles)
|
||||||
|
}
|
||||||
|
floorRaw = int(floorHeight)
|
||||||
|
hasFloor = true
|
||||||
|
|
||||||
|
ceilingHeight := tile.Ceiling.AbsoluteHeight()
|
||||||
|
ceilingHeightInTiles, err := heightShift.ValueFromTileHeight(ceilingHeight)
|
||||||
|
if err == nil {
|
||||||
|
ceilingString = fmt.Sprintf("%2.3f", ceilingHeightInTiles)
|
||||||
|
}
|
||||||
|
ceilingRaw = int(ceilingHeight)
|
||||||
|
hasCeiling = true
|
||||||
|
}
|
||||||
|
} else if objectItem, isObjectItem := display.hoverItems.activeItem.(objectHoverItem); isObjectItem {
|
||||||
|
_, _, heightShift := lvl.Size()
|
||||||
|
obj := lvl.Object(objectItem.id)
|
||||||
|
if obj != nil {
|
||||||
|
typeString = fmt.Sprintf("%3d = %v", objectItem.id, obj.Triple())
|
||||||
|
heightInTiles, err := heightShift.ValueFromObjectHeight(obj.Z)
|
||||||
|
if err == nil {
|
||||||
|
floorString = fmt.Sprintf("%2.3f", heightInTiles)
|
||||||
|
}
|
||||||
|
floorRaw = int(obj.Z)
|
||||||
|
hasFloor = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
imgui.Text("T: " + typeString)
|
||||||
|
if hasPos {
|
||||||
|
imgui.Text(fmt.Sprintf("X: T %2d F %3d", pos.X.Tile(), pos.X.Fine()))
|
||||||
|
imgui.Text(fmt.Sprintf("Y: T %2d F %3d", pos.Y.Tile(), pos.Y.Fine()))
|
||||||
|
} else {
|
||||||
|
imgui.Text("X: T -- F ---")
|
||||||
|
imgui.Text("Y: T -- F ---")
|
||||||
|
}
|
||||||
|
if hasFloor {
|
||||||
|
imgui.Text(fmt.Sprintf("F: %3d = %s", floorRaw, floorString))
|
||||||
|
} else {
|
||||||
|
imgui.Text("F: -- = --.---")
|
||||||
|
}
|
||||||
|
if hasCeiling {
|
||||||
|
imgui.Text(fmt.Sprintf("C: %3d = %s", ceilingRaw, ceilingString))
|
||||||
|
} else {
|
||||||
|
imgui.Text("C: -- = --.---")
|
||||||
|
}
|
||||||
|
imgui.End()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (display *MapDisplay) renderContextMenu() {
|
||||||
|
if display.contextMenuRequested {
|
||||||
|
imgui.OpenPopup(contextMenuName)
|
||||||
|
display.contextMenuRequested = false
|
||||||
|
}
|
||||||
|
if imgui.BeginPopupV(contextMenuName, imgui.PopupFlagsMouseButtonRight) {
|
||||||
|
readOnly := display.editor.IsReadOnly()
|
||||||
|
if imgui.BeginMenu("New...") {
|
||||||
|
implicitTriple := display.editor.NewObjectTriple()
|
||||||
|
canCreateImplicitClass := display.canCreateObjectOf(implicitTriple.Class)
|
||||||
|
if imgui.MenuItemV("Object", "Ctrl+2ndClick", false, canCreateImplicitClass) {
|
||||||
|
display.requestCreateNewObject(false, implicitTriple)
|
||||||
|
}
|
||||||
|
if imgui.MenuItemV("Object (at grid)", "Ctrl+Shift+2ndClick", false, canCreateImplicitClass) {
|
||||||
|
display.requestCreateNewObject(true, implicitTriple)
|
||||||
|
}
|
||||||
|
imgui.EndMenu()
|
||||||
|
}
|
||||||
|
imgui.Separator()
|
||||||
|
if imgui.MenuItemV("Delete Objects", "", false, !readOnly && display.editor.HasSelectedObjects()) {
|
||||||
|
_ = display.editor.DeleteObjects()
|
||||||
|
}
|
||||||
|
if imgui.MenuItemV("Clear Tiles", "", false, !readOnly && display.editor.HasSelectedTiles()) {
|
||||||
|
_ = display.editor.ClearTiles()
|
||||||
|
}
|
||||||
|
imgui.EndPopup()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// WindowResized must be called to notify of a change in window geometry.
|
||||||
|
func (display *MapDisplay) WindowResized(width int, height int) {
|
||||||
|
display.context.ProjectionMatrix = mgl.Ortho2D(0.0, float32(width), float32(height), 0.0)
|
||||||
|
display.camera.SetViewportSize(float32(width), float32(height))
|
||||||
|
display.positionPopupPos.X = float32(width) - 10.0
|
||||||
|
display.positionPopupPos.Y = float32(height) - 10.0
|
||||||
|
}
|
||||||
|
|
||||||
|
func (display *MapDisplay) unprojectPixel(pixelX, pixelY float32) (x, y float32) {
|
||||||
|
pixelVec := mgl.Vec4{pixelX, pixelY, 0.0, 1.0}
|
||||||
|
invertedView := display.camera.ViewMatrix().Inv()
|
||||||
|
result := invertedView.Mul4x1(pixelVec)
|
||||||
|
|
||||||
|
return result[0], result[1]
|
||||||
|
}
|
||||||
|
|
||||||
|
// MouseButtonDown must be called when a button was pressed.
|
||||||
|
func (display *MapDisplay) MouseButtonDown(mouseX, mouseY float32, button uint32) {
|
||||||
|
display.updateMouseWorldPosition(mouseX, mouseY)
|
||||||
|
if button == input.MousePrimary {
|
||||||
|
lastPixelX, lastPixelY := mouseX, mouseY
|
||||||
|
|
||||||
|
display.mouseMoved = false
|
||||||
|
display.moveCapture = func(pixelX, pixelY float32) {
|
||||||
|
lastWorldX, lastWorldY := display.unprojectPixel(lastPixelX, lastPixelY)
|
||||||
|
worldX, worldY := display.unprojectPixel(pixelX, pixelY)
|
||||||
|
|
||||||
|
display.camera.MoveBy(worldX-lastWorldX, worldY-lastWorldY)
|
||||||
|
lastPixelX, lastPixelY = pixelX, pixelY
|
||||||
|
display.mouseMoved = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// MouseButtonUp must be called when a button was released.
|
||||||
|
func (display *MapDisplay) MouseButtonUp(mouseX, mouseY float32, button uint32, modifier input.Modifier) {
|
||||||
|
display.updateMouseWorldPosition(mouseX, mouseY)
|
||||||
|
if button == input.MousePrimary {
|
||||||
|
display.moveCapture = func(float32, float32) {}
|
||||||
|
if !display.mouseMoved && display.positionValid {
|
||||||
|
switch {
|
||||||
|
case modifier.Has(input.ModControl):
|
||||||
|
display.toggleSelectionAtActiveHoverItem()
|
||||||
|
case modifier.Has(input.ModShift) && (display.selectionReference != nil):
|
||||||
|
firstPos := *display.selectionReference
|
||||||
|
|
||||||
|
fromX := int(firstPos.X)
|
||||||
|
fromY := int(firstPos.Y)
|
||||||
|
toX := int(display.position.X.Tile())
|
||||||
|
toY := int(display.position.Y.Tile())
|
||||||
|
xIncrement := 1
|
||||||
|
yIncrement := 1
|
||||||
|
if fromX > toX {
|
||||||
|
xIncrement = -1
|
||||||
|
}
|
||||||
|
if fromY > toY {
|
||||||
|
yIncrement = -1
|
||||||
|
}
|
||||||
|
toX += xIncrement
|
||||||
|
toY += yIncrement
|
||||||
|
var newList []level.TilePosition
|
||||||
|
for y := fromY; y != toY; y += yIncrement {
|
||||||
|
for x := fromX; x != toX; x += xIncrement {
|
||||||
|
newList = append(newList, level.TilePosition{X: byte(x), Y: byte(y)})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
display.levelSelection.SetCurrentSelectedTiles(newList)
|
||||||
|
display.levelSelection.SetCurrentSelectedObjects(display.objectsInTiles(newList))
|
||||||
|
default:
|
||||||
|
display.setSelectionByActiveHoverItem()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else if button == input.MouseSecondary {
|
||||||
|
if modifier.IsClear() {
|
||||||
|
display.contextMenuRequested = true
|
||||||
|
} else if modifier.Has(input.ModControl) {
|
||||||
|
display.requestCreateNewObject(modifier.Has(input.ModShift), display.editor.NewObjectTriple())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (display *MapDisplay) canCreateObjectOf(class object.Class) bool {
|
||||||
|
if display.editor.IsReadOnly() {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
lvl := display.editor.Level()
|
||||||
|
return lvl.HasRoomForObjectOf(class) && display.positionValid
|
||||||
|
}
|
||||||
|
|
||||||
|
func (display *MapDisplay) requestCreateNewObject(snapToGrid bool, triple object.Triple) {
|
||||||
|
if !display.canCreateObjectOf(triple.Class) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
pos := display.position
|
||||||
|
if snapToGrid {
|
||||||
|
toGrid := func(value byte) byte {
|
||||||
|
cornerSnapDistance := level.FineCoordinatesPerTileSide / 4
|
||||||
|
switch {
|
||||||
|
case value < byte(cornerSnapDistance):
|
||||||
|
return 0x00
|
||||||
|
case value >= byte(level.FineCoordinatesPerTileSide-cornerSnapDistance):
|
||||||
|
return level.FineCoordinatesPerTileSide - 1
|
||||||
|
default:
|
||||||
|
return level.FineCoordinatesPerTileSide / 2
|
||||||
|
}
|
||||||
|
}
|
||||||
|
pos.X = level.CoordinateAt(pos.X.Tile(), toGrid(pos.X.Fine()))
|
||||||
|
pos.Y = level.CoordinateAt(pos.Y.Tile(), toGrid(pos.Y.Fine()))
|
||||||
|
}
|
||||||
|
err := display.editor.CreateNewObject(triple, func(obj *level.ObjectMainEntry) {
|
||||||
|
obj.X = pos.X
|
||||||
|
obj.Y = pos.Y
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (display *MapDisplay) setSelectionByActiveHoverItem() {
|
||||||
|
var tiles []level.TilePosition
|
||||||
|
var objects []level.ObjectID
|
||||||
|
if display.hoverItems.activeItem != nil {
|
||||||
|
if tileItem, isTile := display.hoverItems.activeItem.(tileHoverItem); isTile {
|
||||||
|
tiles = append(tiles, tileItem.pos.Tile())
|
||||||
|
} else if objectItem, isObject := display.hoverItems.activeItem.(objectHoverItem); isObject {
|
||||||
|
objects = append(objects, objectItem.id)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
display.levelSelection.SetCurrentSelectedTiles(tiles)
|
||||||
|
if len(tiles) > 0 {
|
||||||
|
display.selectionReference = &tiles[0]
|
||||||
|
display.levelSelection.SetCurrentSelectedObjects(display.objectsInTiles(tiles))
|
||||||
|
} else {
|
||||||
|
display.levelSelection.SetCurrentSelectedObjects(objects)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (display *MapDisplay) toggleSelectionAtActiveHoverItem() {
|
||||||
|
if display.hoverItems.activeItem != nil {
|
||||||
|
if tileItem, isTile := display.hoverItems.activeItem.(tileHoverItem); isTile {
|
||||||
|
wasSelected := display.levelSelection.IsTileSelected(tileItem.pos.Tile())
|
||||||
|
tiles := []level.TilePosition{tileItem.pos.Tile()}
|
||||||
|
if wasSelected {
|
||||||
|
display.levelSelection.RemoveCurrentSelectedTiles(tiles)
|
||||||
|
display.levelSelection.RemoveCurrentSelectedObjects(display.objectsInTiles(tiles))
|
||||||
|
} else {
|
||||||
|
display.levelSelection.AddCurrentSelectedTiles(tiles)
|
||||||
|
display.levelSelection.AddCurrentSelectedObjects(display.objectsInTiles(tiles))
|
||||||
|
}
|
||||||
|
} else if objectItem, isObject := display.hoverItems.activeItem.(objectHoverItem); isObject {
|
||||||
|
display.levelSelection.ToggleObjectSelection([]level.ObjectID{objectItem.id})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (display *MapDisplay) objectsInTiles(tiles []level.TilePosition) []level.ObjectID {
|
||||||
|
tilesContain := func(pos level.TilePosition) bool {
|
||||||
|
for _, entry := range tiles {
|
||||||
|
if entry == pos {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
var objects []level.ObjectID
|
||||||
|
lvl := display.editor.Level()
|
||||||
|
lvl.ForEachObject(func(id level.ObjectID, entry level.ObjectMainEntry) {
|
||||||
|
if tilesContain(entry.TilePosition()) {
|
||||||
|
objects = append(objects, id)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
return objects
|
||||||
|
}
|
||||||
|
|
||||||
|
// MouseMoved must be called for a mouse move.
|
||||||
|
func (display *MapDisplay) MouseMoved(mouseX, mouseY float32) {
|
||||||
|
display.updateMouseWorldPosition(mouseX, mouseY)
|
||||||
|
if display.positionValid {
|
||||||
|
display.hoverItems.find(display.editor.Level(), display.position)
|
||||||
|
} else {
|
||||||
|
display.hoverItems.reset()
|
||||||
|
}
|
||||||
|
display.moveCapture(mouseX, mouseY)
|
||||||
|
}
|
||||||
|
|
||||||
|
// MouseScrolled must be called for a mouse scroll.
|
||||||
|
func (display *MapDisplay) MouseScrolled(mouseX, mouseY float32, deltaX, deltaY float32, modifier input.Modifier) {
|
||||||
|
if modifier.Has(input.ModControl) {
|
||||||
|
display.hoverItems.scroll(deltaY > 0)
|
||||||
|
} else {
|
||||||
|
worldX, worldY := display.unprojectPixel(mouseX, mouseY)
|
||||||
|
|
||||||
|
if deltaY < 0 {
|
||||||
|
display.camera.ZoomAt(-0.5, worldX, worldY)
|
||||||
|
}
|
||||||
|
if deltaY > 0 {
|
||||||
|
display.camera.ZoomAt(0.5, worldX, worldY)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (display *MapDisplay) updateMouseWorldPosition(mouseX, mouseY float32) {
|
||||||
|
worldX, worldY := display.unprojectPixel(mouseX, mouseY)
|
||||||
|
lvl := display.editor.Level()
|
||||||
|
width, height, _ := lvl.Size()
|
||||||
|
worldWidth := float32(width) * level.FineCoordinatesPerTileSide
|
||||||
|
worldHeight := float32(height) * level.FineCoordinatesPerTileSide
|
||||||
|
|
||||||
|
display.positionValid = (worldX >= 0.0) && (worldX < worldWidth) && (worldY >= 0.0) && (worldY < worldHeight)
|
||||||
|
if display.positionValid {
|
||||||
|
display.position = MapPosition{X: level.Coordinate(worldX + 0.5), Y: level.Coordinate(worldY + 0.5)}
|
||||||
|
} else {
|
||||||
|
display.position = MapPosition{}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,312 @@
|
|||||||
|
package levels
|
||||||
|
|
||||||
|
import (
|
||||||
|
mgl "github.com/go-gl/mathgl/mgl32"
|
||||||
|
|
||||||
|
"github.com/inkyblackness/hacked/editor/render"
|
||||||
|
"github.com/inkyblackness/hacked/ss1/content/archive/level"
|
||||||
|
"github.com/inkyblackness/hacked/ui/opengl"
|
||||||
|
)
|
||||||
|
|
||||||
|
var mapTileGridVertexShaderSource = `
|
||||||
|
#version 150
|
||||||
|
precision mediump float;
|
||||||
|
|
||||||
|
in vec4 vertexPosition;
|
||||||
|
out float colorAlpha;
|
||||||
|
|
||||||
|
uniform mat4 alphaMatrix;
|
||||||
|
|
||||||
|
uniform mat4 modelMatrix;
|
||||||
|
uniform mat4 viewMatrix;
|
||||||
|
uniform mat4 projectionMatrix;
|
||||||
|
|
||||||
|
void main(void) {
|
||||||
|
gl_Position = projectionMatrix * viewMatrix * modelMatrix * vec4(vertexPosition.xy, 0.0, 1.0);
|
||||||
|
colorAlpha = alphaMatrix[int(vertexPosition.z)][int(vertexPosition.w)];
|
||||||
|
}
|
||||||
|
`
|
||||||
|
|
||||||
|
var mapTileGridFragmentShaderSource = `
|
||||||
|
#version 150
|
||||||
|
precision mediump float;
|
||||||
|
|
||||||
|
uniform vec4 color;
|
||||||
|
in float colorAlpha;
|
||||||
|
out vec4 fragColor;
|
||||||
|
|
||||||
|
void main(void) {
|
||||||
|
fragColor = vec4(color.rgb, color.a*colorAlpha);
|
||||||
|
}
|
||||||
|
`
|
||||||
|
|
||||||
|
// TileMapper returns basic information to draw a 2D map.
|
||||||
|
type TileMapper interface {
|
||||||
|
MapGridInfo(pos level.TilePosition) (level.TileType, level.TileSlopeControl, level.WallHeights)
|
||||||
|
}
|
||||||
|
|
||||||
|
// MapGrid renders the grid of the map, based on calculated wall heights.
|
||||||
|
type MapGrid struct {
|
||||||
|
context *render.Context
|
||||||
|
|
||||||
|
program uint32
|
||||||
|
vao *opengl.VertexArrayObject
|
||||||
|
vertexPositionBuffer uint32
|
||||||
|
tickVertexPositionBuffer uint32
|
||||||
|
vertexPositionAttrib int32
|
||||||
|
colorUniform opengl.Vector4Uniform
|
||||||
|
|
||||||
|
/*
|
||||||
|
The alpha matrix is set up to specify the alpha value used for the wall sections.
|
||||||
|
There are four cardinal walls with three sections each, plus two diagonal walls (single section), as well
|
||||||
|
as the slope-ticks that re-use the shader.
|
||||||
|
The matrix is meant to be used like this:
|
||||||
|
N N N D1
|
||||||
|
E E E D2
|
||||||
|
S S S -
|
||||||
|
W W W T
|
||||||
|
D1 and D2 being the two diagonals, and T specifying the cell for the tick-marks.
|
||||||
|
The vertex buffers are an array of v4 entries, with z/w specifying the row and column index to take.
|
||||||
|
*/
|
||||||
|
alphaMatrixUniform opengl.Matrix4Uniform
|
||||||
|
modelMatrixUniform opengl.Matrix4Uniform
|
||||||
|
viewMatrixUniform opengl.Matrix4Uniform
|
||||||
|
projectionMatrixUniform opengl.Matrix4Uniform
|
||||||
|
|
||||||
|
slopeTicksByType [][]int
|
||||||
|
floorTickStarts []int32
|
||||||
|
ceilingTickStarts []int32
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewMapGrid returns a new instance.
|
||||||
|
func NewMapGrid(context *render.Context) *MapGrid {
|
||||||
|
gl := context.OpenGL
|
||||||
|
program, programErr := opengl.LinkNewStandardProgram(gl, mapTileGridVertexShaderSource, mapTileGridFragmentShaderSource)
|
||||||
|
if programErr != nil {
|
||||||
|
panic(opengl.NamedShaderError{Name: "MapGrid", Nested: programErr})
|
||||||
|
}
|
||||||
|
grid := &MapGrid{
|
||||||
|
context: context,
|
||||||
|
program: program,
|
||||||
|
vao: opengl.NewVertexArrayObject(gl, program),
|
||||||
|
vertexPositionBuffer: gl.GenBuffers(1)[0],
|
||||||
|
tickVertexPositionBuffer: gl.GenBuffers(1)[0],
|
||||||
|
vertexPositionAttrib: gl.GetAttribLocation(program, "vertexPosition"),
|
||||||
|
colorUniform: opengl.Vector4Uniform(gl.GetUniformLocation(program, "color")),
|
||||||
|
alphaMatrixUniform: opengl.Matrix4Uniform(gl.GetUniformLocation(program, "alphaMatrix")),
|
||||||
|
modelMatrixUniform: opengl.Matrix4Uniform(gl.GetUniformLocation(program, "modelMatrix")),
|
||||||
|
viewMatrixUniform: opengl.Matrix4Uniform(gl.GetUniformLocation(program, "viewMatrix")),
|
||||||
|
projectionMatrixUniform: opengl.Matrix4Uniform(gl.GetUniformLocation(program, "projectionMatrix")),
|
||||||
|
}
|
||||||
|
|
||||||
|
{
|
||||||
|
tickVertices := grid.calculateTickVertices()
|
||||||
|
gl.BindBuffer(opengl.ARRAY_BUFFER, grid.tickVertexPositionBuffer)
|
||||||
|
gl.BufferData(opengl.ARRAY_BUFFER, len(tickVertices)*4, tickVertices, opengl.STATIC_DRAW)
|
||||||
|
gl.BindBuffer(opengl.ARRAY_BUFFER, 0)
|
||||||
|
}
|
||||||
|
{
|
||||||
|
const parts = 3
|
||||||
|
left := float32(-0.5)
|
||||||
|
right := float32(0.5)
|
||||||
|
top := float32(0.5)
|
||||||
|
bottom := float32(-0.5)
|
||||||
|
finePerPart := float32(1.0 / parts)
|
||||||
|
|
||||||
|
var wallVertices []float32
|
||||||
|
for i := 0; i < parts; i++ {
|
||||||
|
wallVertices = append(wallVertices, left+(finePerPart*float32(i)), top, 0.0, float32(i))
|
||||||
|
wallVertices = append(wallVertices, left+(finePerPart*float32(i+1)), top, 0.0, float32(i))
|
||||||
|
}
|
||||||
|
for i := 0; i < parts; i++ {
|
||||||
|
wallVertices = append(wallVertices, right, top-(finePerPart*float32(i)), 1.0, float32(i))
|
||||||
|
wallVertices = append(wallVertices, right, top-(finePerPart*float32(i+1)), 1.0, float32(i))
|
||||||
|
}
|
||||||
|
for i := 0; i < parts; i++ {
|
||||||
|
wallVertices = append(wallVertices, right-(finePerPart*float32(i)), bottom, 2.0, float32(i))
|
||||||
|
wallVertices = append(wallVertices, right-(finePerPart*float32(i+1)), bottom, 2.0, float32(i))
|
||||||
|
}
|
||||||
|
for i := 0; i < parts; i++ {
|
||||||
|
wallVertices = append(wallVertices, left, bottom+(finePerPart*float32(i)), 3.0, float32(i))
|
||||||
|
wallVertices = append(wallVertices, left, bottom+(finePerPart*float32(i+1)), 3.0, float32(i))
|
||||||
|
}
|
||||||
|
wallVertices = append(wallVertices, left, top, 0.0, 3.0)
|
||||||
|
wallVertices = append(wallVertices, right, bottom, 0.0, 3.0)
|
||||||
|
wallVertices = append(wallVertices, right, top, 1.0, 3.0)
|
||||||
|
wallVertices = append(wallVertices, left, bottom, 1.0, 3.0)
|
||||||
|
|
||||||
|
gl.BindBuffer(opengl.ARRAY_BUFFER, grid.vertexPositionBuffer)
|
||||||
|
gl.BufferData(opengl.ARRAY_BUFFER, len(wallVertices)*4, wallVertices, opengl.STATIC_DRAW)
|
||||||
|
gl.BindBuffer(opengl.ARRAY_BUFFER, 0)
|
||||||
|
}
|
||||||
|
{
|
||||||
|
grid.slopeTicksByType = [][]int{
|
||||||
|
nil,
|
||||||
|
nil,
|
||||||
|
|
||||||
|
nil,
|
||||||
|
nil,
|
||||||
|
nil,
|
||||||
|
nil,
|
||||||
|
|
||||||
|
{0, 1},
|
||||||
|
{1, 2},
|
||||||
|
{2, 3},
|
||||||
|
{3, 0},
|
||||||
|
|
||||||
|
{3, 0, 1},
|
||||||
|
{0, 1, 2},
|
||||||
|
{1, 2, 3},
|
||||||
|
{2, 3, 0},
|
||||||
|
|
||||||
|
{2},
|
||||||
|
{3},
|
||||||
|
{0},
|
||||||
|
{1},
|
||||||
|
}
|
||||||
|
grid.floorTickStarts = []int32{0, 6, 12, 18}
|
||||||
|
grid.ceilingTickStarts = []int32{3, 9, 15, 21}
|
||||||
|
}
|
||||||
|
|
||||||
|
return grid
|
||||||
|
}
|
||||||
|
|
||||||
|
// Dispose releases any internal resources.
|
||||||
|
func (grid *MapGrid) Dispose() {
|
||||||
|
gl := grid.context.OpenGL
|
||||||
|
gl.DeleteProgram(grid.program)
|
||||||
|
gl.DeleteBuffers([]uint32{grid.vertexPositionBuffer, grid.tickVertexPositionBuffer})
|
||||||
|
grid.vao.Dispose()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (grid *MapGrid) calculateTickVertices() []float32 {
|
||||||
|
dotHalf := float32(0.05)
|
||||||
|
dotBase := float32(0.5) - (dotHalf * 2.0)
|
||||||
|
|
||||||
|
top := dotBase + dotHalf
|
||||||
|
topEnd := dotBase - dotHalf
|
||||||
|
left := -dotBase - dotHalf
|
||||||
|
leftEnd := -dotBase + dotHalf
|
||||||
|
|
||||||
|
right := top
|
||||||
|
rightEnd := topEnd
|
||||||
|
bottom := left
|
||||||
|
bottomEnd := leftEnd
|
||||||
|
|
||||||
|
return []float32{
|
||||||
|
left, top, 3.0, 3.0, leftEnd, top, 3.0, 3.0, left, topEnd, 3.0, 3.0,
|
||||||
|
leftEnd, top, 3.0, 3.0, leftEnd, topEnd, 3.0, 3.0, left, topEnd, 3.0, 3.0,
|
||||||
|
|
||||||
|
right, top, 3.0, 3.0, right, topEnd, 3.0, 3.0, rightEnd, top, 3.0, 3.0,
|
||||||
|
rightEnd, top, 3.0, 3.0, right, topEnd, 3.0, 3.0, rightEnd, topEnd, 3.0, 3.0,
|
||||||
|
|
||||||
|
right, bottom, 3.0, 3.0, rightEnd, bottom, 3.0, 3.0, right, bottomEnd, 3.0, 3.0,
|
||||||
|
right, bottomEnd, 3.0, 3.0, rightEnd, bottom, 3.0, 3.0, rightEnd, bottomEnd, 3.0, 3.0,
|
||||||
|
|
||||||
|
left, bottomEnd, 3.0, 3.0, leftEnd, bottom, 3.0, 3.0, left, bottom, 3.0, 3.0,
|
||||||
|
left, bottomEnd, 3.0, 3.0, leftEnd, bottomEnd, 3.0, 3.0, leftEnd, bottom, 3.0, 3.0,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Render renders the grid.
|
||||||
|
func (grid *MapGrid) Render(columns, rows int, mapper TileMapper) {
|
||||||
|
gl := grid.context.OpenGL
|
||||||
|
|
||||||
|
grid.vao.OnShader(func() {
|
||||||
|
modelMatrix := mgl.Ident4()
|
||||||
|
lineColor := [4]float32{0.0, 0.8, 0.0, 1.0}
|
||||||
|
|
||||||
|
grid.viewMatrixUniform.Set(gl, grid.context.ViewMatrix)
|
||||||
|
grid.projectionMatrixUniform.Set(gl, &grid.context.ProjectionMatrix)
|
||||||
|
|
||||||
|
heightFactor := [32*2 + 1]float32{
|
||||||
|
0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0,
|
||||||
|
0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0,
|
||||||
|
0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0,
|
||||||
|
0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0,
|
||||||
|
0.0,
|
||||||
|
0.1, 0.2, 0.3, 0.3, 0.4, 0.4, 0.5, 0.7,
|
||||||
|
1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0,
|
||||||
|
1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0,
|
||||||
|
1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0,
|
||||||
|
}
|
||||||
|
|
||||||
|
for y := 0; y < rows; y++ {
|
||||||
|
for x := 0; x < columns; x++ {
|
||||||
|
tileType, slopeControl, wallHeights := mapper.MapGridInfo(level.TilePosition{X: byte(x), Y: byte(y)})
|
||||||
|
if tileType == level.TileTypeSolid {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
modelMatrix = mgl.Ident4().
|
||||||
|
Mul4(mgl.Translate3D((float32(x)+0.5)*level.FineCoordinatesPerTileSide, (float32(y)+0.5)*level.FineCoordinatesPerTileSide, 0.0)).
|
||||||
|
Mul4(mgl.Scale3D(level.FineCoordinatesPerTileSide, level.FineCoordinatesPerTileSide, 1.0))
|
||||||
|
grid.modelMatrixUniform.Set(gl, &modelMatrix)
|
||||||
|
var alphaMatrix mgl.Mat4
|
||||||
|
alphaMatrix[15] = 1.0 // for tick marks
|
||||||
|
|
||||||
|
gl.EnableVertexAttribArray(uint32(grid.vertexPositionAttrib))
|
||||||
|
gl.BindBuffer(opengl.ARRAY_BUFFER, grid.vertexPositionBuffer)
|
||||||
|
gl.VertexAttribOffset(uint32(grid.vertexPositionAttrib), 4, opengl.FLOAT, false, 0, 0)
|
||||||
|
|
||||||
|
alphaMatrix[0+0] = heightFactor[int(wallHeights.North[0])+32]
|
||||||
|
alphaMatrix[0+1] = heightFactor[int(wallHeights.North[1])+32]
|
||||||
|
alphaMatrix[0+2] = heightFactor[int(wallHeights.North[2])+32]
|
||||||
|
|
||||||
|
alphaMatrix[4+0] = heightFactor[int(wallHeights.East[0])+32]
|
||||||
|
alphaMatrix[4+1] = heightFactor[int(wallHeights.East[1])+32]
|
||||||
|
alphaMatrix[4+2] = heightFactor[int(wallHeights.East[2])+32]
|
||||||
|
|
||||||
|
alphaMatrix[8+0] = heightFactor[int(wallHeights.South[0])+32]
|
||||||
|
alphaMatrix[8+1] = heightFactor[int(wallHeights.South[1])+32]
|
||||||
|
alphaMatrix[8+2] = heightFactor[int(wallHeights.South[2])+32]
|
||||||
|
|
||||||
|
alphaMatrix[12+0] = heightFactor[int(wallHeights.West[0])+32]
|
||||||
|
alphaMatrix[12+1] = heightFactor[int(wallHeights.West[1])+32]
|
||||||
|
alphaMatrix[12+2] = heightFactor[int(wallHeights.West[2])+32]
|
||||||
|
if tileType == level.TileTypeDiagonalOpenNorthEast || tileType == level.TileTypeDiagonalOpenSouthWest {
|
||||||
|
alphaMatrix[0+3] = 1.0
|
||||||
|
}
|
||||||
|
if tileType == level.TileTypeDiagonalOpenNorthWest || tileType == level.TileTypeDiagonalOpenSouthEast {
|
||||||
|
alphaMatrix[4+3] = 1.0
|
||||||
|
}
|
||||||
|
|
||||||
|
grid.colorUniform.Set(gl, &lineColor)
|
||||||
|
grid.alphaMatrixUniform.Set(gl, &alphaMatrix)
|
||||||
|
gl.DrawArrays(opengl.LINES, 0, 28)
|
||||||
|
|
||||||
|
grid.renderSlopeTicks(gl, tileType, slopeControl)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
gl.BindBuffer(opengl.ARRAY_BUFFER, 0)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func (grid *MapGrid) renderSlopeTicks(gl opengl.OpenGL, tileType level.TileType, slopeControl level.TileSlopeControl) {
|
||||||
|
var floorTicks []int
|
||||||
|
var ceilingTicks []int
|
||||||
|
|
||||||
|
if slopeControl != level.TileSlopeControlFloorFlat {
|
||||||
|
floorTicks = grid.slopeTicksByType[tileType]
|
||||||
|
}
|
||||||
|
if slopeControl == level.TileSlopeControlCeilingMirrored {
|
||||||
|
ceilingTicks = grid.slopeTicksByType[tileType]
|
||||||
|
} else if slopeControl != level.TileSlopeControlCeilingFlat {
|
||||||
|
ceilingTicks = grid.slopeTicksByType[tileType.Info().SlopeInvertedType]
|
||||||
|
}
|
||||||
|
|
||||||
|
gl.EnableVertexAttribArray(uint32(grid.vertexPositionAttrib))
|
||||||
|
gl.BindBuffer(opengl.ARRAY_BUFFER, grid.tickVertexPositionBuffer)
|
||||||
|
gl.VertexAttribOffset(uint32(grid.vertexPositionAttrib), 4, opengl.FLOAT, false, 0, 0)
|
||||||
|
|
||||||
|
floorTickColor := [4]float32{0.0, 0.8, 0.0, 0.9}
|
||||||
|
grid.colorUniform.Set(gl, &floorTickColor)
|
||||||
|
for _, index := range floorTicks {
|
||||||
|
gl.DrawArrays(opengl.TRIANGLES, grid.floorTickStarts[index], 3)
|
||||||
|
}
|
||||||
|
ceilingTickColor := [4]float32{0.8, 0.0, 0.0, 0.9}
|
||||||
|
grid.colorUniform.Set(gl, &ceilingTickColor)
|
||||||
|
for _, index := range ceilingTicks {
|
||||||
|
gl.DrawArrays(opengl.TRIANGLES, grid.ceilingTickStarts[index], 3)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,198 @@
|
|||||||
|
package levels
|
||||||
|
|
||||||
|
import (
|
||||||
|
mgl "github.com/go-gl/mathgl/mgl32"
|
||||||
|
|
||||||
|
"github.com/inkyblackness/hacked/editor/graphics"
|
||||||
|
"github.com/inkyblackness/hacked/editor/render"
|
||||||
|
"github.com/inkyblackness/hacked/ui/opengl"
|
||||||
|
)
|
||||||
|
|
||||||
|
var mapIconsVertexShaderSource = `
|
||||||
|
#version 150
|
||||||
|
precision mediump float;
|
||||||
|
|
||||||
|
in vec3 vertexPosition;
|
||||||
|
in vec3 uvPosition;
|
||||||
|
|
||||||
|
uniform mat4 modelMatrix;
|
||||||
|
uniform mat4 viewMatrix;
|
||||||
|
uniform mat4 projectionMatrix;
|
||||||
|
uniform vec4 uvLimit;
|
||||||
|
|
||||||
|
out vec2 uv;
|
||||||
|
|
||||||
|
void main(void) {
|
||||||
|
gl_Position = projectionMatrix * viewMatrix * modelMatrix * vec4(vertexPosition, 1.0);
|
||||||
|
|
||||||
|
uv = vec2(uvPosition.x * uvLimit.x, uvPosition.y * uvLimit.y);
|
||||||
|
}
|
||||||
|
`
|
||||||
|
|
||||||
|
var mapIconsFragmentShaderSource = `
|
||||||
|
#version 150
|
||||||
|
precision mediump float;
|
||||||
|
|
||||||
|
uniform sampler2D palette;
|
||||||
|
uniform sampler2D bitmap;
|
||||||
|
|
||||||
|
in vec2 uv;
|
||||||
|
out vec4 fragColor;
|
||||||
|
|
||||||
|
void main(void) {
|
||||||
|
vec4 pixel = texture(bitmap, uv);
|
||||||
|
|
||||||
|
if (pixel.r > 0.0) {
|
||||||
|
fragColor = texture(palette, vec2(pixel.r, 0.5));
|
||||||
|
} else {
|
||||||
|
discard;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`
|
||||||
|
|
||||||
|
// MapIcons is a renderable for simple bitmaps.
|
||||||
|
type MapIcons struct {
|
||||||
|
context *render.Context
|
||||||
|
|
||||||
|
program uint32
|
||||||
|
vao *opengl.VertexArrayObject
|
||||||
|
vertexPositionBuffer uint32
|
||||||
|
vertexPositionAttrib int32
|
||||||
|
uvPositionBuffer uint32
|
||||||
|
uvPositionAttrib int32
|
||||||
|
modelMatrixUniform opengl.Matrix4Uniform
|
||||||
|
viewMatrixUniform opengl.Matrix4Uniform
|
||||||
|
projectionMatrixUniform opengl.Matrix4Uniform
|
||||||
|
uvLimitUniform opengl.Vector4Uniform
|
||||||
|
|
||||||
|
paletteUniform int32
|
||||||
|
bitmapUniform int32
|
||||||
|
}
|
||||||
|
|
||||||
|
type iconData struct {
|
||||||
|
pos MapPosition
|
||||||
|
texture *graphics.BitmapTexture
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewMapIcons returns a new instance.
|
||||||
|
func NewMapIcons(context *render.Context) *MapIcons {
|
||||||
|
gl := context.OpenGL
|
||||||
|
program, programErr := opengl.LinkNewStandardProgram(gl, mapIconsVertexShaderSource, mapIconsFragmentShaderSource)
|
||||||
|
|
||||||
|
if programErr != nil {
|
||||||
|
panic(opengl.NamedShaderError{Name: "MapIconsShader", Nested: programErr})
|
||||||
|
}
|
||||||
|
renderable := &MapIcons{
|
||||||
|
context: context,
|
||||||
|
program: program,
|
||||||
|
|
||||||
|
vao: opengl.NewVertexArrayObject(gl, program),
|
||||||
|
vertexPositionBuffer: gl.GenBuffers(1)[0],
|
||||||
|
vertexPositionAttrib: gl.GetAttribLocation(program, "vertexPosition"),
|
||||||
|
uvPositionBuffer: gl.GenBuffers(1)[0],
|
||||||
|
uvPositionAttrib: gl.GetAttribLocation(program, "uvPosition"),
|
||||||
|
modelMatrixUniform: opengl.Matrix4Uniform(gl.GetUniformLocation(program, "modelMatrix")),
|
||||||
|
viewMatrixUniform: opengl.Matrix4Uniform(gl.GetUniformLocation(program, "viewMatrix")),
|
||||||
|
projectionMatrixUniform: opengl.Matrix4Uniform(gl.GetUniformLocation(program, "projectionMatrix")),
|
||||||
|
uvLimitUniform: opengl.Vector4Uniform(gl.GetUniformLocation(program, "uvLimit")),
|
||||||
|
|
||||||
|
paletteUniform: gl.GetUniformLocation(program, "palette"),
|
||||||
|
bitmapUniform: gl.GetUniformLocation(program, "bitmap")}
|
||||||
|
|
||||||
|
{
|
||||||
|
half := float32(0.5)
|
||||||
|
var vertices = []float32{
|
||||||
|
-half, half, 0.0,
|
||||||
|
half, half, 0.0,
|
||||||
|
half, -half, 0.0,
|
||||||
|
|
||||||
|
half, -half, 0.0,
|
||||||
|
-half, -half, 0.0,
|
||||||
|
-half, half, 0.0,
|
||||||
|
}
|
||||||
|
gl.BindBuffer(opengl.ARRAY_BUFFER, renderable.vertexPositionBuffer)
|
||||||
|
gl.BufferData(opengl.ARRAY_BUFFER, len(vertices)*4, vertices, opengl.STATIC_DRAW)
|
||||||
|
gl.BindBuffer(opengl.ARRAY_BUFFER, 0)
|
||||||
|
}
|
||||||
|
{
|
||||||
|
var uv = []float32{
|
||||||
|
0.0, 0.0, 0.0,
|
||||||
|
1.0, 0.0, 0.0,
|
||||||
|
1.0, 1.0, 0.0,
|
||||||
|
|
||||||
|
1.0, 1.0, 0.0,
|
||||||
|
0.0, 1.0, 0.0,
|
||||||
|
0.0, 0.0, 0.0,
|
||||||
|
}
|
||||||
|
gl.BindBuffer(opengl.ARRAY_BUFFER, renderable.uvPositionBuffer)
|
||||||
|
gl.BufferData(opengl.ARRAY_BUFFER, len(uv)*4, uv, opengl.STATIC_DRAW)
|
||||||
|
gl.BindBuffer(opengl.ARRAY_BUFFER, 0)
|
||||||
|
}
|
||||||
|
renderable.vao.WithSetter(func(gl opengl.OpenGL) {
|
||||||
|
gl.EnableVertexAttribArray(uint32(renderable.vertexPositionAttrib))
|
||||||
|
gl.BindBuffer(opengl.ARRAY_BUFFER, renderable.vertexPositionBuffer)
|
||||||
|
gl.VertexAttribOffset(uint32(renderable.vertexPositionAttrib), 3, opengl.FLOAT, false, 0, 0)
|
||||||
|
gl.EnableVertexAttribArray(uint32(renderable.uvPositionAttrib))
|
||||||
|
gl.BindBuffer(opengl.ARRAY_BUFFER, renderable.uvPositionBuffer)
|
||||||
|
gl.VertexAttribOffset(uint32(renderable.uvPositionAttrib), 3, opengl.FLOAT, false, 0, 0)
|
||||||
|
gl.BindBuffer(opengl.ARRAY_BUFFER, 0)
|
||||||
|
})
|
||||||
|
|
||||||
|
return renderable
|
||||||
|
}
|
||||||
|
|
||||||
|
// Render renders the icons with their center at given position.
|
||||||
|
func (renderable *MapIcons) Render(paletteTexture *graphics.PaletteTexture, iconSize float32, icons []iconData) {
|
||||||
|
gl := renderable.context.OpenGL
|
||||||
|
|
||||||
|
renderable.vao.OnShader(func() {
|
||||||
|
renderable.viewMatrixUniform.Set(gl, renderable.context.ViewMatrix)
|
||||||
|
renderable.projectionMatrixUniform.Set(gl, &renderable.context.ProjectionMatrix)
|
||||||
|
|
||||||
|
textureUnit := int32(0)
|
||||||
|
gl.ActiveTexture(opengl.TEXTURE0 + uint32(textureUnit))
|
||||||
|
gl.BindTexture(opengl.TEXTURE_2D, paletteTexture.Handle())
|
||||||
|
gl.Uniform1i(renderable.paletteUniform, textureUnit)
|
||||||
|
|
||||||
|
textureUnit = 1
|
||||||
|
gl.ActiveTexture(opengl.TEXTURE0 + uint32(textureUnit))
|
||||||
|
gl.Uniform1i(renderable.bitmapUniform, textureUnit)
|
||||||
|
for _, icon := range icons {
|
||||||
|
x, y := float32(icon.pos.X), float32(icon.pos.Y)
|
||||||
|
u, v := icon.texture.UV()
|
||||||
|
width, height := renderable.limitedSize(iconSize, icon.texture)
|
||||||
|
modelMatrix := mgl.Ident4().
|
||||||
|
Mul4(mgl.Translate3D(x, y, 0.0)).
|
||||||
|
Mul4(mgl.Scale3D(width, height, 1.0))
|
||||||
|
|
||||||
|
renderable.modelMatrixUniform.Set(gl, &modelMatrix)
|
||||||
|
|
||||||
|
uvLimit := [4]float32{u, v, 0, 0}
|
||||||
|
renderable.uvLimitUniform.Set(gl, &uvLimit)
|
||||||
|
|
||||||
|
gl.BindTexture(opengl.TEXTURE_2D, icon.texture.Handle())
|
||||||
|
gl.DrawArrays(opengl.TRIANGLES, 0, 6)
|
||||||
|
}
|
||||||
|
gl.BindTexture(opengl.TEXTURE_2D, 0)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func (renderable *MapIcons) limitedSize(iconSize float32, texture *graphics.BitmapTexture) (width, height float32) {
|
||||||
|
width, height = texture.Size()
|
||||||
|
referenceSize := float32(16.0)
|
||||||
|
larger := width
|
||||||
|
|
||||||
|
if larger < height {
|
||||||
|
larger = height
|
||||||
|
}
|
||||||
|
if larger > referenceSize {
|
||||||
|
ratio := referenceSize / larger
|
||||||
|
width *= ratio
|
||||||
|
height *= ratio
|
||||||
|
}
|
||||||
|
factor := iconSize / referenceSize
|
||||||
|
width *= factor
|
||||||
|
height *= factor
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
@@ -0,0 +1,19 @@
|
|||||||
|
package levels
|
||||||
|
|
||||||
|
import "github.com/inkyblackness/hacked/ss1/content/archive/level"
|
||||||
|
|
||||||
|
// MapPosition describes a specific two-dimensional point on the map.
|
||||||
|
type MapPosition struct {
|
||||||
|
X level.Coordinate
|
||||||
|
Y level.Coordinate
|
||||||
|
}
|
||||||
|
|
||||||
|
// Tile returns the tile position of the exact position.
|
||||||
|
func (pos MapPosition) Tile() level.TilePosition {
|
||||||
|
return level.TilePosition{X: pos.X.Tile(), Y: pos.Y.Tile()}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fine returns the fine position of the exact position.
|
||||||
|
func (pos MapPosition) Fine() level.FinePosition {
|
||||||
|
return level.FinePosition{X: pos.X.Fine(), Y: pos.Y.Fine()}
|
||||||
|
}
|
||||||
@@ -0,0 +1,255 @@
|
|||||||
|
package levels
|
||||||
|
|
||||||
|
import (
|
||||||
|
"math"
|
||||||
|
|
||||||
|
mgl "github.com/go-gl/mathgl/mgl32"
|
||||||
|
|
||||||
|
"github.com/inkyblackness/hacked/editor/graphics"
|
||||||
|
"github.com/inkyblackness/hacked/editor/render"
|
||||||
|
"github.com/inkyblackness/hacked/ss1/content/archive/level"
|
||||||
|
"github.com/inkyblackness/hacked/ui/opengl"
|
||||||
|
)
|
||||||
|
|
||||||
|
var mapTexturesVertexShaderSource = `
|
||||||
|
#version 150
|
||||||
|
precision mediump float;
|
||||||
|
|
||||||
|
in vec3 vertexPosition;
|
||||||
|
|
||||||
|
uniform mat4 modelMatrix;
|
||||||
|
uniform mat4 viewMatrix;
|
||||||
|
uniform mat4 projectionMatrix;
|
||||||
|
uniform mat4 uvMatrix;
|
||||||
|
|
||||||
|
out vec2 uv;
|
||||||
|
|
||||||
|
void main(void) {
|
||||||
|
gl_Position = projectionMatrix * viewMatrix * modelMatrix * vec4(vertexPosition, 1.0);
|
||||||
|
|
||||||
|
uv = (uvMatrix * vec4(vertexPosition, 1.0)).xy;
|
||||||
|
}
|
||||||
|
`
|
||||||
|
|
||||||
|
var mapTexturesFragmentShaderSource = `
|
||||||
|
#version 150
|
||||||
|
precision mediump float;
|
||||||
|
|
||||||
|
in vec2 uv;
|
||||||
|
|
||||||
|
uniform sampler2D palette;
|
||||||
|
uniform sampler2D bitmap;
|
||||||
|
|
||||||
|
out vec4 fragColor;
|
||||||
|
|
||||||
|
void main(void) {
|
||||||
|
vec4 pixel = texture(bitmap, uv);
|
||||||
|
|
||||||
|
fragColor = texture(palette, vec2(pixel.r, 0.5));
|
||||||
|
}
|
||||||
|
`
|
||||||
|
|
||||||
|
// TextureQuery is a getter function to retrieve the texture for the given
|
||||||
|
// level texture index.
|
||||||
|
type TextureQuery func(index level.TextureIndex) (*graphics.BitmapTexture, error)
|
||||||
|
|
||||||
|
// TileTextureQuery is a getter function to retrieve properties for rendering a texture of a tile.
|
||||||
|
type TileTextureQuery func(pos level.TilePosition) (tileType level.TileType, textureIndex level.TextureIndex, textureRotations int)
|
||||||
|
|
||||||
|
// MapTextures is a renderable for textures.
|
||||||
|
type MapTextures struct {
|
||||||
|
context *render.Context
|
||||||
|
|
||||||
|
program uint32
|
||||||
|
vao *opengl.VertexArrayObject
|
||||||
|
vertexPositionBuffer uint32
|
||||||
|
vertexPositionAttrib int32
|
||||||
|
modelMatrixUniform opengl.Matrix4Uniform
|
||||||
|
viewMatrixUniform opengl.Matrix4Uniform
|
||||||
|
projectionMatrixUniform opengl.Matrix4Uniform
|
||||||
|
uvMatrixUniform opengl.Matrix4Uniform
|
||||||
|
|
||||||
|
paletteUniform int32
|
||||||
|
bitmapUniform int32
|
||||||
|
|
||||||
|
textureQuery TextureQuery
|
||||||
|
|
||||||
|
lastTileType level.TileType
|
||||||
|
}
|
||||||
|
|
||||||
|
type tileTypeVertices struct {
|
||||||
|
vertices []float32
|
||||||
|
equivalentType level.TileType
|
||||||
|
}
|
||||||
|
|
||||||
|
var tileTypeVerticesLookup []tileTypeVertices
|
||||||
|
var uvRotations [4]*mgl.Mat4
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
for i := 0; i < 4; i++ {
|
||||||
|
matrix := mgl.Translate3D(0.5, 0.5, 0.0).
|
||||||
|
Mul4(mgl.HomogRotate3DZ(math.Pi * float32(i) / -2.0)).
|
||||||
|
Mul4(mgl.Translate3D(-0.5, -0.5, 0.0)).
|
||||||
|
Mul4(mgl.Scale3D(1.0, -1.0, 1.0))
|
||||||
|
uvRotations[i] = &matrix
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tileType := range level.TileTypes() {
|
||||||
|
for int(tileType) >= len(tileTypeVerticesLookup) {
|
||||||
|
tileTypeVerticesLookup = append(tileTypeVerticesLookup, tileTypeVertices{equivalentType: level.TileTypeSolid})
|
||||||
|
}
|
||||||
|
switch tileType {
|
||||||
|
case level.TileTypeSolid:
|
||||||
|
case level.TileTypeDiagonalOpenNorthEast:
|
||||||
|
tileTypeVerticesLookup[int(tileType)] = tileTypeVertices{
|
||||||
|
vertices: []float32{
|
||||||
|
0.0, 1.0, 0.0,
|
||||||
|
1.0, 1.0, 0.0,
|
||||||
|
1.0, 0.0, 0.0,
|
||||||
|
},
|
||||||
|
equivalentType: level.TileTypeDiagonalOpenNorthEast,
|
||||||
|
}
|
||||||
|
case level.TileTypeDiagonalOpenNorthWest:
|
||||||
|
tileTypeVerticesLookup[int(tileType)] = tileTypeVertices{
|
||||||
|
vertices: []float32{
|
||||||
|
0.0, 1.0, 0.0,
|
||||||
|
1.0, 1.0, 0.0,
|
||||||
|
0.0, 0.0, 0.0,
|
||||||
|
},
|
||||||
|
equivalentType: level.TileTypeDiagonalOpenNorthWest,
|
||||||
|
}
|
||||||
|
case level.TileTypeDiagonalOpenSouthEast:
|
||||||
|
tileTypeVerticesLookup[int(tileType)] = tileTypeVertices{
|
||||||
|
vertices: []float32{
|
||||||
|
1.0, 1.0, 0.0,
|
||||||
|
1.0, 0.0, 0.0,
|
||||||
|
0.0, 0.0, 0.0,
|
||||||
|
},
|
||||||
|
equivalentType: level.TileTypeDiagonalOpenSouthEast,
|
||||||
|
}
|
||||||
|
case level.TileTypeDiagonalOpenSouthWest:
|
||||||
|
tileTypeVerticesLookup[int(tileType)] = tileTypeVertices{
|
||||||
|
vertices: []float32{
|
||||||
|
0.0, 1.0, 0.0,
|
||||||
|
1.0, 0.0, 0.0,
|
||||||
|
0.0, 0.0, 0.0,
|
||||||
|
},
|
||||||
|
equivalentType: level.TileTypeDiagonalOpenSouthWest,
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
tileTypeVerticesLookup[int(tileType)] = tileTypeVertices{
|
||||||
|
vertices: []float32{
|
||||||
|
0.0, 0.0, 0.0,
|
||||||
|
1.0, 0.0, 0.0,
|
||||||
|
1.0, 1.0, 0.0,
|
||||||
|
|
||||||
|
1.0, 1.0, 0.0,
|
||||||
|
0.0, 1.0, 0.0,
|
||||||
|
0.0, 0.0, 0.0,
|
||||||
|
},
|
||||||
|
equivalentType: level.TileTypeOpen,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewMapTextures returns a new instance of a renderable for tile map textures.
|
||||||
|
func NewMapTextures(context *render.Context, textureQuery TextureQuery) *MapTextures {
|
||||||
|
gl := context.OpenGL
|
||||||
|
program, programErr := opengl.LinkNewStandardProgram(gl, mapTexturesVertexShaderSource, mapTexturesFragmentShaderSource)
|
||||||
|
if programErr != nil {
|
||||||
|
panic(opengl.NamedShaderError{Name: "MapTexturesShader", Nested: programErr})
|
||||||
|
}
|
||||||
|
renderable := &MapTextures{
|
||||||
|
context: context,
|
||||||
|
program: program,
|
||||||
|
|
||||||
|
vao: opengl.NewVertexArrayObject(gl, program),
|
||||||
|
vertexPositionBuffer: gl.GenBuffers(1)[0],
|
||||||
|
vertexPositionAttrib: gl.GetAttribLocation(program, "vertexPosition"),
|
||||||
|
modelMatrixUniform: opengl.Matrix4Uniform(gl.GetUniformLocation(program, "modelMatrix")),
|
||||||
|
viewMatrixUniform: opengl.Matrix4Uniform(gl.GetUniformLocation(program, "viewMatrix")),
|
||||||
|
projectionMatrixUniform: opengl.Matrix4Uniform(gl.GetUniformLocation(program, "projectionMatrix")),
|
||||||
|
uvMatrixUniform: opengl.Matrix4Uniform(gl.GetUniformLocation(program, "uvMatrix")),
|
||||||
|
paletteUniform: gl.GetUniformLocation(program, "palette"),
|
||||||
|
bitmapUniform: gl.GetUniformLocation(program, "bitmap"),
|
||||||
|
textureQuery: textureQuery,
|
||||||
|
lastTileType: level.TileTypeSolid,
|
||||||
|
}
|
||||||
|
|
||||||
|
renderable.vao.WithSetter(func(gl opengl.OpenGL) {
|
||||||
|
gl.EnableVertexAttribArray(uint32(renderable.vertexPositionAttrib))
|
||||||
|
gl.BindBuffer(opengl.ARRAY_BUFFER, renderable.vertexPositionBuffer)
|
||||||
|
gl.VertexAttribOffset(uint32(renderable.vertexPositionAttrib), 3, opengl.FLOAT, false, 0, 0)
|
||||||
|
gl.BindBuffer(opengl.ARRAY_BUFFER, 0)
|
||||||
|
})
|
||||||
|
|
||||||
|
return renderable
|
||||||
|
}
|
||||||
|
|
||||||
|
// Dispose releases any internal resources.
|
||||||
|
func (renderable *MapTextures) Dispose() {
|
||||||
|
gl := renderable.context.OpenGL
|
||||||
|
|
||||||
|
renderable.vao.Dispose()
|
||||||
|
gl.DeleteProgram(renderable.program)
|
||||||
|
gl.DeleteBuffers([]uint32{renderable.vertexPositionBuffer})
|
||||||
|
}
|
||||||
|
|
||||||
|
// Render renders the textures.
|
||||||
|
func (renderable *MapTextures) Render(columns, rows int, tileTextureQuery TileTextureQuery, paletteTexture *graphics.PaletteTexture) {
|
||||||
|
gl := renderable.context.OpenGL
|
||||||
|
|
||||||
|
renderable.vao.OnShader(func() {
|
||||||
|
renderable.viewMatrixUniform.Set(gl, renderable.context.ViewMatrix)
|
||||||
|
renderable.projectionMatrixUniform.Set(gl, &renderable.context.ProjectionMatrix)
|
||||||
|
|
||||||
|
textureUnit := int32(0)
|
||||||
|
gl.ActiveTexture(opengl.TEXTURE0 + uint32(textureUnit))
|
||||||
|
gl.BindTexture(opengl.TEXTURE_2D, paletteTexture.Handle())
|
||||||
|
gl.Uniform1i(renderable.paletteUniform, textureUnit)
|
||||||
|
|
||||||
|
textureUnit = 1
|
||||||
|
gl.ActiveTexture(opengl.TEXTURE0 + uint32(textureUnit))
|
||||||
|
gl.Uniform1i(renderable.bitmapUniform, textureUnit)
|
||||||
|
|
||||||
|
scaling := mgl.Scale3D(level.FineCoordinatesPerTileSide, level.FineCoordinatesPerTileSide, 1.0)
|
||||||
|
for y := 0; y < rows; y++ {
|
||||||
|
for x := 0; x < columns; x++ {
|
||||||
|
tileType, textureIndex, textureRotations := tileTextureQuery(level.TilePosition{X: byte(x), Y: byte(y)})
|
||||||
|
if tileType == level.TileTypeSolid {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
texture, _ := renderable.textureQuery(textureIndex)
|
||||||
|
if texture == nil {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
modelMatrix := mgl.Translate3D(float32(x)*level.FineCoordinatesPerTileSide, float32(y)*level.FineCoordinatesPerTileSide, 0.0).
|
||||||
|
Mul4(scaling)
|
||||||
|
|
||||||
|
uvMatrix := uvRotations[textureRotations]
|
||||||
|
renderable.uvMatrixUniform.Set(gl, uvMatrix)
|
||||||
|
renderable.modelMatrixUniform.Set(gl, &modelMatrix)
|
||||||
|
gl.BindTexture(opengl.TEXTURE_2D, texture.Handle())
|
||||||
|
renderable.renderTileType(tileType)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
gl.BindTexture(opengl.TEXTURE_2D, 0)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func (renderable *MapTextures) renderTileType(tileType level.TileType) {
|
||||||
|
info := tileTypeVerticesLookup[int(tileType)]
|
||||||
|
vertexCount := len(info.vertices) / 3
|
||||||
|
gl := renderable.context.OpenGL
|
||||||
|
if info.equivalentType != renderable.lastTileType {
|
||||||
|
renderable.lastTileType = info.equivalentType
|
||||||
|
|
||||||
|
gl.BindBuffer(opengl.ARRAY_BUFFER, renderable.vertexPositionBuffer)
|
||||||
|
gl.BufferData(opengl.ARRAY_BUFFER, len(info.vertices)*4, info.vertices, opengl.STATIC_DRAW)
|
||||||
|
gl.BindBuffer(opengl.ARRAY_BUFFER, 0)
|
||||||
|
}
|
||||||
|
gl.DrawArrays(opengl.TRIANGLES, 0, int32(vertexCount))
|
||||||
|
}
|
||||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,10 @@
|
|||||||
|
package levels
|
||||||
|
|
||||||
|
type objectsViewModel struct {
|
||||||
|
restoreFocus bool
|
||||||
|
windowOpen bool
|
||||||
|
}
|
||||||
|
|
||||||
|
func freshObjectsViewModel() objectsViewModel {
|
||||||
|
return objectsViewModel{}
|
||||||
|
}
|
||||||
@@ -0,0 +1,64 @@
|
|||||||
|
package levels
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
|
||||||
|
"github.com/inkyblackness/hacked/ss1/content/archive/level"
|
||||||
|
)
|
||||||
|
|
||||||
|
// TextureDisplay is an enumeration which texture to display in a 2D map view.
|
||||||
|
type TextureDisplay int
|
||||||
|
|
||||||
|
// TextureDisplay constants are listed below.
|
||||||
|
const (
|
||||||
|
TextureDisplayFloor TextureDisplay = 0
|
||||||
|
TextureDisplayWall TextureDisplay = 1
|
||||||
|
TextureDisplayCeiling TextureDisplay = 2
|
||||||
|
)
|
||||||
|
|
||||||
|
// String returns a textual representation.
|
||||||
|
func (display TextureDisplay) String() string {
|
||||||
|
switch display {
|
||||||
|
case TextureDisplayFloor:
|
||||||
|
return "Floor"
|
||||||
|
case TextureDisplayWall:
|
||||||
|
return "Wall"
|
||||||
|
case TextureDisplayCeiling:
|
||||||
|
return "Ceiling"
|
||||||
|
default:
|
||||||
|
return fmt.Sprintf("Unknown%d", int(display))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// TextureDisplays returns all TextureDisplay constants.
|
||||||
|
func TextureDisplays() []TextureDisplay {
|
||||||
|
return []TextureDisplay{TextureDisplayFloor, TextureDisplayWall, TextureDisplayCeiling}
|
||||||
|
}
|
||||||
|
|
||||||
|
// TextureDisplayFunc is one that resolves texture properties.
|
||||||
|
type TextureDisplayFunc func(*level.TileMapEntry) (atlasIndex level.AtlasIndex, textureRotations int)
|
||||||
|
|
||||||
|
// FloorTexture returns the properties for the floor.
|
||||||
|
func FloorTexture(tile *level.TileMapEntry) (atlasIndex level.AtlasIndex, textureRotations int) {
|
||||||
|
return tile.TextureInfo.FloorTextureIndex(), tile.Floor.TextureRotations()
|
||||||
|
}
|
||||||
|
|
||||||
|
// CeilingTexture returns the properties for the ceiling.
|
||||||
|
func CeilingTexture(tile *level.TileMapEntry) (atlasIndex level.AtlasIndex, textureRotations int) {
|
||||||
|
return tile.TextureInfo.CeilingTextureIndex(), tile.Ceiling.TextureRotations()
|
||||||
|
}
|
||||||
|
|
||||||
|
// WallTexture returns the properties for the floor.
|
||||||
|
func WallTexture(tile *level.TileMapEntry) (atlasIndex level.AtlasIndex, textureRotations int) {
|
||||||
|
return tile.TextureInfo.WallTextureIndex(), 0
|
||||||
|
}
|
||||||
|
|
||||||
|
// Func returns the display func for the current display setting.
|
||||||
|
func (display TextureDisplay) Func() TextureDisplayFunc {
|
||||||
|
if display == TextureDisplayFloor {
|
||||||
|
return FloorTexture
|
||||||
|
} else if display == TextureDisplayCeiling {
|
||||||
|
return CeilingTexture
|
||||||
|
}
|
||||||
|
return WallTexture
|
||||||
|
}
|
||||||
@@ -0,0 +1,551 @@
|
|||||||
|
package levels
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
|
||||||
|
"github.com/inkyblackness/imgui-go/v3"
|
||||||
|
|
||||||
|
"github.com/inkyblackness/hacked/editor/graphics"
|
||||||
|
"github.com/inkyblackness/hacked/editor/render"
|
||||||
|
"github.com/inkyblackness/hacked/editor/values"
|
||||||
|
"github.com/inkyblackness/hacked/ss1/content/archive/level"
|
||||||
|
"github.com/inkyblackness/hacked/ss1/content/bitmap"
|
||||||
|
"github.com/inkyblackness/hacked/ss1/content/text"
|
||||||
|
"github.com/inkyblackness/hacked/ss1/edit"
|
||||||
|
"github.com/inkyblackness/hacked/ss1/edit/undoable/cmd"
|
||||||
|
"github.com/inkyblackness/hacked/ss1/resource"
|
||||||
|
"github.com/inkyblackness/hacked/ss1/world"
|
||||||
|
"github.com/inkyblackness/hacked/ss1/world/ids"
|
||||||
|
)
|
||||||
|
|
||||||
|
// TilesView is for tile properties.
|
||||||
|
type TilesView struct {
|
||||||
|
editor *edit.LevelEditorService
|
||||||
|
textCache *text.Cache
|
||||||
|
textureCache *graphics.TextureCache
|
||||||
|
|
||||||
|
guiScale float32
|
||||||
|
registry cmd.Registry
|
||||||
|
model tilesViewModel
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewTilesView returns a new instance.
|
||||||
|
func NewTilesView(editor *edit.LevelEditorService,
|
||||||
|
guiScale float32, textCache *text.Cache, textureCache *graphics.TextureCache, registry cmd.Registry) *TilesView {
|
||||||
|
view := &TilesView{
|
||||||
|
editor: editor,
|
||||||
|
textCache: textCache,
|
||||||
|
textureCache: textureCache,
|
||||||
|
|
||||||
|
guiScale: guiScale,
|
||||||
|
model: freshTilesViewModel(),
|
||||||
|
registry: registry,
|
||||||
|
}
|
||||||
|
return view
|
||||||
|
}
|
||||||
|
|
||||||
|
// WindowOpen returns the flag address, to be used with the main menu.
|
||||||
|
func (view *TilesView) WindowOpen() *bool {
|
||||||
|
return &view.model.windowOpen
|
||||||
|
}
|
||||||
|
|
||||||
|
// TextureDisplay returns the current setting which textures should be displayed.
|
||||||
|
func (view TilesView) TextureDisplay() TextureDisplay {
|
||||||
|
return view.model.textureDisplay
|
||||||
|
}
|
||||||
|
|
||||||
|
// ColorDisplay returns the current setting which colors should be displayed.
|
||||||
|
func (view TilesView) ColorDisplay() ColorDisplay {
|
||||||
|
lvl := view.editor.Level()
|
||||||
|
if lvl.IsCyberspace() {
|
||||||
|
return view.model.cyberColorDisplay
|
||||||
|
}
|
||||||
|
return view.model.shadowDisplay
|
||||||
|
}
|
||||||
|
|
||||||
|
// Render renders the view.
|
||||||
|
func (view *TilesView) Render() {
|
||||||
|
if view.model.restoreFocus {
|
||||||
|
imgui.SetNextWindowFocus()
|
||||||
|
view.model.restoreFocus = false
|
||||||
|
view.model.windowOpen = true
|
||||||
|
}
|
||||||
|
if view.model.windowOpen {
|
||||||
|
lvl := view.editor.Level()
|
||||||
|
tiles := view.editor.Tiles()
|
||||||
|
readOnly := view.editor.IsReadOnly()
|
||||||
|
title := fmt.Sprintf("Level Tiles, %d selected", len(tiles))
|
||||||
|
if readOnly {
|
||||||
|
title += hintReadOnly
|
||||||
|
}
|
||||||
|
imgui.SetNextWindowSizeV(imgui.Vec2{X: 400 * view.guiScale, Y: 500 * view.guiScale}, imgui.ConditionFirstUseEver)
|
||||||
|
if imgui.BeginV(title+"###Level Tiles", view.WindowOpen(), 0) {
|
||||||
|
view.renderContent(lvl, tiles, readOnly)
|
||||||
|
}
|
||||||
|
imgui.End()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (view *TilesView) renderContent(lvl *level.Level, tiles []*level.TileMapEntry, readOnly bool) {
|
||||||
|
isCyberspace := lvl.IsCyberspace()
|
||||||
|
tileTypeUnifier := values.NewUnifier()
|
||||||
|
floorHeightUnifier := values.NewUnifier()
|
||||||
|
ceilingHeightUnifier := values.NewUnifier()
|
||||||
|
slopeHeightUnifier := values.NewUnifier()
|
||||||
|
slopeControlUnifier := values.NewUnifier()
|
||||||
|
musicZoneUnifier := values.NewUnifier()
|
||||||
|
|
||||||
|
floorPaletteIndexUnifier := values.NewUnifier()
|
||||||
|
ceilingPaletteIndexUnifier := values.NewUnifier()
|
||||||
|
flightPullTypeUnifier := values.NewUnifier()
|
||||||
|
gameOfLightStateUnifier := values.NewUnifier()
|
||||||
|
|
||||||
|
floorTextureIndexUnifier := values.NewUnifier()
|
||||||
|
floorTextureRotationsUnifier := values.NewUnifier()
|
||||||
|
ceilingTextureIndexUnifier := values.NewUnifier()
|
||||||
|
ceilingTextureRotationsUnifier := values.NewUnifier()
|
||||||
|
wallTextureIndexUnifier := values.NewUnifier()
|
||||||
|
wallTextureOffsetUnifier := values.NewUnifier()
|
||||||
|
useAdjacentWallTextureUnifier := values.NewUnifier()
|
||||||
|
wallTexturePatternUnifier := values.NewUnifier()
|
||||||
|
floorLightUnifier := values.NewUnifier()
|
||||||
|
floorLightDeltaUnifier := values.NewUnifier()
|
||||||
|
ceilingLightUnifier := values.NewUnifier()
|
||||||
|
ceilingLightDeltaUnifier := values.NewUnifier()
|
||||||
|
deconstructedUnifier := values.NewUnifier()
|
||||||
|
perildUnifier := values.NewUnifier()
|
||||||
|
floorHazardUnifier := values.NewUnifier()
|
||||||
|
ceilingHazardUnifier := values.NewUnifier()
|
||||||
|
|
||||||
|
for _, tile := range tiles {
|
||||||
|
tileTypeUnifier.Add(tile.Type)
|
||||||
|
floorHeightUnifier.Add(tile.Floor.AbsoluteHeight())
|
||||||
|
ceilingHeightUnifier.Add(tile.Ceiling.AbsoluteHeight())
|
||||||
|
slopeHeightUnifier.Add(tile.SlopeHeight)
|
||||||
|
slopeControlUnifier.Add(tile.Flags.SlopeControl())
|
||||||
|
musicZoneUnifier.Add(tile.Flags.MusicZone())
|
||||||
|
perildUnifier.Add(tile.Flags.Peril())
|
||||||
|
if isCyberspace {
|
||||||
|
floorPaletteIndexUnifier.Add(tile.TextureInfo.FloorPaletteIndex())
|
||||||
|
ceilingPaletteIndexUnifier.Add(tile.TextureInfo.CeilingPaletteIndex())
|
||||||
|
flightPullTypeUnifier.Add(tile.Flags.ForCyberspace().FlightPull())
|
||||||
|
gameOfLightStateUnifier.Add(tile.Flags.ForCyberspace().GameOfLifeState())
|
||||||
|
} else {
|
||||||
|
flags := tile.Flags.ForRealWorld()
|
||||||
|
floorTextureIndexUnifier.Add(int(tile.TextureInfo.FloorTextureIndex()))
|
||||||
|
floorTextureRotationsUnifier.Add(tile.Floor.TextureRotations())
|
||||||
|
ceilingTextureIndexUnifier.Add(int(tile.TextureInfo.CeilingTextureIndex()))
|
||||||
|
ceilingTextureRotationsUnifier.Add(tile.Ceiling.TextureRotations())
|
||||||
|
wallTextureIndexUnifier.Add(int(tile.TextureInfo.WallTextureIndex()))
|
||||||
|
wallTextureOffsetUnifier.Add(flags.WallTextureOffset())
|
||||||
|
useAdjacentWallTextureUnifier.Add(flags.UseAdjacentWallTexture())
|
||||||
|
wallTexturePatternUnifier.Add(flags.WallTexturePattern())
|
||||||
|
floorLightUnifier.Add(level.GradesOfShadow - 1 - flags.FloorShadow())
|
||||||
|
floorLightDeltaUnifier.Add(tile.LightDelta.OfFloor())
|
||||||
|
ceilingLightUnifier.Add(level.GradesOfShadow - 1 - flags.CeilingShadow())
|
||||||
|
ceilingLightDeltaUnifier.Add(tile.LightDelta.OfCeiling())
|
||||||
|
deconstructedUnifier.Add(flags.Deconstructed())
|
||||||
|
floorHazardUnifier.Add(tile.Floor.HasHazard())
|
||||||
|
ceilingHazardUnifier.Add(tile.Ceiling.HasHazard())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
imgui.PushItemWidth(-250 * view.guiScale)
|
||||||
|
|
||||||
|
_, _, levelHeight := lvl.Size()
|
||||||
|
tileHeightFormatter := tileHeightFormatterFor(levelHeight)
|
||||||
|
|
||||||
|
tileTypes := level.TileTypes()
|
||||||
|
values.RenderUnifiedCombo(readOnly, "Tile Type", tileTypeUnifier,
|
||||||
|
func(u values.Unifier) int { return int(u.Unified().(level.TileType)) },
|
||||||
|
func(value int) string { return tileTypes[value].String() },
|
||||||
|
len(tileTypes),
|
||||||
|
func(newValue int) { view.changeTiles(setTileTypeTo(level.TileType(newValue))) })
|
||||||
|
values.RenderUnifiedSliderInt(readOnly, "Floor Height", floorHeightUnifier,
|
||||||
|
func(u values.Unifier) int { return int(u.Unified().(level.TileHeightUnit)) },
|
||||||
|
tileHeightFormatter,
|
||||||
|
0, int(level.TileHeightUnitMax)-1,
|
||||||
|
func(newValue int) { view.changeTiles(setFloorHeightTo(level.TileHeightUnit(newValue))) })
|
||||||
|
values.RenderUnifiedSliderInt(readOnly, "Ceiling Height (abs)", ceilingHeightUnifier,
|
||||||
|
func(u values.Unifier) int { return int(u.Unified().(level.TileHeightUnit)) },
|
||||||
|
tileHeightFormatter,
|
||||||
|
1, int(level.TileHeightUnitMax),
|
||||||
|
func(newValue int) { view.changeTiles(setCeilingHeightTo(level.TileHeightUnit(newValue))) })
|
||||||
|
values.RenderUnifiedSliderInt(readOnly, "Slope Height", slopeHeightUnifier,
|
||||||
|
func(u values.Unifier) int { return int(u.Unified().(level.TileHeightUnit)) },
|
||||||
|
tileHeightFormatter,
|
||||||
|
0, int(level.TileHeightUnitMax)-1,
|
||||||
|
func(newValue int) { view.changeTiles(setSlopeHeightTo(level.TileHeightUnit(newValue))) })
|
||||||
|
slopeControls := level.TileSlopeControls()
|
||||||
|
values.RenderUnifiedCombo(readOnly, "Slope Control", slopeControlUnifier,
|
||||||
|
func(u values.Unifier) int { return int(u.Unified().(level.TileSlopeControl)) },
|
||||||
|
func(value int) string { return slopeControls[value].String() },
|
||||||
|
len(slopeControls),
|
||||||
|
func(newValue int) { view.changeTiles(setSlopeControlTo(slopeControls[newValue])) })
|
||||||
|
musicZones := level.TileMusicZones()
|
||||||
|
values.RenderUnifiedCombo(readOnly, "Music Zone", musicZoneUnifier,
|
||||||
|
func(u values.Unifier) int { return int(u.Unified().(level.TileMusicZone)) },
|
||||||
|
func(value int) string { return musicZones[value].String() },
|
||||||
|
len(musicZones),
|
||||||
|
func(newValue int) { view.changeTiles(setMusicZoneTo(musicZones[newValue])) })
|
||||||
|
values.RenderUnifiedCheckboxCombo(readOnly, "Music Peril", perildUnifier,
|
||||||
|
func(newValue bool) { view.changeTiles(setPerilTo(newValue)) })
|
||||||
|
|
||||||
|
imgui.Separator()
|
||||||
|
|
||||||
|
if isCyberspace {
|
||||||
|
if imgui.BeginCombo("Color View", view.model.cyberColorDisplay.String()) {
|
||||||
|
displays := ColorDisplays()
|
||||||
|
for _, display := range displays {
|
||||||
|
displayString := display.String()
|
||||||
|
|
||||||
|
if imgui.SelectableV(displayString, display == view.model.cyberColorDisplay, 0, imgui.Vec2{}) {
|
||||||
|
view.model.cyberColorDisplay = display
|
||||||
|
}
|
||||||
|
}
|
||||||
|
imgui.EndCombo()
|
||||||
|
}
|
||||||
|
|
||||||
|
values.RenderUnifiedSliderInt(readOnly, "Floor Color", floorPaletteIndexUnifier,
|
||||||
|
func(u values.Unifier) int { return int(u.Unified().(byte)) },
|
||||||
|
func(value int) string { return "%d" },
|
||||||
|
0, bitmap.PaletteSize-1,
|
||||||
|
func(newValue int) { view.changeTiles(setFloorPaletteIndexTo(newValue)) })
|
||||||
|
values.RenderUnifiedSliderInt(readOnly, "Ceiling Color", ceilingPaletteIndexUnifier,
|
||||||
|
func(u values.Unifier) int { return int(u.Unified().(byte)) },
|
||||||
|
func(value int) string { return "%d" },
|
||||||
|
0, bitmap.PaletteSize-1,
|
||||||
|
func(newValue int) { view.changeTiles(setCeilingPaletteIndexTo(newValue)) })
|
||||||
|
|
||||||
|
imgui.Separator()
|
||||||
|
|
||||||
|
flightPulls := level.CyberspaceFlightPulls()
|
||||||
|
values.RenderUnifiedCombo(readOnly, "Flight Pull Type", flightPullTypeUnifier,
|
||||||
|
func(u values.Unifier) int { return int(u.Unified().(level.CyberspaceFlightPull)) },
|
||||||
|
func(value int) string { return flightPulls[value].String() },
|
||||||
|
len(flightPulls),
|
||||||
|
func(newValue int) { view.changeTiles(setFlightPullTypeTo(flightPulls[newValue])) })
|
||||||
|
values.RenderUnifiedSliderInt(readOnly, "Game Of Life State", gameOfLightStateUnifier,
|
||||||
|
func(u values.Unifier) int { return u.Unified().(int) },
|
||||||
|
func(value int) string { return "%d" },
|
||||||
|
0, 3,
|
||||||
|
func(newValue int) { view.changeTiles(setGameOfLightStateTo(newValue)) })
|
||||||
|
} else {
|
||||||
|
atlas := lvl.TextureAtlas()
|
||||||
|
|
||||||
|
if imgui.BeginCombo("Texture View", view.model.textureDisplay.String()) {
|
||||||
|
displays := TextureDisplays()
|
||||||
|
for _, display := range displays {
|
||||||
|
displayString := display.String()
|
||||||
|
|
||||||
|
if imgui.SelectableV(displayString, display == view.model.textureDisplay, 0, imgui.Vec2{}) {
|
||||||
|
view.model.textureDisplay = display
|
||||||
|
}
|
||||||
|
}
|
||||||
|
imgui.EndCombo()
|
||||||
|
}
|
||||||
|
|
||||||
|
values.RenderUnifiedSliderInt(readOnly, "Floor Texture (atlas index)", floorTextureIndexUnifier,
|
||||||
|
func(u values.Unifier) int { return u.Unified().(int) },
|
||||||
|
func(value int) string { return "%d" },
|
||||||
|
0, level.FloorCeilingTextureLimit-1,
|
||||||
|
func(newValue int) { view.changeTiles(setFloorTextureIndexTo(level.AtlasIndex(newValue))) })
|
||||||
|
view.renderTextureSelector(readOnly, "Floor Texture", floorTextureIndexUnifier, atlas, 0, level.FloorCeilingTextureLimit-1,
|
||||||
|
func(newValue int) { view.changeTiles(setFloorTextureIndexTo(level.AtlasIndex(newValue))) })
|
||||||
|
values.RenderUnifiedSliderInt(readOnly, "Floor Texture Rotations", floorTextureRotationsUnifier,
|
||||||
|
func(u values.Unifier) int { return u.Unified().(int) },
|
||||||
|
func(value int) string { return "%d" },
|
||||||
|
0, 3,
|
||||||
|
func(newValue int) { view.changeTiles(setFloorTextureRotationsTo(newValue)) })
|
||||||
|
|
||||||
|
values.RenderUnifiedSliderInt(readOnly, "Ceiling Texture (atlas index)", ceilingTextureIndexUnifier,
|
||||||
|
func(u values.Unifier) int { return u.Unified().(int) },
|
||||||
|
func(value int) string { return "%d" },
|
||||||
|
0, level.FloorCeilingTextureLimit-1,
|
||||||
|
func(newValue int) { view.changeTiles(setCeilingTextureIndexTo(level.AtlasIndex(newValue))) })
|
||||||
|
view.renderTextureSelector(readOnly, "Ceiling Texture", ceilingTextureIndexUnifier, atlas, 0, level.FloorCeilingTextureLimit-1,
|
||||||
|
func(newValue int) { view.changeTiles(setCeilingTextureIndexTo(level.AtlasIndex(newValue))) })
|
||||||
|
values.RenderUnifiedSliderInt(readOnly, "Ceiling Texture Rotations", ceilingTextureRotationsUnifier,
|
||||||
|
func(u values.Unifier) int { return u.Unified().(int) },
|
||||||
|
func(value int) string { return "%d" },
|
||||||
|
0, 3,
|
||||||
|
func(newValue int) { view.changeTiles(setCeilingTextureRotationsTo(newValue)) })
|
||||||
|
|
||||||
|
values.RenderUnifiedSliderInt(readOnly, "Wall Texture (atlas index)", wallTextureIndexUnifier,
|
||||||
|
func(u values.Unifier) int { return u.Unified().(int) },
|
||||||
|
func(value int) string { return "%d" },
|
||||||
|
0, len(atlas)-1,
|
||||||
|
func(newValue int) { view.changeTiles(setWallTextureIndexTo(level.AtlasIndex(newValue))) })
|
||||||
|
view.renderTextureSelector(readOnly, "Wall Texture", wallTextureIndexUnifier, atlas, 0, len(atlas)-1,
|
||||||
|
func(newValue int) { view.changeTiles(setWallTextureIndexTo(level.AtlasIndex(newValue))) })
|
||||||
|
values.RenderUnifiedSliderInt(readOnly, "Wall Texture Offset", wallTextureOffsetUnifier,
|
||||||
|
func(u values.Unifier) int { return int(u.Unified().(level.TileHeightUnit)) },
|
||||||
|
tileHeightFormatter,
|
||||||
|
0, int(level.TileHeightUnitMax)-1,
|
||||||
|
func(newValue int) { view.changeTiles(setWallTextureOffsetTo(level.TileHeightUnit(newValue))) })
|
||||||
|
|
||||||
|
values.RenderUnifiedCheckboxCombo(readOnly, "Use Adjacent Wall Texture", useAdjacentWallTextureUnifier,
|
||||||
|
func(newValue bool) { view.changeTiles(setUseAdjacentWallTextureTo(newValue)) })
|
||||||
|
wallTexturePatterns := level.WallTexturePatterns()
|
||||||
|
values.RenderUnifiedCombo(readOnly, "Wall Texture Pattern", wallTexturePatternUnifier,
|
||||||
|
func(u values.Unifier) int { return int(u.Unified().(level.WallTexturePattern)) },
|
||||||
|
func(value int) string { return wallTexturePatterns[value].String() },
|
||||||
|
len(wallTexturePatterns),
|
||||||
|
func(newValue int) { view.changeTiles(setWallTexturePatternTo(wallTexturePatterns[newValue])) })
|
||||||
|
|
||||||
|
imgui.Separator()
|
||||||
|
|
||||||
|
if imgui.BeginCombo("Shadow View", view.model.shadowDisplay.String()) {
|
||||||
|
displays := ColorDisplays()
|
||||||
|
for _, display := range displays {
|
||||||
|
displayString := display.String()
|
||||||
|
|
||||||
|
if imgui.SelectableV(displayString, display == view.model.shadowDisplay, 0, imgui.Vec2{}) {
|
||||||
|
view.model.shadowDisplay = display
|
||||||
|
}
|
||||||
|
}
|
||||||
|
imgui.EndCombo()
|
||||||
|
}
|
||||||
|
|
||||||
|
values.RenderUnifiedSliderInt(readOnly, "Floor Light", floorLightUnifier,
|
||||||
|
func(u values.Unifier) int { return u.Unified().(int) },
|
||||||
|
func(value int) string { return "%d" },
|
||||||
|
0, level.GradesOfShadow-1,
|
||||||
|
func(newValue int) { view.changeTiles(setFloorLightTo(newValue)) })
|
||||||
|
values.RenderUnifiedSliderInt(readOnly, "Floor Light Delta", floorLightDeltaUnifier,
|
||||||
|
func(u values.Unifier) int { return u.Unified().(int) },
|
||||||
|
func(value int) string { return "%d" },
|
||||||
|
0, level.GradesOfShadow-1,
|
||||||
|
func(newValue int) { view.changeTiles(setFloorLightDeltaTo(newValue)) })
|
||||||
|
values.RenderUnifiedSliderInt(readOnly, "Ceiling Light", ceilingLightUnifier,
|
||||||
|
func(u values.Unifier) int { return u.Unified().(int) },
|
||||||
|
func(value int) string { return "%d" },
|
||||||
|
0, level.GradesOfShadow-1,
|
||||||
|
func(newValue int) { view.changeTiles(setCeilingLightTo(newValue)) })
|
||||||
|
values.RenderUnifiedSliderInt(readOnly, "Ceiling Light Delta", ceilingLightDeltaUnifier,
|
||||||
|
func(u values.Unifier) int { return u.Unified().(int) },
|
||||||
|
func(value int) string { return "%d" },
|
||||||
|
0, level.GradesOfShadow-1,
|
||||||
|
func(newValue int) { view.changeTiles(setCeilingLightDeltaTo(newValue)) })
|
||||||
|
|
||||||
|
imgui.Separator()
|
||||||
|
|
||||||
|
values.RenderUnifiedCheckboxCombo(readOnly, "Deconstructed", deconstructedUnifier,
|
||||||
|
func(newValue bool) { view.changeTiles(setDeconstructedTo(newValue)) })
|
||||||
|
values.RenderUnifiedCheckboxCombo(readOnly, "Floor Hazard", floorHazardUnifier,
|
||||||
|
func(newValue bool) { view.changeTiles(setFloorHazardTo(newValue)) })
|
||||||
|
values.RenderUnifiedCheckboxCombo(readOnly, "Ceiling Hazard", ceilingHazardUnifier,
|
||||||
|
func(newValue bool) { view.changeTiles(setCeilingHazardTo(newValue)) })
|
||||||
|
}
|
||||||
|
|
||||||
|
imgui.PopItemWidth()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (view *TilesView) renderTextureSelector(readOnly bool, label string, unifier values.Unifier,
|
||||||
|
atlas level.TextureAtlas, minIndex, maxIndex int, changeHandler func(int)) {
|
||||||
|
selectedIndex := -1
|
||||||
|
if unifier.IsUnique() {
|
||||||
|
selectedIndex = unifier.Unified().(int)
|
||||||
|
}
|
||||||
|
|
||||||
|
count := maxIndex - minIndex + 1
|
||||||
|
if count > len(atlas) {
|
||||||
|
count = len(atlas)
|
||||||
|
}
|
||||||
|
render.TextureSelector(label, -1, view.guiScale, count, selectedIndex-minIndex,
|
||||||
|
view.textureCache,
|
||||||
|
func(index int) resource.Key {
|
||||||
|
return resource.KeyOf(ids.LargeTextures.Plus(int(atlas[minIndex+index])), resource.LangAny, 0)
|
||||||
|
},
|
||||||
|
func(index int) string { return view.textureName(int(atlas[minIndex+index])) },
|
||||||
|
func(index int) {
|
||||||
|
if !readOnly {
|
||||||
|
changeHandler(index)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func (view *TilesView) textureName(index int) string {
|
||||||
|
key := resource.KeyOf(ids.TextureNames, resource.LangDefault, index)
|
||||||
|
name, err := view.textCache.Text(key)
|
||||||
|
suffix := ""
|
||||||
|
if err == nil {
|
||||||
|
suffix = ": " + name
|
||||||
|
}
|
||||||
|
return fmt.Sprintf("%3d", index) + suffix
|
||||||
|
}
|
||||||
|
|
||||||
|
func setTileTypeTo(tileType level.TileType) tileMapEntryModifier {
|
||||||
|
return func(tile *level.TileMapEntry) {
|
||||||
|
tile.Type = tileType
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func setFloorHeightTo(height level.TileHeightUnit) tileMapEntryModifier {
|
||||||
|
return func(tile *level.TileMapEntry) {
|
||||||
|
tile.Floor = tile.Floor.WithAbsoluteHeight(height)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func setCeilingHeightTo(height level.TileHeightUnit) tileMapEntryModifier {
|
||||||
|
return func(tile *level.TileMapEntry) {
|
||||||
|
tile.Ceiling = tile.Ceiling.WithAbsoluteHeight(height)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func setSlopeHeightTo(height level.TileHeightUnit) tileMapEntryModifier {
|
||||||
|
return func(tile *level.TileMapEntry) {
|
||||||
|
tile.SlopeHeight = height
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func setSlopeControlTo(value level.TileSlopeControl) tileMapEntryModifier {
|
||||||
|
return func(tile *level.TileMapEntry) {
|
||||||
|
tile.Flags = tile.Flags.WithSlopeControl(value)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func setPerilTo(value bool) tileMapEntryModifier {
|
||||||
|
return func(tile *level.TileMapEntry) {
|
||||||
|
tile.Flags = tile.Flags.WithPeril(value)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func setMusicZoneTo(value level.TileMusicZone) tileMapEntryModifier {
|
||||||
|
return func(tile *level.TileMapEntry) {
|
||||||
|
tile.Flags = tile.Flags.WithMusicZone(value)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func setFloorTextureIndexTo(value level.AtlasIndex) tileMapEntryModifier {
|
||||||
|
return func(tile *level.TileMapEntry) {
|
||||||
|
tile.TextureInfo = tile.TextureInfo.WithFloorTextureIndex(value)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func setFloorTextureRotationsTo(value int) tileMapEntryModifier {
|
||||||
|
return func(tile *level.TileMapEntry) {
|
||||||
|
tile.Floor = tile.Floor.WithTextureRotations(value)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func setCeilingTextureIndexTo(value level.AtlasIndex) tileMapEntryModifier {
|
||||||
|
return func(tile *level.TileMapEntry) {
|
||||||
|
tile.TextureInfo = tile.TextureInfo.WithCeilingTextureIndex(value)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func setCeilingTextureRotationsTo(value int) tileMapEntryModifier {
|
||||||
|
return func(tile *level.TileMapEntry) {
|
||||||
|
tile.Ceiling = tile.Ceiling.WithTextureRotations(value)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func setWallTextureIndexTo(value level.AtlasIndex) tileMapEntryModifier {
|
||||||
|
return func(tile *level.TileMapEntry) {
|
||||||
|
tile.TextureInfo = tile.TextureInfo.WithWallTextureIndex(value)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func setWallTextureOffsetTo(value level.TileHeightUnit) tileMapEntryModifier {
|
||||||
|
return func(tile *level.TileMapEntry) {
|
||||||
|
tile.Flags = tile.Flags.ForRealWorld().WithWallTextureOffset(value).AsTileFlag()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func setUseAdjacentWallTextureTo(value bool) tileMapEntryModifier {
|
||||||
|
return func(tile *level.TileMapEntry) {
|
||||||
|
tile.Flags = tile.Flags.ForRealWorld().WithUseAdjacentWallTexture(value).AsTileFlag()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func setWallTexturePatternTo(value level.WallTexturePattern) tileMapEntryModifier {
|
||||||
|
return func(tile *level.TileMapEntry) {
|
||||||
|
tile.Flags = tile.Flags.ForRealWorld().WithWallTexturePattern(value).AsTileFlag()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func setFloorLightTo(value int) tileMapEntryModifier {
|
||||||
|
return func(tile *level.TileMapEntry) {
|
||||||
|
tile.Flags = tile.Flags.ForRealWorld().WithFloorShadow(level.GradesOfShadow - 1 - value).AsTileFlag()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func setFloorLightDeltaTo(value int) tileMapEntryModifier {
|
||||||
|
return func(tile *level.TileMapEntry) {
|
||||||
|
tile.LightDelta = tile.LightDelta.WithFloor(value)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func setCeilingLightTo(value int) tileMapEntryModifier {
|
||||||
|
return func(tile *level.TileMapEntry) {
|
||||||
|
tile.Flags = tile.Flags.ForRealWorld().WithCeilingShadow(level.GradesOfShadow - 1 - value).AsTileFlag()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func setCeilingLightDeltaTo(value int) tileMapEntryModifier {
|
||||||
|
return func(tile *level.TileMapEntry) {
|
||||||
|
tile.LightDelta = tile.LightDelta.WithCeiling(value)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func setDeconstructedTo(value bool) tileMapEntryModifier {
|
||||||
|
return func(tile *level.TileMapEntry) {
|
||||||
|
tile.Flags = tile.Flags.ForRealWorld().WithDeconstructed(value).AsTileFlag()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func setFloorHazardTo(value bool) tileMapEntryModifier {
|
||||||
|
return func(tile *level.TileMapEntry) {
|
||||||
|
tile.Floor = tile.Floor.WithHazard(value)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func setCeilingHazardTo(value bool) tileMapEntryModifier {
|
||||||
|
return func(tile *level.TileMapEntry) {
|
||||||
|
tile.Ceiling = tile.Ceiling.WithHazard(value)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func setFloorPaletteIndexTo(value int) tileMapEntryModifier {
|
||||||
|
return func(tile *level.TileMapEntry) {
|
||||||
|
tile.TextureInfo = tile.TextureInfo.WithFloorPaletteIndex(byte(value))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func setCeilingPaletteIndexTo(value int) tileMapEntryModifier {
|
||||||
|
return func(tile *level.TileMapEntry) {
|
||||||
|
tile.TextureInfo = tile.TextureInfo.WithCeilingPaletteIndex(byte(value))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func setFlightPullTypeTo(value level.CyberspaceFlightPull) tileMapEntryModifier {
|
||||||
|
return func(tile *level.TileMapEntry) {
|
||||||
|
tile.Flags = tile.Flags.ForCyberspace().WithFlightPull(value).AsTileFlag()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func setGameOfLightStateTo(value int) tileMapEntryModifier {
|
||||||
|
return func(tile *level.TileMapEntry) {
|
||||||
|
tile.Flags = tile.Flags.ForCyberspace().WithGameOfLifeState(value).AsTileFlag()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
type tileMapEntryModifier func(*level.TileMapEntry)
|
||||||
|
|
||||||
|
func (view *TilesView) changeTiles(modifier tileMapEntryModifier) {
|
||||||
|
err := view.registry.Register(cmd.Named("ChangeTiles"),
|
||||||
|
cmd.Forward(view.restoreFocusTask()),
|
||||||
|
cmd.Nested(func() error { return view.editor.ChangeTiles(modifier) }),
|
||||||
|
cmd.Reverse(view.restoreFocusTask()))
|
||||||
|
if err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (view *TilesView) restoreFocusTask() cmd.Task {
|
||||||
|
return func(modder world.Modder) error {
|
||||||
|
view.model.restoreFocus = true
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,18 @@
|
|||||||
|
package levels
|
||||||
|
|
||||||
|
type tilesViewModel struct {
|
||||||
|
textureDisplay TextureDisplay
|
||||||
|
shadowDisplay ColorDisplay
|
||||||
|
cyberColorDisplay ColorDisplay
|
||||||
|
|
||||||
|
restoreFocus bool
|
||||||
|
windowOpen bool
|
||||||
|
}
|
||||||
|
|
||||||
|
func freshTilesViewModel() tilesViewModel {
|
||||||
|
return tilesViewModel{
|
||||||
|
textureDisplay: TextureDisplayFloor,
|
||||||
|
shadowDisplay: ColorDisplayNone,
|
||||||
|
cyberColorDisplay: ColorDisplayNone,
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,64 @@
|
|||||||
|
package levels
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
|
||||||
|
"github.com/inkyblackness/hacked/ss1/content/archive/level"
|
||||||
|
)
|
||||||
|
|
||||||
|
type hazardFormatter func(int) string
|
||||||
|
|
||||||
|
func rawHazardValue(int) string {
|
||||||
|
return "raw: %d"
|
||||||
|
}
|
||||||
|
|
||||||
|
func lbpHazardValue(value int) string {
|
||||||
|
return fmt.Sprintf("%3.1f LBP - raw: %%d", float32(value)/2)
|
||||||
|
}
|
||||||
|
|
||||||
|
func percentHazardValue(value int) string {
|
||||||
|
return fmt.Sprintf("%v%%%% - raw: %%d", value*25)
|
||||||
|
}
|
||||||
|
|
||||||
|
type ceilingHazardInfo struct {
|
||||||
|
title string
|
||||||
|
radiationRegister bool
|
||||||
|
formatter hazardFormatter
|
||||||
|
}
|
||||||
|
|
||||||
|
var ceilingHazards = []ceilingHazardInfo{
|
||||||
|
{title: "Off", radiationRegister: false, formatter: rawHazardValue},
|
||||||
|
{title: "Radiation", radiationRegister: true, formatter: lbpHazardValue},
|
||||||
|
}
|
||||||
|
|
||||||
|
func currentCeilingHazard(param *level.Parameters) ceilingHazardInfo {
|
||||||
|
radiationRegister := param.RadiationRegister > 1
|
||||||
|
if radiationRegister {
|
||||||
|
return ceilingHazards[1]
|
||||||
|
}
|
||||||
|
return ceilingHazards[0]
|
||||||
|
}
|
||||||
|
|
||||||
|
type floorHazardInfo struct {
|
||||||
|
title string
|
||||||
|
biohazardRegister bool
|
||||||
|
isGravity bool
|
||||||
|
formatter hazardFormatter
|
||||||
|
}
|
||||||
|
|
||||||
|
var floorHazards = []floorHazardInfo{
|
||||||
|
{title: "Off", biohazardRegister: false, isGravity: false, formatter: rawHazardValue},
|
||||||
|
{title: "Gravity", biohazardRegister: false, isGravity: true, formatter: percentHazardValue},
|
||||||
|
{title: "Biohazard", biohazardRegister: true, isGravity: false, formatter: lbpHazardValue},
|
||||||
|
}
|
||||||
|
|
||||||
|
func currentFloorHazard(param *level.Parameters) floorHazardInfo {
|
||||||
|
biohazardRegister := param.BiohazardRegister > 1
|
||||||
|
isGravity := param.FloorHazardIsGravity != 0
|
||||||
|
if isGravity {
|
||||||
|
return floorHazards[1]
|
||||||
|
} else if biohazardRegister {
|
||||||
|
return floorHazards[2]
|
||||||
|
}
|
||||||
|
return floorHazards[0]
|
||||||
|
}
|
||||||
@@ -0,0 +1,52 @@
|
|||||||
|
package messages
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/inkyblackness/hacked/ss1/resource"
|
||||||
|
"github.com/inkyblackness/hacked/ss1/world"
|
||||||
|
)
|
||||||
|
|
||||||
|
type messageDataEntry struct {
|
||||||
|
oldData [][]byte
|
||||||
|
newData [][]byte
|
||||||
|
}
|
||||||
|
|
||||||
|
type setMessageDataCommand struct {
|
||||||
|
model *viewModel
|
||||||
|
|
||||||
|
key resource.Key
|
||||||
|
showVerboseText bool
|
||||||
|
|
||||||
|
textEntries map[resource.Language]messageDataEntry
|
||||||
|
audioEntries map[resource.Language]messageDataEntry
|
||||||
|
}
|
||||||
|
|
||||||
|
func (cmd setMessageDataCommand) Do(modder world.Modder) error {
|
||||||
|
return cmd.perform(modder, func(entry messageDataEntry) [][]byte { return entry.newData })
|
||||||
|
}
|
||||||
|
|
||||||
|
func (cmd setMessageDataCommand) Undo(modder world.Modder) error {
|
||||||
|
return cmd.perform(modder, func(entry messageDataEntry) [][]byte { return entry.oldData })
|
||||||
|
}
|
||||||
|
|
||||||
|
func (cmd setMessageDataCommand) perform(modder world.Modder, dataResolver func(messageDataEntry) [][]byte) error {
|
||||||
|
cmd.saveEntries(modder, cmd.key.ID.Plus(cmd.key.Index), cmd.textEntries, dataResolver)
|
||||||
|
cmd.saveEntries(modder, cmd.key.ID.Plus(cmd.key.Index).Plus(300), cmd.audioEntries, dataResolver)
|
||||||
|
|
||||||
|
cmd.model.restoreFocus = true
|
||||||
|
cmd.model.currentKey = cmd.key
|
||||||
|
cmd.model.showVerboseText = cmd.showVerboseText
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (cmd setMessageDataCommand) saveEntries(modder world.Modder, id resource.ID,
|
||||||
|
entries map[resource.Language]messageDataEntry,
|
||||||
|
dataResolver func(messageDataEntry) [][]byte) {
|
||||||
|
for lang, entry := range entries {
|
||||||
|
data := dataResolver(entry)
|
||||||
|
if len(data) > 0 {
|
||||||
|
modder.SetResourceBlocks(lang, id, data)
|
||||||
|
} else {
|
||||||
|
modder.DelResource(lang, id)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,488 @@
|
|||||||
|
package messages
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
|
||||||
|
"github.com/inkyblackness/imgui-go/v3"
|
||||||
|
|
||||||
|
"github.com/inkyblackness/hacked/editor/external"
|
||||||
|
"github.com/inkyblackness/hacked/editor/graphics"
|
||||||
|
"github.com/inkyblackness/hacked/editor/render"
|
||||||
|
"github.com/inkyblackness/hacked/editor/values"
|
||||||
|
"github.com/inkyblackness/hacked/ss1/content/audio"
|
||||||
|
"github.com/inkyblackness/hacked/ss1/content/movie"
|
||||||
|
"github.com/inkyblackness/hacked/ss1/content/text"
|
||||||
|
"github.com/inkyblackness/hacked/ss1/edit/undoable/cmd"
|
||||||
|
"github.com/inkyblackness/hacked/ss1/resource"
|
||||||
|
"github.com/inkyblackness/hacked/ss1/world"
|
||||||
|
"github.com/inkyblackness/hacked/ss1/world/ids"
|
||||||
|
"github.com/inkyblackness/hacked/ui/gui"
|
||||||
|
)
|
||||||
|
|
||||||
|
type messageInfo struct {
|
||||||
|
title string
|
||||||
|
}
|
||||||
|
|
||||||
|
var knownMessageTypes = map[resource.ID]messageInfo{
|
||||||
|
ids.MailsStart: {title: "Mails"},
|
||||||
|
ids.LogsStart: {title: "Logs"},
|
||||||
|
ids.FragmentsStart: {title: "Fragments"},
|
||||||
|
}
|
||||||
|
var knownMessageTypesOrder = []resource.ID{ids.MailsStart, ids.LogsStart, ids.FragmentsStart}
|
||||||
|
|
||||||
|
var knownMailTypes = []string{
|
||||||
|
"Audio/Text",
|
||||||
|
"VMail: 'Shield' (0)",
|
||||||
|
"VMail: 'Grove' (1)",
|
||||||
|
"VMail: 'Bridge' (2)",
|
||||||
|
"VMail: 'Laser' (3, 4)",
|
||||||
|
"VMail: 'Status' (11)",
|
||||||
|
"VMail: 'Explode' (5..9)",
|
||||||
|
}
|
||||||
|
|
||||||
|
const videoMailDisplayBase = 256
|
||||||
|
|
||||||
|
// View provides edit controls for messages.
|
||||||
|
type View struct {
|
||||||
|
mod *world.Mod
|
||||||
|
messageCache *text.ElectronicMessageCache
|
||||||
|
cp text.Codepage
|
||||||
|
movieCache *movie.Cache
|
||||||
|
imageCache *graphics.TextureCache
|
||||||
|
|
||||||
|
modalStateMachine gui.ModalStateMachine
|
||||||
|
clipboard external.Clipboard
|
||||||
|
guiScale float32
|
||||||
|
commander cmd.Commander
|
||||||
|
|
||||||
|
model viewModel
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewMessagesView returns a new instance.
|
||||||
|
func NewMessagesView(mod *world.Mod, messageCache *text.ElectronicMessageCache, cp text.Codepage,
|
||||||
|
movieCache *movie.Cache, imageCache *graphics.TextureCache,
|
||||||
|
modalStateMachine gui.ModalStateMachine, clipboard external.Clipboard,
|
||||||
|
guiScale float32, commander cmd.Commander) *View {
|
||||||
|
view := &View{
|
||||||
|
mod: mod,
|
||||||
|
messageCache: messageCache,
|
||||||
|
cp: cp,
|
||||||
|
movieCache: movieCache,
|
||||||
|
imageCache: imageCache,
|
||||||
|
|
||||||
|
modalStateMachine: modalStateMachine,
|
||||||
|
clipboard: clipboard,
|
||||||
|
guiScale: guiScale,
|
||||||
|
commander: commander,
|
||||||
|
|
||||||
|
model: freshViewModel(),
|
||||||
|
}
|
||||||
|
return view
|
||||||
|
}
|
||||||
|
|
||||||
|
// WindowOpen returns the flag address, to be used with the main menu.
|
||||||
|
func (view *View) WindowOpen() *bool {
|
||||||
|
return &view.model.windowOpen
|
||||||
|
}
|
||||||
|
|
||||||
|
// Render renders the view.
|
||||||
|
func (view *View) Render() {
|
||||||
|
if view.model.restoreFocus {
|
||||||
|
imgui.SetNextWindowFocus()
|
||||||
|
view.model.restoreFocus = false
|
||||||
|
view.model.windowOpen = true
|
||||||
|
}
|
||||||
|
if view.model.windowOpen {
|
||||||
|
imgui.SetNextWindowSizeV(imgui.Vec2{X: 640 * view.guiScale, Y: 480 * view.guiScale}, imgui.ConditionFirstUseEver)
|
||||||
|
if imgui.BeginV("Messages", view.WindowOpen(), imgui.WindowFlagsNoCollapse) {
|
||||||
|
view.renderContent()
|
||||||
|
}
|
||||||
|
imgui.End()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (view *View) renderContent() {
|
||||||
|
if imgui.BeginChildV("Properties", imgui.Vec2{X: 350 * view.guiScale, Y: -200 * view.guiScale}, false, 0) {
|
||||||
|
imgui.PushItemWidth(-150 * view.guiScale)
|
||||||
|
if imgui.BeginCombo("Message Type", knownMessageTypes[view.model.currentKey.ID].title) {
|
||||||
|
for _, id := range knownMessageTypesOrder {
|
||||||
|
if imgui.SelectableV(knownMessageTypes[id].title, id == view.model.currentKey.ID, 0, imgui.Vec2{}) {
|
||||||
|
view.model.currentKey.ID = id
|
||||||
|
view.model.currentKey.Index = 0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
imgui.EndCombo()
|
||||||
|
}
|
||||||
|
info, _ := ids.Info(view.model.currentKey.ID)
|
||||||
|
index := view.model.currentKey.Index
|
||||||
|
if gui.StepSliderInt("Index", &index, 0, info.MaxCount-1) {
|
||||||
|
view.model.currentKey.Index = index
|
||||||
|
}
|
||||||
|
if imgui.BeginCombo("Language", view.model.currentKey.Lang.String()) {
|
||||||
|
languages := resource.Languages()
|
||||||
|
for _, lang := range languages {
|
||||||
|
if imgui.SelectableV(lang.String(), lang == view.model.currentKey.Lang, 0, imgui.Vec2{}) {
|
||||||
|
view.model.currentKey.Lang = lang
|
||||||
|
}
|
||||||
|
}
|
||||||
|
imgui.EndCombo()
|
||||||
|
}
|
||||||
|
|
||||||
|
message, msgReadOnly := view.currentMessage()
|
||||||
|
availableDisplays := view.availableDisplays()
|
||||||
|
|
||||||
|
if imgui.ButtonV("Clear All", imgui.Vec2{}) {
|
||||||
|
view.requestClearAll()
|
||||||
|
}
|
||||||
|
imgui.SameLine()
|
||||||
|
if !msgReadOnly {
|
||||||
|
if imgui.ButtonV("Remove All", imgui.Vec2{}) {
|
||||||
|
view.requestRemoveAll()
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
imgui.Text("(read-only)")
|
||||||
|
}
|
||||||
|
if view.hasAudio() {
|
||||||
|
imgui.PushID("audio")
|
||||||
|
imgui.Separator()
|
||||||
|
sound, soundReadOnly := view.currentSound()
|
||||||
|
if !sound.Empty() {
|
||||||
|
imgui.LabelText("Audio", fmt.Sprintf("%.2f sec", sound.Duration()))
|
||||||
|
if imgui.Button("Export") {
|
||||||
|
view.requestExportAudio(sound)
|
||||||
|
}
|
||||||
|
if !soundReadOnly {
|
||||||
|
imgui.SameLine()
|
||||||
|
if imgui.Button("Remove") {
|
||||||
|
view.requestRemoveAudio()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
imgui.SameLine()
|
||||||
|
} else {
|
||||||
|
imgui.LabelText("Audio", "(no sound)")
|
||||||
|
}
|
||||||
|
if imgui.Button("Import") {
|
||||||
|
view.requestImportAudio()
|
||||||
|
}
|
||||||
|
imgui.SameLine()
|
||||||
|
if imgui.Button("Clear") {
|
||||||
|
view.requestClearAudio()
|
||||||
|
}
|
||||||
|
imgui.PopID()
|
||||||
|
}
|
||||||
|
imgui.Separator()
|
||||||
|
|
||||||
|
textModes := map[bool]string{true: "Verbose", false: "Terse"}
|
||||||
|
if imgui.BeginCombo("Text Mode", textModes[view.model.showVerboseText]) {
|
||||||
|
for _, setting := range []bool{true, false} {
|
||||||
|
if imgui.SelectableV(textModes[setting], view.model.showVerboseText == setting, 0, imgui.Vec2{}) {
|
||||||
|
view.model.showVerboseText = setting
|
||||||
|
}
|
||||||
|
}
|
||||||
|
imgui.EndCombo()
|
||||||
|
}
|
||||||
|
view.renderText(false, "Title", message.Title, func(newValue string) {
|
||||||
|
view.requestTextChange(func(msg *text.ElectronicMessage) { msg.Title = text.Blocked(newValue)[0] })
|
||||||
|
})
|
||||||
|
view.renderText(false, "Sender", message.Sender, func(newValue string) {
|
||||||
|
view.requestTextChange(func(msg *text.ElectronicMessage) { msg.Sender = text.Blocked(newValue)[0] })
|
||||||
|
})
|
||||||
|
view.renderText(false, "Subject", message.Subject, func(newValue string) {
|
||||||
|
view.requestTextChange(func(msg *text.ElectronicMessage) { msg.Subject = text.Blocked(newValue)[0] })
|
||||||
|
})
|
||||||
|
nextMessageIndex := values.NewUnifier()
|
||||||
|
nextMessageIndex.Add(message.NextMessage)
|
||||||
|
values.RenderUnifiedSliderInt(false, "Next Message Index", nextMessageIndex,
|
||||||
|
func(u values.Unifier) int { return u.Unified().(int) },
|
||||||
|
func(int) string { return "%d" },
|
||||||
|
-1, 255,
|
||||||
|
func(newValue int) {
|
||||||
|
view.requestPropertyChange(func(msg *text.ElectronicMessage) { msg.NextMessage = newValue })
|
||||||
|
})
|
||||||
|
|
||||||
|
isInterrupt := values.NewUnifier()
|
||||||
|
isInterrupt.Add(message.IsInterrupt)
|
||||||
|
values.RenderUnifiedCheckboxCombo(false, "Is Interrupt", isInterrupt,
|
||||||
|
func(newValue bool) {
|
||||||
|
view.requestPropertyChange(func(msg *text.ElectronicMessage) { msg.IsInterrupt = newValue })
|
||||||
|
})
|
||||||
|
|
||||||
|
colorIndex := values.NewUnifier()
|
||||||
|
colorIndex.Add(message.ColorIndex)
|
||||||
|
values.RenderUnifiedSliderInt(false, "Color Index", colorIndex,
|
||||||
|
func(u values.Unifier) int { return u.Unified().(int) },
|
||||||
|
func(int) string { return "%d" },
|
||||||
|
-1, 255,
|
||||||
|
func(newValue int) {
|
||||||
|
view.requestPropertyChange(func(msg *text.ElectronicMessage) { msg.ColorIndex = newValue })
|
||||||
|
})
|
||||||
|
|
||||||
|
if view.model.currentKey.ID == ids.MailsStart {
|
||||||
|
mailType := message.LeftDisplay - videoMailDisplayBase + 1
|
||||||
|
selectedType := fmt.Sprintf("Unknown (0x%03X)", message.LeftDisplay)
|
||||||
|
if mailType < 0 {
|
||||||
|
mailType = 0
|
||||||
|
}
|
||||||
|
if mailType < len(knownMailTypes) {
|
||||||
|
selectedType = knownMailTypes[mailType]
|
||||||
|
}
|
||||||
|
if msgReadOnly {
|
||||||
|
imgui.LabelText("Mail Type", selectedType)
|
||||||
|
} else if imgui.BeginCombo("Mail Type", selectedType) {
|
||||||
|
for typeIndex, typeString := range knownMailTypes {
|
||||||
|
if imgui.SelectableV(typeString, mailType == typeIndex, 0, imgui.Vec2{}) {
|
||||||
|
newIndex := -1
|
||||||
|
if typeIndex > 0 {
|
||||||
|
newIndex = videoMailDisplayBase + (typeIndex - 1)
|
||||||
|
}
|
||||||
|
view.requestPropertyChange(func(msg *text.ElectronicMessage) { msg.LeftDisplay = newIndex })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
imgui.EndCombo()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if message.LeftDisplay < videoMailDisplayBase {
|
||||||
|
view.renderDisplayGallery(false, "Left Display", message.LeftDisplay, availableDisplays,
|
||||||
|
func(newValue int) {
|
||||||
|
view.requestPropertyChange(func(msg *text.ElectronicMessage) { msg.LeftDisplay = newValue })
|
||||||
|
})
|
||||||
|
view.renderDisplayGallery(false, "Right Display", message.RightDisplay, availableDisplays,
|
||||||
|
func(newValue int) {
|
||||||
|
view.requestPropertyChange(func(msg *text.ElectronicMessage) { msg.RightDisplay = newValue })
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
imgui.PopItemWidth()
|
||||||
|
}
|
||||||
|
imgui.EndChild()
|
||||||
|
|
||||||
|
view.renderMFDs()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (view *View) renderMFDs() {
|
||||||
|
message, _ := view.currentMessage()
|
||||||
|
|
||||||
|
view.renderSideImage("LeftMFD", message.LeftDisplay)
|
||||||
|
imgui.SameLineV(0, 0)
|
||||||
|
textToDisplay := message.VerboseText
|
||||||
|
if imgui.BeginChildV("Text", imgui.Vec2{X: -250 * view.guiScale, Y: 190 * view.guiScale}, true, 0) {
|
||||||
|
if !view.model.showVerboseText {
|
||||||
|
textToDisplay = message.TerseText
|
||||||
|
}
|
||||||
|
|
||||||
|
imgui.PushTextWrapPos()
|
||||||
|
if len(textToDisplay) == 0 {
|
||||||
|
imgui.PushStyleColor(imgui.StyleColorText, imgui.Vec4{X: 1.0, Y: 1.0, Z: 1.0, W: 0.5})
|
||||||
|
imgui.Text("(empty)")
|
||||||
|
imgui.PopStyleColor()
|
||||||
|
} else {
|
||||||
|
imgui.Text(textToDisplay)
|
||||||
|
}
|
||||||
|
imgui.PopTextWrapPos()
|
||||||
|
}
|
||||||
|
imgui.EndChild()
|
||||||
|
view.clipboardPopup(false, "Text", textToDisplay, func(newValue string) {
|
||||||
|
view.requestTextChange(func(msg *text.ElectronicMessage) {
|
||||||
|
if view.model.showVerboseText {
|
||||||
|
msg.VerboseText = newValue
|
||||||
|
} else {
|
||||||
|
msg.TerseText = newValue
|
||||||
|
}
|
||||||
|
})
|
||||||
|
})
|
||||||
|
imgui.SameLineV(0, 0)
|
||||||
|
view.renderSideImage("RightMFD", message.RightDisplay)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (view View) currentMessage() (msg text.ElectronicMessage, readOnly bool) {
|
||||||
|
msg = view.messageOf(view.model.currentKey)
|
||||||
|
readOnly = len(view.mod.ModifiedBlocks(view.model.currentKey.Lang, view.model.currentKey.ID.Plus(view.model.currentKey.Index))) == 0
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
func (view View) hasAudio() bool {
|
||||||
|
return (view.model.currentKey.ID == ids.MailsStart) || (view.model.currentKey.ID == ids.LogsStart)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (view *View) currentSound() (audio.L8, bool) {
|
||||||
|
if view.hasAudio() {
|
||||||
|
key := view.model.currentKey
|
||||||
|
key.ID = key.ID.Plus(300)
|
||||||
|
sound, _ := view.movieCache.Audio(key)
|
||||||
|
readOnly := len(view.mod.ModifiedBlocks(key.Lang, key.ID.Plus(view.model.currentKey.Index))) == 0
|
||||||
|
return sound, readOnly
|
||||||
|
}
|
||||||
|
return audio.L8{}, true
|
||||||
|
}
|
||||||
|
|
||||||
|
func (view View) messageOf(key resource.Key) text.ElectronicMessage {
|
||||||
|
msg, cacheErr := view.messageCache.Message(key)
|
||||||
|
if cacheErr != nil {
|
||||||
|
msg = text.EmptyElectronicMessage()
|
||||||
|
}
|
||||||
|
return msg
|
||||||
|
}
|
||||||
|
|
||||||
|
func (view *View) availableDisplays() int {
|
||||||
|
selector := view.mod.LocalizedResources(view.model.currentKey.Lang)
|
||||||
|
res, err := selector.Select(ids.MfdDataBitmaps)
|
||||||
|
if err != nil {
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
return res.BlockCount()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (view *View) renderText(readOnly bool, label string, value string, changeCallback func(string)) {
|
||||||
|
imgui.LabelText(label, value)
|
||||||
|
view.clipboardPopup(readOnly, label, value, changeCallback)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (view *View) clipboardPopup(readOnly bool, label string, value string, changeCallback func(string)) {
|
||||||
|
if imgui.BeginPopupContextItemV(label+"-Popup", 1) {
|
||||||
|
if imgui.Selectable("Copy to Clipboard") {
|
||||||
|
view.clipboard.SetString(value)
|
||||||
|
}
|
||||||
|
if !readOnly && imgui.Selectable("Copy from Clipboard") {
|
||||||
|
newValue, err := view.clipboard.String()
|
||||||
|
if err == nil {
|
||||||
|
changeCallback(newValue)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
imgui.EndPopup()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (view *View) renderDisplayGallery(readOnly bool, label string, index, availableDisplays int,
|
||||||
|
changeCallback func(int)) {
|
||||||
|
unifier := values.NewUnifier()
|
||||||
|
unifier.Add(index)
|
||||||
|
values.RenderUnifiedSliderInt(readOnly, label, unifier,
|
||||||
|
func(u values.Unifier) int { return u.Unified().(int) },
|
||||||
|
func(int) string { return "%d" },
|
||||||
|
-1, availableDisplays-1,
|
||||||
|
changeCallback)
|
||||||
|
render.TextureSelector("###"+label+" Texture", -1, view.guiScale, availableDisplays,
|
||||||
|
index, view.imageCache,
|
||||||
|
func(index int) resource.Key {
|
||||||
|
return resource.KeyOf(ids.MfdDataBitmaps, view.model.currentKey.Lang, index)
|
||||||
|
},
|
||||||
|
func(index int) string { return fmt.Sprintf("%d", index) },
|
||||||
|
changeCallback)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (view *View) renderSideImage(label string, index int) {
|
||||||
|
size := imgui.Vec2{X: 250 * view.guiScale, Y: 190 * view.guiScale}
|
||||||
|
imgui.PushStyleVarVec2(imgui.StyleVarWindowPadding, imgui.Vec2{X: size.X, Y: -size.Y})
|
||||||
|
if imgui.BeginChildV(label, size, false, 0) {
|
||||||
|
if index >= 0 {
|
||||||
|
key := resource.KeyOf(ids.MfdDataBitmaps, view.model.currentKey.Lang, index)
|
||||||
|
render.TextureImage(label, view.imageCache, key, size)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
imgui.PopStyleVar()
|
||||||
|
imgui.EndChild()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (view *View) requestExportAudio(sound audio.L8) {
|
||||||
|
filename := fmt.Sprintf("%05d_%s.wav",
|
||||||
|
view.model.currentKey.ID.Plus(view.model.currentKey.Index).Plus(300).Value(),
|
||||||
|
view.model.currentKey.Lang.String())
|
||||||
|
|
||||||
|
external.ExportAudio(view.modalStateMachine, filename, sound)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (view *View) requestRemoveAudio() {
|
||||||
|
view.requestAudioChange(nil)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (view *View) requestImportAudio() {
|
||||||
|
external.ImportAudio(view.modalStateMachine, func(sound audio.L8) {
|
||||||
|
movieData := movie.ContainSoundData(sound)
|
||||||
|
view.requestAudioChange(movieData)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func (view *View) requestClearAudio() {
|
||||||
|
view.requestAudioChange(view.silence())
|
||||||
|
}
|
||||||
|
|
||||||
|
func (view View) silence() []byte {
|
||||||
|
silence := audio.L8{SampleRate: 22050, Samples: []byte{0x80}}
|
||||||
|
return movie.ContainSoundData(silence)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (view *View) requestClearAll() {
|
||||||
|
view.requestWipe(text.EmptyElectronicMessage().Encode(view.cp), view.silence())
|
||||||
|
}
|
||||||
|
|
||||||
|
func (view *View) requestRemoveAll() {
|
||||||
|
view.requestWipe(nil, nil)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (view *View) requestWipe(newTextData [][]byte, newAudioData []byte) {
|
||||||
|
textEntries := make(map[resource.Language]messageDataEntry)
|
||||||
|
audioEntries := make(map[resource.Language]messageDataEntry)
|
||||||
|
textID := view.model.currentKey.ID.Plus(view.model.currentKey.Index)
|
||||||
|
for lang := resource.Language(0); lang < resource.LanguageCount; lang++ {
|
||||||
|
textEntries[lang] = messageDataEntry{
|
||||||
|
oldData: view.mod.ModifiedBlocks(lang, textID),
|
||||||
|
newData: newTextData,
|
||||||
|
}
|
||||||
|
if view.hasAudio() {
|
||||||
|
audioEntries[lang] = messageDataEntry{
|
||||||
|
oldData: view.mod.ModifiedBlocks(lang, textID.Plus(300)),
|
||||||
|
newData: [][]byte{newAudioData},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
view.requestSetMessageData(textEntries, audioEntries)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (view *View) requestTextChange(modifier func(*text.ElectronicMessage)) {
|
||||||
|
msg := view.messageOf(view.model.currentKey)
|
||||||
|
entries := make(map[resource.Language]messageDataEntry)
|
||||||
|
modifier(&msg)
|
||||||
|
|
||||||
|
entries[view.model.currentKey.Lang] = messageDataEntry{
|
||||||
|
oldData: view.mod.ModifiedBlocks(view.model.currentKey.Lang, view.model.currentKey.ID.Plus(view.model.currentKey.Index)),
|
||||||
|
newData: msg.Encode(view.cp),
|
||||||
|
}
|
||||||
|
view.requestSetMessageData(entries, nil)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (view *View) requestPropertyChange(modifier func(*text.ElectronicMessage)) {
|
||||||
|
entries := make(map[resource.Language]messageDataEntry)
|
||||||
|
for lang := resource.Language(0); lang < resource.LanguageCount; lang++ {
|
||||||
|
key := view.model.currentKey
|
||||||
|
key.Lang = lang
|
||||||
|
msg := view.messageOf(key)
|
||||||
|
modifier(&msg)
|
||||||
|
|
||||||
|
entries[lang] = messageDataEntry{
|
||||||
|
oldData: view.mod.ModifiedBlocks(lang, key.ID.Plus(key.Index)),
|
||||||
|
newData: msg.Encode(view.cp),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
view.requestSetMessageData(entries, nil)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (view *View) requestAudioChange(movieData []byte) {
|
||||||
|
entries := make(map[resource.Language]messageDataEntry)
|
||||||
|
|
||||||
|
entries[view.model.currentKey.Lang] = messageDataEntry{
|
||||||
|
oldData: view.mod.ModifiedBlocks(view.model.currentKey.Lang, view.model.currentKey.ID.Plus(view.model.currentKey.Index).Plus(300)),
|
||||||
|
newData: [][]byte{movieData},
|
||||||
|
}
|
||||||
|
view.requestSetMessageData(nil, entries)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (view *View) requestSetMessageData(textEntries map[resource.Language]messageDataEntry, audioEntries map[resource.Language]messageDataEntry) {
|
||||||
|
command := setMessageDataCommand{
|
||||||
|
key: view.model.currentKey,
|
||||||
|
showVerboseText: view.model.showVerboseText,
|
||||||
|
model: &view.model,
|
||||||
|
textEntries: textEntries,
|
||||||
|
audioEntries: audioEntries,
|
||||||
|
}
|
||||||
|
view.commander.Queue(command)
|
||||||
|
}
|
||||||
@@ -0,0 +1,21 @@
|
|||||||
|
package messages
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/inkyblackness/hacked/ss1/resource"
|
||||||
|
"github.com/inkyblackness/hacked/ss1/world/ids"
|
||||||
|
)
|
||||||
|
|
||||||
|
type viewModel struct {
|
||||||
|
windowOpen bool
|
||||||
|
restoreFocus bool
|
||||||
|
|
||||||
|
currentKey resource.Key
|
||||||
|
showVerboseText bool
|
||||||
|
}
|
||||||
|
|
||||||
|
func freshViewModel() viewModel {
|
||||||
|
return viewModel{
|
||||||
|
currentKey: resource.KeyOf(ids.MailsStart, resource.LangDefault, 0),
|
||||||
|
showVerboseText: true,
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,36 @@
|
|||||||
|
package movies
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/inkyblackness/imgui-go/v3"
|
||||||
|
|
||||||
|
"github.com/inkyblackness/hacked/ss1/content/movie"
|
||||||
|
"github.com/inkyblackness/hacked/ui/gui"
|
||||||
|
)
|
||||||
|
|
||||||
|
type compressionListenerFunc func(compressionResult)
|
||||||
|
|
||||||
|
type compressingStartState struct {
|
||||||
|
machine gui.ModalStateMachine
|
||||||
|
view *View
|
||||||
|
|
||||||
|
width int
|
||||||
|
height int
|
||||||
|
input movie.Scene
|
||||||
|
listener compressionListenerFunc
|
||||||
|
}
|
||||||
|
|
||||||
|
func (state compressingStartState) Render() {
|
||||||
|
imgui.OpenPopup("Compressing...")
|
||||||
|
task := newCompressionTask(state.input, state.width, state.height)
|
||||||
|
state.machine.SetState(&compressingWaitingState{
|
||||||
|
machine: state.machine,
|
||||||
|
view: state.view,
|
||||||
|
input: state.input,
|
||||||
|
listener: state.listener,
|
||||||
|
task: task,
|
||||||
|
})
|
||||||
|
go task.run()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (state compressingStartState) HandleFiles(names []string) {
|
||||||
|
}
|
||||||
@@ -0,0 +1,45 @@
|
|||||||
|
package movies
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/inkyblackness/imgui-go/v3"
|
||||||
|
|
||||||
|
"github.com/inkyblackness/hacked/ss1/content/movie"
|
||||||
|
"github.com/inkyblackness/hacked/ui/gui"
|
||||||
|
)
|
||||||
|
|
||||||
|
type compressingWaitingState struct {
|
||||||
|
machine gui.ModalStateMachine
|
||||||
|
view *View
|
||||||
|
|
||||||
|
input movie.Scene
|
||||||
|
listener compressionListenerFunc
|
||||||
|
task *compressionTask
|
||||||
|
}
|
||||||
|
|
||||||
|
func (state *compressingWaitingState) Render() {
|
||||||
|
if imgui.BeginPopupModalV("Compressing...", nil,
|
||||||
|
imgui.WindowFlagsNoResize|imgui.WindowFlagsNoMove|imgui.WindowFlagsNoSavedSettings|imgui.WindowFlagsAlwaysAutoResize) {
|
||||||
|
imgui.Text("This may take a while.\n" +
|
||||||
|
"Yes, your PC is probably capable of recoding HD movies way quicker;\n" +
|
||||||
|
"Sadly, this codec of '94 is quite tricky.")
|
||||||
|
|
||||||
|
if imgui.Button("Cancel") {
|
||||||
|
state.task.cancel()
|
||||||
|
}
|
||||||
|
|
||||||
|
result := state.task.update()
|
||||||
|
if result != nil {
|
||||||
|
state.machine.SetState(nil)
|
||||||
|
imgui.CloseCurrentPopup()
|
||||||
|
|
||||||
|
state.listener(result)
|
||||||
|
}
|
||||||
|
|
||||||
|
imgui.EndPopup()
|
||||||
|
} else {
|
||||||
|
state.machine.SetState(nil)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (state *compressingWaitingState) HandleFiles(names []string) {
|
||||||
|
}
|
||||||
@@ -0,0 +1,65 @@
|
|||||||
|
package movies
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
|
||||||
|
"github.com/inkyblackness/hacked/ss1/content/movie"
|
||||||
|
)
|
||||||
|
|
||||||
|
type compressionTask struct {
|
||||||
|
width int
|
||||||
|
height int
|
||||||
|
input movie.Scene
|
||||||
|
ctx context.Context
|
||||||
|
ctxCancel context.CancelFunc
|
||||||
|
resultChan chan compressionResult
|
||||||
|
}
|
||||||
|
|
||||||
|
type compressionResult interface{}
|
||||||
|
|
||||||
|
type compressionAborted struct{}
|
||||||
|
|
||||||
|
type compressionFailed struct{ err error }
|
||||||
|
|
||||||
|
type compressionFinished struct{ scene movie.HighResScene }
|
||||||
|
|
||||||
|
func newCompressionTask(scene movie.Scene, width, height int) *compressionTask {
|
||||||
|
task := &compressionTask{
|
||||||
|
width: width,
|
||||||
|
height: height,
|
||||||
|
input: scene,
|
||||||
|
resultChan: make(chan compressionResult),
|
||||||
|
}
|
||||||
|
task.ctx, task.ctxCancel = context.WithCancel(context.Background())
|
||||||
|
|
||||||
|
return task
|
||||||
|
}
|
||||||
|
|
||||||
|
func (task *compressionTask) run() {
|
||||||
|
defer close(task.resultChan)
|
||||||
|
highResScene, err := movie.HighResSceneFrom(task.ctx, task.input, task.width, task.height)
|
||||||
|
switch {
|
||||||
|
case task.ctx.Err() != nil:
|
||||||
|
task.resultChan <- compressionAborted{}
|
||||||
|
case err != nil:
|
||||||
|
task.resultChan <- compressionFailed{err: err}
|
||||||
|
default:
|
||||||
|
task.resultChan <- compressionFinished{scene: highResScene}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (task *compressionTask) update() compressionResult {
|
||||||
|
select {
|
||||||
|
case result, ok := <-task.resultChan:
|
||||||
|
if ok {
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
return compressionAborted{}
|
||||||
|
default:
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (task *compressionTask) cancel() {
|
||||||
|
task.ctxCancel()
|
||||||
|
}
|
||||||
@@ -0,0 +1,615 @@
|
|||||||
|
package movies
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"image"
|
||||||
|
"image/color"
|
||||||
|
"image/gif"
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/asticode/go-astisub"
|
||||||
|
"github.com/inkyblackness/imgui-go/v3"
|
||||||
|
|
||||||
|
"github.com/inkyblackness/hacked/editor/external"
|
||||||
|
"github.com/inkyblackness/hacked/editor/graphics"
|
||||||
|
"github.com/inkyblackness/hacked/editor/render"
|
||||||
|
"github.com/inkyblackness/hacked/ss1/content/audio"
|
||||||
|
"github.com/inkyblackness/hacked/ss1/content/bitmap"
|
||||||
|
"github.com/inkyblackness/hacked/ss1/content/movie"
|
||||||
|
"github.com/inkyblackness/hacked/ss1/edit/undoable"
|
||||||
|
"github.com/inkyblackness/hacked/ss1/edit/undoable/cmd"
|
||||||
|
"github.com/inkyblackness/hacked/ss1/resource"
|
||||||
|
"github.com/inkyblackness/hacked/ss1/world"
|
||||||
|
"github.com/inkyblackness/hacked/ss1/world/ids"
|
||||||
|
"github.com/inkyblackness/hacked/ui/gui"
|
||||||
|
)
|
||||||
|
|
||||||
|
type movieInfo struct {
|
||||||
|
title string
|
||||||
|
multilang bool
|
||||||
|
}
|
||||||
|
|
||||||
|
var knownMovies = map[resource.ID]movieInfo{
|
||||||
|
ids.MovieIntro: {title: "Intro", multilang: true},
|
||||||
|
ids.MovieDeath: {title: "Death", multilang: false},
|
||||||
|
ids.MovieEnd: {title: "End", multilang: false},
|
||||||
|
}
|
||||||
|
|
||||||
|
var knownMoviesOrder = []resource.ID{ids.MovieIntro, ids.MovieDeath, ids.MovieEnd}
|
||||||
|
|
||||||
|
// View provides edit controls for animations.
|
||||||
|
type View struct {
|
||||||
|
mod *world.Mod
|
||||||
|
|
||||||
|
frameCache *graphics.FrameCache
|
||||||
|
frameCacheKey graphics.FrameCacheKey
|
||||||
|
|
||||||
|
movieService undoable.MovieService
|
||||||
|
|
||||||
|
modalStateMachine gui.ModalStateMachine
|
||||||
|
guiScale float32
|
||||||
|
commander cmd.Commander
|
||||||
|
|
||||||
|
model viewModel
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewMoviesView returns a new instance.
|
||||||
|
func NewMoviesView(mod *world.Mod, frameCache *graphics.FrameCache,
|
||||||
|
movieService undoable.MovieService,
|
||||||
|
modalStateMachine gui.ModalStateMachine, guiScale float32, commander cmd.Commander) *View {
|
||||||
|
view := &View{
|
||||||
|
mod: mod,
|
||||||
|
|
||||||
|
frameCache: frameCache,
|
||||||
|
frameCacheKey: frameCache.AllocateKey(),
|
||||||
|
|
||||||
|
movieService: movieService,
|
||||||
|
|
||||||
|
modalStateMachine: modalStateMachine,
|
||||||
|
guiScale: guiScale,
|
||||||
|
commander: commander,
|
||||||
|
|
||||||
|
model: freshViewModel(),
|
||||||
|
}
|
||||||
|
return view
|
||||||
|
}
|
||||||
|
|
||||||
|
// WindowOpen returns the flag address, to be used with the main menu.
|
||||||
|
func (view *View) WindowOpen() *bool {
|
||||||
|
return &view.model.windowOpen
|
||||||
|
}
|
||||||
|
|
||||||
|
// Render renders the view.
|
||||||
|
func (view *View) Render() {
|
||||||
|
if view.model.restoreFocus {
|
||||||
|
imgui.SetNextWindowFocus()
|
||||||
|
view.model.restoreFocus = false
|
||||||
|
view.model.windowOpen = true
|
||||||
|
}
|
||||||
|
if view.model.windowOpen {
|
||||||
|
imgui.SetNextWindowSizeV(imgui.Vec2{X: 800 * view.guiScale, Y: 300 * view.guiScale}, imgui.ConditionFirstUseEver)
|
||||||
|
if imgui.BeginV("Movies", view.WindowOpen(), imgui.WindowFlagsNoCollapse|imgui.WindowFlagsHorizontalScrollbar) {
|
||||||
|
view.renderContent()
|
||||||
|
}
|
||||||
|
imgui.End()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (view *View) renderContent() {
|
||||||
|
if imgui.BeginChildV("Properties", imgui.Vec2{X: 350 * view.guiScale, Y: 0}, false, 0) {
|
||||||
|
imgui.PushItemWidth(-150 * view.guiScale)
|
||||||
|
if imgui.BeginCombo("Movie", knownMovies[view.model.currentKey.ID].title) {
|
||||||
|
for _, id := range knownMoviesOrder {
|
||||||
|
if imgui.SelectableV(knownMovies[id].title, id == view.model.currentKey.ID, 0, imgui.Vec2{}) {
|
||||||
|
view.model.currentKey.ID = id
|
||||||
|
view.model.currentKey.Index = 0
|
||||||
|
view.model.currentScene = 0
|
||||||
|
view.model.currentFrame = 0
|
||||||
|
view.model.frameTimeFraction = -1
|
||||||
|
}
|
||||||
|
}
|
||||||
|
imgui.EndCombo()
|
||||||
|
}
|
||||||
|
|
||||||
|
if knownMovies[view.model.currentKey.ID].multilang {
|
||||||
|
if imgui.BeginCombo("Language", view.model.currentKey.Lang.String()) {
|
||||||
|
languages := resource.Languages()
|
||||||
|
for _, lang := range languages {
|
||||||
|
if imgui.SelectableV(lang.String(), lang == view.model.currentKey.Lang, 0, imgui.Vec2{}) {
|
||||||
|
view.model.currentKey.Lang = lang
|
||||||
|
}
|
||||||
|
}
|
||||||
|
imgui.EndCombo()
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
imgui.LabelText("Language", "(not localized)")
|
||||||
|
}
|
||||||
|
|
||||||
|
imgui.Separator()
|
||||||
|
|
||||||
|
view.renderProperties()
|
||||||
|
|
||||||
|
imgui.PopItemWidth()
|
||||||
|
}
|
||||||
|
imgui.EndChild()
|
||||||
|
|
||||||
|
scenes := view.movieService.Video(view.model.currentKey)
|
||||||
|
imgui.SameLine()
|
||||||
|
imgui.BeginGroup()
|
||||||
|
imgui.BeginChildV("Scenes", imgui.Vec2{X: 200 * view.guiScale, Y: -60 * view.guiScale}, true,
|
||||||
|
imgui.WindowFlagsHorizontalScrollbar|imgui.WindowFlagsAlwaysVerticalScrollbar)
|
||||||
|
for index, scene := range scenes {
|
||||||
|
sceneInfo := fmt.Sprintf("Scene %02d (%d frames)", index, len(scene.Frames))
|
||||||
|
if imgui.SelectableV(sceneInfo, view.model.currentScene == index, 0, imgui.Vec2{}) {
|
||||||
|
view.model.currentScene = index
|
||||||
|
view.model.currentFrame = 0
|
||||||
|
view.model.frameTimeFraction = -1
|
||||||
|
}
|
||||||
|
}
|
||||||
|
imgui.EndChild()
|
||||||
|
if imgui.Button("Up") {
|
||||||
|
view.requestMoveSceneEarlier()
|
||||||
|
}
|
||||||
|
imgui.SameLine()
|
||||||
|
if imgui.Button("Down") {
|
||||||
|
view.requestMoveSceneLater()
|
||||||
|
}
|
||||||
|
imgui.SameLine()
|
||||||
|
if imgui.Button("Remove") {
|
||||||
|
view.requestRemoveScene()
|
||||||
|
}
|
||||||
|
if imgui.Button("Import") {
|
||||||
|
view.requestImportScene("")
|
||||||
|
}
|
||||||
|
imgui.SameLine()
|
||||||
|
if imgui.Button("Export") {
|
||||||
|
view.requestExportScene()
|
||||||
|
}
|
||||||
|
imgui.EndGroup()
|
||||||
|
imgui.SameLine()
|
||||||
|
if imgui.BeginChildV("Frames", imgui.Vec2{X: -1, Y: 0}, false, 0) {
|
||||||
|
var scene *movie.Scene
|
||||||
|
if (view.model.currentScene >= 0) && (view.model.currentScene < len(scenes)) {
|
||||||
|
scene = &scenes[view.model.currentScene]
|
||||||
|
}
|
||||||
|
if scene != nil {
|
||||||
|
var frame *movie.Frame
|
||||||
|
if (view.model.currentFrame >= 0) && (view.model.currentFrame < len(scene.Frames)) {
|
||||||
|
frame = &scene.Frames[view.model.currentFrame]
|
||||||
|
}
|
||||||
|
if frame != nil {
|
||||||
|
// This code updates the texture every render cycle. In case of performance loss, this is a point to optimize.
|
||||||
|
view.frameCache.SetTexture(view.frameCacheKey, 600, 300, frame.Pixels, &scene.Palette)
|
||||||
|
|
||||||
|
render.FrameImage("Frame", view.frameCache, view.frameCacheKey,
|
||||||
|
imgui.Vec2{
|
||||||
|
X: float32(movie.HighResDefaultWidth) * view.guiScale,
|
||||||
|
Y: float32(movie.HighResDefaultHeight) * view.guiScale,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
imgui.PushItemWidth(-150 * view.guiScale)
|
||||||
|
gui.StepSliderInt("Frame Index", &view.model.currentFrame, 0, len(scene.Frames)-1)
|
||||||
|
imgui.PopItemWidth()
|
||||||
|
if (frame != nil) && (frame.DisplayTime < (time.Second / 4)) {
|
||||||
|
imgui.Separator()
|
||||||
|
if view.model.frameTimeFraction < 0 {
|
||||||
|
view.model.frameTimeFraction = int((frame.DisplayTime * 0x10000) / time.Second)
|
||||||
|
}
|
||||||
|
imgui.PushItemWidth(-150 * view.guiScale)
|
||||||
|
gui.StepSliderInt("Frame time fraction", &view.model.frameTimeFraction, 0, 0x3FFF)
|
||||||
|
imgui.PopItemWidth()
|
||||||
|
if imgui.Button("Set for scene") {
|
||||||
|
displayTime := (time.Duration(view.model.frameTimeFraction) * time.Second) / 0x10000
|
||||||
|
view.requestSetFramesDisplayTime(displayTime)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
imgui.EndChild()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (view *View) renderProperties() {
|
||||||
|
view.renderAudioProperties()
|
||||||
|
view.renderSubtitlesProperties()
|
||||||
|
|
||||||
|
if view.movieService.SizeWarning(view.model.currentKey) {
|
||||||
|
view.renderSizeWarning()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (view *View) renderAudioProperties() {
|
||||||
|
imgui.PushID("audio")
|
||||||
|
imgui.Separator()
|
||||||
|
sound := view.currentSound()
|
||||||
|
imgui.LabelText("Audio", fmt.Sprintf("%.2f sec", sound.Duration()))
|
||||||
|
if imgui.Button("Import") {
|
||||||
|
view.requestImportAudio()
|
||||||
|
}
|
||||||
|
if !sound.Empty() {
|
||||||
|
imgui.SameLine()
|
||||||
|
if imgui.Button("Export") {
|
||||||
|
view.requestExportAudio(sound)
|
||||||
|
}
|
||||||
|
imgui.SameLine()
|
||||||
|
if imgui.Button("Clear") {
|
||||||
|
view.requestClearAudio()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
imgui.PopID()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (view *View) renderSubtitlesProperties() {
|
||||||
|
imgui.PushID("subtitles")
|
||||||
|
imgui.Separator()
|
||||||
|
if imgui.BeginCombo("Sub Language", view.model.currentSubtitleLang.String()) {
|
||||||
|
languages := resource.Languages()
|
||||||
|
for _, lang := range languages {
|
||||||
|
if imgui.SelectableV(lang.String(), lang == view.model.currentSubtitleLang, 0, imgui.Vec2{}) {
|
||||||
|
view.model.currentSubtitleLang = lang
|
||||||
|
}
|
||||||
|
}
|
||||||
|
imgui.EndCombo()
|
||||||
|
}
|
||||||
|
sub := view.currentSubtitles()
|
||||||
|
imgui.Text(fmt.Sprintf("%d lines", len(sub.Entries)))
|
||||||
|
if imgui.Button("Import") {
|
||||||
|
view.requestImportSubtitles()
|
||||||
|
}
|
||||||
|
if len(sub.Entries) > 0 {
|
||||||
|
imgui.SameLine()
|
||||||
|
if imgui.Button("Export") {
|
||||||
|
view.requestExportSubtitles()
|
||||||
|
}
|
||||||
|
imgui.SameLine()
|
||||||
|
if imgui.Button("Clear") {
|
||||||
|
view.requestClearSubtitles()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
imgui.PopID()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (view *View) renderSizeWarning() {
|
||||||
|
imgui.Separator()
|
||||||
|
imgui.PushStyleColor(imgui.StyleColorText, imgui.Vec4{X: 1, Y: 0, Z: 0, W: 1})
|
||||||
|
imgui.Text("Size warning - reduce content!")
|
||||||
|
imgui.PopStyleColor()
|
||||||
|
if imgui.IsItemHovered() {
|
||||||
|
imgui.BeginTooltip()
|
||||||
|
imgui.SetTooltip("The movie has reached a size larger than officially supported.\n" +
|
||||||
|
"While the game might still show the movie, a future update may stop supporting it.")
|
||||||
|
imgui.EndTooltip()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (view *View) restoreFunc() func() {
|
||||||
|
return view.restoreFuncWithScene(view.model.currentScene)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (view *View) restoreFuncWithScene(oldScene int) func() {
|
||||||
|
oldKey := view.model.currentKey
|
||||||
|
oldSubtitlesLang := view.model.currentSubtitleLang
|
||||||
|
oldFrame := view.model.currentFrame
|
||||||
|
|
||||||
|
return func() {
|
||||||
|
view.model.restoreFocus = true
|
||||||
|
view.model.currentKey = oldKey
|
||||||
|
view.model.currentSubtitleLang = oldSubtitlesLang
|
||||||
|
view.model.currentScene = oldScene
|
||||||
|
view.model.currentFrame = oldFrame
|
||||||
|
view.model.frameTimeFraction = -1
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (view *View) currentSound() audio.L8 {
|
||||||
|
return view.movieService.Audio(view.model.currentKey)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (view *View) requestExportAudio(sound audio.L8) {
|
||||||
|
filename := fmt.Sprintf("%s_%s.wav", knownMovies[view.model.currentKey.ID].title, view.model.currentKey.Lang.String())
|
||||||
|
|
||||||
|
external.ExportAudio(view.modalStateMachine, filename, sound)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (view *View) requestImportAudio() {
|
||||||
|
external.ImportAudio(view.modalStateMachine, func(sound audio.L8) {
|
||||||
|
view.movieService.RequestSetAudio(view.model.currentKey, sound, view.restoreFunc())
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func (view *View) requestClearAudio() {
|
||||||
|
view.movieService.RequestSetAudio(view.model.currentKey, audio.L8{}, view.restoreFunc())
|
||||||
|
}
|
||||||
|
|
||||||
|
func (view *View) currentSubtitles() movie.SubtitleList {
|
||||||
|
return view.movieService.Subtitles(view.model.currentKey, view.model.currentSubtitleLang)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (view View) requestExportSubtitles() {
|
||||||
|
filename := fmt.Sprintf("%s_%s.srt", knownMovies[view.model.currentKey.ID].title, view.model.currentSubtitleLang.String())
|
||||||
|
info := "File to be written: " + filename
|
||||||
|
var exportTo func(string)
|
||||||
|
currentSubtitles := view.currentSubtitles()
|
||||||
|
|
||||||
|
exportTo = func(dirname string) {
|
||||||
|
writer, err := os.Create(filepath.Join(dirname, filename))
|
||||||
|
if err != nil {
|
||||||
|
external.Export(view.modalStateMachine, "Could not create file.\n"+info, exportTo, true)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
defer func() { _ = writer.Close() }()
|
||||||
|
|
||||||
|
sub := astisub.NewSubtitles()
|
||||||
|
|
||||||
|
var lastItem *astisub.Item
|
||||||
|
for _, entry := range currentSubtitles.Entries {
|
||||||
|
var item astisub.Item
|
||||||
|
var line astisub.Line
|
||||||
|
line.Items = append(line.Items, astisub.LineItem{Text: entry.Text})
|
||||||
|
item.Lines = []astisub.Line{line}
|
||||||
|
item.StartAt = entry.Timestamp
|
||||||
|
item.EndAt = item.StartAt
|
||||||
|
if lastItem != nil {
|
||||||
|
lastItem.EndAt = item.StartAt
|
||||||
|
}
|
||||||
|
lastItem = &item
|
||||||
|
sub.Items = append(sub.Items, lastItem)
|
||||||
|
}
|
||||||
|
|
||||||
|
err = sub.WriteToSRT(writer)
|
||||||
|
if err != nil {
|
||||||
|
external.Export(view.modalStateMachine, "Could not export subtitles.\n"+info, exportTo, true)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
external.Export(view.modalStateMachine, info, exportTo, false)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (view *View) requestImportSubtitles() {
|
||||||
|
info := "File must be an .SRT file."
|
||||||
|
types := []external.TypeInfo{{Title: "Subtitle files (*.srt)", Extensions: []string{"srt"}}}
|
||||||
|
var fileHandler func(string)
|
||||||
|
|
||||||
|
fileHandler = func(filename string) {
|
||||||
|
reader, err := os.Open(filename)
|
||||||
|
if err != nil {
|
||||||
|
external.Import(view.modalStateMachine, "Could not open file.\n"+info, types, fileHandler, true)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
defer func() { _ = reader.Close() }()
|
||||||
|
|
||||||
|
subtitles, err := astisub.ReadFromSRT(reader)
|
||||||
|
if err != nil {
|
||||||
|
external.Import(view.modalStateMachine, "File not recognized as SRT.\n"+info, types, fileHandler, true)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
var newSubtitles movie.SubtitleList
|
||||||
|
for _, item := range subtitles.Items {
|
||||||
|
var newEntry movie.Subtitle
|
||||||
|
newEntry.Timestamp = item.StartAt
|
||||||
|
for _, line := range item.Lines {
|
||||||
|
for _, lineItem := range line.Items {
|
||||||
|
if len(newEntry.Text) > 0 {
|
||||||
|
newEntry.Text += "\n"
|
||||||
|
}
|
||||||
|
newEntry.Text += lineItem.Text
|
||||||
|
}
|
||||||
|
}
|
||||||
|
newSubtitles.Entries = append(newSubtitles.Entries, newEntry)
|
||||||
|
}
|
||||||
|
|
||||||
|
view.movieService.RequestSetSubtitles(view.model.currentKey, view.model.currentSubtitleLang,
|
||||||
|
newSubtitles, view.restoreFunc())
|
||||||
|
}
|
||||||
|
|
||||||
|
external.Import(view.modalStateMachine, info, types, fileHandler, false)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (view *View) requestClearSubtitles() {
|
||||||
|
view.movieService.RequestSetSubtitles(view.model.currentKey, view.model.currentSubtitleLang,
|
||||||
|
movie.SubtitleList{}, view.restoreFunc())
|
||||||
|
}
|
||||||
|
|
||||||
|
func (view *View) requestImportScene(returningInfo string) {
|
||||||
|
info := fmt.Sprintf("File must be an animated GIF file in size %dx%d.",
|
||||||
|
movie.HighResDefaultWidth, movie.HighResDefaultHeight)
|
||||||
|
types := []external.TypeInfo{{Title: "Animation files (*.gif)", Extensions: []string{"gif"}}}
|
||||||
|
var fileHandler func(string)
|
||||||
|
|
||||||
|
fileHandler = func(filename string) {
|
||||||
|
reader, err := os.Open(filename)
|
||||||
|
if err != nil {
|
||||||
|
external.Import(view.modalStateMachine, "Could not open file.\n"+info, types, fileHandler, true)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
defer func() { _ = reader.Close() }()
|
||||||
|
data, err := gif.DecodeAll(reader)
|
||||||
|
if err != nil {
|
||||||
|
external.Import(view.modalStateMachine, "File not recognized as GIF.\n"+info, types, fileHandler, true)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if (data.Config.Width != movie.HighResDefaultWidth) || (data.Config.Height != movie.HighResDefaultHeight) {
|
||||||
|
external.Import(view.modalStateMachine, info, types, fileHandler, true)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
var scene movie.Scene
|
||||||
|
scene.Frames = make([]movie.Frame, len(data.Image))
|
||||||
|
var palette bitmap.Palette
|
||||||
|
if gifPalette, isPal := data.Config.ColorModel.(color.Palette); isPal {
|
||||||
|
for index, clr := range gifPalette {
|
||||||
|
r, g, b, _ := clr.RGBA()
|
||||||
|
palette[index].Red = byte(r >> 8)
|
||||||
|
palette[index].Green = byte(g >> 8)
|
||||||
|
palette[index].Blue = byte(b >> 8)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
framebuffer := make([]byte, data.Config.Width*data.Config.Height)
|
||||||
|
framebufferSnapshot := func() []byte {
|
||||||
|
buf := make([]byte, len(framebuffer))
|
||||||
|
copy(buf, framebuffer)
|
||||||
|
return buf
|
||||||
|
}
|
||||||
|
|
||||||
|
var usedColors [256]bool
|
||||||
|
for _, img := range data.Image {
|
||||||
|
for y := img.Bounds().Min.Y; y < img.Bounds().Max.Y; y++ {
|
||||||
|
for x := img.Bounds().Min.X; x < img.Bounds().Max.X; x++ {
|
||||||
|
colorIndex := img.ColorIndexAt(x, y)
|
||||||
|
usedColors[colorIndex] = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
var indexMap [256]byte
|
||||||
|
for i := 0; i < 256; i++ {
|
||||||
|
indexMap[byte(i)] = byte(i)
|
||||||
|
}
|
||||||
|
// Ensure color index 0 is not used.
|
||||||
|
if usedColors[0] && (data.BackgroundIndex != 0) {
|
||||||
|
if data.BackgroundIndex == 255 {
|
||||||
|
indexMap[0] = palette.IndexClosestTo(palette[0], []byte{0, 255})
|
||||||
|
} else {
|
||||||
|
palette[0], palette[data.BackgroundIndex] = palette[data.BackgroundIndex], palette[0]
|
||||||
|
indexMap[0] = data.BackgroundIndex
|
||||||
|
indexMap[data.BackgroundIndex] = 0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
palette[0] = bitmap.RGB{Red: 0x00, Green: 0x00, Blue: 0x00} // default color for background
|
||||||
|
// In case the last index is in use by the animation, use a similar color as this entry is reserved for subtitles.
|
||||||
|
if usedColors[255] && (data.BackgroundIndex != 255) {
|
||||||
|
indexMap[255] = palette.IndexClosestTo(palette[255], []byte{0, 255})
|
||||||
|
}
|
||||||
|
palette[255] = bitmap.RGB{Red: 0x9A, Green: 0x35, Blue: 0x35} // default color in intro
|
||||||
|
|
||||||
|
scene.Palette = palette
|
||||||
|
for index, img := range data.Image {
|
||||||
|
for y := img.Bounds().Min.Y; y < img.Bounds().Max.Y; y++ {
|
||||||
|
for x := img.Bounds().Min.X; x < img.Bounds().Max.X; x++ {
|
||||||
|
colorIndex := img.ColorIndexAt(x, y)
|
||||||
|
if colorIndex != data.BackgroundIndex {
|
||||||
|
framebuffer[y*data.Config.Width+x] = indexMap[colorIndex]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
scene.Frames[index].DisplayTime = time.Duration(data.Delay[index]) * 10 * time.Millisecond // nolint: durationcheck
|
||||||
|
scene.Frames[index].Pixels = framebufferSnapshot()
|
||||||
|
}
|
||||||
|
|
||||||
|
view.compressAndAddScene(scene, data.Config.Width, data.Config.Height)
|
||||||
|
}
|
||||||
|
|
||||||
|
external.Import(view.modalStateMachine, returningInfo+info, types, fileHandler, false)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (view *View) compressAndAddScene(scene movie.Scene, width, height int) {
|
||||||
|
view.modalStateMachine.SetState(&compressingStartState{
|
||||||
|
machine: view.modalStateMachine,
|
||||||
|
view: view,
|
||||||
|
width: width,
|
||||||
|
height: height,
|
||||||
|
input: scene,
|
||||||
|
listener: view.onCompressionResult,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func (view *View) onCompressionResult(result compressionResult) {
|
||||||
|
switch typedResult := result.(type) {
|
||||||
|
case compressionAborted:
|
||||||
|
case compressionFinished:
|
||||||
|
view.requestAddScene(typedResult.scene)
|
||||||
|
case compressionFailed:
|
||||||
|
view.requestImportScene("Could not compress. Follow recommendations and retry.\n" +
|
||||||
|
"Technical details:\n" + typedResult.err.Error() + "\n\n")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (view *View) requestAddScene(scene movie.HighResScene) {
|
||||||
|
view.movieService.RequestAddScene(view.model.currentKey, scene, view.restoreFunc())
|
||||||
|
}
|
||||||
|
|
||||||
|
func (view *View) requestExportScene() {
|
||||||
|
filename := fmt.Sprintf("%s_Scene%02d_%s.gif",
|
||||||
|
knownMovies[view.model.currentKey.ID].title,
|
||||||
|
view.model.currentScene,
|
||||||
|
view.model.currentKey.Lang.String())
|
||||||
|
info := "File to be written: " + filename
|
||||||
|
var exportTo func(string)
|
||||||
|
|
||||||
|
scenes := view.movieService.Video(view.model.currentKey)
|
||||||
|
var scene *movie.Scene
|
||||||
|
if view.model.currentScene >= 0 && view.model.currentScene < len(scenes) {
|
||||||
|
scene = &scenes[view.model.currentScene]
|
||||||
|
}
|
||||||
|
|
||||||
|
if (scene == nil) || len(scene.Frames) == 0 {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
exportTo = func(dirname string) {
|
||||||
|
writer, err := os.Create(filepath.Join(dirname, filename))
|
||||||
|
if err != nil {
|
||||||
|
external.Export(view.modalStateMachine, "Could not create file.\n"+info, exportTo, true)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
defer func() { _ = writer.Close() }()
|
||||||
|
|
||||||
|
colorPalette := scene.Palette.ColorPalette(false)
|
||||||
|
data := gif.GIF{
|
||||||
|
Config: image.Config{
|
||||||
|
Width: 600,
|
||||||
|
Height: 300,
|
||||||
|
ColorModel: colorPalette,
|
||||||
|
},
|
||||||
|
LoopCount: -1,
|
||||||
|
}
|
||||||
|
|
||||||
|
imageRect := image.Rect(0, 0, data.Config.Width, data.Config.Height)
|
||||||
|
for _, frame := range scene.Frames {
|
||||||
|
frameImg := image.NewPaletted(imageRect, colorPalette)
|
||||||
|
frameImg.Pix = frame.Pixels
|
||||||
|
frameImg.Stride = data.Config.Width
|
||||||
|
data.Image = append(data.Image, frameImg)
|
||||||
|
data.Delay = append(data.Delay, int(((frame.DisplayTime/time.Millisecond)+5)/10))
|
||||||
|
}
|
||||||
|
|
||||||
|
err = gif.EncodeAll(writer, &data)
|
||||||
|
if err != nil {
|
||||||
|
external.Export(view.modalStateMachine, info, exportTo, true)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
external.Export(view.modalStateMachine, info, exportTo, false)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (view *View) requestMoveSceneEarlier() {
|
||||||
|
scenes := view.movieService.Video(view.model.currentKey)
|
||||||
|
if (view.model.currentScene > 0) && (view.model.currentScene < len(scenes)) {
|
||||||
|
view.movieService.RequestMoveSceneEarlier(view.model.currentKey, view.model.currentScene,
|
||||||
|
view.restoreFuncWithScene(view.model.currentScene-1))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (view *View) requestMoveSceneLater() {
|
||||||
|
scenes := view.movieService.Video(view.model.currentKey)
|
||||||
|
if (view.model.currentScene >= 0) && (view.model.currentScene < (len(scenes) - 1)) {
|
||||||
|
view.movieService.RequestMoveSceneLater(view.model.currentKey, view.model.currentScene,
|
||||||
|
view.restoreFuncWithScene(view.model.currentScene+1))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (view *View) requestRemoveScene() {
|
||||||
|
scenes := view.movieService.Video(view.model.currentKey)
|
||||||
|
if (view.model.currentScene >= 0) && (view.model.currentScene < len(scenes)) {
|
||||||
|
view.movieService.RequestRemoveScene(view.model.currentKey, view.model.currentScene, view.restoreFunc())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (view *View) requestSetFramesDisplayTime(displayTime time.Duration) {
|
||||||
|
view.movieService.RequestSetSceneFramesDisplayTime(view.model.currentKey, view.model.currentScene, displayTime, view.restoreFunc())
|
||||||
|
}
|
||||||
@@ -0,0 +1,26 @@
|
|||||||
|
package movies
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/inkyblackness/hacked/ss1/resource"
|
||||||
|
"github.com/inkyblackness/hacked/ss1/world/ids"
|
||||||
|
)
|
||||||
|
|
||||||
|
type viewModel struct {
|
||||||
|
windowOpen bool
|
||||||
|
restoreFocus bool
|
||||||
|
|
||||||
|
currentKey resource.Key
|
||||||
|
currentSubtitleLang resource.Language
|
||||||
|
currentScene int
|
||||||
|
currentFrame int
|
||||||
|
|
||||||
|
frameTimeFraction int
|
||||||
|
}
|
||||||
|
|
||||||
|
func freshViewModel() viewModel {
|
||||||
|
return viewModel{
|
||||||
|
currentKey: resource.KeyOf(ids.MovieIntro, resource.LangDefault, 0),
|
||||||
|
currentSubtitleLang: resource.LangDefault,
|
||||||
|
frameTimeFraction: -1,
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,35 @@
|
|||||||
|
package objects
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/inkyblackness/hacked/ss1/content/object"
|
||||||
|
"github.com/inkyblackness/hacked/ss1/resource"
|
||||||
|
"github.com/inkyblackness/hacked/ss1/world"
|
||||||
|
)
|
||||||
|
|
||||||
|
type setObjectBitmapCommand struct {
|
||||||
|
model *viewModel
|
||||||
|
|
||||||
|
triple object.Triple
|
||||||
|
bitmap int
|
||||||
|
|
||||||
|
resourceKey resource.Key
|
||||||
|
oldData []byte
|
||||||
|
newData []byte
|
||||||
|
}
|
||||||
|
|
||||||
|
func (command setObjectBitmapCommand) Do(modder world.Modder) error {
|
||||||
|
return command.perform(modder, command.newData)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (command setObjectBitmapCommand) Undo(modder world.Modder) error {
|
||||||
|
return command.perform(modder, command.oldData)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (command setObjectBitmapCommand) perform(modder world.Modder, data []byte) error {
|
||||||
|
modder.SetResourceBlock(command.resourceKey.Lang, command.resourceKey.ID, command.resourceKey.Index, data)
|
||||||
|
|
||||||
|
command.model.restoreFocus = true
|
||||||
|
command.model.currentObject = command.triple
|
||||||
|
command.model.currentBitmap = command.bitmap
|
||||||
|
return nil
|
||||||
|
}
|
||||||
@@ -0,0 +1,33 @@
|
|||||||
|
package objects
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/inkyblackness/hacked/ss1/content/object"
|
||||||
|
"github.com/inkyblackness/hacked/ss1/world"
|
||||||
|
)
|
||||||
|
|
||||||
|
type setObjectPropertiesCommand struct {
|
||||||
|
model *viewModel
|
||||||
|
|
||||||
|
triple object.Triple
|
||||||
|
bitmap int
|
||||||
|
|
||||||
|
oldProperties object.Properties
|
||||||
|
newProperties object.Properties
|
||||||
|
}
|
||||||
|
|
||||||
|
func (command setObjectPropertiesCommand) Do(modder world.Modder) error {
|
||||||
|
return command.perform(modder, command.newProperties)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (command setObjectPropertiesCommand) Undo(modder world.Modder) error {
|
||||||
|
return command.perform(modder, command.oldProperties)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (command setObjectPropertiesCommand) perform(modder world.Modder, properties object.Properties) error {
|
||||||
|
modder.SetObjectProperties(command.triple, properties)
|
||||||
|
|
||||||
|
command.model.restoreFocus = true
|
||||||
|
command.model.currentObject = command.triple
|
||||||
|
command.model.currentBitmap = command.bitmap
|
||||||
|
return nil
|
||||||
|
}
|
||||||
@@ -0,0 +1,35 @@
|
|||||||
|
package objects
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/inkyblackness/hacked/ss1/content/object"
|
||||||
|
"github.com/inkyblackness/hacked/ss1/resource"
|
||||||
|
"github.com/inkyblackness/hacked/ss1/world"
|
||||||
|
)
|
||||||
|
|
||||||
|
type setObjectTextCommand struct {
|
||||||
|
model *viewModel
|
||||||
|
|
||||||
|
triple object.Triple
|
||||||
|
bitmap int
|
||||||
|
key resource.Key
|
||||||
|
|
||||||
|
oldData []byte
|
||||||
|
newData []byte
|
||||||
|
}
|
||||||
|
|
||||||
|
func (command setObjectTextCommand) Do(modder world.Modder) error {
|
||||||
|
return command.perform(modder, command.newData)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (command setObjectTextCommand) Undo(modder world.Modder) error {
|
||||||
|
return command.perform(modder, command.oldData)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (command setObjectTextCommand) perform(modder world.Modder, data []byte) error {
|
||||||
|
modder.SetResourceBlock(command.key.Lang, command.key.ID, command.key.Index, data)
|
||||||
|
command.model.restoreFocus = true
|
||||||
|
command.model.currentObject = command.triple
|
||||||
|
command.model.currentBitmap = command.bitmap
|
||||||
|
command.model.currentLang = command.key.Lang
|
||||||
|
return nil
|
||||||
|
}
|
||||||
@@ -0,0 +1,710 @@
|
|||||||
|
package objects
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/inkyblackness/imgui-go/v3"
|
||||||
|
|
||||||
|
"github.com/inkyblackness/hacked/editor/external"
|
||||||
|
"github.com/inkyblackness/hacked/editor/graphics"
|
||||||
|
"github.com/inkyblackness/hacked/editor/render"
|
||||||
|
"github.com/inkyblackness/hacked/editor/values"
|
||||||
|
"github.com/inkyblackness/hacked/ss1/content/bitmap"
|
||||||
|
"github.com/inkyblackness/hacked/ss1/content/interpreters"
|
||||||
|
"github.com/inkyblackness/hacked/ss1/content/object"
|
||||||
|
"github.com/inkyblackness/hacked/ss1/content/object/objprop"
|
||||||
|
"github.com/inkyblackness/hacked/ss1/content/text"
|
||||||
|
"github.com/inkyblackness/hacked/ss1/edit/undoable/cmd"
|
||||||
|
"github.com/inkyblackness/hacked/ss1/resource"
|
||||||
|
"github.com/inkyblackness/hacked/ss1/world"
|
||||||
|
"github.com/inkyblackness/hacked/ss1/world/ids"
|
||||||
|
"github.com/inkyblackness/hacked/ui/gui"
|
||||||
|
)
|
||||||
|
|
||||||
|
// View provides edit controls for game objects.
|
||||||
|
type View struct {
|
||||||
|
mod *world.Mod
|
||||||
|
textCache *text.Cache
|
||||||
|
cp text.Codepage
|
||||||
|
imageCache *graphics.TextureCache
|
||||||
|
paletteCache *graphics.PaletteCache
|
||||||
|
|
||||||
|
modalStateMachine gui.ModalStateMachine
|
||||||
|
clipboard external.Clipboard
|
||||||
|
guiScale float32
|
||||||
|
commander cmd.Commander
|
||||||
|
|
||||||
|
model viewModel
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewView returns a new instance.
|
||||||
|
func NewView(mod *world.Mod, textCache *text.Cache, cp text.Codepage,
|
||||||
|
imageCache *graphics.TextureCache, paletteCache *graphics.PaletteCache,
|
||||||
|
modalStateMachine gui.ModalStateMachine,
|
||||||
|
clipboard external.Clipboard, guiScale float32, commander cmd.Commander) *View {
|
||||||
|
view := &View{
|
||||||
|
mod: mod,
|
||||||
|
textCache: textCache,
|
||||||
|
cp: cp,
|
||||||
|
imageCache: imageCache,
|
||||||
|
paletteCache: paletteCache,
|
||||||
|
|
||||||
|
modalStateMachine: modalStateMachine,
|
||||||
|
clipboard: clipboard,
|
||||||
|
guiScale: guiScale,
|
||||||
|
commander: commander,
|
||||||
|
|
||||||
|
model: freshViewModel(),
|
||||||
|
}
|
||||||
|
return view
|
||||||
|
}
|
||||||
|
|
||||||
|
// WindowOpen returns the flag address, to be used with the main menu.
|
||||||
|
func (view *View) WindowOpen() *bool {
|
||||||
|
return &view.model.windowOpen
|
||||||
|
}
|
||||||
|
|
||||||
|
// Render renders the view.
|
||||||
|
func (view *View) Render() {
|
||||||
|
if view.model.restoreFocus {
|
||||||
|
imgui.SetNextWindowFocus()
|
||||||
|
view.model.restoreFocus = false
|
||||||
|
view.model.windowOpen = true
|
||||||
|
}
|
||||||
|
if view.model.windowOpen {
|
||||||
|
imgui.SetNextWindowSizeV(imgui.Vec2{X: 800 * view.guiScale, Y: 600 * view.guiScale}, imgui.ConditionFirstUseEver)
|
||||||
|
if imgui.BeginV("Game Objects", view.WindowOpen(), imgui.WindowFlagsNoCollapse|imgui.WindowFlagsHorizontalScrollbar) {
|
||||||
|
view.renderContent()
|
||||||
|
}
|
||||||
|
imgui.End()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (view *View) renderContent() {
|
||||||
|
if imgui.BeginChildV("Properties", imgui.Vec2{X: -330 * view.guiScale, Y: 0}, false, imgui.WindowFlagsHorizontalScrollbar) {
|
||||||
|
imgui.PushItemWidth(-260 * view.guiScale)
|
||||||
|
classString := func(class object.Class) string {
|
||||||
|
return fmt.Sprintf("%2d: %v", int(class), class)
|
||||||
|
}
|
||||||
|
if imgui.BeginCombo("Object Class", classString(view.model.currentObject.Class)) {
|
||||||
|
for _, class := range object.Classes() {
|
||||||
|
if imgui.SelectableV(classString(class), class == view.model.currentObject.Class, 0, imgui.Vec2{}) {
|
||||||
|
view.model.currentObject = object.TripleFrom(int(class), 0, 0)
|
||||||
|
view.model.currentBitmap = 0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
imgui.EndCombo()
|
||||||
|
}
|
||||||
|
if imgui.BeginCombo("Object Type", view.tripleName(view.model.currentObject)) {
|
||||||
|
allTypes := view.mod.ObjectProperties().TriplesInClass(view.model.currentObject.Class)
|
||||||
|
for _, triple := range allTypes {
|
||||||
|
if imgui.SelectableV(view.tripleName(triple), triple == view.model.currentObject, 0, imgui.Vec2{}) {
|
||||||
|
view.model.currentObject = triple
|
||||||
|
view.model.currentBitmap = 0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
imgui.EndCombo()
|
||||||
|
}
|
||||||
|
|
||||||
|
readOnly := !view.mod.HasModifiableObjectProperties()
|
||||||
|
properties, propErr := view.mod.ObjectProperties().ForObject(view.model.currentObject)
|
||||||
|
|
||||||
|
imgui.Separator()
|
||||||
|
bitmapLimit := 0
|
||||||
|
if propErr == nil {
|
||||||
|
bitmapLimit = int(properties.Common.Bitmap3D.FrameNumber() + 2)
|
||||||
|
}
|
||||||
|
gui.StepSliderInt("Bitmap", &view.model.currentBitmap, 0, bitmapLimit)
|
||||||
|
render.TextureSelector("BitmapSelector", -1, view.guiScale, bitmapLimit+1, view.model.currentBitmap,
|
||||||
|
view.imageCache, view.currentBitmapKeyFor, func(int) string { return "" }, func(newValue int) {
|
||||||
|
view.model.currentBitmap = newValue
|
||||||
|
})
|
||||||
|
|
||||||
|
imgui.Separator()
|
||||||
|
|
||||||
|
if imgui.BeginCombo("Language", view.model.currentLang.String()) {
|
||||||
|
languages := resource.Languages()
|
||||||
|
for _, lang := range languages {
|
||||||
|
if imgui.SelectableV(lang.String(), lang == view.model.currentLang, 0, imgui.Vec2{}) {
|
||||||
|
view.model.currentLang = lang
|
||||||
|
}
|
||||||
|
}
|
||||||
|
imgui.EndCombo()
|
||||||
|
}
|
||||||
|
view.renderText(false, "Long Name",
|
||||||
|
view.objectName(view.model.currentObject, view.model.currentLang, true),
|
||||||
|
func(newValue string) {
|
||||||
|
view.requestSetObjectName(view.model.currentObject, true, newValue)
|
||||||
|
})
|
||||||
|
view.renderText(false, "Short Name",
|
||||||
|
view.objectName(view.model.currentObject, view.model.currentLang, false),
|
||||||
|
func(newValue string) {
|
||||||
|
view.requestSetObjectName(view.model.currentObject, false, newValue)
|
||||||
|
})
|
||||||
|
|
||||||
|
if propErr == nil {
|
||||||
|
if imgui.TreeNodeV("Common Properties", imgui.TreeNodeFlagsDefaultOpen|imgui.TreeNodeFlagsFramed) {
|
||||||
|
view.renderCommonProperties(readOnly, properties)
|
||||||
|
imgui.TreePop()
|
||||||
|
}
|
||||||
|
if imgui.TreeNodeV("Generic Properties", imgui.TreeNodeFlagsFramed) {
|
||||||
|
view.renderGenericProperties(readOnly, properties)
|
||||||
|
imgui.TreePop()
|
||||||
|
}
|
||||||
|
if imgui.TreeNodeV("Specific Properties", imgui.TreeNodeFlagsFramed) {
|
||||||
|
view.renderSpecificProperties(readOnly, properties)
|
||||||
|
imgui.TreePop()
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
imgui.Text("(properties unavailable)")
|
||||||
|
}
|
||||||
|
|
||||||
|
imgui.PopItemWidth()
|
||||||
|
}
|
||||||
|
imgui.EndChild()
|
||||||
|
imgui.SameLine()
|
||||||
|
|
||||||
|
imgui.BeginGroup()
|
||||||
|
view.renderObjectBitmap()
|
||||||
|
imgui.EndGroup()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (view *View) renderText(readOnly bool, label string, value string, changeCallback func(string)) {
|
||||||
|
imgui.LabelText(label, value)
|
||||||
|
view.clipboardPopup(readOnly, label, value, changeCallback)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (view *View) tripleName(triple object.Triple) string {
|
||||||
|
return triple.String() + ": " + view.objectName(triple, resource.LangDefault, true)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (view *View) objectName(triple object.Triple, lang resource.Language, longName bool) string {
|
||||||
|
result := "???"
|
||||||
|
linearIndex := view.mod.ObjectProperties().TripleIndex(triple)
|
||||||
|
if linearIndex >= 0 {
|
||||||
|
nameID := ids.ObjectShortNames
|
||||||
|
if longName {
|
||||||
|
nameID = ids.ObjectLongNames
|
||||||
|
}
|
||||||
|
key := resource.KeyOf(nameID, lang, linearIndex)
|
||||||
|
objName, err := view.textCache.Text(key)
|
||||||
|
if err == nil {
|
||||||
|
result = objName
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
func (view *View) clipboardPopup(readOnly bool, label string, value string, changeCallback func(string)) {
|
||||||
|
if imgui.BeginPopupContextItemV(label+"-Popup", 1) {
|
||||||
|
if imgui.Selectable("Copy to Clipboard") {
|
||||||
|
view.clipboard.SetString(value)
|
||||||
|
}
|
||||||
|
if !readOnly && imgui.Selectable("Copy from Clipboard") {
|
||||||
|
newValue, err := view.clipboard.String()
|
||||||
|
if err == nil {
|
||||||
|
changeCallback(newValue)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
imgui.EndPopup()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (view *View) requestSetObjectName(triple object.Triple, longName bool, newValue string) {
|
||||||
|
linearIndex := view.mod.ObjectProperties().TripleIndex(triple)
|
||||||
|
if linearIndex >= 0 {
|
||||||
|
id := ids.ObjectShortNames
|
||||||
|
if longName {
|
||||||
|
id = ids.ObjectLongNames
|
||||||
|
}
|
||||||
|
key := resource.KeyOf(id, view.model.currentLang, linearIndex)
|
||||||
|
oldValue, _ := view.textCache.Text(key)
|
||||||
|
|
||||||
|
if oldValue != newValue {
|
||||||
|
command := setObjectTextCommand{
|
||||||
|
model: &view.model,
|
||||||
|
triple: view.model.currentObject,
|
||||||
|
bitmap: view.model.currentBitmap,
|
||||||
|
key: key,
|
||||||
|
oldData: view.cp.Encode(oldValue),
|
||||||
|
newData: view.cp.Encode(text.Blocked(newValue)[0]),
|
||||||
|
}
|
||||||
|
view.commander.Queue(command)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (view *View) requestSetObjectProperties(modifier func(*object.Properties)) {
|
||||||
|
command := setObjectPropertiesCommand{
|
||||||
|
model: &view.model,
|
||||||
|
triple: view.model.currentObject,
|
||||||
|
bitmap: view.model.currentBitmap,
|
||||||
|
}
|
||||||
|
currentProp, err := view.mod.ObjectProperties().ForObject(command.triple)
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
command.oldProperties = currentProp.Clone()
|
||||||
|
command.newProperties = currentProp.Clone()
|
||||||
|
modifier(&command.newProperties)
|
||||||
|
view.commander.Queue(command)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (view *View) renderCommonProperties(readOnly bool, properties *object.Properties) {
|
||||||
|
intIdentity := func(u values.Unifier) int { return u.Unified().(int) }
|
||||||
|
intFormat := func(value int) string { return "%d" }
|
||||||
|
|
||||||
|
if imgui.TreeNode("Flags") {
|
||||||
|
renderFlag := func(flag object.CommonFlag) {
|
||||||
|
flagUnifier := values.NewUnifier()
|
||||||
|
flagUnifier.Add(properties.Common.Flags.Has(flag))
|
||||||
|
values.RenderUnifiedCheckboxCombo(readOnly, flag.String(), flagUnifier, func(newValue bool) {
|
||||||
|
view.requestSetObjectProperties(func(prop *object.Properties) {
|
||||||
|
if newValue {
|
||||||
|
prop.Common.Flags = prop.Common.Flags.With(flag)
|
||||||
|
} else {
|
||||||
|
prop.Common.Flags = prop.Common.Flags.Without(flag)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
})
|
||||||
|
}
|
||||||
|
for _, flag := range object.CommonFlags() {
|
||||||
|
renderFlag(flag)
|
||||||
|
}
|
||||||
|
|
||||||
|
imgui.TreePop()
|
||||||
|
}
|
||||||
|
|
||||||
|
lightTypes := object.LightTypes()
|
||||||
|
lightTypeUnifier := values.NewUnifier()
|
||||||
|
lightTypeUnifier.Add(int(properties.Common.Flags.LightType()))
|
||||||
|
values.RenderUnifiedCombo(readOnly, "LightType", lightTypeUnifier, intIdentity,
|
||||||
|
func(value int) string {
|
||||||
|
return object.LightType(value).String()
|
||||||
|
}, len(lightTypes), func(newValue int) {
|
||||||
|
view.requestSetObjectProperties(func(prop *object.Properties) {
|
||||||
|
prop.Common.Flags = prop.Common.Flags.WithLightType(object.LightType(newValue))
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
useModeUnifier := values.NewUnifier()
|
||||||
|
useModeUnifier.Add(int(properties.Common.Flags.UseMode()))
|
||||||
|
values.RenderUnifiedCombo(readOnly, "UseMode", useModeUnifier, intIdentity,
|
||||||
|
func(value int) string {
|
||||||
|
return object.UseMode(value).String()
|
||||||
|
}, 4, func(newValue int) {
|
||||||
|
view.requestSetObjectProperties(func(prop *object.Properties) {
|
||||||
|
prop.Common.Flags = prop.Common.Flags.WithUseMode(object.UseMode(newValue))
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
massUnifier := values.NewUnifier()
|
||||||
|
massUnifier.Add(int(properties.Common.Mass))
|
||||||
|
values.RenderUnifiedSliderInt(readOnly, "Mass", massUnifier, intIdentity, intFormat, -1, 5000,
|
||||||
|
func(newValue int) {
|
||||||
|
view.requestSetObjectProperties(func(prop *object.Properties) {
|
||||||
|
prop.Common.Mass = int32(newValue)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
hitpointsUnifier := values.NewUnifier()
|
||||||
|
hitpointsUnifier.Add(int(properties.Common.Hitpoints))
|
||||||
|
values.RenderUnifiedSliderInt(readOnly, "Hitpoints", hitpointsUnifier, intIdentity, intFormat, 0, 10000,
|
||||||
|
func(newValue int) {
|
||||||
|
view.requestSetObjectProperties(func(prop *object.Properties) {
|
||||||
|
prop.Common.Hitpoints = int16(newValue)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
armorUnifier := values.NewUnifier()
|
||||||
|
armorUnifier.Add(int(properties.Common.Armor))
|
||||||
|
values.RenderUnifiedSliderInt(readOnly, "Armor", armorUnifier, intIdentity, intFormat, 0, 255,
|
||||||
|
func(newValue int) {
|
||||||
|
view.requestSetObjectProperties(func(prop *object.Properties) {
|
||||||
|
prop.Common.Armor = byte(newValue)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
renderTypeUnifier := values.NewUnifier()
|
||||||
|
renderTypeUnifier.Add(int(properties.Common.RenderType))
|
||||||
|
values.RenderUnifiedCombo(readOnly, "Render Type", renderTypeUnifier, intIdentity,
|
||||||
|
func(value int) string { return object.RenderType(value).String() },
|
||||||
|
len(object.RenderTypes()), func(newValue int) {
|
||||||
|
view.requestSetObjectProperties(func(prop *object.Properties) {
|
||||||
|
prop.Common.RenderType = object.RenderType(newValue)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
physicsModelUnifier := values.NewUnifier()
|
||||||
|
physicsModelUnifier.Add(int(properties.Common.PhysicsModel))
|
||||||
|
values.RenderUnifiedCombo(readOnly, "Physics Model", physicsModelUnifier, intIdentity,
|
||||||
|
func(value int) string { return object.PhysicsModel(value).String() },
|
||||||
|
len(object.PhysicsModels()), func(newValue int) {
|
||||||
|
view.requestSetObjectProperties(func(prop *object.Properties) {
|
||||||
|
prop.Common.PhysicsModel = object.PhysicsModel(newValue)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
hardnessUnifier := values.NewUnifier()
|
||||||
|
hardnessUnifier.Add(int(properties.Common.Hardness))
|
||||||
|
values.RenderUnifiedSliderInt(readOnly, "Hardness", hardnessUnifier, intIdentity, intFormat, 0, object.HardnessLimit,
|
||||||
|
func(newValue int) {
|
||||||
|
view.requestSetObjectProperties(func(prop *object.Properties) {
|
||||||
|
prop.Common.Hardness = byte(newValue)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
physicsXRUnifier := values.NewUnifier()
|
||||||
|
physicsXRUnifier.Add(int(properties.Common.PhysicsXR))
|
||||||
|
values.RenderUnifiedSliderInt(readOnly, "Physics XR", physicsXRUnifier, intIdentity, intFormat, 0, object.PhysicsXRLimit,
|
||||||
|
func(newValue int) {
|
||||||
|
view.requestSetObjectProperties(func(prop *object.Properties) {
|
||||||
|
prop.Common.PhysicsXR = byte(newValue)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
physicsZUnifier := values.NewUnifier()
|
||||||
|
physicsZUnifier.Add(int(properties.Common.PhysicsZ))
|
||||||
|
values.RenderUnifiedSliderInt(readOnly, "Physics Z", physicsZUnifier, intIdentity, intFormat, 0, 255,
|
||||||
|
func(newValue int) {
|
||||||
|
view.requestSetObjectProperties(func(prop *object.Properties) {
|
||||||
|
prop.Common.PhysicsZ = byte(newValue)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
if imgui.TreeNode("Vulnerabilities") {
|
||||||
|
renderVulnerability := func(damageType object.DamageType) {
|
||||||
|
damageUnifier := values.NewUnifier()
|
||||||
|
damageUnifier.Add(properties.Common.Vulnerabilities.Has(damageType))
|
||||||
|
values.RenderUnifiedCheckboxCombo(readOnly, damageType.String(), damageUnifier, func(newValue bool) {
|
||||||
|
view.requestSetObjectProperties(func(prop *object.Properties) {
|
||||||
|
if newValue {
|
||||||
|
prop.Common.Vulnerabilities = prop.Common.Vulnerabilities.With(damageType)
|
||||||
|
} else {
|
||||||
|
prop.Common.Vulnerabilities = prop.Common.Vulnerabilities.Without(damageType)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
})
|
||||||
|
}
|
||||||
|
for _, damageType := range object.DamageTypes() {
|
||||||
|
renderVulnerability(damageType)
|
||||||
|
}
|
||||||
|
|
||||||
|
primaryUnifier := values.NewUnifier()
|
||||||
|
primaryUnifier.Add(properties.Common.SpecialVulnerabilities.PrimaryValue())
|
||||||
|
values.RenderUnifiedSliderInt(readOnly, "Primary (double dmg)", primaryUnifier, intIdentity, intFormat, 0, object.SpecialDamageTypeLimit,
|
||||||
|
func(newValue int) {
|
||||||
|
view.requestSetObjectProperties(func(prop *object.Properties) {
|
||||||
|
prop.Common.SpecialVulnerabilities = prop.Common.SpecialVulnerabilities.WithPrimaryValue(newValue)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
superUnifier := values.NewUnifier()
|
||||||
|
superUnifier.Add(properties.Common.SpecialVulnerabilities.SuperValue())
|
||||||
|
values.RenderUnifiedSliderInt(readOnly, "Super (quad dmg)", superUnifier, intIdentity, intFormat, 0, object.SpecialDamageTypeLimit,
|
||||||
|
func(newValue int) {
|
||||||
|
view.requestSetObjectProperties(func(prop *object.Properties) {
|
||||||
|
prop.Common.SpecialVulnerabilities = prop.Common.SpecialVulnerabilities.WithSuperValue(newValue)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
imgui.TreePop()
|
||||||
|
}
|
||||||
|
|
||||||
|
defenseUnifier := values.NewUnifier()
|
||||||
|
defenseUnifier.Add(int(properties.Common.Defense))
|
||||||
|
values.RenderUnifiedSliderInt(readOnly, "Defense", defenseUnifier, intIdentity,
|
||||||
|
func(value int) string {
|
||||||
|
if value == object.DefenseNoCriticals {
|
||||||
|
return "%d - (no criticals)"
|
||||||
|
}
|
||||||
|
return "%d"
|
||||||
|
}, 0, 255,
|
||||||
|
func(newValue int) {
|
||||||
|
view.requestSetObjectProperties(func(prop *object.Properties) {
|
||||||
|
prop.Common.Defense = byte(newValue)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
toughnessUnifier := values.NewUnifier()
|
||||||
|
toughnessUnifier.Add(int(properties.Common.Toughness))
|
||||||
|
values.RenderUnifiedSliderInt(readOnly, "Toughness", toughnessUnifier, intIdentity,
|
||||||
|
func(value int) string {
|
||||||
|
if value == object.ToughnessNoDamage {
|
||||||
|
return "(no damage) -- raw: %d"
|
||||||
|
}
|
||||||
|
return fmt.Sprintf("%d:1 dmg -- raw: %%d", 1<<uint(value))
|
||||||
|
},
|
||||||
|
0, 7,
|
||||||
|
func(newValue int) {
|
||||||
|
view.requestSetObjectProperties(func(prop *object.Properties) {
|
||||||
|
prop.Common.Toughness = byte(newValue)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
mfdOrMeshIDUnifier := values.NewUnifier()
|
||||||
|
mfdOrMeshIDUnifier.Add(int(properties.Common.MfdOrMeshID))
|
||||||
|
values.RenderUnifiedSliderInt(readOnly, "MFD/Mesh ID", mfdOrMeshIDUnifier, intIdentity, intFormat, 0, 1000,
|
||||||
|
func(newValue int) {
|
||||||
|
view.requestSetObjectProperties(func(prop *object.Properties) {
|
||||||
|
prop.Common.MfdOrMeshID = uint16(newValue)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
bitmap3DBitmapNumUnifier := values.NewUnifier()
|
||||||
|
bitmap3DBitmapNumUnifier.Add(int(properties.Common.Bitmap3D.BitmapNumber()))
|
||||||
|
values.RenderUnifiedSliderInt(readOnly, "Bitmap Number", bitmap3DBitmapNumUnifier, intIdentity, intFormat, 0, int(object.Bitmap3DBitmapNumberLimit),
|
||||||
|
func(newValue int) {
|
||||||
|
view.requestSetObjectProperties(func(prop *object.Properties) {
|
||||||
|
prop.Common.Bitmap3D = prop.Common.Bitmap3D.WithBitmapNumber(uint16(newValue))
|
||||||
|
})
|
||||||
|
})
|
||||||
|
bitmap3DFrameNumUnifier := values.NewUnifier()
|
||||||
|
bitmap3DFrameNumUnifier.Add(int(properties.Common.Bitmap3D.FrameNumber()))
|
||||||
|
values.RenderUnifiedSliderInt(readOnly, "Frame Number", bitmap3DFrameNumUnifier, intIdentity, intFormat, 0, int(object.Bitmap3DFrameNumberLimit),
|
||||||
|
func(newValue int) {
|
||||||
|
view.requestSetObjectProperties(func(prop *object.Properties) {
|
||||||
|
prop.Common.Bitmap3D = prop.Common.Bitmap3D.WithFrameNumber(uint16(newValue))
|
||||||
|
})
|
||||||
|
})
|
||||||
|
bitmap3DAnimUnifier := values.NewUnifier()
|
||||||
|
bitmap3DAnimUnifier.Add(properties.Common.Bitmap3D.Animation())
|
||||||
|
values.RenderUnifiedCheckboxCombo(readOnly, "Animation", bitmap3DAnimUnifier,
|
||||||
|
func(newValue bool) {
|
||||||
|
view.requestSetObjectProperties(func(prop *object.Properties) {
|
||||||
|
prop.Common.Bitmap3D = prop.Common.Bitmap3D.WithAnimation(newValue)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
bitmap3DRepeatUnifier := values.NewUnifier()
|
||||||
|
bitmap3DRepeatUnifier.Add(properties.Common.Bitmap3D.Repeat())
|
||||||
|
values.RenderUnifiedCheckboxCombo(readOnly, "Repeat", bitmap3DRepeatUnifier,
|
||||||
|
func(newValue bool) {
|
||||||
|
view.requestSetObjectProperties(func(prop *object.Properties) {
|
||||||
|
prop.Common.Bitmap3D = prop.Common.Bitmap3D.WithRepeat(newValue)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
destroyEffectValueUnifier := values.NewUnifier()
|
||||||
|
destroyEffectValueUnifier.Add(int(properties.Common.DestroyEffect.Value()))
|
||||||
|
values.RenderUnifiedSliderInt(readOnly, "DestroyEffect Value", destroyEffectValueUnifier,
|
||||||
|
intIdentity, intFormat, 0, int(object.DestroyEffectValueLimit),
|
||||||
|
func(newValue int) {
|
||||||
|
view.requestSetObjectProperties(func(prop *object.Properties) {
|
||||||
|
prop.Common.DestroyEffect = prop.Common.DestroyEffect.WithValue(byte(newValue))
|
||||||
|
})
|
||||||
|
})
|
||||||
|
destroyEffectPlaySoundUnifier := values.NewUnifier()
|
||||||
|
destroyEffectPlaySoundUnifier.Add(properties.Common.DestroyEffect.PlaySound())
|
||||||
|
values.RenderUnifiedCheckboxCombo(readOnly, "DestroyEffect PlaySound", destroyEffectPlaySoundUnifier,
|
||||||
|
func(newValue bool) {
|
||||||
|
view.requestSetObjectProperties(func(prop *object.Properties) {
|
||||||
|
prop.Common.DestroyEffect = prop.Common.DestroyEffect.WithSound(newValue)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
destroyEffectShowExplosionUnifier := values.NewUnifier()
|
||||||
|
destroyEffectShowExplosionUnifier.Add(properties.Common.DestroyEffect.ShowExplosion())
|
||||||
|
values.RenderUnifiedCheckboxCombo(readOnly, "DestroyEffect ShowExplosion", destroyEffectShowExplosionUnifier,
|
||||||
|
func(newValue bool) {
|
||||||
|
view.requestSetObjectProperties(func(prop *object.Properties) {
|
||||||
|
prop.Common.DestroyEffect = prop.Common.DestroyEffect.WithExplosion(newValue)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func (view *View) renderGenericProperties(readOnly bool, properties *object.Properties) {
|
||||||
|
readInterpreter := objprop.GenericProperties(view.model.currentObject.Class, properties.Generic)
|
||||||
|
view.createPropertyControls(readOnly, readInterpreter, func(key string, modifier func(uint32) uint32) {
|
||||||
|
view.requestSetObjectProperties(func(prop *object.Properties) {
|
||||||
|
writeInterpreter := objprop.GenericProperties(view.model.currentObject.Class, prop.Generic)
|
||||||
|
view.setInterpreterValueKeyed(writeInterpreter, key, modifier)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func (view *View) renderSpecificProperties(readOnly bool, properties *object.Properties) {
|
||||||
|
readInterpreter := objprop.SpecificProperties(view.model.currentObject, properties.Specific)
|
||||||
|
view.createPropertyControls(readOnly, readInterpreter, func(key string, modifier func(uint32) uint32) {
|
||||||
|
view.requestSetObjectProperties(func(prop *object.Properties) {
|
||||||
|
writeInterpreter := objprop.SpecificProperties(view.model.currentObject, prop.Specific)
|
||||||
|
view.setInterpreterValueKeyed(writeInterpreter, key, modifier)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func (view *View) createPropertyControls(readOnly bool, rootInterpreter *interpreters.Instance,
|
||||||
|
updater func(string, func(uint32) uint32)) {
|
||||||
|
objTypeRenderer := values.ObjectTypeControlRenderer{
|
||||||
|
Meta: view.mod.ObjectProperties(),
|
||||||
|
TextCache: view.textCache,
|
||||||
|
}
|
||||||
|
|
||||||
|
var processInterpreter func(string, *interpreters.Instance)
|
||||||
|
processInterpreter = func(path string, interpreter *interpreters.Instance) {
|
||||||
|
for _, key := range interpreter.Keys() {
|
||||||
|
fullKey := path + key
|
||||||
|
unifier := values.NewUnifier()
|
||||||
|
unifier.Add(int32(interpreter.Get(key)))
|
||||||
|
simplifier := values.StandardSimplifier(readOnly, fullKey, unifier,
|
||||||
|
func(modifier func(uint32) uint32) {
|
||||||
|
updater(fullKey, modifier)
|
||||||
|
}, objTypeRenderer)
|
||||||
|
|
||||||
|
interpreter.Describe(key, simplifier)
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, key := range interpreter.ActiveRefinements() {
|
||||||
|
fullKey := path + key
|
||||||
|
if len(fullKey) > 0 {
|
||||||
|
imgui.Separator()
|
||||||
|
imgui.Text(fullKey + ":")
|
||||||
|
}
|
||||||
|
processInterpreter(fullKey+".", interpreter.Refined(key))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
processInterpreter("", rootInterpreter)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (view *View) setInterpreterValueKeyed(instance *interpreters.Instance, key string, modifier func(uint32) uint32) {
|
||||||
|
resolvedInterpreter := instance
|
||||||
|
keys := strings.Split(key, ".")
|
||||||
|
keyCount := len(keys)
|
||||||
|
if len(keys) > 1 {
|
||||||
|
for _, subKey := range keys[:keyCount-1] {
|
||||||
|
resolvedInterpreter = resolvedInterpreter.Refined(subKey)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
resolvedInterpreter.Set(keys[keyCount-1], modifier(resolvedInterpreter.Get(keys[keyCount-1])))
|
||||||
|
}
|
||||||
|
|
||||||
|
func (view *View) renderObjectBitmap() {
|
||||||
|
render.TextureImage("BitmapImage", view.imageCache, view.currentBitmapKey(), imgui.Vec2{X: 320 * view.guiScale, Y: 240 * view.guiScale})
|
||||||
|
if imgui.Button("Clear") {
|
||||||
|
view.requestClearBitmap()
|
||||||
|
}
|
||||||
|
imgui.SameLine()
|
||||||
|
if imgui.Button("Import") {
|
||||||
|
view.requestImportBitmap()
|
||||||
|
}
|
||||||
|
imgui.SameLine()
|
||||||
|
if imgui.Button("Export") {
|
||||||
|
view.requestExportBitmap()
|
||||||
|
}
|
||||||
|
if view.hasModCurrentBitmap() {
|
||||||
|
imgui.SameLine()
|
||||||
|
if imgui.Button("Remove") {
|
||||||
|
view.requestSetBitmapData(nil)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (view *View) requestClearBitmap() {
|
||||||
|
bmp := bitmap.Bitmap{
|
||||||
|
Header: bitmap.Header{
|
||||||
|
Width: 1,
|
||||||
|
Height: 1,
|
||||||
|
},
|
||||||
|
Pixels: []byte{0x00},
|
||||||
|
}
|
||||||
|
view.requestSetBitmap(bmp)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (view *View) requestImportBitmap() {
|
||||||
|
paletteRetriever := func() (bitmap.Palette, error) {
|
||||||
|
palette, err := view.paletteCache.Palette(0)
|
||||||
|
if err != nil {
|
||||||
|
return bitmap.Palette{}, err
|
||||||
|
}
|
||||||
|
return palette.Palette(), nil
|
||||||
|
}
|
||||||
|
external.ImportImage(view.modalStateMachine, paletteRetriever, func(bmp bitmap.Bitmap) {
|
||||||
|
view.requestSetBitmap(bmp)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func (view *View) requestExportBitmap() {
|
||||||
|
key := view.currentBitmapKey()
|
||||||
|
texture, err := view.imageCache.Texture(key)
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
palette, err := view.paletteCache.Palette(0)
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
rawPalette := palette.Palette()
|
||||||
|
filename := fmt.Sprintf("%02d_%d_%02d-%02d.png",
|
||||||
|
view.model.currentObject.Class, view.model.currentObject.Subclass, view.model.currentObject.Type, view.model.currentBitmap)
|
||||||
|
width, height := texture.Size()
|
||||||
|
bmp := bitmap.Bitmap{
|
||||||
|
Header: bitmap.Header{
|
||||||
|
Width: int16(width),
|
||||||
|
Height: int16(height),
|
||||||
|
},
|
||||||
|
Pixels: texture.PixelData(),
|
||||||
|
Palette: &rawPalette,
|
||||||
|
}
|
||||||
|
|
||||||
|
external.ExportImage(view.modalStateMachine, filename, bmp)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (view *View) hasModCurrentBitmap() bool {
|
||||||
|
key := view.currentBitmapKey()
|
||||||
|
return len(view.mod.ModifiedBlock(key.Lang, key.ID, key.Index)) > 0
|
||||||
|
}
|
||||||
|
|
||||||
|
func (view *View) requestSetBitmap(bmp bitmap.Bitmap) {
|
||||||
|
highestBitShift := func(value int16) (result byte) {
|
||||||
|
if value != 0 {
|
||||||
|
for (value >> result) != 1 {
|
||||||
|
result++
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
bmp.Header.Flags = bitmap.FlagTransparent
|
||||||
|
bmp.Header.Type = bitmap.TypeFlat8Bit
|
||||||
|
bmp.Header.WidthFactor = highestBitShift(bmp.Header.Width)
|
||||||
|
bmp.Header.HeightFactor = highestBitShift(bmp.Header.Height)
|
||||||
|
bmp.Header.Stride = uint16(bmp.Header.Width)
|
||||||
|
data := bitmap.Encode(&bmp, 0)
|
||||||
|
view.requestSetBitmapData(data)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (view *View) requestSetBitmapData(newData []byte) {
|
||||||
|
resourceKey := view.currentBitmapKey()
|
||||||
|
|
||||||
|
command := setObjectBitmapCommand{
|
||||||
|
model: &view.model,
|
||||||
|
triple: view.model.currentObject,
|
||||||
|
bitmap: view.model.currentBitmap,
|
||||||
|
|
||||||
|
resourceKey: resourceKey,
|
||||||
|
oldData: view.mod.ModifiedBlock(resourceKey.Lang, resourceKey.ID, resourceKey.Index),
|
||||||
|
newData: newData,
|
||||||
|
}
|
||||||
|
view.commander.Queue(command)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (view *View) currentBitmapKey() resource.Key {
|
||||||
|
return view.currentBitmapKeyFor(view.model.currentBitmap)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (view *View) currentBitmapKeyFor(offset int) resource.Key {
|
||||||
|
baseOffset := 1
|
||||||
|
done := false
|
||||||
|
view.mod.ObjectProperties().Iterate(func(triple object.Triple, prop *object.Properties) bool {
|
||||||
|
if !done && triple == view.model.currentObject {
|
||||||
|
done = true
|
||||||
|
}
|
||||||
|
if done {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
numExtra := int(prop.Common.Bitmap3D.FrameNumber())
|
||||||
|
baseOffset += 3 + numExtra
|
||||||
|
return true
|
||||||
|
})
|
||||||
|
|
||||||
|
return resource.KeyOf(ids.ObjectBitmaps, resource.LangAny, baseOffset+offset)
|
||||||
|
}
|
||||||
@@ -0,0 +1,19 @@
|
|||||||
|
package objects
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/inkyblackness/hacked/ss1/content/object"
|
||||||
|
"github.com/inkyblackness/hacked/ss1/resource"
|
||||||
|
)
|
||||||
|
|
||||||
|
type viewModel struct {
|
||||||
|
windowOpen bool
|
||||||
|
restoreFocus bool
|
||||||
|
|
||||||
|
currentObject object.Triple
|
||||||
|
currentBitmap int
|
||||||
|
currentLang resource.Language
|
||||||
|
}
|
||||||
|
|
||||||
|
func freshViewModel() viewModel {
|
||||||
|
return viewModel{}
|
||||||
|
}
|
||||||
@@ -0,0 +1,23 @@
|
|||||||
|
package project
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/inkyblackness/imgui-go/v3"
|
||||||
|
|
||||||
|
"github.com/inkyblackness/hacked/ui/gui"
|
||||||
|
)
|
||||||
|
|
||||||
|
type addManifestEntryStartState struct {
|
||||||
|
machine gui.ModalStateMachine
|
||||||
|
view *View
|
||||||
|
}
|
||||||
|
|
||||||
|
func (state addManifestEntryStartState) Render() {
|
||||||
|
imgui.OpenPopup("Add static world data")
|
||||||
|
state.machine.SetState(&addManifestEntryWaitingState{
|
||||||
|
machine: state.machine,
|
||||||
|
view: state.view,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func (state addManifestEntryStartState) HandleFiles(names []string) {
|
||||||
|
}
|
||||||
@@ -0,0 +1,61 @@
|
|||||||
|
package project
|
||||||
|
|
||||||
|
import (
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/inkyblackness/imgui-go/v3"
|
||||||
|
"github.com/sqweek/dialog"
|
||||||
|
|
||||||
|
"github.com/inkyblackness/hacked/ui/gui"
|
||||||
|
)
|
||||||
|
|
||||||
|
type addManifestEntryWaitingState struct {
|
||||||
|
machine gui.ModalStateMachine
|
||||||
|
view *View
|
||||||
|
failureTime time.Time
|
||||||
|
}
|
||||||
|
|
||||||
|
func (state *addManifestEntryWaitingState) Render() {
|
||||||
|
if imgui.BeginPopupModalV("Add static world data", nil,
|
||||||
|
imgui.WindowFlagsNoResize|imgui.WindowFlagsNoMove|imgui.WindowFlagsNoSavedSettings|imgui.WindowFlagsAlwaysAutoResize) {
|
||||||
|
imgui.Text("Waiting for folders/files.")
|
||||||
|
if !state.failureTime.IsZero() {
|
||||||
|
imgui.PushStyleColor(imgui.StyleColorText, imgui.Vec4{X: 1, Y: 0, Z: 0, W: 1})
|
||||||
|
imgui.Text("Previous attempt failed, no usable data detected.\nPlease check and try again.")
|
||||||
|
imgui.PopStyleColor()
|
||||||
|
if time.Since(state.failureTime).Seconds() > 5 {
|
||||||
|
state.failureTime = time.Time{}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
imgui.Text(`From your file browser drag'n'drop the folder (or files)
|
||||||
|
of the static data you want to reference into the editor window.
|
||||||
|
Typically, you would use the main "data" directory of the game
|
||||||
|
(where all the .res files are).
|
||||||
|
`)
|
||||||
|
imgui.Separator()
|
||||||
|
if imgui.Button("Browse...") {
|
||||||
|
dlgBuilder := dialog.Directory()
|
||||||
|
filename, err := dlgBuilder.Browse()
|
||||||
|
if err == nil {
|
||||||
|
state.HandleFiles([]string{filename})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
imgui.SameLine()
|
||||||
|
if imgui.Button("Cancel") {
|
||||||
|
state.machine.SetState(nil)
|
||||||
|
imgui.CloseCurrentPopup()
|
||||||
|
}
|
||||||
|
imgui.EndPopup()
|
||||||
|
} else {
|
||||||
|
state.machine.SetState(nil)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (state *addManifestEntryWaitingState) HandleFiles(names []string) {
|
||||||
|
err := state.view.tryAddManifestEntryFrom(names)
|
||||||
|
if err != nil {
|
||||||
|
state.failureTime = time.Now()
|
||||||
|
} else {
|
||||||
|
state.machine.SetState(nil)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,23 @@
|
|||||||
|
package project
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/inkyblackness/imgui-go/v3"
|
||||||
|
|
||||||
|
"github.com/inkyblackness/hacked/ui/gui"
|
||||||
|
)
|
||||||
|
|
||||||
|
type loadModStartState struct {
|
||||||
|
machine gui.ModalStateMachine
|
||||||
|
view *View
|
||||||
|
}
|
||||||
|
|
||||||
|
func (state loadModStartState) Render() {
|
||||||
|
imgui.OpenPopup("Load mod")
|
||||||
|
state.machine.SetState(&loadModWaitingState{
|
||||||
|
machine: state.machine,
|
||||||
|
view: state.view,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func (state loadModStartState) HandleFiles(names []string) {
|
||||||
|
}
|
||||||
@@ -0,0 +1,62 @@
|
|||||||
|
package project
|
||||||
|
|
||||||
|
import (
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/inkyblackness/imgui-go/v3"
|
||||||
|
"github.com/sqweek/dialog"
|
||||||
|
|
||||||
|
"github.com/inkyblackness/hacked/ui/gui"
|
||||||
|
)
|
||||||
|
|
||||||
|
type loadModWaitingState struct {
|
||||||
|
machine gui.ModalStateMachine
|
||||||
|
view *View
|
||||||
|
failureTime time.Time
|
||||||
|
}
|
||||||
|
|
||||||
|
func (state *loadModWaitingState) Render() {
|
||||||
|
if imgui.BeginPopupModalV("Load mod", nil,
|
||||||
|
imgui.WindowFlagsNoResize|imgui.WindowFlagsNoMove|imgui.WindowFlagsNoSavedSettings|imgui.WindowFlagsAlwaysAutoResize) {
|
||||||
|
imgui.Text("Waiting for folder.")
|
||||||
|
if !state.failureTime.IsZero() {
|
||||||
|
imgui.PushStyleColor(imgui.StyleColorText, imgui.Vec4{X: 1, Y: 0, Z: 0, W: 1})
|
||||||
|
imgui.Text("Previous attempt failed, no usable data detected.\nPlease check and try again.")
|
||||||
|
imgui.PopStyleColor()
|
||||||
|
if time.Since(state.failureTime).Seconds() > 5 {
|
||||||
|
state.failureTime = time.Time{}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
imgui.Text(`From your file browser drag'n'drop the folder
|
||||||
|
of the mod you want to work on into the editor window.
|
||||||
|
If you want to modify the main game files,
|
||||||
|
use the main "data" directory of the game.
|
||||||
|
`)
|
||||||
|
imgui.Text("This action will clear the undo/redo buffer\nand you will lose any unsaved changes.")
|
||||||
|
imgui.Separator()
|
||||||
|
if imgui.Button("Browse...") {
|
||||||
|
dlgBuilder := dialog.Directory()
|
||||||
|
filename, err := dlgBuilder.Browse()
|
||||||
|
if err == nil {
|
||||||
|
state.HandleFiles([]string{filename})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
imgui.SameLine()
|
||||||
|
if imgui.Button("Cancel") {
|
||||||
|
state.machine.SetState(nil)
|
||||||
|
imgui.CloseCurrentPopup()
|
||||||
|
}
|
||||||
|
imgui.EndPopup()
|
||||||
|
} else {
|
||||||
|
state.machine.SetState(nil)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (state *loadModWaitingState) HandleFiles(names []string) {
|
||||||
|
err := state.view.tryLoadModFrom(names)
|
||||||
|
if err != nil {
|
||||||
|
state.failureTime = time.Now()
|
||||||
|
} else {
|
||||||
|
state.machine.SetState(nil)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,23 @@
|
|||||||
|
package project
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/inkyblackness/imgui-go/v3"
|
||||||
|
|
||||||
|
"github.com/inkyblackness/hacked/ui/gui"
|
||||||
|
)
|
||||||
|
|
||||||
|
type saveModAsStartState struct {
|
||||||
|
machine gui.ModalStateMachine
|
||||||
|
view *View
|
||||||
|
}
|
||||||
|
|
||||||
|
func (state saveModAsStartState) Render() {
|
||||||
|
imgui.OpenPopup("Save mod as")
|
||||||
|
state.machine.SetState(&saveModAsWaitingState{
|
||||||
|
machine: state.machine,
|
||||||
|
view: state.view,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func (state saveModAsStartState) HandleFiles(names []string) {
|
||||||
|
}
|
||||||
@@ -0,0 +1,82 @@
|
|||||||
|
package project
|
||||||
|
|
||||||
|
import (
|
||||||
|
"os"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/inkyblackness/imgui-go/v3"
|
||||||
|
"github.com/sqweek/dialog"
|
||||||
|
|
||||||
|
"github.com/inkyblackness/hacked/ui/gui"
|
||||||
|
)
|
||||||
|
|
||||||
|
type saveModAsWaitingState struct {
|
||||||
|
machine gui.ModalStateMachine
|
||||||
|
view *View
|
||||||
|
failureTime time.Time
|
||||||
|
errorInfo string
|
||||||
|
}
|
||||||
|
|
||||||
|
func (state *saveModAsWaitingState) Render() {
|
||||||
|
if imgui.BeginPopupModalV("Save mod as", nil,
|
||||||
|
imgui.WindowFlagsNoResize|imgui.WindowFlagsNoMove|imgui.WindowFlagsNoSavedSettings|imgui.WindowFlagsAlwaysAutoResize) {
|
||||||
|
imgui.Text("Waiting for folder.")
|
||||||
|
if !state.failureTime.IsZero() {
|
||||||
|
imgui.PushStyleColor(imgui.StyleColorText, imgui.Vec4{X: 1, Y: 0, Z: 0, W: 1})
|
||||||
|
imgui.Text("Previous attempt failed, not a proper folder.\nPlease check and try again.")
|
||||||
|
if len(state.errorInfo) > 0 {
|
||||||
|
imgui.Text(state.errorInfo)
|
||||||
|
}
|
||||||
|
imgui.PopStyleColor()
|
||||||
|
if time.Since(state.failureTime).Seconds() > 5 {
|
||||||
|
state.failureTime = time.Time{}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
imgui.Text(`From your file browser drag'n'drop the folder
|
||||||
|
where to save the data into the editor window.
|
||||||
|
`)
|
||||||
|
imgui.PushStyleColor(imgui.StyleColorText, imgui.Vec4{X: 1, Y: 0, Z: 0, W: 1})
|
||||||
|
imgui.Text("It is recommended to use an empty folder.\nSaving will potentially overwrite existing files.")
|
||||||
|
imgui.PopStyleColor()
|
||||||
|
imgui.Separator()
|
||||||
|
if imgui.Button("Browse...") {
|
||||||
|
dlgBuilder := dialog.Directory()
|
||||||
|
filename, err := dlgBuilder.Browse()
|
||||||
|
if err == nil {
|
||||||
|
state.HandleFiles([]string{filename})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
imgui.SameLine()
|
||||||
|
if imgui.Button("Cancel") {
|
||||||
|
state.machine.SetState(nil)
|
||||||
|
imgui.CloseCurrentPopup()
|
||||||
|
}
|
||||||
|
imgui.EndPopup()
|
||||||
|
} else {
|
||||||
|
state.machine.SetState(nil)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (state *saveModAsWaitingState) HandleFiles(names []string) {
|
||||||
|
modPath, ok := state.verifyDir(names)
|
||||||
|
if ok {
|
||||||
|
state.machine.SetState(nil)
|
||||||
|
state.view.requestSaveMod(modPath)
|
||||||
|
} else {
|
||||||
|
state.failureTime = time.Now()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (state saveModAsWaitingState) verifyDir(names []string) (string, bool) {
|
||||||
|
if len(names) != 1 {
|
||||||
|
return "", false
|
||||||
|
}
|
||||||
|
fileInfo, err := os.Stat(names[0])
|
||||||
|
if err != nil {
|
||||||
|
return "", false
|
||||||
|
}
|
||||||
|
if !fileInfo.IsDir() {
|
||||||
|
return "", false
|
||||||
|
}
|
||||||
|
return names[0], true
|
||||||
|
}
|
||||||
@@ -0,0 +1,28 @@
|
|||||||
|
package project
|
||||||
|
|
||||||
|
import (
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/inkyblackness/imgui-go/v3"
|
||||||
|
|
||||||
|
"github.com/inkyblackness/hacked/ui/gui"
|
||||||
|
)
|
||||||
|
|
||||||
|
type saveModFailedState struct {
|
||||||
|
machine gui.ModalStateMachine
|
||||||
|
view *View
|
||||||
|
errorInfo string
|
||||||
|
}
|
||||||
|
|
||||||
|
func (state saveModFailedState) Render() {
|
||||||
|
imgui.OpenPopup("Save mod as")
|
||||||
|
state.machine.SetState(&saveModAsWaitingState{
|
||||||
|
machine: state.machine,
|
||||||
|
view: state.view,
|
||||||
|
failureTime: time.Now(),
|
||||||
|
errorInfo: state.errorInfo,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func (state saveModFailedState) HandleFiles(names []string) {
|
||||||
|
}
|
||||||
@@ -0,0 +1,246 @@
|
|||||||
|
package project
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
|
||||||
|
"github.com/inkyblackness/imgui-go/v3"
|
||||||
|
|
||||||
|
"github.com/inkyblackness/hacked/ss1/edit"
|
||||||
|
"github.com/inkyblackness/hacked/ss1/edit/undoable/cmd"
|
||||||
|
"github.com/inkyblackness/hacked/ss1/world"
|
||||||
|
"github.com/inkyblackness/hacked/ui/gui"
|
||||||
|
)
|
||||||
|
|
||||||
|
// View handles the project display.
|
||||||
|
type View struct {
|
||||||
|
service *edit.ProjectService
|
||||||
|
|
||||||
|
modalStateMachine gui.ModalStateMachine
|
||||||
|
guiScale float32
|
||||||
|
commander cmd.Registry
|
||||||
|
|
||||||
|
model viewModel
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewView creates a new instance for the project display.
|
||||||
|
func NewView(service *edit.ProjectService, modalStateMachine gui.ModalStateMachine, guiScale float32, commander cmd.Registry) *View {
|
||||||
|
return &View{
|
||||||
|
service: service,
|
||||||
|
|
||||||
|
modalStateMachine: modalStateMachine,
|
||||||
|
guiScale: guiScale,
|
||||||
|
commander: commander,
|
||||||
|
|
||||||
|
model: freshViewModel(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// WindowOpen returns the flag address, to be used with the main menu.
|
||||||
|
func (view *View) WindowOpen() *bool {
|
||||||
|
return &view.model.windowOpen
|
||||||
|
}
|
||||||
|
|
||||||
|
// Render requests to render the view.
|
||||||
|
func (view *View) Render() {
|
||||||
|
if view.model.restoreFocus {
|
||||||
|
imgui.SetNextWindowFocus()
|
||||||
|
view.model.restoreFocus = false
|
||||||
|
view.model.windowOpen = true
|
||||||
|
}
|
||||||
|
title := "Project"
|
||||||
|
saveStatus := view.service.SaveStatus()
|
||||||
|
if saveStatus.FilesModified > 0 {
|
||||||
|
title += fmt.Sprintf(" - %d file(s) pending save", saveStatus.FilesModified)
|
||||||
|
if saveStatus.SavePending && (saveStatus.SaveIn.Seconds() < 4) {
|
||||||
|
title += fmt.Sprintf(" - auto-save in %d", int(saveStatus.SaveIn.Seconds()))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if view.model.windowOpen {
|
||||||
|
imgui.SetNextWindowSizeV(imgui.Vec2{X: 400 * view.guiScale, Y: 300 * view.guiScale}, imgui.ConditionFirstUseEver)
|
||||||
|
if imgui.BeginV(title+"###Project", view.WindowOpen(), 0) {
|
||||||
|
view.renderContent()
|
||||||
|
}
|
||||||
|
imgui.End()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (view *View) renderContent() {
|
||||||
|
imgui.Text("Mod Location")
|
||||||
|
imgui.PushStyleVarVec2(imgui.StyleVarWindowPadding, imgui.Vec2{X: 1, Y: 0})
|
||||||
|
imgui.BeginChildV("ModLocation", imgui.Vec2{X: -200*view.guiScale - 10*view.guiScale, Y: imgui.TextLineHeight() * 1.5}, true,
|
||||||
|
imgui.WindowFlagsNoScrollbar|imgui.WindowFlagsNoScrollWithMouse)
|
||||||
|
modPath := view.service.ModPath()
|
||||||
|
if len(modPath) > 0 {
|
||||||
|
imgui.Text(modPath)
|
||||||
|
} else {
|
||||||
|
imgui.PushStyleColor(imgui.StyleColorText, imgui.Vec4{X: 1.0, Y: 1.0, Z: 1.0, W: 0.5})
|
||||||
|
imgui.Text("(new mod)")
|
||||||
|
imgui.PopStyleColor()
|
||||||
|
}
|
||||||
|
imgui.EndChild()
|
||||||
|
imgui.PopStyleVar()
|
||||||
|
imgui.BeginGroup()
|
||||||
|
imgui.SameLine()
|
||||||
|
if imgui.ButtonV("Save", imgui.Vec2{X: 100 * view.guiScale, Y: 0}) {
|
||||||
|
view.StartSavingMod()
|
||||||
|
}
|
||||||
|
imgui.SameLine()
|
||||||
|
if imgui.ButtonV("Load...", imgui.Vec2{X: 100 * view.guiScale, Y: 0}) {
|
||||||
|
view.startLoadingMod()
|
||||||
|
}
|
||||||
|
imgui.EndGroup()
|
||||||
|
|
||||||
|
imgui.Text("Static World Data")
|
||||||
|
imgui.BeginChildV("ManifestEntries", imgui.Vec2{X: -100 * view.guiScale, Y: 0}, true, 0)
|
||||||
|
manifest := view.service.Mod().World()
|
||||||
|
entries := manifest.EntryCount()
|
||||||
|
for i := entries - 1; i >= 0; i-- {
|
||||||
|
entry, _ := manifest.Entry(i)
|
||||||
|
if imgui.SelectableV(entry.ID, view.model.selectedManifestEntry == i, 0, imgui.Vec2{}) {
|
||||||
|
view.model.selectedManifestEntry = i
|
||||||
|
}
|
||||||
|
}
|
||||||
|
imgui.EndChild()
|
||||||
|
imgui.SameLine()
|
||||||
|
imgui.BeginGroup()
|
||||||
|
if imgui.ButtonV("Add...", imgui.Vec2{X: -1, Y: 0}) {
|
||||||
|
view.startAddingManifestEntry()
|
||||||
|
}
|
||||||
|
if imgui.ButtonV("Up", imgui.Vec2{X: -1, Y: 0}) {
|
||||||
|
view.requestMoveManifestEntryUp()
|
||||||
|
}
|
||||||
|
if imgui.ButtonV("Down", imgui.Vec2{X: -1, Y: 0}) {
|
||||||
|
view.requestMoveManifestEntryDown()
|
||||||
|
}
|
||||||
|
if imgui.ButtonV("Remove", imgui.Vec2{X: -1, Y: 0}) {
|
||||||
|
view.requestRemoveManifestEntry()
|
||||||
|
}
|
||||||
|
imgui.EndGroup()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (view *View) startLoadingMod() {
|
||||||
|
view.modalStateMachine.SetState(&loadModStartState{
|
||||||
|
machine: view.modalStateMachine,
|
||||||
|
view: view,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// StartSavingMod initiates to save the mod.
|
||||||
|
// It either opens the save-as dialog, or simply saves under the current folder.
|
||||||
|
func (view *View) StartSavingMod() {
|
||||||
|
if view.service.ModHasStorageLocation() {
|
||||||
|
err := view.service.SaveMod()
|
||||||
|
if err != nil {
|
||||||
|
view.handleSaveModFailure(err)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
view.modalStateMachine.SetState(&saveModAsStartState{
|
||||||
|
machine: view.modalStateMachine,
|
||||||
|
view: view,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (view *View) startAddingManifestEntry() {
|
||||||
|
view.modalStateMachine.SetState(&addManifestEntryStartState{
|
||||||
|
machine: view.modalStateMachine,
|
||||||
|
view: view,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func (view *View) requestMoveManifestEntryUp() {
|
||||||
|
manifest := view.service.Mod().World()
|
||||||
|
entries := manifest.EntryCount()
|
||||||
|
if (view.model.selectedManifestEntry >= 0) && (view.model.selectedManifestEntry < (entries - 1)) {
|
||||||
|
view.requestMoveManifestEntry(view.model.selectedManifestEntry+1, view.model.selectedManifestEntry)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (view *View) requestMoveManifestEntryDown() {
|
||||||
|
if view.model.selectedManifestEntry > 0 {
|
||||||
|
view.requestMoveManifestEntry(view.model.selectedManifestEntry-1, view.model.selectedManifestEntry)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (view *View) requestMoveManifestEntry(to, from int) {
|
||||||
|
_ = view.commander.Register(
|
||||||
|
cmd.Named("moveManifestEntry"),
|
||||||
|
cmd.Reverse(view.taskToRestoreFocus()),
|
||||||
|
cmd.Reverse(view.taskToSelectEntry(from)),
|
||||||
|
cmd.Nested(func() error { return view.service.MoveManifestEntry(to, from) }),
|
||||||
|
cmd.Forward(view.taskToSelectEntry(to)),
|
||||||
|
cmd.Forward(view.taskToRestoreFocus()),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (view *View) tryAddManifestEntryFrom(names []string) error {
|
||||||
|
entry, err := world.NewManifestEntryFrom(names)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
view.requestAddManifestEntry(entry)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (view *View) requestAddManifestEntry(entry *world.ManifestEntry) {
|
||||||
|
at := view.model.selectedManifestEntry + 1
|
||||||
|
_ = view.commander.Register(
|
||||||
|
cmd.Named("addManifestEntry"),
|
||||||
|
cmd.Reverse(view.taskToRestoreFocus()),
|
||||||
|
cmd.Reverse(view.taskToSelectEntry(view.model.selectedManifestEntry)),
|
||||||
|
cmd.Nested(func() error { return view.service.AddManifestEntry(at, entry) }),
|
||||||
|
cmd.Forward(view.taskToSelectEntry(at)),
|
||||||
|
cmd.Forward(view.taskToRestoreFocus()),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (view *View) requestRemoveManifestEntry() {
|
||||||
|
manifest := view.service.Mod().World()
|
||||||
|
at := view.model.selectedManifestEntry
|
||||||
|
if (at < 0) || (at >= manifest.EntryCount()) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
_ = view.commander.Register(
|
||||||
|
cmd.Named("removeManifestEntry"),
|
||||||
|
cmd.Reverse(view.taskToRestoreFocus()),
|
||||||
|
cmd.Reverse(view.taskToSelectEntry(view.model.selectedManifestEntry)),
|
||||||
|
cmd.Nested(func() error { return view.service.RemoveManifestEntry(at) }),
|
||||||
|
cmd.Forward(view.taskToSelectEntry(view.model.selectedManifestEntry-1)),
|
||||||
|
cmd.Forward(view.taskToRestoreFocus()),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (view *View) tryLoadModFrom(names []string) error {
|
||||||
|
return view.service.TryLoadModFrom(names)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (view *View) requestSaveMod(modPath string) {
|
||||||
|
err := view.service.SaveModUnder(modPath)
|
||||||
|
if err != nil {
|
||||||
|
view.handleSaveModFailure(err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (view *View) handleSaveModFailure(err error) {
|
||||||
|
view.modalStateMachine.SetState(&saveModFailedState{
|
||||||
|
machine: view.modalStateMachine,
|
||||||
|
view: view,
|
||||||
|
errorInfo: err.Error(),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func (view *View) taskToRestoreFocus() cmd.Task {
|
||||||
|
return func(modder world.Modder) error {
|
||||||
|
view.model.restoreFocus = true
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (view *View) taskToSelectEntry(index int) cmd.Task {
|
||||||
|
return func(modder world.Modder) error {
|
||||||
|
view.model.selectedManifestEntry = index
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,14 @@
|
|||||||
|
package project
|
||||||
|
|
||||||
|
type viewModel struct {
|
||||||
|
restoreFocus bool
|
||||||
|
windowOpen bool
|
||||||
|
selectedManifestEntry int
|
||||||
|
}
|
||||||
|
|
||||||
|
func freshViewModel() viewModel {
|
||||||
|
return viewModel{
|
||||||
|
windowOpen: false,
|
||||||
|
selectedManifestEntry: -1,
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,15 @@
|
|||||||
|
package render
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/inkyblackness/hacked/ui/opengl"
|
||||||
|
|
||||||
|
mgl "github.com/go-gl/mathgl/mgl32"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Context provides current render properties.
|
||||||
|
type Context struct {
|
||||||
|
OpenGL opengl.OpenGL
|
||||||
|
|
||||||
|
ViewMatrix *mgl.Mat4
|
||||||
|
ProjectionMatrix mgl.Mat4
|
||||||
|
}
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user