using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Linq;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Controls.Primitives;
using System.Windows.Media;
namespace codelanguage;
///
/// Craft++ Skin System — mirrors BYOND's .dmsk / winset / winget skin architecture.
///
/// In BYOND, the Dream Seeker client window is composed of "skin controls":
/// - map: the 2D tile canvas
/// - output: text output boxes
/// - input: the command input bar
/// - info: stat panels
/// - browser: browser panels
/// - button: clickable buttons / verb shortcuts
///
/// This class manages a runtime dictionary of skin control properties that can
/// be get/set by code through winset()/winget()/winexists()/winshow(),
/// exactly as BYOND's DM proc reference specifies.
///
public class DmSkinSystem
{
/// Fired when a browser control has initialized its BYOND JavaScript
/// bridge. Hosts can use this to dispatch the 516+ byond-ready event.
public event Action? BrowserReady;
/// Raised after winset/winshow/winclone mutate skin state, with the
/// affected control id ("." for window-level changes). The Seeker subscribes
/// to apply changes its bound WPF elements did not receive directly (window
/// chrome, reflow geometry, map sizing).
public event Action? ControlPropertyChanged;
private void RaiseControlChanged(string controlId)
=> ControlPropertyChanged?.Invoke(controlId);
// ── Skin control types (mirrors BYOND's types) ──────────────────────────
public enum SkinControlType
{
Window,
Map,
Output,
Input,
Info,
Browser,
Label,
Button,
Child,
Bar,
Grid,
Tab,
Canvas,
}
// ── Skin control descriptor ─────────────────────────────────────────────
public class SkinControl
{
public string Id { get; set; } = "";
public SkinControlType ControlType { get; set; } = SkinControlType.Output;
public Dictionary Properties { get; } = new(StringComparer.OrdinalIgnoreCase);
// Visual backing (WPF element, resolved at render time)
public FrameworkElement? Element { get; set; }
public string Get(string key, string defaultVal = "")
=> Properties.TryGetValue(key, out var v) ? v : defaultVal;
public void Set(string key, string value)
=> Properties[key] = value;
}
// ── Singleton instance ───────────────────────────────────────────────────
private static DmSkinSystem? _instance;
public static DmSkinSystem Current => _instance ??= new DmSkinSystem();
public string? DefaultOutputControlId =>
_controls.Values.FirstOrDefault(c => c.ControlType == SkinControlType.Output && string.Equals(c.Get("is-default"), "true", StringComparison.OrdinalIgnoreCase))?.Id;
public string? DefaultMapControlId =>
_controls.Values.FirstOrDefault(c => c.ControlType == SkinControlType.Map && string.Equals(c.Get("is-default"), "true", StringComparison.OrdinalIgnoreCase))?.Id;
public string? DefaultInfoControlId =>
_controls.Values.FirstOrDefault(c => c.ControlType == SkinControlType.Info && string.Equals(c.Get("is-default"), "true", StringComparison.OrdinalIgnoreCase))?.Id;
// ── Control store ────────────────────────────────────────────────────────
private readonly ConcurrentDictionary _controls = new(StringComparer.OrdinalIgnoreCase);
private readonly ConcurrentDictionary _visibility = new(StringComparer.OrdinalIgnoreCase);
// ── Default Craft++ skin layout (mirrors BYOND's default.dmsk) ───────────
public DmSkinSystem()
{
RegisterDefaultSkin();
}
private void RegisterDefaultSkin()
{
// Window: "main" — the root window container
AddControl("main", SkinControlType.Window, new Dictionary
{
["title"] = "Craft++ World",
["size"] = "800x600",
["min-size"] = "400x300",
["can-resize"] = "true",
["icon"] = "",
["menu"] = "",
["background-color"] = "#000000",
["is-default"] = "true",
["titlebar"] = "true",
["macro"] = "default",
});
// Map control: the tile canvas
AddControl("map", SkinControlType.Map, new Dictionary
{
["pos"] = "0,0",
["size"] = "480x480",
["anchor1"] = "0,0",
["anchor2"] = "80,100",
["is-visible"] = "true",
["zoom"] = "0", // 0 = auto-fit
["scrollbar-x"] = "false",
["scrollbar-y"] = "false",
["icon-size"] = "32",
["tile-movement"] = "true",
["letterbox"] = "true",
["zoom-mode"] = "normal",
["left-click-only"] = "false",
["allow-default-scroll"] = "true",
});
// Output control: the main chat/output box
AddControl("output", SkinControlType.Output, new Dictionary
{
["pos"] = "480,0",
["size"] = "320x432",
["anchor1"] = "80,0",
["anchor2"] = "100,72",
["is-visible"] = "true",
["font-family"] = "Verdana",
["font-size"] = "13",
["background-color"] = "#000000",
["color"] = "#FFFFFF",
["link-color"] = "#00AAFF",
["highlight-color"] = "#FFFF00",
["flashback"] = "false",
["select-color"] = "#1A1A80",
["enable-http-links"] = "true",
});
// Input control: command/chat input bar
AddControl("input", SkinControlType.Input, new Dictionary
{
["pos"] = "480,432",
["size"] = "320x24",
["anchor1"] = "80,72",
["anchor2"] = "100,100",
["is-visible"] = "true",
["font-family"] = "Verdana",
["font-size"] = "13",
["background-color"] = "#1A1A1A",
["color"] = "#FFFFFF",
["max-history"] = "100",
["command"] = "",
});
// Info panel: stat display area
AddControl("info", SkinControlType.Info, new Dictionary
{
["pos"] = "480,456",
["size"] = "320x144",
["anchor1"] = "80,76",
["anchor2"] = "100,100",
["is-visible"] = "true",
["background-color"] = "#1A1A1A",
["font-family"] = "Verdana",
["font-size"] = "13",
["tab-text-color"] = "#FFFFFF",
["tab-background-color"] = "#2A2A2A",
});
// Browser panel: HTML output (browse() / output() with HTML)
AddControl("browser", SkinControlType.Browser, new Dictionary
{
["pos"] = "0,480",
["size"] = "800x120",
["anchor1"] = "0,80",
["anchor2"] = "100,100",
["is-visible"] = "false",
["on-close"] = "",
["can-close"] = "false",
["browser-options"] = "",
["inner-background-color"] = "",
["mouse-pos"] = "0,0",
["inner-mouse-pos"] = "0,0",
["outer-mouse-pos"] = "0,0",
});
}
// ── Public API ────────────────────────────────────────────────────────────
public void NotifyBrowserReady(string controlId = "browser")
=> BrowserReady?.Invoke(controlId);
///
/// winset(Player, ControlId, Params) — sets one or more control properties.
/// Params is a URL-encoded string: "pos=100,200;size=400x300;color=#FF0000"
///
public void Winset(string? controlId, string paramString)
{
var props = ParseParams(paramString);
if (string.IsNullOrEmpty(controlId) || controlId == ".")
{
// Apply to all controls (window-level)
foreach (var ctrl in _controls.Values)
ApplyProperties(ctrl, props);
RaiseControlChanged(".");
}
else
{
var ctrl = GetOrCreate(controlId);
ApplyProperties(ctrl, props);
RaiseControlChanged(controlId);
}
}
/// Registers a control discovered from an interface document (type +
/// initial properties) without applying to elements or raising
/// : the host has just built these
/// elements from the same document, so pushes would be redundant.
public void RegisterControl(string id, string type, IReadOnlyDictionary properties)
{
if (string.IsNullOrWhiteSpace(id)) return;
var ctrl = GetOrCreate(id);
if (Enum.TryParse(type, true, out var parsedType))
ctrl.ControlType = parsedType;
foreach (var pair in properties)
ctrl.Properties[pair.Key] = pair.Value;
}
/// Detaches all element bindings. Called when a host rebuilds its
/// visual tree so stale FrameworkElement references are never written to.
public void ClearBindings()
{
foreach (var ctrl in _controls.Values)
ctrl.Element = null;
}
///
/// winget(Player, ControlId, Params) — gets one or more control properties.
/// Returns the value(s) as a URL-encoded string.
///
public string Winget(string? controlId, string paramString)
{
if (string.IsNullOrEmpty(controlId)) return "";
if (!_controls.TryGetValue(controlId, out var ctrl)) return "";
var keys = paramString.Split(new[] { ';', ',' }, StringSplitOptions.RemoveEmptyEntries)
.Select(k => k.Trim())
.ToList();
if (keys.Count == 1)
return ctrl.Get(keys[0]);
var parts = keys.Select(k => $"{k}={ctrl.Get(k)}");
return string.Join(";", parts);
}
///
/// winexists(Player, ControlId) — returns whether the control exists.
///
public bool Winexists(string? controlId)
=> !string.IsNullOrEmpty(controlId) && _controls.ContainsKey(controlId);
///
/// winshow(Player, Window, Show) — shows or hides a control/window.
///
public void Winshow(string? controlId, bool show)
{
if (string.IsNullOrEmpty(controlId)) return;
_visibility[controlId] = show;
if (_controls.TryGetValue(controlId, out var ctrl))
{
ctrl.Set("is-visible", show ? "true" : "false");
ApplyVisibility(ctrl, show);
}
RaiseControlChanged(controlId);
}
/// Returns a control by ID, or null.
public SkinControl? GetControl(string controlId)
=> _controls.TryGetValue(controlId, out var c) ? c : null;
/// Creates a new control by copying another control's skin state.
public bool Winclone(string sourceId, string destinationId)
{
if (string.IsNullOrWhiteSpace(sourceId) || string.IsNullOrWhiteSpace(destinationId)
|| !_controls.TryGetValue(sourceId, out var source))
return false;
var clone = new SkinControl { Id = destinationId, ControlType = source.ControlType };
foreach (var property in source.Properties)
clone.Properties[property.Key] = property.Value;
_controls[destinationId] = clone;
if (_visibility.TryGetValue(sourceId, out var visible))
_visibility[destinationId] = visible;
RaiseControlChanged(destinationId);
return true;
}
/// Returns all registered controls.
public IEnumerable GetAllControls() => _controls.Values;
/// Registers a WPF element as the visual backing for a control.
public void BindElement(string controlId, FrameworkElement element)
{
var ctrl = GetOrCreate(controlId);
ctrl.Element = element;
}
/// Resets to the default skin.
public void Reset()
{
_controls.Clear();
_visibility.Clear();
RegisterDefaultSkin();
}
// ── Internal helpers ──────────────────────────────────────────────────────
private void AddControl(string id, SkinControlType type, Dictionary defaults)
{
var ctrl = new SkinControl { Id = id, ControlType = type };
foreach (var kvp in defaults) ctrl.Properties[kvp.Key] = kvp.Value;
_controls[id] = ctrl;
}
private SkinControl GetOrCreate(string id)
{
if (!_controls.TryGetValue(id, out var ctrl))
{
ctrl = new SkinControl { Id = id, ControlType = SkinControlType.Output };
_controls[id] = ctrl;
}
return ctrl;
}
private static void ApplyProperties(SkinControl ctrl, Dictionary props)
{
foreach (var kvp in props)
ctrl.Set(kvp.Key, kvp.Value);
// Apply to visual element if bound
if (ctrl.Element == null) return;
try
{
Application.Current?.Dispatcher?.BeginInvoke(() =>
{
if (props.TryGetValue("is-visible", out var vis))
ctrl.Element.Visibility = vis.Equals("true", StringComparison.OrdinalIgnoreCase)
? Visibility.Visible : Visibility.Collapsed;
if (props.TryGetValue("background-color", out var bg))
{
var color = ParseColor(bg);
if (color.HasValue)
{
if (ctrl.Element is Panel panel)
panel.Background = new SolidColorBrush(color.Value);
else if (ctrl.Element is Control controlElement)
controlElement.Background = new SolidColorBrush(color.Value);
else if (ctrl.Element is Window windowElement)
windowElement.Background = new SolidColorBrush(color.Value);
}
}
if (props.TryGetValue("size", out var size))
{
var parts = size.Split('x');
if (parts.Length == 2
&& double.TryParse(parts[0], out double w)
&& double.TryParse(parts[1], out double h))
{
ctrl.Element.Width = w;
ctrl.Element.Height = h;
}
}
if (props.TryGetValue("pos", out var pos))
{
var parts = pos.Split(',');
if (parts.Length == 2
&& double.TryParse(parts[0], out double x)
&& double.TryParse(parts[1], out double y))
{
if (ctrl.Element is Window positionedWindow)
{
// Windows are positioned through Left/Top; canvas
// attached properties do not move them.
positionedWindow.Left = x;
positionedWindow.Top = y;
}
else
{
Canvas.SetLeft(ctrl.Element, x);
Canvas.SetTop(ctrl.Element, y);
}
}
}
// Resolve the logical control: many skin elements are wrapped in
// a Border, and text can live on a Content, TextBox, or TextBlock.
Control? control = ctrl.Element as Control ?? (ctrl.Element as Border)?.Child as Control;
TextBlock? textBlock = ctrl.Element as TextBlock
?? (ctrl.Element as Border)?.Child as TextBlock
?? (control as ContentControl)?.Content as TextBlock;
if (props.TryGetValue("title", out var title) && ctrl.Element is Window titled)
titled.Title = title;
if (props.TryGetValue("text", out var text))
{
if (ctrl.Element is TextBox directTextBox) directTextBox.Text = text;
else if (control is TextBox textBox) textBox.Text = text;
else if (control is ButtonBase buttonContent) buttonContent.Content = text;
else if (control is Label label) label.Content = text;
else if (textBlock != null) textBlock.Text = text;
else if ((ctrl.Element as Border)?.Child is ContentControl borderContent) borderContent.Content = text;
}
if (props.TryGetValue("value", out var value))
{
if (ctrl.Element is TextBox valueBox) valueBox.Text = value;
else if (control is TextBox valueControlBox) valueControlBox.Text = value;
}
if (props.TryGetValue("color", out var fg) && ParseColor(fg) is Color foreground)
{
if (control != null) control.Foreground = new SolidColorBrush(foreground);
else if (textBlock != null) textBlock.Foreground = new SolidColorBrush(foreground);
}
if (props.TryGetValue("font-family", out var family) && !string.IsNullOrWhiteSpace(family))
{
try
{
var parsedFamily = new FontFamily(family);
if (control != null) control.FontFamily = parsedFamily;
else if (textBlock != null) textBlock.FontFamily = parsedFamily;
}
catch { }
}
if (props.TryGetValue("font-size", out var sizeText)
&& double.TryParse(sizeText, System.Globalization.NumberStyles.Float,
System.Globalization.CultureInfo.InvariantCulture, out double parsedFontSize)
&& double.IsFinite(parsedFontSize) && parsedFontSize is >= 1 and <= 512)
{
if (control != null) control.FontSize = parsedFontSize;
else if (textBlock != null) textBlock.FontSize = parsedFontSize;
}
if (props.TryGetValue("is-disabled", out var disabled))
ctrl.Element.IsEnabled = !disabled.Equals("true", StringComparison.OrdinalIgnoreCase);
if (props.TryGetValue("is-checked", out var checkedText))
{
if (ctrl.Element is ToggleButton directToggle) directToggle.IsChecked = ParseTruthy(checkedText);
else if (control is ToggleButton toggle) toggle.IsChecked = ParseTruthy(checkedText);
}
});
}
catch { }
}
private static bool ParseTruthy(string? text)
=> text != null && (text.Equals("true", StringComparison.OrdinalIgnoreCase) || text == "1");
private static void ApplyVisibility(SkinControl ctrl, bool show)
{
if (ctrl.Element == null) return;
try
{
Application.Current?.Dispatcher?.BeginInvoke(() =>
ctrl.Element.Visibility = show ? Visibility.Visible : Visibility.Collapsed);
}
catch { }
}
private static Dictionary ParseParams(string paramString)
{
// Support both URL-encoded and semicolon-delimited formats:
// "pos=100,200;size=400x300" (DM winset format)
// "pos=100,200&size=400x300" (URL query format)
var result = new Dictionary(StringComparer.OrdinalIgnoreCase);
foreach (var pair in paramString.Split(new[] { ';', '&' }, StringSplitOptions.RemoveEmptyEntries))
{
var idx = pair.IndexOf('=');
if (idx > 0)
{
string key = pair[..idx].Trim();
string val = pair[(idx + 1)..].Trim();
result[key] = Uri.UnescapeDataString(val);
}
}
return result;
}
private static Color? ParseColor(string hex)
{
if (hex.StartsWith("#"))
{
try { return (Color)ColorConverter.ConvertFromString(hex); }
catch { }
}
return null;
}
}