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; } }