static void Main(string[] args) { if (args.Length == 0) { Console.WriteLine("Needs path to spring"); return; } var us = new UnitSync(Utils.Glue(args)); BinaryFormatter bf = new BinaryFormatter(); using (var fs = new FileStream("mapinfo.dat", FileMode.Create)) bf.Serialize(fs, us.MapList); using (var fs = new FileStream("modinfo.dat", FileMode.Create)) bf.Serialize(fs, us.ModList); }
public UnitsyncResourcePresenceChecker(SpringPaths paths, string engine) { if (string.IsNullOrEmpty(engine) || !paths.HasEngineVersion(engine)) { Trace.TraceWarning("Engine {0} not found, trying backup", engine); engine = paths.GetEngineList().FirstOrDefault(); if (engine == null) { throw new Exception("No engine found for unitsync"); } } unitsync = new UnitSync(paths, engine); }
public void Dispose() { WatchingEnabled = false; isDisposed = true; if (unitSync != null) { unitSync.Dispose(); //(visual studio recommend a dispose) } unitSync = null; if (isCacheDirty) { SaveCache(); } GC.SuppressFinalize(this); }
/// <summary>UnInitUnitsync() check whether SpringScanner have any more mods/map to Unitsynced and call unitsync's UnInit() when there's no more work. /// </summary> public void UnInitUnitsync() { //upon completion of any work: dispose unitsync. It can be re-initialize again later by VerifyUnitSync() if (unitSync != null && GetWorkCost() < 1) { try { unitSync.Dispose(); unitSync = null; } catch (Exception ex) { Trace.TraceWarning("Error disposing unitsync: {0}", ex); } } }
public void UpdateMission(ZkDataContext db, Mission mission, SpringPaths paths, string engine) { var file = mission.Mutator.ToArray(); var targetPath = Path.Combine(paths.WritableDirectory, "games", mission.SanitizedFileName); File.WriteAllBytes(targetPath, file); Mod modInfo; using (var unitsync = new UnitSync(paths, engine)) { modInfo = unitsync.GetResourceFromFileName(targetPath) as Mod; } File.Delete(targetPath); UpdateMission(db, mission, modInfo); }
public void Scan() { using (var unitsync = new UnitSync(Paths, Engine)) { unitsync.ReInit(); var archiveCache = unitsync.GetArchiveCache(); using (var db = new ZkDataContext()) { var registered = db.Resources.Select(x => x.InternalName).ToDictionary(x => x, x => true); foreach (var archive in archiveCache.Archives) { if (!registered.ContainsKey(archive.Name) && !UnitSync.DependencyExceptions.Contains(archive.Name)) { Register(unitsync, archive); } } } } }
public List <ScanResult> Scan() { var results = new List <ScanResult>(); using (var unitsync = new UnitSync(Paths, Engine)) { unitsync.ReInit(); var archiveCache = unitsync.GetArchiveCache(); using (var db = new ZkDataContext()) { var registered = db.Resources.Select(x => x.InternalName).ToDictionary(x => x, x => true); foreach (var archive in archiveCache.Archives) { if (!UnitSync.DependencyExceptions.Contains(archive.Name)) { if (registered.ContainsKey(archive.Name)) { results.Add(new ScanResult() { ResourceInfo = archive, Status = ResourceFileStatus.AlreadyExists, }); } else { var fullInfo = Register(unitsync, archive); results.Add(new ScanResult() { ResourceInfo = fullInfo ?? archive, Status = fullInfo != null ? ResourceFileStatus.Registered : ResourceFileStatus.RegistrationError }); } } } unitsync.Reset(); } return(results); } }
private static ResourceInfo Register(UnitSync unitsync, ResourceInfo resource) { Trace.TraceInformation("UnitSyncer: registering {0}", resource.Name); ResourceInfo info = null; try { info = unitsync.GetResourceFromFileName(resource.ArchivePath); if (info != null) { var serializedData = MetaDataCache.SerializeAndCompressMetaData(info); var map = info as Map; var creator = new TorrentCreator(); creator.Path = resource.ArchivePath; var ms = new MemoryStream(); creator.Create(ms); byte[] minimap = null; byte[] metalMap = null; byte[] heightMap = null; if (map != null) { minimap = map.Minimap.ToBytes(ImageSize); metalMap = map.Metalmap.ToBytes(ImageSize); heightMap = map.Heightmap.ToBytes(ImageSize); } var hash = Hash.HashBytes(File.ReadAllBytes(resource.ArchivePath)); var length = new FileInfo(resource.ArchivePath).Length; Trace.TraceInformation("UnitSyncer: uploading {0} to server", info.Name); ReturnValue e; try { var service = GlobalConst.GetContentService(); e = service.RegisterResource(PlasmaServiceVersion, null, hash.ToString(), (int)length, info.ResourceType, resource.ArchiveName, info.Name, serializedData, info.Dependencies, minimap, metalMap, heightMap, ms.ToArray()); } catch (Exception ex) { Trace.TraceError("UnitSyncer: Error uploading data to server: {0}", ex); return(null); } if (e != ReturnValue.Ok) { Trace.TraceWarning("UnitSyncer: Resource registering failed: {0}", e); } } } catch (Exception ex) { Trace.TraceError("Error registering resource {0} : {1}", resource.ArchivePath, ex); return(null); } return(info); }
public static void Main(string[] args) { try { //Stopwatch stopWatch = new Stopwatch(); stopWatch.Start(); Trace.Listeners.Add(new ConsoleTraceListener()); if (Environment.OSVersion.Platform != PlatformID.Unix) { var ver = GetNetVersionFromRegistry(); if (ver < 378675) { MessageBox.Show(new Form { TopMost = true }, "Zero-K launcher needs Microsoft .NET framework 4.5.1\nPlease download and install it first", "Program is unable to run", MessageBoxButtons.OK, MessageBoxIcon.Error); } } Directory.SetCurrentDirectory(StartupPath); // extract fonts EmbeddedResourceExtractor.ExtractFile("ZeroKLobby.NativeLibs.SM.ttf", "SM.ttf"); EmbeddedResourceExtractor.ExtractFile("ZeroKLobby.NativeLibs.OpenSans-Regular.ttf", "OpenSans-Regular.ttf"); Conf = new Config(); IsSteamFolder = File.Exists(Path.Combine(StartupPath, "steamfolder.txt")); SelfUpdater = new SelfUpdater("Zero-K"); StartupArgs = args; try { Application.EnableVisualStyles(); Application.SetCompatibleTextRenderingDefault(false); } catch (Exception ex) { Trace.TraceWarning("Failed to set rendering compatibility: {0}", ex); } if (!Debugger.IsAttached) { try { AppDomain.CurrentDomain.UnhandledException += CurrentDomain_UnhandledException; Thread.GetDomain().UnhandledException += UnhandledException; Application.ThreadException += Application_ThreadException; Application.SetUnhandledExceptionMode(UnhandledExceptionMode.CatchException); } catch (Exception ex) { Trace.TraceWarning("Failed to set exception handling :{0}", ex); } } //HttpWebRequest.DefaultCachePolicy = new RequestCachePolicy(RequestCacheLevel.NoCacheNoStore); Trace.TraceInformation("Starting with version {0}", SelfUpdater.CurrentVersion); WebRequest.DefaultWebProxy = null; ThreadPool.SetMaxThreads(500, 2000); ServicePointManager.Expect100Continue = false; LoadConfig(); Trace.Listeners.Add(new LogTraceListener()); if (Environment.OSVersion.Platform != PlatformID.Unix && !Conf.UseExternalBrowser) { Utils.SetIeCompatibility(); //set to current IE version } var contentDir = !string.IsNullOrEmpty(Conf.DataFolder) ? Conf.DataFolder : StartupPath; if (!Directory.Exists(contentDir) || !SpringPaths.IsDirectoryWritable(contentDir)) { var dc = new SelectWritableFolder { SelectedPath = SpringPaths.GetMySpringDocPath() }; if (dc.ShowDialog() != DialogResult.OK) { return; } contentDir = dc.SelectedPath; } if (Conf.DataFolder != StartupPath) { Conf.DataFolder = contentDir; } else { Conf.DataFolder = null; } if (!SpringPaths.IsDirectoryWritable(StartupPath)) { var newTarget = Path.Combine(contentDir, "Zero-K.exe"); if (SelfUpdater.CheckForUpdate(newTarget, true)) { Conf.Save(Path.Combine(contentDir, Config.ConfigFileName)); Process.Start(newTarget); return; } MessageBox.Show(new Form { TopMost = true }, "Move failed, please copy Zero-K.exe to a writable folder"); return; } SpringPaths = new SpringPaths(contentDir, true, true); if ( MessageBox.Show(new Form() { TopMost = true }, "WARNING: Zero-K lobby is now obsolete. Starting Chobby instead, ok? ", "WARNING: launcher obsolete", MessageBoxButtons.YesNo, MessageBoxIcon.Question) == DialogResult.Yes) { var targetPath = Path.Combine(SpringPaths.WritableDirectory, "Chobby.exe"); if (!File.Exists(targetPath)) { var wc = new WebClient(); wc.DownloadFile(GlobalConst.BaseSiteUrl + "/lobby/Chobby.exe", targetPath); } Process.Start(targetPath); Environment.Exit(0); } // speed up spring start SpringPaths.SpringVersionChanged += (sender, engine) => { ZkData.Utils.StartAsync( () => { UnitSync unitSync = null; try { unitSync = new UnitSync(SpringPaths, engine); // initialize unitsync to avoid slowdowns when starting if (unitSync.UnitsyncWritableFolder != SpringPaths.WritableDirectory) { // unitsync created its cache in different folder than is used to start spring -> move it var fi = ArchiveCache.GetCacheFile(unitSync.UnitsyncWritableFolder); if (fi != null) { File.Copy(fi.FullName, Path.Combine(SpringPaths.WritableDirectory, "cache", fi.Name), true); } } } finally { unitSync?.Dispose(); } }); }; SaveConfig(); // write license files try { var path = SpringPaths.WritableDirectory; var pathGPL = Utils.MakePath(path, "license_GPLv3"); var gpl = Encoding.UTF8.GetString(License.GPLv3); if (!File.Exists(pathGPL)) { File.WriteAllText(pathGPL, gpl); } var pathMIT = Utils.MakePath(path, "license_MIT"); var mit = Encoding.UTF8.GetString(License.MITlicense); if (!File.Exists(pathMIT)) { File.WriteAllText(pathMIT, mit); } } catch (Exception ex) { Trace.TraceError(ex.ToString()); } if (Conf.IsFirstRun) { if (!IsSteamFolder) { Utils.CreateDesktopShortcut(); } if (Environment.OSVersion.Platform != PlatformID.Unix) { Utils.RegisterProtocol(); } } MetaData = new MetaDataCache(SpringPaths); AutoJoinManager = new AutoJoinManager(); EngineConfigurator = new EngineConfigurator(SpringPaths.WritableDirectory); SpringScanner = new PlasmaResourceChecker(SpringPaths); SpringScanner.LocalResourceAdded += (s, e) => Trace.TraceInformation("New resource found: {0}", e.Item.InternalName); SpringScanner.LocalResourceRemoved += (s, e) => Trace.TraceInformation("Resource removed: {0}", e.Item.InternalName); Downloader = new PlasmaDownloader.PlasmaDownloader(SpringScanner, SpringPaths); //rapid Downloader.DownloadAdded += (s, e) => Trace.TraceInformation("Download started: {0}", e.Data.Name); //Downloader.GetResource(DownloadType.ENGINE, GlobalConst.DefaultEngineOverride); var isLinux = Environment.OSVersion.Platform == PlatformID.Unix; TasClient = new TasClient(string.Format("ZK {0}{1}", SelfUpdater.CurrentVersion, isLinux ? " linux" : "")); SayCommandHandler = new SayCommandHandler(TasClient); ServerImages = new ServerImagesHandler(SpringPaths, TasClient); // log, for debugging TasClient.Connected += (s, e) => Trace.TraceInformation("TASC connected"); TasClient.LoginAccepted += (s, e) => { Trace.TraceInformation("TASC login accepted"); Trace.TraceInformation("Server is using Spring version {0}", TasClient.ServerSpringVersion); if (Environment.OSVersion.Platform == PlatformID.Unix || Conf.UseExternalBrowser) { if (MainWindow != null) { MainWindow.navigationControl.Path = "battles"; } } }; TasClient.LoginDenied += (s, e) => Trace.TraceInformation("TASC login denied"); TasClient.ChannelJoined += (s, e) => { Trace.TraceInformation("TASC channel joined: " + e.Name); }; TasClient.ConnectionLost += (s, e) => Trace.TraceInformation("Connection lost"); TasClient.WelcomeReceived += (s, e) => { Downloader.GetResource(DownloadType.ENGINE, e.Engine); Downloader.GetResource(DownloadType.RAPID, e.Game); }; Program.AreYouReadyDialog = new AreYouReadyDialog(TasClient); // special handling TasClient.PreviewSaid += (s, e) => { var tas = (TasClient)s; User user = null; if (e.Data.UserName != null) { tas.ExistingUsers.TryGetValue(e.Data.UserName, out user); if ((user != null && user.BanMute) || TasClient.Ignores.Contains(e.Data.UserName)) { e.Cancel = true; } } }; TasClient.SiteToLobbyCommandReceived += (eventArgs, o) => { if (MainWindow != null) { MainWindow.navigationControl.Path = o.Command; MainWindow.PopupSelf(); } }; ModStore = new ModStore(); ConnectBar = new ConnectBar(TasClient); ToolTip = new ToolTipHandler(); BrowserInterop = new BrowserInterop(TasClient, Conf); BattleIconManager = new BattleIconManager(); Application.AddMessageFilter(ToolTip); SteamHandler = new ZklSteamHandler(TasClient); MainWindow = new MainWindow(); Application.AddMessageFilter(new ScrollMessageFilter()); MainWindow.Size = new Size( Math.Min(SystemInformation.VirtualScreen.Width - 30, MainWindow.Width), Math.Min(SystemInformation.VirtualScreen.Height - 30, MainWindow.Height)); //in case user have less space than 1024x768 BattleBar = new BattleBar(); VoteBar = new VoteBar(); PwBar = new PwBar(); MatchMakerBar = new MatchMakerBar(TasClient); SelfUpdater.ProgramUpdated += s => { Program.MainWindow.InvokeFunc( () => WarningBar.DisplayWarning($"New version of Zero-K launcher downloaded, restart it to apply changes", "Restart", Restart)); }; if (!Debugger.IsAttached && !Conf.DisableAutoUpdate && !IsSteamFolder) { SelfUpdater.StartChecking(); } if (GlobalConst.Mode != ModeType.Local) { SteamHandler.Connect(); } Application.Run(MainWindow); ShutDown(); } catch (Exception ex) { ErrorHandling.HandleException(ex, true); if (Debugger.IsAttached) { Debugger.Break(); } } finally { ShutDown(); } if (ErrorHandling.HasFatalException && !CloseOnNext) { if (Debugger.IsAttached) { Debugger.Break(); } Application.Restart(); } }
void PerformUnitSyncOperation(WorkItem workItem) { Trace.TraceInformation("PerformUnitSyncOperation"); VerifyUnitSync(); if (unitSync == null) { Trace.TraceError("Skipping file after unitsync loading errors: {0}", workItem.CacheItem.ShortPath); CacheMarkFailedUnitSync(workItem.CacheItem.ShortPath); return; } var info = GetUnitSyncData(workItem.CacheItem.FileName); //upon completion of any work: dispose unitsync. It can be re-initialize again later by VerifyUnitSync() if (unitSync != null && GetWorkCost() < 1) { try { unitSync.Dispose(); unitSync = null; } catch (Exception ex) { Trace.TraceWarning("Error disposing unitsync: {0}", ex); } } if (info != null) { workItem.CacheItem.InternalName = info.Name; workItem.CacheItem.ResourceType = info is Map ? ResourceType.Map : ResourceType.Mod; var hashes = new List <SpringHashEntry>(); if (workItem.CacheItem.SpringHash != null) { hashes.AddRange(workItem.CacheItem.SpringHash.Where(x => x.SpringVersion != springPaths.SpringVersion)); } hashes.Add(new SpringHashEntry() { SpringHash = info.Checksum, SpringVersion = springPaths.SpringVersion }); workItem.CacheItem.SpringHash = hashes.ToArray(); CacheItemAdd(workItem.CacheItem); var serializedData = MetaDataCache.SerializeAndCompressMetaData(info); var map = info as Map; object userState = null; try { var creator = new TorrentCreator(); creator.Path = GetFullPath(workItem); var ms = new MemoryStream(); creator.Create(ms); byte[] minimap = null; byte[] metalMap = null; byte[] heightMap = null; if (map != null) { minimap = map.Minimap.ToBytes(ImageSize); metalMap = map.Metalmap.ToBytes(ImageSize); heightMap = map.Heightmap.ToBytes(ImageSize); userState = new MapRegisteredEventArgs(info.Name, map, minimap, metalMap, heightMap, serializedData); } var mod = info as Mod; if (mod != null) { userState = new KeyValuePair <Mod, byte[]>(mod, serializedData); } Trace.TraceInformation("uploading {0} to server", info.Name); service.RegisterResourceAsync(PlasmaServiceVersion, springPaths.SpringVersion, workItem.CacheItem.Md5.ToString(), workItem.CacheItem.Length, info is Map ? ResourceType.Map : ResourceType.Mod, workItem.CacheItem.FileName, info.Name, info.Checksum, serializedData, mod != null ? mod.Dependencies : null, minimap, metalMap, heightMap, ms.ToArray(), userState); Interlocked.Increment(ref itemsSending); } catch (Exception e) { Trace.TraceError("Error registering new resource {0}: {1}", workItem.CacheItem.ShortPath, e); } } else { Trace.TraceError("Could not unitsync file {0}", workItem.CacheItem.ShortPath); CacheMarkFailedUnitSync(workItem.CacheItem.ShortPath); } return; }
void MainThreadFunction() { try { InitialScan(); } catch (Exception ex) { Trace.TraceError("Error in scanner initial scan: {0}", ex); } try { var isWorking = false; var workDone = 0; while (!isDisposed) { try { Thread.Sleep(ScannerCycleTime); if (isCacheDirty && DateTime.Now.Subtract(lastCacheSave).TotalSeconds > DirtyCacheSave) { lastCacheSave = DateTime.Now; isCacheDirty = false; SaveCache(); } WorkItem workItem; while ((workItem = GetNextWorkItem()) != null) { if (isDisposed) { return; } if (!isWorking) { isWorking = true; workDone = 0; workTotal = GetWorkCost(); WorkStarted(this, new ProgressEventArgs(workDone, workTotal, workItem.CacheItem.FileName)); } else { workDone++; workTotal = Math.Max(GetWorkCost(), workTotal); WorkProgressChanged(this, new ProgressEventArgs(workDone, workTotal, string.Format("{0} {1}", workItem.Operation, workItem.CacheItem.FileName))); } if (workItem.Operation == WorkItem.OperationType.Hash) { PerformHashOperation(workItem); } if (workItem.Operation == WorkItem.OperationType.UnitSync) { if (springPaths.UnitSyncDirectory != null) { PerformUnitSyncOperation(workItem); // if there is no unitsync, retry later } else { AddWork(workItem.CacheItem, WorkItem.OperationType.UnitSync, DateTime.Now.AddSeconds(RescheduleServerQuery), false); } } if (workItem.Operation == WorkItem.OperationType.ReAskServer) { GetResourceData(workItem); } } if (isWorking) { isWorking = false; WorkStopped(this, EventArgs.Empty); } } catch (Exception ex) { Trace.TraceError("Exception in scanning thread: {0}", ex); } } } finally { if (unitSync != null) { unitSync.Dispose(); } unitSync = null; } }
//[STAThread] private static void Main(params string[] args) { var startupPath = Path.GetDirectoryName(Path.GetFullPath(Application.ExecutablePath)); var springPaths = new SpringPaths(startupPath); Spring runningSpring = null; TcpTransport connection = null; // speed up spring start springPaths.SpringVersionChanged += (sender, engine) => { Utils.StartAsync( () => { UnitSync unitSync = null; try { unitSync = new UnitSync(springPaths, engine); // initialize unitsync to avoid slowdowns when starting if (unitSync.UnitsyncWritableFolder != springPaths.WritableDirectory) { // unitsync created its cache in different folder than is used to start spring -> move it var fi = ArchiveCache.GetCacheFile(unitSync.UnitsyncWritableFolder); if (fi != null) { File.Copy(fi.FullName, Path.Combine(springPaths.WritableDirectory, "cache", fi.Name), true); } } } finally { unitSync?.Dispose(); } }); }; Config config = null; try { config = JsonConvert.DeserializeObject <Config>(File.ReadAllText(startupPath + "/config.json")); } catch (Exception) { } CefWrapper.Initialize(startupPath + "/render", args); var springScanner = new SpringScanner(springPaths); springScanner.Start(); EventHandler <ProgressEventArgs> workHandler = (s, e) => { CefWrapper.ExecuteJavascript("on_spring_scanner_work(" + JsonConvert.SerializeObject(e) + ");"); }; springScanner.WorkStarted += workHandler; springScanner.WorkProgressChanged += workHandler; springScanner.WorkStopped += (s, e) => { CefWrapper.ExecuteJavascript("on_spring_scanner_work(null);"); }; springScanner.LocalResourceAdded += (s, e) => { CefWrapper.ExecuteJavascript("on_spring_scanner_add(" + JsonConvert.SerializeObject(e.Item) + ")"); }; springScanner.LocalResourceRemoved += (s, e) => { CefWrapper.ExecuteJavascript("on_spring_scanner_remove(" + JsonConvert.SerializeObject(e.Item) + ")"); }; var downloader = new PlasmaDownloader.PlasmaDownloader(springScanner, springPaths); //rapid downloader.GetResource(DownloadType.ENGINE, GlobalConst.DefaultEngineOverride); // ZKL's downloader doesn't send events to monitor download progress, so we have to poll it. Timer pollDownloads = new Timer(); pollDownloads.Interval = 250; pollDownloads.Tick += (s, e) => { CefWrapper.ExecuteJavascript("on_downloads_change(" + JsonConvert.SerializeObject(downloader.Downloads) + ")"); }; // Through some WinAPI dark magic it manages to use the message pump in the window that is run by CEF. // Can this be dangerous? pollDownloads.Start(); CefWrapper.RegisterApiFunction( "getEngines", () => { return(new List <string> { "100.0" }); // TODO: stub }); CefWrapper.RegisterApiFunction("getMods", () => { return(springScanner.GetAllModResource()); }); CefWrapper.RegisterApiFunction("getMaps", () => { return(springScanner.GetAllMapResource()); }); CefWrapper.RegisterApiFunction( "downloadEngine", (string engine) => { // Don't let GetAndSwitchEngine() touch the main SpringPaths. var path = new SpringPaths(springPaths.WritableDirectory); downloader.GetResource(DownloadType.ENGINE, engine); }); CefWrapper.RegisterApiFunction("downloadMod", (string game) => { downloader.GetResource(DownloadType.MOD, game); }); CefWrapper.RegisterApiFunction("downloadMap", (string map) => { downloader.GetResource(DownloadType.MAP, map); }); CefWrapper.RegisterApiFunction( "abortDownload", (string name) => { downloader.Downloads.FirstOrDefault(d => d.Name == name)?.Abort(); }); CefWrapper.RegisterApiFunction( "startSpringScript", (string engineVer, string script) => { if (runningSpring != null) { return(null); } // Ultimately we should get rid of the concept of a "current set engine", but for now let's work around it. var path = new SpringPaths(springPaths.WritableDirectory); runningSpring = new Spring(path); runningSpring.SpringExited += (obj, evt) => { CefWrapper.ExecuteJavascript("on_spring_exit(" + (evt.IsCrash ? "true" : "false") + ");"); runningSpring = null; }; try { runningSpring.RunLocalScriptGame(script, engineVer); return(null); } catch (Exception e) { runningSpring = null; return(e.Message); } }); CefWrapper.RegisterApiFunction( "connect", (string host, int port) => { if (connection != null) { connection.RequestClose(); } connection = new TcpTransport(host, port); connection.ConnectAndRun( async(s) => CefWrapper.ExecuteJavascript( $"on_lobby_message({CefWrapper.mangleUtf8(JsonConvert.SerializeObject(s))})"), async() => { }, async(requested) => CefWrapper.ExecuteJavascript( $"on_connection_closed({CefWrapper.mangleUtf8(JsonConvert.SerializeObject(requested))})") ); }); CefWrapper.RegisterApiFunction("disconnect", () => connection?.RequestClose()); CefWrapper.RegisterApiFunction("sendLobbyMessage", (string msg) => connection?.SendLine(CefWrapper.unmangleUtf8(msg) + '\n')); CefWrapper.RegisterApiFunction( "readConfig", () => { try { return(JsonConvert.DeserializeObject(File.ReadAllText(startupPath + "/config.json"))); } catch (FileNotFoundException) { return(null); } }); CefWrapper.RegisterApiFunction("saveConfig", (object conf) => File.WriteAllText(startupPath + "/config.json", JsonConvert.SerializeObject(conf, Formatting.Indented))); CefWrapper.RegisterApiFunction("setFullscreen", (bool fullscreen) => CefWrapper.SetFullscreen(fullscreen)); var fileUrl = new Uri(startupPath + "/zkwl/index.html"); CefWrapper.StartMessageLoop(fileUrl.AbsoluteUri, "black", !config?.lobbyWindowed ?? true); CefWrapper.Deinitialize(); downloader.Dispose(); springScanner.Dispose(); }