Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
34 commits
Select commit Hold shift + click to select a range
a3cda17
chore: upgrade to latest environment (SDK 3.10.2, Unity 2022.3, seman…
Copilot Apr 7, 2026
20ca0c5
revert: revert com.unity.textmeshpro version bump due to malware advi…
Copilot Apr 7, 2026
6f7eb63
feat: remove UdonToolkit dependency from all scripts and package.json
Copilot Apr 7, 2026
316b656
Changes before error encountered
Copilot Apr 7, 2026
9c816b7
Address review feedback: fix ATCRadar Update safety, remove dead OnAf…
Copilot Apr 8, 2026
e09359d
Apply second review: fix ATCRadar warning, guard empty tuners, add _R…
Copilot Apr 8, 2026
a293d5e
Fix Index setter divide-by-zero and bounds, init previousPositions/Ti…
Copilot Apr 8, 2026
fc9649f
Guard groundSpeed divide-by-zero; clamp Index against standbyTuners l…
Copilot Apr 8, 2026
c73475e
Fix array injection; guard SelectedTuner/StandbyTuner null access in …
Copilot Apr 8, 2026
6e80533
ci: introduce udonsharp-lint via dotnet tool manifest + GitHub Action…
Copilot Apr 8, 2026
63c3223
ci: add permissions block and remove branch exclusion from lint workflow
Copilot Apr 8, 2026
66e121f
migrate virtual-cns to current sdk and address review feedback
esnya Apr 9, 2026
45fd8a2
refactor: replace RadioTuner toolkit inspector
esnya Apr 9, 2026
979c493
refactor: trim toolkit editor coupling
esnya Apr 9, 2026
2b8d010
feat: add custom editors for ATIS and radio slices
esnya Apr 9, 2026
22969df
fix: harden ATIS and radio tuner validation
esnya Apr 9, 2026
567ccfb
fix: improve ATIS misconfiguration recovery
esnya Apr 9, 2026
4dd8d7f
fix: harden ATIS and audio selector state
esnya Apr 9, 2026
65ac3dc
fix: keep ATIS playback alive on empty runway config
esnya Apr 9, 2026
2b991ec
chore: allow release config on main too
esnya Apr 9, 2026
24d5f82
fix: guard ATIS playback against empty clip lists
esnya Apr 9, 2026
9f88cac
ci: add reusable static and UdonSharp checks
esnya Apr 9, 2026
151d61d
fix: skip empty ATIS tokens during encoding
esnya Apr 9, 2026
f9f35d6
fix: make static checks validate committed changes
esnya Apr 9, 2026
d3b87ce
fix: scope static whitespace checks to head commit
esnya Apr 9, 2026
5dd2f63
fix: limit static whitespace checks to code and config diffs
esnya Apr 9, 2026
1dbb760
fix: harden static checks on CI merge commits
esnya Apr 9, 2026
755905c
fix: scope CI static checks to checked commits
esnya Apr 9, 2026
2be5356
fix: check only changed files in CI static checks
esnya Apr 9, 2026
f581a4e
chore: harden checks and follow alpha dependencies
esnya Apr 9, 2026
9358a04
fix: validate static checks against PR range
esnya Apr 9, 2026
b1fb181
fix: declare connector sf flight dependency
esnya Apr 9, 2026
8162e05
fix: normalize virtual cns package dependency contracts
esnya Apr 9, 2026
c35a100
fix: restore virtual-cns release config and presets
esnya Apr 9, 2026
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
12 changes: 12 additions & 0 deletions .config/dotnet-tools.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
{
"version": 1,
"isRoot": true,
"tools": {
"tktco.udonsharplinter": {
"version": "0.3.1",
"commands": [
"udonsharp-lint"
]
}
}
}
39 changes: 39 additions & 0 deletions .github/workflows/check.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
name: Check

on:
push:
pull_request:

jobs:
static-checks:
runs-on: ubuntu-latest
permissions:
contents: read
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 0
- uses: actions/setup-node@v4
with:
node-version: "22"
- uses: reviewdog/action-actionlint@v1
with:
fail_level: error
- name: Run static checks
run: bash scripts/run-static-checks.sh

udonsharp-lint:
runs-on: ubuntu-latest
permissions:
contents: read
steps:
- uses: actions/checkout@v4
- uses: actions/setup-dotnet@v4
with:
dotnet-version: |
8.0.x
6.0.x
- name: Restore dotnet tools
run: dotnet tool restore
- name: Run UdonSharp Linter
run: dotnet tool run udonsharp-lint Packages
4 changes: 2 additions & 2 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,8 @@ jobs:
release:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- uses: actions/setup-node@v3
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: lts/*
- run: npm ci
Expand Down
26 changes: 26 additions & 0 deletions .github/workflows/udonsharp-lint.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
name: UdonSharp Lint
on:
push:
paths:
- "Packages/**/*.cs"
- ".config/dotnet-tools.json"
- ".github/workflows/udonsharp-lint.yml"
pull_request:
paths:
- "Packages/**/*.cs"
- ".config/dotnet-tools.json"
- ".github/workflows/udonsharp-lint.yml"
jobs:
lint:
runs-on: ubuntu-latest
permissions:
contents: read
steps:
- uses: actions/checkout@v4
- uses: actions/setup-dotnet@v4
with:
dotnet-version: "6.x"
- name: Restore dotnet tools
run: dotnet tool restore
- name: Run UdonSharp Linter
run: dotnet tool run udonsharp-lint Packages
9 changes: 8 additions & 1 deletion .releaserc.yml
Original file line number Diff line number Diff line change
@@ -1,5 +1,12 @@
branches:
- main
- master
Comment thread
esnya marked this conversation as resolved.
Comment thread
esnya marked this conversation as resolved.
Comment thread
esnya marked this conversation as resolved.
Comment thread
esnya marked this conversation as resolved.
- name: alpha
prerelease: true

plugins:
- semantic-release-gitmoji
- "@semantic-release/npm"
- - "@semantic-release/npm"
- npmPublish: false
- "@semantic-release/github"
- "@semantic-release/git"
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@

using System;
using UdonSharp;
using UdonToolkit;
using UnityEngine;
using VRC.Udon;

Expand All @@ -15,8 +14,7 @@ public class ATISGenerator : UdonSharpBehaviour
readonly private char[] trimChars = new[] { '[', ']', ',', '.', ' ' };

[TextArea] public string template = "AERODROME information [{0}] [{1}] [Z]. {2}. wind {3}. visibility 10 kilometers, sky clear. temperature [25], dewpoint [20], qnh [2992]. advise you have information [{0}]";
[ListView("Runway Operations")] public float[] windHeadings = { 0.0f, 180.0f };
[ListView("Runway Operations")]
public float[] windHeadings = { 0.0f, 180.0f };
public string[] runwayTemplates = {
"ils runway [36] approach. using runway [36]",
"ils runway [18] approach. using runway [18]",
Expand All @@ -26,14 +24,14 @@ public class ATISGenerator : UdonSharpBehaviour
public string windWithGustTemplate = "[{0:000}] degrees {1:0} knots, maximum {2:0} knots";

public UdonSharpBehaviour windSource;
[Popup("programVariable", "@windSource", "vector")] public string windVariableName = "Wind";
[Popup("programVariable", "@windSource", "float")] public string windGustVariableName = "WindGustStrength";
public string windVariableName = "Wind";
public string windGustVariableName = "WindGustStrength";
[Tooltip("Knots")] public float minWind = 0.5f;

public AudioClip[] digits = { }, phonetics = { };
public AudioClip periodInterval, repeatInterval;
[ListView("Vocabulary")] public string[] clipWords = { };
[ListView("Vocabulary")] public AudioClip[] clips = { };
public string[] clipWords = { };
public AudioClip[] clips = { };

private float magneticDeclination;
private int informationIndex;
Expand All @@ -47,6 +45,14 @@ private void Start()

public AudioClip[] _Generate()
{
var runwayOperationCount = GetRunwayOperationCount();
if (runwayOperationCount == 0)
{
Debug.LogError("[Virtual-CNS][ATIS] No runway operations configured. Fix the ATISGenerator inspector setup. Reusing the previous ATIS sequence if available.", gameObject);
if (words != null && words.Length > 0) return words;
return repeatInterval ? new[] { repeatInterval } : Array.Empty<AudioClip>();
}

var now = DateTime.UtcNow;
var hour = now.Hour;
var minute = now.Minute / 30 * 30;
Expand All @@ -67,7 +73,9 @@ public AudioClip[] _Generate()
var maxWindSpeed = windSpeed + Mathf.RoundToInt(windGustStrength * KNOTS);

var windString = windCalm ? "calm" : string.Format(gusty ? windWithGustTemplate : windTemplate, new object[] { windHeading, windSpeed, maxWindSpeed });
var runwayOperationIndex = windCalm ? defaultRunwayIndex : IndexOfRunwayOperation(windHeading);
var runwayOperationIndex = windCalm
? Mathf.Clamp(defaultRunwayIndex, 0, runwayOperationCount - 1)
: IndexOfRunwayOperation(windHeading, runwayOperationCount);

var rawText = string.Format(template, (char)('A' + informationIndex), timestamp, runwayTemplates[runwayOperationIndex], windString);
Debug.Log($"[Virtual-CNS][ATIS] Encoding: {rawText}");
Expand All @@ -78,6 +86,11 @@ public AudioClip[] _Generate()
foreach (var rawWord in rawWords)
{
var word = rawWord.Trim(trimChars);
if (string.IsNullOrEmpty(word))
{
period = rawWord.EndsWith(".");
continue;
}
var chars = word.ToCharArray();
var firstChar = chars[0];

Expand Down Expand Up @@ -173,12 +186,20 @@ private AudioClip GetPhoneticClip(char c)
return phonetics[c - 'A'];
}

private int IndexOfRunwayOperation(float windHeading)
private int GetRunwayOperationCount()
{
if (windHeadings == null || runwayTemplates == null) return 0;
return Mathf.Min(windHeadings.Length, runwayTemplates.Length);
}

private int IndexOfRunwayOperation(float windHeading, int runwayOperationCount)
{
if (runwayOperationCount <= 0) return 0;

var minDifference = float.MaxValue;
var minIndex = 0;

for (var i = 0; i < windHeadings.Length; i++)
for (var i = 0; i < runwayOperationCount; i++)
{
var difference = Mathf.Abs(Mathf.DeltaAngle(windHeading, windHeadings[i]));
if (difference < minDifference)
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,176 @@
#if !COMPILER_UDONSHARP && UNITY_EDITOR
using UdonSharpEditor;
using UnityEditor;
using UnityEngine;

namespace VirtualCNS
{
[CustomEditor(typeof(ATISGenerator))]
public class ATISGeneratorEditor : Editor
{
private SerializedProperty template;
private SerializedProperty windHeadings;
private SerializedProperty runwayTemplates;
private SerializedProperty defaultRunwayIndex;
private SerializedProperty windTemplate;
private SerializedProperty windWithGustTemplate;
private SerializedProperty windSource;
private SerializedProperty windVariableName;
private SerializedProperty windGustVariableName;
private SerializedProperty minWind;
private SerializedProperty digits;
private SerializedProperty phonetics;
private SerializedProperty periodInterval;
private SerializedProperty repeatInterval;
private SerializedProperty clipWords;
private SerializedProperty clips;

private void OnEnable()
{
template = serializedObject.FindProperty(nameof(ATISGenerator.template));
windHeadings = serializedObject.FindProperty(nameof(ATISGenerator.windHeadings));
runwayTemplates = serializedObject.FindProperty(nameof(ATISGenerator.runwayTemplates));
defaultRunwayIndex = serializedObject.FindProperty(nameof(ATISGenerator.defaultRunwayIndex));
windTemplate = serializedObject.FindProperty(nameof(ATISGenerator.windTemplate));
windWithGustTemplate = serializedObject.FindProperty(nameof(ATISGenerator.windWithGustTemplate));
windSource = serializedObject.FindProperty(nameof(ATISGenerator.windSource));
windVariableName = serializedObject.FindProperty(nameof(ATISGenerator.windVariableName));
windGustVariableName = serializedObject.FindProperty(nameof(ATISGenerator.windGustVariableName));
minWind = serializedObject.FindProperty(nameof(ATISGenerator.minWind));
digits = serializedObject.FindProperty(nameof(ATISGenerator.digits));
phonetics = serializedObject.FindProperty(nameof(ATISGenerator.phonetics));
periodInterval = serializedObject.FindProperty(nameof(ATISGenerator.periodInterval));
repeatInterval = serializedObject.FindProperty(nameof(ATISGenerator.repeatInterval));
clipWords = serializedObject.FindProperty(nameof(ATISGenerator.clipWords));
clips = serializedObject.FindProperty(nameof(ATISGenerator.clips));
}

public override void OnInspectorGUI()
{
UdonSharpGUI.DrawDefaultUdonSharpBehaviourHeader(target);

serializedObject.Update();

EditorGUILayout.PropertyField(template);
EditorGUILayout.Space();

DrawRunwayOperations();

EditorGUILayout.Space();
EditorGUILayout.LabelField("Wind Source", EditorStyles.boldLabel);
EditorGUILayout.PropertyField(windSource);
EditorGUILayout.PropertyField(windVariableName);
EditorGUILayout.PropertyField(windGustVariableName);
EditorGUILayout.PropertyField(minWind);
EditorGUILayout.PropertyField(windTemplate);
EditorGUILayout.PropertyField(windWithGustTemplate);

EditorGUILayout.Space();
EditorGUILayout.LabelField("Audio Tables", EditorStyles.boldLabel);
EditorGUILayout.PropertyField(digits, true);
EditorGUILayout.PropertyField(phonetics, true);
EditorGUILayout.PropertyField(periodInterval);
EditorGUILayout.PropertyField(repeatInterval);

EditorGUILayout.Space();
DrawClipTable();

serializedObject.ApplyModifiedProperties();
}

private void DrawRunwayOperations()
{
EditorGUILayout.LabelField("Runway Operations", EditorStyles.boldLabel);

var count = DrawCountField("Operation Count", 1, windHeadings, runwayTemplates);
var effectiveCount = Mathf.Min(windHeadings.arraySize, runwayTemplates.arraySize);
if (effectiveCount == 0)
{
EditorGUILayout.HelpBox("Add at least one runway operation. ATIS generation is blocked until this is fixed.", MessageType.Error);
defaultRunwayIndex.intValue = 0;
if (GUILayout.Button("Create Default Runway Operation"))
{
ResizeArrays(1, windHeadings, runwayTemplates);
windHeadings.GetArrayElementAtIndex(0).floatValue = 0.0f;
runwayTemplates.GetArrayElementAtIndex(0).stringValue = "runway operation unavailable";
}
return;
}

if (effectiveCount != count)
{
EditorGUILayout.HelpBox("Only the overlapping runway operation entries are active until the backing arrays are normalized.", MessageType.Warning);
if (GUILayout.Button("Normalize Runway Operation Arrays"))
{
ResizeArrays(count, windHeadings, runwayTemplates);
effectiveCount = count;
}
}

defaultRunwayIndex.intValue = Mathf.Clamp(defaultRunwayIndex.intValue, 0, effectiveCount - 1);
defaultRunwayIndex.intValue = EditorGUILayout.IntSlider("Default Runway Index", defaultRunwayIndex.intValue, 0, effectiveCount - 1);

for (var i = 0; i < effectiveCount; i++)
{
using (new EditorGUILayout.VerticalScope(EditorStyles.helpBox))
{
EditorGUILayout.LabelField($"Operation {i}", EditorStyles.boldLabel);
EditorGUILayout.PropertyField(windHeadings.GetArrayElementAtIndex(i), new GUIContent("Wind Heading"));
EditorGUILayout.PropertyField(runwayTemplates.GetArrayElementAtIndex(i), new GUIContent("Runway Template"));
}
}
}

private void DrawClipTable()
{
EditorGUILayout.LabelField("Word Clips", EditorStyles.boldLabel);

var count = DrawCountField("Clip Count", 0, clipWords, clips);

for (var i = 0; i < count; i++)
{
using (new EditorGUILayout.HorizontalScope())
{
EditorGUILayout.PropertyField(clipWords.GetArrayElementAtIndex(i), GUIContent.none);
EditorGUILayout.PropertyField(clips.GetArrayElementAtIndex(i), GUIContent.none);
}
}
}

private static int DrawCountField(string label, int minimumSize, params SerializedProperty[] properties)
{
var minSize = int.MaxValue;
var maxSize = 0;
for (var i = 0; i < properties.Length; i++)
{
minSize = Mathf.Min(minSize, properties[i].arraySize);
maxSize = Mathf.Max(maxSize, properties[i].arraySize);
}

if (minSize == int.MaxValue) minSize = 0;

if (minSize != maxSize)
{
EditorGUILayout.HelpBox($"{label} backing arrays are out of sync. Adjust the count to normalize them.", MessageType.Info);
}

var requestedSize = Mathf.Max(minimumSize, EditorGUILayout.IntField(label, Mathf.Max(minimumSize, maxSize)));
if (requestedSize != maxSize)
{
ResizeArrays(requestedSize, properties);
return requestedSize;
}

return minSize;
}

private static void ResizeArrays(int size, params SerializedProperty[] properties)
{
for (var i = 0; i < properties.Length; i++)
{
properties[i].arraySize = size;
}
}
}
}
#endif

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Loading
Loading