Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
51 changes: 51 additions & 0 deletions .github/workflows/test.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
name: Unity tests

on:
push:
branches: [main]
pull_request:
branches: [main]
workflow_dispatch:

jobs:
test:
name: Edit-mode tests
runs-on: ubuntu-latest
permissions:
checks: write
contents: read

steps:
- name: Checkout
uses: actions/checkout@v4
with:
lfs: true

- name: Cache Unity Library
uses: actions/cache@v4
with:
path: Tests/UnityProject/Library
key: Library-${{ runner.os }}-2022.3
restore-keys: |
Library-${{ runner.os }}-

- name: Run edit-mode tests
uses: game-ci/unity-test-runner@v4
env:
UNITY_LICENSE: ${{ secrets.UNITY_LICENSE }}
UNITY_EMAIL: ${{ secrets.UNITY_EMAIL }}
UNITY_PASSWORD: ${{ secrets.UNITY_PASSWORD }}
with:
projectPath: Tests/UnityProject
unityVersion: 2022.3.40f1
testMode: editmode
artifactsPath: test-results
githubToken: ${{ secrets.GITHUB_TOKEN }}
checkName: Edit-mode test results

- name: Upload test results
if: always()
uses: actions/upload-artifact@v4
with:
name: test-results
path: test-results
11 changes: 9 additions & 2 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1,8 +1,7 @@
# UPM package — installed inside other Unity projects' Packages/ directory.
# This .gitignore covers only what's relevant when developing the package itself.

# Build outputs
/[Bb]uild/
# Build outputs (Unity artefacts)
/[Bb]uilds/
/[Ll]ibrary/
/[Tt]emp/
Expand All @@ -11,6 +10,14 @@
/[Uu]ser[Ss]ettings/
*.log

# tests/UnityProject is a host project for CI; only its source-tracked
# settings + manifest belong in version control.
/Tests/UnityProject/Library/
/Tests/UnityProject/Temp/
/Tests/UnityProject/Logs/
/Tests/UnityProject/obj/
/Tests/UnityProject/Build/

# Plugins/ contents are produced by the Core build (DLL drop). Track the directory
# but not the binaries on day one — uncomment the next line if you want to commit
# specific DLL versions. For now, build provides them.
Expand Down
82 changes: 82 additions & 0 deletions Editor/Importers/GltfEnvironmentPostprocessor.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
using System.Text.RegularExpressions;
using UnityEditor;
using UnityEngine;

namespace OpenApparatus.Unity.Editor.Importers
{
internal sealed class GltfEnvironmentPostprocessor : AssetPostprocessor
{
static readonly Regex RoomNameRx = new Regex(@"^Room_(\d+)$", RegexOptions.Compiled);
static readonly Regex WallNameRx = new Regex(@"^Room_(\d+)_wall_(\d+)$", RegexOptions.Compiled);
static readonly Regex ObjectNameRx = new Regex(@"^Room_(\d+)_object_(\d+)_(\d+)$", RegexOptions.Compiled);

void OnPostprocessModel(GameObject root)
{
if (root == null) return;
if (!LooksLikeOpenApparatusGltf(root)) return;

AttachComponents(root.transform);
}

static bool LooksLikeOpenApparatusGltf(GameObject root)
{
foreach (Transform child in root.transform)
{
if (RoomNameRx.IsMatch(child.name)) return true;
}
foreach (var t in root.GetComponentsInChildren<Transform>(includeInactive: true))
{
if (RoomNameRx.IsMatch(t.name)) return true;
}
return false;
}

static void AttachComponents(Transform root)
{
foreach (var t in root.GetComponentsInChildren<Transform>(includeInactive: true))
{
var m = RoomNameRx.Match(t.name);
if (m.Success)
{
var room = t.gameObject.GetComponent<Room>() ?? t.gameObject.AddComponent<Room>();
room.RoomId = int.Parse(m.Groups[1].Value);
continue;
}

m = WallNameRx.Match(t.name);
if (m.Success)
{
var wall = t.gameObject.GetComponent<Wall>() ?? t.gameObject.AddComponent<Wall>();
wall.WallNumber = int.Parse(m.Groups[2].Value);
wall.PassageKind = PassageKind.Closed;
var bounds = ComputeMeshBoundsLocal(t.gameObject);
if (bounds.HasValue)
{
var b = bounds.Value;
wall.StartLocal = new Vector3(b.min.x, 0f, b.min.z);
wall.EndLocal = new Vector3(b.max.x, 0f, b.max.z);
}
continue;
}

m = ObjectNameRx.Match(t.name);
if (m.Success)
{
var instance = t.gameObject.GetComponent<RoomObjectInstance>()
?? t.gameObject.AddComponent<RoomObjectInstance>();
instance.OwningRoomId = int.Parse(m.Groups[1].Value);
instance.Slot = int.Parse(m.Groups[2].Value);
instance.LocalRotationY = t.localRotation.eulerAngles.y * Mathf.Deg2Rad;
continue;
}
}
}

static Bounds? ComputeMeshBoundsLocal(GameObject go)
{
var mf = go.GetComponent<MeshFilter>();
if (mf == null || mf.sharedMesh == null) return null;
return mf.sharedMesh.bounds;
}
}
}
42 changes: 42 additions & 0 deletions Editor/Importers/JsonEnvironmentDiscriminator.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
using System;
using Newtonsoft.Json.Linq;

namespace OpenApparatus.Unity.Editor.Importers
{
internal static class JsonEnvironmentDiscriminator
{
public const string SchemaMarker = "openapparatus.environment";

public static bool IsOpenApparatus(string json)
{
if (string.IsNullOrWhiteSpace(json)) return false;

try
{
var root = JToken.Parse(json) as JObject;
if (root == null) return false;

var schema = root["schema"]?.Type == JTokenType.String
? (string)root["schema"]
: null;
if (schema == SchemaMarker) return true;

var version = root["version"];
if (version == null || version.Type != JTokenType.Integer ||
(int)version != 3)
{
return false;
}

if (root["parameters"] == null) return false;
if (root["grid"] == null) return false;
var rooms = root["rooms"];
return rooms != null && rooms.Type == JTokenType.Array;
}
catch (Exception)
{
return false;
}
}
}
}
99 changes: 99 additions & 0 deletions Editor/Importers/JsonEnvironmentDocument.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,99 @@
using System.Collections.Generic;

namespace OpenApparatus.Unity.Editor.Importers
{
internal sealed class JsonEnvironmentDocument
{
public string schema { get; set; }
public int version { get; set; }
public JsonParameters parameters { get; set; }
public JsonGrid grid { get; set; }
public List<JsonObjectSlot> objectSlots { get; set; }
public List<JsonRoom> rooms { get; set; }
public JsonOutside outside { get; set; }
}

internal sealed class JsonParameters
{
public float tileSize { get; set; }
public float wallThickness { get; set; }
public float wallHeight { get; set; }
public float doorWidth { get; set; }
public float doorHeight { get; set; }
public float windowWidth { get; set; }
public float windowHeight { get; set; }
public float windowSillHeight { get; set; }
public int gridSubdivision { get; set; }
public float defaultObjectY { get; set; }
}

internal sealed class JsonGrid
{
public int width { get; set; }
public int length { get; set; }
public List<List<int>> tiles { get; set; }
}

internal sealed class JsonObjectSlot
{
public int id { get; set; }
public string shape { get; set; }
public List<float> color { get; set; }
public float size { get; set; }
public string displayName { get; set; }
public string objectType { get; set; }
}

internal sealed class JsonRoom
{
public int id { get; set; }
public JsonRoomShape shape { get; set; }
public List<float> position { get; set; }
public List<List<int>> tiles { get; set; }
public List<JsonWall> walls { get; set; }
public List<JsonObjectInstance> objects { get; set; }
}

internal sealed class JsonRoomShape
{
public string type { get; set; }
public float width { get; set; }
public float depth { get; set; }
}

internal sealed class JsonWall
{
public int number { get; set; }
public string side { get; set; }
public List<float> start { get; set; }
public List<float> end { get; set; }
public int? neighborRoomId { get; set; }
public JsonPassage passage { get; set; }
}

internal sealed class JsonPassage
{
public string type { get; set; }
public List<JsonOpening> openings { get; set; }
}

internal sealed class JsonOpening
{
public float offsetAlongEdge { get; set; }
public float width { get; set; }
public float height { get; set; }
public float sillHeight { get; set; }
}

internal sealed class JsonObjectInstance
{
public int slot { get; set; }
public List<float> position { get; set; }
public float rotation { get; set; }
}

internal sealed class JsonOutside
{
public List<JsonObjectInstance> objects { get; set; }
}
}
Loading
Loading