Add pitch support for audio items

Add focus item to fields
Improve UISysten plugin to allow better tracking of initial load vs. change of selection
Make debug options more accessible, layout labels more accessible
Update Tolk plugin to support focus tracking
This commit is contained in:
ficedula 2023-07-02 21:11:22 +01:00
parent 02c7898023
commit adb65618d7
19 changed files with 257 additions and 69 deletions

View File

@ -24,7 +24,7 @@ namespace Braver {
}
public interface IAudioItem : IDisposable {
void Play(float volume, float pan, bool loop);
void Play(float volume, float pan, bool loop, float pitch);
void Pause();
void Resume();
void Stop();

View File

@ -4,6 +4,7 @@
//
// SPDX-License-Identifier: EPL-2.0
using Braver.Field;
using Microsoft.Xna.Framework;
using System;
using System.Collections.Generic;
@ -11,9 +12,44 @@ using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace Braver.Field {
public class FocusState {
public string TargetName { get; set; }
public Vector3 TargetPosition { get; set; }
public int WalkmeshDistance { get; set; }
}
[Flags]
public enum FieldOptions {
None = 0,
PlayerControls = 0x1,
//LinesActive = 0x2,
MenuEnabled = 0x4,
CameraTracksPlayer = 0x8,
CameraIsAsyncScrolling = 0x10,
MusicLocked = 0x20,
UseMovieCam = 0x40,
GatewaysEnabled = 0x80,
ShowPlayerHand = 0x100,
NoScripts = 0x1000,
DEFAULT = PlayerControls | MenuEnabled | CameraTracksPlayer | UseMovieCam | GatewaysEnabled | ShowPlayerHand,
}
}
namespace Braver.Plugins.Field {
public interface IField {
Vector3 PlayerPosition { get; }
FocusState GetFocusState();
public FieldOptions Options { get; }
}
public interface IFieldLocation : IPluginInstance {
void Step();
void Step(IField field);
void EntityMoved(IFieldEntity entity, bool isRunning, Vector3 from, Vector3 to);
void FocusChanged();
}
}

View File

@ -28,6 +28,7 @@ namespace Braver.Plugins {
public interface IScreen {
string Description { get; }
bool HasFinishedLoading { get; }
}
public interface IFieldEntity {

View File

@ -18,6 +18,6 @@ namespace Braver.Plugins.UI {
void Dialog(string dialog);
void Choices(IEnumerable<string> choices, int selected);
void Menu(IEnumerable<string> items, int selected);
void Menu(IEnumerable<string> items, int selected, object container);
}
}

View File

@ -17,8 +17,6 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Braver.Core", "Braver.Core\
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "BraverBattleSim", "BraverBattleSim\BraverBattleSim.csproj", "{D2704960-29CC-4A4D-B725-73AC95C1E5F5}"
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "BraverSetup", "BraverSetup\BraverSetup.csproj", "{BC7A0739-2DEE-4396-BF05-9242D4833A5F}"
EndProject
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution Items", "{54809439-31F3-4C7E-B002-98BC2B476EB9}"
ProjectSection(SolutionItems) = preProject
.editorconfig = .editorconfig
@ -64,10 +62,6 @@ Global
{D2704960-29CC-4A4D-B725-73AC95C1E5F5}.Debug|Any CPU.Build.0 = Debug|Any CPU
{D2704960-29CC-4A4D-B725-73AC95C1E5F5}.Release|Any CPU.ActiveCfg = Release|Any CPU
{D2704960-29CC-4A4D-B725-73AC95C1E5F5}.Release|Any CPU.Build.0 = Release|Any CPU
{BC7A0739-2DEE-4396-BF05-9242D4833A5F}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{BC7A0739-2DEE-4396-BF05-9242D4833A5F}.Debug|Any CPU.Build.0 = Debug|Any CPU
{BC7A0739-2DEE-4396-BF05-9242D4833A5F}.Release|Any CPU.ActiveCfg = Release|Any CPU
{BC7A0739-2DEE-4396-BF05-9242D4833A5F}.Release|Any CPU.Build.0 = Release|Any CPU
{4C8A556C-F596-4F35-8E09-C79E5BEAFCEB}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{4C8A556C-F596-4F35-8E09-C79E5BEAFCEB}.Debug|Any CPU.Build.0 = Debug|Any CPU
{4C8A556C-F596-4F35-8E09-C79E5BEAFCEB}.Release|Any CPU.ActiveCfg = Release|Any CPU

View File

@ -376,14 +376,19 @@ namespace Braver {
private NAudio.Vorbis.VorbisWaveReader _vorbis;
private VolumeSampleProvider _volume;
private PanningSampleProvider _pan;
private SmbPitchShiftingSampleProvider _pitch;
private bool _shouldLoop;
public LoadedAudioItem(Stream s) {
_waveOut = new WaveOut();
_waveOut.PlaybackStopped += _waveOut_PlaybackStopped;
_vorbis = new NAudio.Vorbis.VorbisWaveReader(s, true);
_pan = new PanningSampleProvider(_volume = new VolumeSampleProvider(_vorbis));
_waveOut.Init(_pan);
_pitch = new SmbPitchShiftingSampleProvider(
_pan = new PanningSampleProvider(_volume = new VolumeSampleProvider(_vorbis))
) {
PitchFactor = 1f
};
_waveOut.Init(_pitch);
}
private void _waveOut_PlaybackStopped(object sender, StoppedEventArgs e) {
@ -405,10 +410,13 @@ namespace Braver {
_waveOut.Resume();
}
public void Play(float volume, float pan, bool loop) {
public void Play(float volume, float pan, bool loop, float pitch) {
_vorbis.Position = 0;
_shouldLoop = loop;
_volume.Volume = volume;
_pan.Pan = pan;
_pitch.PitchFactor = pitch;
System.Diagnostics.Debug.WriteLine($"AudioItem play vol {volume} pan {pan} pitch {pitch}");
_waveOut.Play();
}

View File

@ -5,7 +5,7 @@
<PublishReadyToRun>false</PublishReadyToRun>
<TieredCompilation>false</TieredCompilation>
<UseWindowsForms>true</UseWindowsForms>
<Version>0.1.1</Version>
<Version>0.1.2</Version>
</PropertyGroup>
<PropertyGroup>
<ApplicationManifest>app.manifest</ApplicationManifest>

View File

@ -39,26 +39,9 @@ namespace Braver.Field {
}
}
[Flags]
public enum FieldOptions {
None = 0,
PlayerControls = 0x1,
//LinesActive = 0x2,
MenuEnabled = 0x4,
CameraTracksPlayer = 0x8,
CameraIsAsyncScrolling = 0x10,
MusicLocked = 0x20,
UseMovieCam = 0x40,
GatewaysEnabled = 0x80,
ShowPlayerHand = 0x100,
NoScripts = 0x1000,
DEFAULT = PlayerControls | MenuEnabled | CameraTracksPlayer | UseMovieCam | GatewaysEnabled | ShowPlayerHand,
}
public class FieldScreen : Screen, Net.IListen<Net.FieldModelMessage>, Net.IListen<Net.FieldBGMessage>,
Net.IListen<Net.FieldEntityModelMessage>, Net.IListen<Net.FieldBGScrollMessage> {
Net.IListen<Net.FieldEntityModelMessage>, Net.IListen<Net.FieldBGScrollMessage>,
IField {
private PerspView3D _view3D;
private View2D _view2D;
@ -94,6 +77,17 @@ namespace Braver.Field {
public TriggersAndGateways TriggersAndGateways { get; private set; }
public Shake ShakeEffect { get; private set; }
private class Focusable {
public string Name { get; set; }
public Func<Vector3> Position { get; set; }
public Func<int> WalkmeshTri { get; set; }
public Func<bool> Active { get; set; }
public object Source { get; set; }
}
private List<Focusable> _focusables = new();
private Focusable _currentFocus;
private EncounterTable[] _encounters;
public FieldOptions Options { get; set; } = FieldOptions.DEFAULT;
@ -378,6 +372,29 @@ namespace Braver.Field {
g.Net.Send(new Net.ScreenReadyMessage());
}
Entity.DEBUG_OUT = false;
foreach(var entity in Entities.Where(e => e.Model != null)) {
_focusables.Add(new Focusable {
Name = entity.Name,
Position = () => entity.Model.Translation,
WalkmeshTri = () => entity.WalkmeshTri,
Source = entity,
Active = () => entity.Model.Visible && (entity != Player)
});
}
foreach(var gateway in TriggersAndGateways.Gateways) {
var middle = (gateway.V0.ToX() + gateway.V1.ToX()) * 0.5f;
var tri = FindWalkmeshForPosition(middle);
if (tri != null) {
_focusables.Add(new Focusable {
Name = "Exit " + TriggersAndGateways.Gateways.IndexOf(gateway),
Position = () => middle,
WalkmeshTri = () => tri.Value,
Source = gateway,
Active = () => true,
});
}
}
}
private int _nextModelIndex = 0;
@ -482,7 +499,7 @@ namespace Braver.Field {
entity.Model?.FrameStep();
}
}
_plugins.Call<IFieldLocation>(loc => loc.Step());
_plugins.Call<IFieldLocation>(loc => loc.Step(this));
_frame++;
}
@ -619,6 +636,19 @@ namespace Braver.Field {
(LineEvents.GoAway, 6),
};
private void SwitchFocus(int offset) {
_currentFocusState = null;
foreach (int i in Enumerable.Range(1, _focusables.Count - 1)) {
int newIndex = (_focusables.IndexOf(_currentFocus) + offset * i + _focusables.Count) % _focusables.Count;
var candidate = _focusables[newIndex];
if (candidate.Active()) {
_currentFocus = candidate;
return;
}
}
_currentFocus = null;
}
public override void ProcessInput(InputState input) {
base.ProcessInput(input);
if (!(Game.Net is Net.Server)) return;
@ -639,6 +669,11 @@ namespace Braver.Field {
ReportAllModelPositions();
}
if (input.IsJustDown(InputKey.PanLeft))
SwitchFocus(-1);
if (input.IsJustDown(InputKey.PanRight))
SwitchFocus(+1);
if (input.IsJustDown(InputKey.Debug5))
Game.PushScreen(new UI.Layout.LayoutScreen("FieldDebugger", parm: this));
@ -1171,6 +1206,7 @@ namespace Braver.Field {
case LeaveTriResult.Success:
case LeaveTriResult.SlideNewTri:
eMove.WalkmeshTri = newTri.Value;
_currentFocusState = null; //Could be a bit cleverer about when to invalidate this
currentTri = _walkmesh[newTri.Value];
ClampToTriangle(ref newDest, currentTri);
break; //Treat same as success, code below will move us accordingly
@ -1253,6 +1289,18 @@ namespace Braver.Field {
}
}
public int? FindWalkmeshForPosition(Vector3 position) {
foreach(int t in Enumerable.Range(0, _walkmesh.Count)) {
var height = HeightInTriangle(t, position.X, position.Y, false);
if (height != null) {
if (((height.Value - 5) <= position.Z) && (height.Value > (position.Z - 5))) //TODO - is this fudge factor needed, the right size, ...?
return t;
}
}
return null;
}
public void DropToWalkmesh(Entity e, Vector2 position, int walkmeshTri, bool exceptOnFailure = true) {
var tri = _walkmesh[walkmeshTri];
@ -1263,6 +1311,7 @@ namespace Braver.Field {
e.Model.Translation = new Vector3(position.X, position.Y, height.GetValueOrDefault());
e.WalkmeshTri = walkmeshTri;
_currentFocusState = null; //Could be a bit cleverer about when to invalidate this
ReportDebugEntityPos(e);
}
@ -1352,6 +1401,35 @@ namespace Braver.Field {
public void Received(Net.FieldEntityModelMessage message) {
Entities[message.EntityID].Model = FieldModels[message.ModelID];
}
private FocusState _currentFocusState;
Vector3 IField.PlayerPosition => Player?.Model?.Translation ?? Vector3.Zero;
FocusState IField.GetFocusState() {
if ((_currentFocus != null) && (_currentFocusState == null) && (Player != null) && (_currentFocus.Source != Player)) {
Dictionary<WalkmeshTriangle, int> distance = new();
distance[_walkmesh[Player.WalkmeshTri]] = 0;
Queue<WalkmeshTriangle> toConsider = new Queue<WalkmeshTriangle>();
toConsider.Enqueue(_walkmesh[Player.WalkmeshTri]);
while (toConsider.Any()) {
var tri = toConsider.Dequeue();
foreach(var adjacent in tri.AdjacentTris()) {
var adj = _walkmesh[adjacent];
if (!distance.ContainsKey(adj)) {
distance[adj] = distance[tri] + 1;
toConsider.Enqueue(adj);
}
}
}
_currentFocusState = new FocusState {
TargetName = _currentFocus.Name,
TargetPosition = _currentFocus.Position(),
WalkmeshDistance = distance[_walkmesh[_currentFocus.WalkmeshTri()]],
};
}
return _currentFocusState;
}
}
public class CachedField {

View File

@ -74,10 +74,13 @@ namespace Braver {
public abstract string Description { get; }
bool IScreen.HasFinishedLoading => _frames > 0;
protected SpriteBatch _fxBatch;
private Transition _transition;
private Action _transitionAction;
private int _frames = 0;
protected bool _readyToRender;
protected Plugins.PluginInstances _plugins;
@ -96,7 +99,9 @@ namespace Braver {
protected abstract void DoRender();
public virtual void Reactivated() { }
public virtual void Dispose() { }
public virtual void Dispose() {
_plugins?.Dispose();
}
public void FadeOut(Action then, int frames = 30) {
_transition = new FadeTransition(
@ -126,6 +131,7 @@ namespace Braver {
_transitionAction?.Invoke();
}
}
_frames++;
}
public void Render() {

View File

@ -24,11 +24,17 @@ namespace Braver.UI.Layout {
}
private void Update() {
lNoFieldScripts.Color = Game.GameOptions.NoFieldScripts ? Color.White : Color.Gray;
lNoRandomBattles.Color = Game.GameOptions.NoRandomBattles ? Color.White : Color.Gray;
lSkipBattleMenu.Color = Game.GameOptions.SkipBattleMenu ? Color.White : Color.Gray;
lAutoSaveOnFieldEntry.Color = Game.GameOptions.AutoSaveOnFieldEntry ? Color.White : Color.Gray;
lSeparateSaveFiles.Color = Game.GameOptions.SeparateSaveFiles ? Color.White : Color.Gray;
void DoLabel(Label L, bool option) {
L.Color = option ? Color.White : Color.Gray;
L.FocusDescription = L.Text + " " + (option ? "On" : "Off");
}
DoLabel(lNoFieldScripts, Game.GameOptions.NoFieldScripts);
DoLabel(lNoRandomBattles, Game.GameOptions.NoRandomBattles);
DoLabel(lSkipBattleMenu, Game.GameOptions.SkipBattleMenu);
DoLabel(lAutoSaveOnFieldEntry, Game.GameOptions.AutoSaveOnFieldEntry);
DoLabel(lSeparateSaveFiles, Game.GameOptions.SeparateSaveFiles);
}
public void LabelClick(Label L) {
@ -44,6 +50,7 @@ namespace Braver.UI.Layout {
Game.GameOptions.SeparateSaveFiles = !Game.GameOptions.SeparateSaveFiles;
Update();
ChangeFocus(Focus); //to re-announce new state
}
public override bool ProcessInput(InputState input) {

View File

@ -57,7 +57,7 @@ namespace Braver.UI.Layout {
public virtual Action OnFocussed { get; set; }
[XmlIgnore]
public virtual string Description => FocusDescription ?? "Unknown";
public virtual string Description => FocusDescription ?? null;
[XmlIgnore]
public Container Parent { get; internal set; }
@ -227,7 +227,7 @@ namespace Braver.UI.Layout {
public bool Enabled { get; set; } = true;
[XmlIgnore]
public override string Description => Text ?? base.Description;
public override string Description => base.Description ?? Text;
public override void Draw(LayoutModel model, UIBatch ui, int offsetX, int offsetY, Func<float> getZ) {
if (!string.IsNullOrWhiteSpace(Text))
@ -351,7 +351,7 @@ namespace Braver.UI.Layout {
Focus?.OnFocussed?.Invoke();
var group = FocusGroup.FocussableChildren().Select(child => child.Component).ToList();
Game.InvokeOnMainThread(
() => Game.UIPlugins.Call<UISystem>(ui => ui.Menu(group.Select(c => c.Description), group.IndexOf(f))),
() => Game.UIPlugins.Call<UISystem>(ui => ui.Menu(group.Select(c => c.Description), group.IndexOf(f), FocusGroup)),
1
); //delay so initial announcement happens after loading has finished
}

View File

@ -34,7 +34,7 @@ namespace Braver.UI {
}
private void Announce() {
Game.UIPlugins.Call<UISystem>(ui => ui.Menu(_items, _menu));
Game.UIPlugins.Call<UISystem>(ui => ui.Menu(_items, _menu, this));
}
public override void Init(FGame g, GraphicsDevice graphics) {

View File

@ -81,7 +81,7 @@ namespace BraverLauncher {
txtMovies.Text = settings.GetValueOrDefault("Movies");
txtSave.Text = settings.GetValueOrDefault("Save");
if (txtSave.Text == ".") txtSave.Text = "";
slMusicVolume.Value = double.Parse(settings.GetValueOrDefault("Options.MusicVolume") ?? "100");
slMusicVolume.Value = double.Parse(settings.GetValueOrDefault("Options.MusicVolume") ?? "1") * 100;
}
}

View File

@ -230,6 +230,12 @@ namespace Ficedula.FF7.Field {
yield return V1;
yield return V2;
}
public IEnumerable<short> AdjacentTris() {
if (V01Tri != null) yield return V01Tri.Value;
if (V12Tri != null) yield return V12Tri.Value;
if (V20Tri != null) yield return V20Tri.Value;
}
}
public class Walkmesh {

View File

@ -12,6 +12,9 @@
</ItemGroup>
<ItemGroup>
<None Update="focus.ogg">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</None>
<None Update="footsteps.ogg">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</None>

View File

@ -4,6 +4,7 @@
//
// SPDX-License-Identifier: EPL-2.0
using Braver.Field;
using Braver.Plugins;
using Braver.Plugins.Field;
using Braver.Plugins.UI;
@ -15,6 +16,7 @@ namespace Braver.Tolk {
public class TolkConfig {
public bool EnableSAPI { get; set; } = true;
public bool EnableFootsteps { get; set; } = true;
public bool EnableFocusTracking { get; set; } = true;
}
public class TolkPlugin : Plugin {
@ -29,14 +31,14 @@ namespace Braver.Tolk {
if (t == typeof(UISystem))
return new TolkInstance(_config);
else if (t == typeof(IFieldLocation))
return new FootstepPlugin(_game);
return new FootstepFocusPlugin(_game, _config.EnableFootsteps, _config.EnableFocusTracking);
else
throw new NotSupportedException();
}
public override IEnumerable<Type> GetPluginInstances() {
yield return typeof(UISystem);
if (_config.EnableFootsteps)
if (_config.EnableFootsteps || _config.EnableFocusTracking)
yield return typeof(IFieldLocation);
}
@ -67,25 +69,33 @@ namespace Braver.Tolk {
DavyKager.Tolk.Speak(dialog, false);
}
public void Menu(IEnumerable<string> items, int selected) {
private object _lastMenuContainer = null;
public void Menu(IEnumerable<string> items, int selected, object container) {
DavyKager.Tolk.Speak(
$"Menu {items.ElementAtOrDefault(selected)}, {selected + 1} of {items.Count()}",
false
_lastMenuContainer == container
);
_lastMenuContainer = container;
}
}
public class FootstepPlugin : IFieldLocation, IDisposable {
public class FootstepFocusPlugin : IFieldLocation, IDisposable {
private IAudioItem _footsteps;
private IAudioItem _footsteps, _focusSound;
private bool _playing = false;
private Queue<Vector3> _positions = new();
private Vector3 _lastPosition;
private string _lastFocusName;
public FootstepPlugin(BGame game) {
_footsteps = game.Audio.LoadStream(typeof(TolkPlugin).FullName, "footsteps.ogg");
_footsteps.Play(1f, 0f, true);
_footsteps.Pause();
public FootstepFocusPlugin(BGame game, bool footsteps, bool focus) {
if (footsteps) {
_footsteps = game.Audio.LoadStream(typeof(TolkPlugin).FullName, "footsteps.ogg");
_footsteps.Play(1f, 0f, true, 1f);
_footsteps.Pause();
}
if (focus) {
_focusSound = game.Audio.LoadStream(typeof(TolkPlugin).FullName, "focus.ogg");
}
}
public void EntityMoved(IFieldEntity entity, bool isRunning, Vector3 from, Vector3 to) {
@ -95,24 +105,47 @@ namespace Braver.Tolk {
}
}
public void Step() {
while (_positions.Count > 5)
_positions.Dequeue();
_positions.Enqueue(_lastPosition);
bool shouldPlay = (_positions.First() - _positions.Last()).Length() > 13f;
if (shouldPlay != _playing) {
System.Diagnostics.Debug.WriteLine($"Tolk change!");
if (shouldPlay)
_footsteps.Resume();
else
_footsteps.Pause();
_playing = shouldPlay;
private int _focusCountdown=180;
public void Step(IField field) {
if (_footsteps != null) {
while (_positions.Count > 5)
_positions.Dequeue();
_positions.Enqueue(_lastPosition);
bool shouldPlay = (_positions.First() - _positions.Last()).Length() > 13f;
if (shouldPlay != _playing) {
//System.Diagnostics.Debug.WriteLine($"Tolk change!");
if (shouldPlay)
_footsteps.Resume();
else
_footsteps.Pause();
_playing = shouldPlay;
}
//System.Diagnostics.Debug.WriteLine($"Tolk step! {string.Join(",", _positions)}");
}
if ((_focusSound != null) && (--_focusCountdown == 0)) {
var focusState = field.GetFocusState();
if (focusState != null) {
System.Diagnostics.Debug.WriteLine($"Focus at walkmesh distance {focusState.WalkmeshDistance}");
if (field.Options.HasFlag(FieldOptions.PlayerControls))
_focusSound.Play(1f, 0f, false, (float)Math.Pow(0.9, focusState.WalkmeshDistance));
if (_lastFocusName != focusState.TargetName) {
_lastFocusName = focusState.TargetName;
DavyKager.Tolk.Output("Focus " + _lastFocusName);
}
}
_focusCountdown = 180;
}
System.Diagnostics.Debug.WriteLine($"Tolk step! {string.Join(",", _positions)}");
}
public void Dispose() {
_footsteps.Dispose();
_footsteps?.Dispose();
}
public void FocusChanged() {
_focusCountdown = 1;
}
}
}

Binary file not shown.

View File

@ -2,3 +2,19 @@
Braver is an open source reimplementation of the original FF7 game engine.
More information is [available at the main website](https://braver.ficedula.co.uk/).
# Getting Started
After downloading the latest release, run BraverLauncher.exe; you can configure the necessary paths (e.g. where FF7 is installed) here and then click Launch Braver to run the game.
# Plugins
Braver comes with one plugin by default, which adds Tolk (text to speech) support to the game. Plugins are disabled by default so you will need to enable this plugin from within the configuration in BraverLauncher before it will activate.
Once activated, the name of a screen is announced (e.g. which field location has loaded) when the game changes to a new screen; in a menu, the current menu item (and how many options there are) is announced; and when dialog is triggered, the dialog and any choices are announced.
The Tolk plugin also has options to enable footstep sounds which wil play when the controllable character is moving, and a focus sound. When enabled, in the field screens you can cycle through focusable objects using the shoulder buttons (L1/R1 on the controller; the left/right square bracket keys on the keyboard). When a focusable object is selected, the name of the object is announced, and a tone plays every few seconds, higher pitched the closer you get to the object.
Note that because the game wasn't designed for this, the names of the objects aren't necessarily very friendly, and it's possible to focus on an object that isn't actually reachable. Hopefully support for this will improve in future versions!
The battle engine does not have any plugin support yet, so no Tolk output happens here yet. This will improve in a future version as well.

View File

@ -1,5 +1,5 @@
<?xml version="1.0"?>
<Layout xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:xsd="http://www.w3.org/2001/XMLSchema">
<Layout xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:xsd="http://www.w3.org/2001/XMLSchema" Description="Debug Options">
<Root xsi:type="Box" X="0" Y="0" W="1280" H="720" ID="Root">
<Component xsi:type="Label" X="200" Y="30">DEBUG OPTIONS</Component>