using System; using System.IO; namespace codelanguage; public enum IdeFileKind { Code, Icon, Map, Interface, Environment, Other } /// /// The environment flow the IDE uses: a new file has a kind, that kind opens /// one editor, and a checked file is the only one the environment compiles. /// public static class IdeFileFlow { public static IdeFileKind Classify(string path) { string ext = Path.GetExtension(path); if (ext.Equals(".cc", StringComparison.OrdinalIgnoreCase) || ext.Equals(".cp", StringComparison.OrdinalIgnoreCase) || ext.Equals(".craft", StringComparison.OrdinalIgnoreCase) || ext.Equals(".dm", StringComparison.OrdinalIgnoreCase)) return IdeFileKind.Code; if (ext.Equals(".cpi", StringComparison.OrdinalIgnoreCase) || ext.Equals(".cicon", StringComparison.OrdinalIgnoreCase)) return IdeFileKind.Icon; if (ext.Equals(".cpm", StringComparison.OrdinalIgnoreCase) || ext.Equals(".cmap", StringComparison.OrdinalIgnoreCase) || ext.Equals(".dmm", StringComparison.OrdinalIgnoreCase)) return IdeFileKind.Map; if (ext.Equals(".cpf", StringComparison.OrdinalIgnoreCase) || ext.Equals(".cskin", StringComparison.OrdinalIgnoreCase) || ext.Equals(".dmf", StringComparison.OrdinalIgnoreCase) || ext.Equals(".dmsk", StringComparison.OrdinalIgnoreCase)) return IdeFileKind.Interface; if (ext.Equals(".cpe", StringComparison.OrdinalIgnoreCase)) return IdeFileKind.Environment; return IdeFileKind.Other; } public static string UniquePath(string directory, string stem, string extension) { string path = Path.Combine(directory, stem + extension); int count = 1; while (File.Exists(path)) path = Path.Combine(directory, $"{stem}{count++}{extension}"); return path; } public static string Create(string directory, IdeFileKind kind, bool directionalIcon) { Directory.CreateDirectory(directory); switch (kind) { case IdeFileKind.Code: { string path = UniquePath(directory, "code", ".cc"); File.WriteAllText(path, """ // Craft++ code file. Indentation defines the block. turf floor icon = 'turf.cpi' icon_state = "floor" wall icon = 'turf.cpi' icon_state = "wall" density = 1 mob icon = 'mob.cpi' icon_state = "player" Login() world << "Hello, world." """); return path; } case IdeFileKind.Icon: { string path = UniquePath(directory, "icon", ".cpi"); var icon = new CraftIconFile(path) { IconWidth = 32, IconHeight = 32 }; var state = icon.AddState("icon", 32, 32); if (directionalIcon) state.DirCount = 4; if (!icon.Save()) throw new IOException(icon.LastError ?? "Could not create the icon file."); return path; } case IdeFileKind.Map: { string path = UniquePath(directory, "map", ".cpm"); var map = new CraftMapFile(path) { MaxX = 10, MaxY = 10, MaxZ = 1 }; for (int x = 1; x <= 10; x++) for (int y = 1; y <= 10; y++) map.Tiles[(x, y, 1)] = "/turf/floor"; if (!map.Save()) throw new IOException("Could not create the map file."); return path; } case IdeFileKind.Interface: { string path = UniquePath(directory, "interface", ".cpf"); File.WriteAllText(path, """ // Craft++ interface. The Seeker builds this skin at run time. main = Window(title="Craft++";size=800x600;is-default=true;titlebar=true;macro=default) map = Map(pos=0,0;size=480x480;anchor1=0,0;anchor2=80,100;is-visible=true;icon-size=32) output = Output(pos=480,0;size=320x432;anchor1=80,0;anchor2=100,72;is-visible=true) input = Input(pos=480,432;size=320x24;anchor1=80,72;anchor2=100,76;is-visible=true) info = Info(pos=480,456;size=320x144;anchor1=80,76;anchor2=100,100;is-visible=true) browser = Browser(pos=0,480;size=800x120;anchor1=0,80;anchor2=100,100;is-visible=false) """); return path; } default: throw new ArgumentOutOfRangeException(nameof(kind)); } } public static bool IsIncluded(string environmentPath, string filePath) { if (!File.Exists(environmentPath)) return false; string relative = Relative(environmentPath, filePath); foreach (string line in File.ReadLines(environmentPath)) { if (IncludeMatches(line, relative)) return true; } return false; } public static void SetIncluded(string environmentPath, string filePath, bool included) { string relative = Relative(environmentPath, filePath); var lines = File.ReadAllLines(environmentPath).ToList(); int index = lines.FindIndex(line => IncludeMatches(line, relative)); if (included && index < 0) lines.Add($"#include \"{relative}\""); else if (!included && index >= 0) lines.RemoveAt(index); else return; File.WriteAllLines(environmentPath, lines); } public static string? FirstOfKind(string directory, IdeFileKind kind) { if (!Directory.Exists(directory)) return null; return Directory.EnumerateFiles(directory) .FirstOrDefault(path => Classify(path) == kind); } private static string Relative(string environmentPath, string filePath) { string? directory = Path.GetDirectoryName(environmentPath); if (string.IsNullOrEmpty(directory)) return Path.GetFileName(filePath); return Path.GetRelativePath(directory, filePath).Replace('\\', '/'); } private static bool IncludeMatches(string line, string relative) { string trimmed = line.Trim(); if (!trimmed.StartsWith("#include", StringComparison.Ordinal)) return false; string rest = trimmed["#include".Length..].Trim().Trim('"', '\'', '<', '>'); return string.Equals(rest.Replace('\\', '/'), relative, StringComparison.OrdinalIgnoreCase); } } public sealed class CraftTextFile : CraftFile { public CraftTextFile(string path) : base(path) { } public override bool Load() => File.Exists(Path); public override bool Save() => true; }