Both writing to perf.log frequently as well as GetTimestamp aren't free and hurt performance particularly on slower systems (which can have notably higher output to perf.log, further amplifying the problem). Therefore we make simulation perf logging opt-in. Additionally, logging of the current tick and tick type (local/net) is removed from debug.log, and some remnant debug logging for kills and pips is removed to keep performance-sensitive logging limited to perf.log.
1002 lines
31 KiB
C#
1002 lines
31 KiB
C#
#region Copyright & License Information
|
|
/*
|
|
* Copyright 2007-2020 The OpenRA Developers (see AUTHORS)
|
|
* This file is part of OpenRA, which is free software. It is made
|
|
* available to you under the terms of the GNU General Public License
|
|
* as published by the Free Software Foundation, either version 3 of
|
|
* the License, or (at your option) any later version. For more
|
|
* information, see COPYING.
|
|
*/
|
|
#endregion
|
|
|
|
using System;
|
|
using System.Collections.Generic;
|
|
using System.Diagnostics;
|
|
using System.Globalization;
|
|
using System.IO;
|
|
using System.Linq;
|
|
using System.Net;
|
|
using System.Reflection;
|
|
using System.Runtime;
|
|
using System.Threading;
|
|
using System.Threading.Tasks;
|
|
using OpenRA.Graphics;
|
|
using OpenRA.Network;
|
|
using OpenRA.Primitives;
|
|
using OpenRA.Server;
|
|
using OpenRA.Support;
|
|
using OpenRA.Widgets;
|
|
|
|
namespace OpenRA
|
|
{
|
|
public static class Game
|
|
{
|
|
public const int NetTickScale = 3; // 120 ms net tick for 40 ms local tick
|
|
public const int TimestepJankThreshold = 250; // Don't catch up for delays larger than 250ms
|
|
|
|
public static InstalledMods Mods { get; private set; }
|
|
public static ExternalMods ExternalMods { get; private set; }
|
|
|
|
public static ModData ModData;
|
|
public static Settings Settings;
|
|
public static CursorManager Cursor;
|
|
public static bool HideCursor;
|
|
static WorldRenderer worldRenderer;
|
|
static string modLaunchWrapper;
|
|
|
|
internal static OrderManager OrderManager;
|
|
static Server.Server server;
|
|
|
|
public static MersenneTwister CosmeticRandom = new MersenneTwister(); // not synced
|
|
|
|
public static Renderer Renderer;
|
|
public static Sound Sound;
|
|
|
|
public static string EngineVersion { get; private set; }
|
|
public static LocalPlayerProfile LocalPlayerProfile;
|
|
|
|
static bool takeScreenshot = false;
|
|
static Benchmark benchmark = null;
|
|
|
|
public static event Action OnShellmapLoaded = () => { };
|
|
|
|
public static OrderManager JoinServer(ConnectionTarget endpoint, string password, bool recordReplay = true)
|
|
{
|
|
var connection = new NetworkConnection(endpoint);
|
|
if (recordReplay)
|
|
connection.StartRecording(() => { return TimestampedFilename(); });
|
|
|
|
var om = new OrderManager(endpoint, password, connection);
|
|
JoinInner(om);
|
|
return om;
|
|
}
|
|
|
|
public static string TimestampedFilename(bool includemilliseconds = false, string extra = "")
|
|
{
|
|
var format = includemilliseconds ? "yyyy-MM-ddTHHmmssfffZ" : "yyyy-MM-ddTHHmmssZ";
|
|
return ModData.Manifest.Id + extra + "-" + DateTime.UtcNow.ToString(format, CultureInfo.InvariantCulture);
|
|
}
|
|
|
|
static void JoinInner(OrderManager om)
|
|
{
|
|
OrderManager?.Dispose();
|
|
OrderManager = om;
|
|
lastConnectionState = ConnectionState.PreConnecting;
|
|
ConnectionStateChanged(OrderManager);
|
|
}
|
|
|
|
public static void JoinReplay(string replayFile)
|
|
{
|
|
JoinInner(new OrderManager(new ConnectionTarget(), "", new ReplayConnection(replayFile)));
|
|
}
|
|
|
|
static void JoinLocal()
|
|
{
|
|
JoinInner(new OrderManager(new ConnectionTarget(), "", new EchoConnection()));
|
|
}
|
|
|
|
// More accurate replacement for Environment.TickCount
|
|
static Stopwatch stopwatch = Stopwatch.StartNew();
|
|
public static long RunTime => stopwatch.ElapsedMilliseconds;
|
|
|
|
public static int RenderFrame = 0;
|
|
public static int NetFrameNumber => OrderManager.NetFrameNumber;
|
|
public static int LocalTick => OrderManager.LocalFrameNumber;
|
|
|
|
public static event Action<ConnectionTarget> OnRemoteDirectConnect = _ => { };
|
|
public static event Action<OrderManager> ConnectionStateChanged = _ => { };
|
|
static ConnectionState lastConnectionState = ConnectionState.PreConnecting;
|
|
public static int LocalClientId => OrderManager.Connection.LocalClientId;
|
|
|
|
public static void RemoteDirectConnect(ConnectionTarget endpoint)
|
|
{
|
|
OnRemoteDirectConnect(endpoint);
|
|
}
|
|
|
|
// Hacky workaround for orderManager visibility
|
|
public static Widget OpenWindow(World world, string widget)
|
|
{
|
|
return Ui.OpenWindow(widget, new WidgetArgs() { { "world", world }, { "orderManager", OrderManager }, { "worldRenderer", worldRenderer } });
|
|
}
|
|
|
|
// Who came up with the great idea of making these things
|
|
// impossible for the things that want them to access them directly?
|
|
public static Widget OpenWindow(string widget, WidgetArgs args)
|
|
{
|
|
return Ui.OpenWindow(widget, new WidgetArgs(args)
|
|
{
|
|
{ "world", worldRenderer.World },
|
|
{ "orderManager", OrderManager },
|
|
{ "worldRenderer", worldRenderer },
|
|
});
|
|
}
|
|
|
|
// Load a widget with world, orderManager, worldRenderer args, without adding it to the widget tree
|
|
public static Widget LoadWidget(World world, string id, Widget parent, WidgetArgs args)
|
|
{
|
|
return ModData.WidgetLoader.LoadWidget(new WidgetArgs(args)
|
|
{
|
|
{ "world", world },
|
|
{ "orderManager", OrderManager },
|
|
{ "worldRenderer", worldRenderer },
|
|
}, parent, id);
|
|
}
|
|
|
|
public static event Action LobbyInfoChanged = () => { };
|
|
|
|
internal static void SyncLobbyInfo()
|
|
{
|
|
LobbyInfoChanged();
|
|
}
|
|
|
|
public static event Action BeforeGameStart = () => { };
|
|
internal static void StartGame(string mapUID, WorldType type)
|
|
{
|
|
// Dispose of the old world before creating a new one.
|
|
worldRenderer?.Dispose();
|
|
|
|
Cursor.SetCursor(null);
|
|
BeforeGameStart();
|
|
|
|
Map map;
|
|
|
|
using (new PerfTimer("PrepareMap"))
|
|
map = ModData.PrepareMap(mapUID);
|
|
|
|
using (new PerfTimer("NewWorld"))
|
|
{
|
|
OrderManager.World = new World(ModData, map, OrderManager, type);
|
|
OrderManager.FramesAhead = OrderManager.World.OrderLatency;
|
|
}
|
|
|
|
OrderManager.World.GameOver += FinishBenchmark;
|
|
|
|
worldRenderer = new WorldRenderer(ModData, OrderManager.World);
|
|
|
|
// Proactively collect memory during loading to reduce peak memory.
|
|
GC.Collect();
|
|
|
|
using (new PerfTimer("LoadComplete"))
|
|
OrderManager.World.LoadComplete(worldRenderer);
|
|
|
|
// Proactively collect memory during loading to reduce peak memory.
|
|
GC.Collect();
|
|
|
|
if (OrderManager.GameStarted)
|
|
return;
|
|
|
|
Ui.MouseFocusWidget = null;
|
|
Ui.KeyboardFocusWidget = null;
|
|
|
|
OrderManager.LocalFrameNumber = 0;
|
|
OrderManager.LastTickTime = RunTime;
|
|
OrderManager.StartGame();
|
|
worldRenderer.RefreshPalette();
|
|
Cursor.SetCursor(ChromeMetrics.Get<string>("DefaultCursor"));
|
|
|
|
// Now loading is completed, now is the ideal time to run a GC and compact the LOH.
|
|
// - All the temporary garbage created during loading can be collected.
|
|
// - Live objects are likely to live for the length of the game or longer,
|
|
// thus promoting them into a higher generation is not an issue.
|
|
// - We can remove any fragmentation in the LOH caused by temporary loading garbage.
|
|
// - A loading screen is visible, so a delay won't matter to the user.
|
|
// Much better to clean up now then to drop frames during gameplay for GC pauses.
|
|
GCSettings.LargeObjectHeapCompactionMode = GCLargeObjectHeapCompactionMode.CompactOnce;
|
|
GC.Collect();
|
|
}
|
|
|
|
public static void RestartGame()
|
|
{
|
|
var replay = OrderManager.Connection as ReplayConnection;
|
|
var replayName = replay?.Filename;
|
|
var lobbyInfo = OrderManager.LobbyInfo;
|
|
|
|
// Reseed the RNG so this isn't an exact repeat of the last game
|
|
lobbyInfo.GlobalSettings.RandomSeed = CosmeticRandom.Next();
|
|
|
|
var orders = new[]
|
|
{
|
|
Order.Command("sync_lobby {0}".F(lobbyInfo.Serialize())),
|
|
Order.Command("startgame")
|
|
};
|
|
|
|
// Disconnect from the current game
|
|
Disconnect();
|
|
Ui.ResetAll();
|
|
|
|
// Restart the game with the same replay/mission
|
|
if (replay != null)
|
|
JoinReplay(replayName);
|
|
else
|
|
CreateAndStartLocalServer(lobbyInfo.GlobalSettings.Map, orders);
|
|
}
|
|
|
|
public static void CreateAndStartLocalServer(string mapUID, IEnumerable<Order> setupOrders)
|
|
{
|
|
OrderManager om = null;
|
|
|
|
Action lobbyReady = null;
|
|
lobbyReady = () =>
|
|
{
|
|
LobbyInfoChanged -= lobbyReady;
|
|
foreach (var o in setupOrders)
|
|
om.IssueOrder(o);
|
|
};
|
|
|
|
LobbyInfoChanged += lobbyReady;
|
|
|
|
om = JoinServer(CreateLocalServer(mapUID), "");
|
|
}
|
|
|
|
public static bool IsHost
|
|
{
|
|
get
|
|
{
|
|
var id = OrderManager.Connection.LocalClientId;
|
|
var client = OrderManager.LobbyInfo.ClientWithIndex(id);
|
|
return client != null && client.IsAdmin;
|
|
}
|
|
}
|
|
|
|
static Modifiers modifiers;
|
|
public static Modifiers GetModifierKeys() { return modifiers; }
|
|
internal static void HandleModifierKeys(Modifiers mods) { modifiers = mods; }
|
|
|
|
public static void InitializeSettings(Arguments args)
|
|
{
|
|
Settings = new Settings(Path.Combine(Platform.SupportDir, "settings.yaml"), args);
|
|
}
|
|
|
|
public static RunStatus InitializeAndRun(string[] args)
|
|
{
|
|
Initialize(new Arguments(args));
|
|
|
|
// Proactively collect memory during loading to reduce peak memory.
|
|
GC.Collect();
|
|
return Run();
|
|
}
|
|
|
|
static void Initialize(Arguments args)
|
|
{
|
|
var engineDirArg = args.GetValue("Engine.EngineDir", null);
|
|
if (!string.IsNullOrEmpty(engineDirArg))
|
|
Platform.OverrideEngineDir(engineDirArg);
|
|
|
|
var supportDirArg = args.GetValue("Engine.SupportDir", null);
|
|
if (!string.IsNullOrEmpty(supportDirArg))
|
|
Platform.OverrideSupportDir(supportDirArg);
|
|
|
|
Console.WriteLine("Platform is {0}", Platform.CurrentPlatform);
|
|
|
|
// Load the engine version as early as possible so it can be written to exception logs
|
|
try
|
|
{
|
|
EngineVersion = File.ReadAllText(Path.Combine(Platform.EngineDir, "VERSION")).Trim();
|
|
}
|
|
catch { }
|
|
|
|
if (string.IsNullOrEmpty(EngineVersion))
|
|
EngineVersion = "Unknown";
|
|
|
|
Console.WriteLine("Engine version is {0}", EngineVersion);
|
|
Console.WriteLine("Runtime: {0}", Platform.RuntimeVersion);
|
|
|
|
// Special case handling of Game.Mod argument: if it matches a real filesystem path
|
|
// then we use this to override the mod search path, and replace it with the mod id
|
|
var modID = args.GetValue("Game.Mod", null);
|
|
var explicitModPaths = new string[0];
|
|
if (modID != null && (File.Exists(modID) || Directory.Exists(modID)))
|
|
{
|
|
explicitModPaths = new[] { modID };
|
|
modID = Path.GetFileNameWithoutExtension(modID);
|
|
}
|
|
|
|
InitializeSettings(args);
|
|
|
|
Log.AddChannel("perf", "perf.log");
|
|
Log.AddChannel("debug", "debug.log");
|
|
Log.AddChannel("server", "server.log", true);
|
|
Log.AddChannel("sound", "sound.log");
|
|
Log.AddChannel("graphics", "graphics.log");
|
|
Log.AddChannel("geoip", "geoip.log");
|
|
Log.AddChannel("nat", "nat.log");
|
|
Log.AddChannel("client", "client.log");
|
|
|
|
var platforms = new[] { Settings.Game.Platform, "Default", null };
|
|
foreach (var p in platforms)
|
|
{
|
|
if (p == null)
|
|
throw new InvalidOperationException("Failed to initialize platform-integration library. Check graphics.log for details.");
|
|
|
|
Settings.Game.Platform = p;
|
|
try
|
|
{
|
|
var rendererPath = Path.Combine(Platform.BinDir, "OpenRA.Platforms." + p + ".dll");
|
|
|
|
#if !MONO
|
|
var loader = new AssemblyLoader(rendererPath);
|
|
var platformType = loader.LoadDefaultAssembly().GetTypes().SingleOrDefault(t => typeof(IPlatform).IsAssignableFrom(t));
|
|
|
|
#else
|
|
var assembly = Assembly.LoadFile(rendererPath);
|
|
var platformType = assembly.GetTypes().SingleOrDefault(t => typeof(IPlatform).IsAssignableFrom(t));
|
|
#endif
|
|
|
|
if (platformType == null)
|
|
throw new InvalidOperationException("Platform dll must include exactly one IPlatform implementation.");
|
|
|
|
var platform = (IPlatform)platformType.GetConstructor(Type.EmptyTypes).Invoke(null);
|
|
Renderer = new Renderer(platform, Settings.Graphics);
|
|
Sound = new Sound(platform, Settings.Sound);
|
|
|
|
break;
|
|
}
|
|
catch (Exception e)
|
|
{
|
|
Log.Write("graphics", "{0}", e);
|
|
Console.WriteLine("Renderer initialization failed. Check graphics.log for details.");
|
|
|
|
Renderer?.Dispose();
|
|
|
|
Sound?.Dispose();
|
|
}
|
|
}
|
|
|
|
Nat.Initialize();
|
|
|
|
var modSearchArg = args.GetValue("Engine.ModSearchPaths", null);
|
|
var modSearchPaths = modSearchArg != null ?
|
|
FieldLoader.GetValue<string[]>("Engine.ModsPath", modSearchArg) :
|
|
new[] { Path.Combine(Platform.EngineDir, "mods") };
|
|
|
|
Mods = new InstalledMods(modSearchPaths, explicitModPaths);
|
|
Console.WriteLine("Internal mods:");
|
|
foreach (var mod in Mods)
|
|
Console.WriteLine("\t{0}: {1} ({2})", mod.Key, mod.Value.Metadata.Title, mod.Value.Metadata.Version);
|
|
|
|
modLaunchWrapper = args.GetValue("Engine.LaunchWrapper", null);
|
|
|
|
ExternalMods = new ExternalMods();
|
|
|
|
if (modID != null && Mods.TryGetValue(modID, out _))
|
|
{
|
|
var launchPath = args.GetValue("Engine.LaunchPath", null);
|
|
var launchArgs = new List<string>();
|
|
|
|
// Sanitize input from platform-specific launchers
|
|
// Process.Start requires paths to not be quoted, even if they contain spaces
|
|
if (launchPath != null && launchPath.First() == '"' && launchPath.Last() == '"')
|
|
launchPath = launchPath.Substring(1, launchPath.Length - 2);
|
|
|
|
if (launchPath == null)
|
|
{
|
|
// When launching the assembly directly we must propagate the Engine.EngineDir argument if defined
|
|
// Platform-specific launchers are expected to manage this internally.
|
|
launchPath = Assembly.GetEntryAssembly().Location;
|
|
if (!string.IsNullOrEmpty(engineDirArg))
|
|
launchArgs.Add("Engine.EngineDir=\"" + engineDirArg + "\"");
|
|
}
|
|
|
|
ExternalMods.Register(Mods[modID], launchPath, launchArgs, ModRegistration.User);
|
|
|
|
if (ExternalMods.TryGetValue(ExternalMod.MakeKey(Mods[modID]), out var activeMod))
|
|
ExternalMods.ClearInvalidRegistrations(activeMod, ModRegistration.User);
|
|
}
|
|
|
|
Console.WriteLine("External mods:");
|
|
foreach (var mod in ExternalMods)
|
|
Console.WriteLine("\t{0}: {1} ({2})", mod.Key, mod.Value.Title, mod.Value.Version);
|
|
|
|
InitializeMod(modID, args);
|
|
}
|
|
|
|
public static void InitializeMod(string mod, Arguments args)
|
|
{
|
|
// Clear static state if we have switched mods
|
|
LobbyInfoChanged = () => { };
|
|
ConnectionStateChanged = om => { };
|
|
BeforeGameStart = () => { };
|
|
OnRemoteDirectConnect = endpoint => { };
|
|
delayedActions = new ActionQueue();
|
|
|
|
Ui.ResetAll();
|
|
|
|
worldRenderer?.Dispose();
|
|
worldRenderer = null;
|
|
server?.Shutdown();
|
|
OrderManager?.Dispose();
|
|
|
|
if (ModData != null)
|
|
{
|
|
ModData.ModFiles.UnmountAll();
|
|
ModData.Dispose();
|
|
}
|
|
|
|
ModData = null;
|
|
|
|
if (mod == null)
|
|
throw new InvalidOperationException("Game.Mod argument missing.");
|
|
|
|
if (!Mods.ContainsKey(mod))
|
|
throw new InvalidOperationException("Unknown or invalid mod '{0}'.".F(mod));
|
|
|
|
Console.WriteLine("Loading mod: {0}", mod);
|
|
|
|
Sound.StopVideo();
|
|
|
|
ModData = new ModData(Mods[mod], Mods, true);
|
|
|
|
LocalPlayerProfile = new LocalPlayerProfile(Path.Combine(Platform.SupportDir, Settings.Game.AuthProfile), ModData.Manifest.Get<PlayerDatabase>());
|
|
|
|
if (!ModData.LoadScreen.BeforeLoad())
|
|
return;
|
|
|
|
using (new PerfTimer("LoadMaps"))
|
|
ModData.MapCache.LoadMaps();
|
|
|
|
ModData.InitializeLoaders(ModData.DefaultFileSystem);
|
|
Renderer.InitializeFonts(ModData);
|
|
|
|
var grid = ModData.Manifest.Contains<MapGrid>() ? ModData.Manifest.Get<MapGrid>() : null;
|
|
Renderer.InitializeDepthBuffer(grid);
|
|
|
|
Cursor?.Dispose();
|
|
|
|
Cursor = new CursorManager(ModData.CursorProvider);
|
|
|
|
PerfHistory.Items["render"].HasNormalTick = false;
|
|
PerfHistory.Items["batches"].HasNormalTick = false;
|
|
PerfHistory.Items["render_world"].HasNormalTick = false;
|
|
PerfHistory.Items["render_widgets"].HasNormalTick = false;
|
|
PerfHistory.Items["render_flip"].HasNormalTick = false;
|
|
PerfHistory.Items["terrain_lighting"].HasNormalTick = false;
|
|
|
|
JoinLocal();
|
|
|
|
ChromeMetrics.TryGet("ChatMessageColor", out chatMessageColor);
|
|
ChromeMetrics.TryGet("SystemMessageColor", out systemMessageColor);
|
|
if (!ChromeMetrics.TryGet("SystemMessageLabel", out systemMessageLabel))
|
|
systemMessageLabel = "Battlefield Control";
|
|
|
|
ModData.LoadScreen.StartGame(args);
|
|
}
|
|
|
|
public static void LoadEditor(string mapUid)
|
|
{
|
|
StartGame(mapUid, WorldType.Editor);
|
|
}
|
|
|
|
public static void LoadShellMap()
|
|
{
|
|
var shellmap = ChooseShellmap();
|
|
|
|
using (new PerfTimer("StartGame"))
|
|
{
|
|
StartGame(shellmap, WorldType.Shellmap);
|
|
OnShellmapLoaded();
|
|
}
|
|
}
|
|
|
|
static string ChooseShellmap()
|
|
{
|
|
var shellmaps = ModData.MapCache
|
|
.Where(m => m.Status == MapStatus.Available && m.Visibility.HasFlag(MapVisibility.Shellmap))
|
|
.Select(m => m.Uid);
|
|
|
|
if (!shellmaps.Any())
|
|
throw new InvalidDataException("No valid shellmaps available");
|
|
|
|
return shellmaps.Random(CosmeticRandom);
|
|
}
|
|
|
|
public static void SwitchToExternalMod(ExternalMod mod, string[] launchArguments = null, Action onFailed = null)
|
|
{
|
|
try
|
|
{
|
|
var path = mod.LaunchPath;
|
|
var args = launchArguments != null ? mod.LaunchArgs.Append(launchArguments) : mod.LaunchArgs;
|
|
if (modLaunchWrapper != null)
|
|
{
|
|
path = modLaunchWrapper;
|
|
args = new[] { mod.LaunchPath }.Concat(args);
|
|
}
|
|
|
|
var p = Process.Start(path, args.Select(a => "\"" + a + "\"").JoinWith(" "));
|
|
if (p == null || p.HasExited)
|
|
onFailed();
|
|
else
|
|
{
|
|
p.Close();
|
|
Exit();
|
|
}
|
|
}
|
|
catch (Exception e)
|
|
{
|
|
Log.Write("debug", "Failed to switch to external mod.");
|
|
Log.Write("debug", "Error was: " + e.Message);
|
|
onFailed();
|
|
}
|
|
}
|
|
|
|
static RunStatus state = RunStatus.Running;
|
|
public static event Action OnQuit = () => { };
|
|
|
|
// Note: These delayed actions should only be used by widgets or disposing objects
|
|
// - things that depend on a particular world should be queuing them on the world actor.
|
|
static volatile ActionQueue delayedActions = new ActionQueue();
|
|
static Color systemMessageColor = Color.White;
|
|
static Color chatMessageColor = Color.White;
|
|
static string systemMessageLabel;
|
|
|
|
public static void RunAfterTick(Action a) { delayedActions.Add(a, RunTime); }
|
|
public static void RunAfterDelay(int delayMilliseconds, Action a) { delayedActions.Add(a, RunTime + delayMilliseconds); }
|
|
|
|
static void TakeScreenshotInner()
|
|
{
|
|
using (new PerfTimer("Renderer.SaveScreenshot"))
|
|
{
|
|
var mod = ModData.Manifest.Metadata;
|
|
var directory = Path.Combine(Platform.SupportDir, "Screenshots", ModData.Manifest.Id, mod.Version);
|
|
Directory.CreateDirectory(directory);
|
|
|
|
var filename = TimestampedFilename(true);
|
|
var path = Path.Combine(directory, string.Concat(filename, ".png"));
|
|
Log.Write("debug", "Taking screenshot " + path);
|
|
|
|
Renderer.SaveScreenshot(path);
|
|
Debug("Saved screenshot " + filename);
|
|
}
|
|
}
|
|
|
|
static void InnerLogicTick(OrderManager orderManager)
|
|
{
|
|
var tick = RunTime;
|
|
|
|
var world = orderManager.World;
|
|
|
|
var uiTickDelta = tick - Ui.LastTickTime;
|
|
if (uiTickDelta >= Ui.Timestep)
|
|
{
|
|
// Explained below for the world tick calculation
|
|
var integralTickTimestep = (uiTickDelta / Ui.Timestep) * Ui.Timestep;
|
|
Ui.LastTickTime += integralTickTimestep >= TimestepJankThreshold ? integralTickTimestep : Ui.Timestep;
|
|
|
|
Sync.RunUnsynced(Settings.Debug.SyncCheckUnsyncedCode, world, Ui.Tick);
|
|
Cursor.Tick();
|
|
}
|
|
|
|
var worldTimestep = world == null ? Ui.Timestep :
|
|
world.IsLoadingGameSave ? 1 :
|
|
world.IsReplay ? world.ReplayTimestep :
|
|
world.Timestep;
|
|
|
|
var worldTickDelta = tick - orderManager.LastTickTime;
|
|
if (worldTimestep != 0 && worldTickDelta >= worldTimestep)
|
|
{
|
|
using (new PerfSample("tick_time"))
|
|
{
|
|
// Tick the world to advance the world time to match real time:
|
|
// If dt < TickJankThreshold then we should try and catch up by repeatedly ticking
|
|
// If dt >= TickJankThreshold then we should accept the jank and progress at the normal rate
|
|
// dt is rounded down to an integer tick count in order to preserve fractional tick components.
|
|
var integralTickTimestep = (worldTickDelta / worldTimestep) * worldTimestep;
|
|
orderManager.LastTickTime += integralTickTimestep >= TimestepJankThreshold ? integralTickTimestep : worldTimestep;
|
|
|
|
Sound.Tick();
|
|
Sync.RunUnsynced(Settings.Debug.SyncCheckUnsyncedCode, world, orderManager.TickImmediate);
|
|
|
|
if (world == null)
|
|
return;
|
|
|
|
var isNetTick = LocalTick % NetTickScale == 0;
|
|
|
|
if (!isNetTick || orderManager.IsReadyForNextFrame)
|
|
{
|
|
++orderManager.LocalFrameNumber;
|
|
|
|
if (isNetTick)
|
|
orderManager.Tick();
|
|
|
|
Sync.RunUnsynced(Settings.Debug.SyncCheckUnsyncedCode, world, () =>
|
|
{
|
|
world.OrderGenerator.Tick(world);
|
|
});
|
|
|
|
world.Tick();
|
|
|
|
PerfHistory.Tick();
|
|
}
|
|
else if (orderManager.NetFrameNumber == 0)
|
|
orderManager.LastTickTime = RunTime;
|
|
|
|
// Wait until we have done our first world Tick before TickRendering
|
|
if (orderManager.LocalFrameNumber > 0)
|
|
Sync.RunUnsynced(Settings.Debug.SyncCheckUnsyncedCode, world, () => world.TickRender(worldRenderer));
|
|
}
|
|
|
|
benchmark?.Tick(LocalTick);
|
|
}
|
|
}
|
|
|
|
static void LogicTick()
|
|
{
|
|
PerformDelayedActions();
|
|
|
|
if (OrderManager.Connection.ConnectionState != lastConnectionState)
|
|
{
|
|
lastConnectionState = OrderManager.Connection.ConnectionState;
|
|
ConnectionStateChanged(OrderManager);
|
|
}
|
|
|
|
InnerLogicTick(OrderManager);
|
|
if (worldRenderer != null && OrderManager.World != worldRenderer.World)
|
|
InnerLogicTick(worldRenderer.World.OrderManager);
|
|
}
|
|
|
|
public static void PerformDelayedActions()
|
|
{
|
|
delayedActions.PerformActions(RunTime);
|
|
}
|
|
|
|
public static void TakeScreenshot()
|
|
{
|
|
takeScreenshot = true;
|
|
}
|
|
|
|
static void RenderTick()
|
|
{
|
|
using (new PerfSample("render"))
|
|
{
|
|
++RenderFrame;
|
|
|
|
// Prepare renderables (i.e. render voxels) before calling BeginFrame
|
|
using (new PerfSample("render_prepare"))
|
|
{
|
|
Renderer.WorldModelRenderer.BeginFrame();
|
|
|
|
// World rendering is disabled while the loading screen is displayed
|
|
if (worldRenderer != null && !worldRenderer.World.IsLoadingGameSave)
|
|
{
|
|
worldRenderer.Viewport.Tick();
|
|
worldRenderer.PrepareRenderables();
|
|
}
|
|
|
|
Ui.PrepareRenderables();
|
|
Renderer.WorldModelRenderer.EndFrame();
|
|
}
|
|
|
|
// worldRenderer is null during the initial install/download screen
|
|
// World rendering is disabled while the loading screen is displayed
|
|
// Use worldRenderer.World instead of OrderManager.World to avoid a rendering mismatch while processing orders
|
|
if (worldRenderer != null && !worldRenderer.World.IsLoadingGameSave)
|
|
{
|
|
Renderer.BeginWorld(worldRenderer.Viewport.Rectangle);
|
|
Sound.SetListenerPosition(worldRenderer.Viewport.CenterPosition);
|
|
using (new PerfSample("render_world"))
|
|
worldRenderer.Draw();
|
|
}
|
|
|
|
using (new PerfSample("render_widgets"))
|
|
{
|
|
Renderer.BeginUI();
|
|
|
|
if (worldRenderer != null && !worldRenderer.World.IsLoadingGameSave)
|
|
worldRenderer.DrawAnnotations();
|
|
|
|
Ui.Draw();
|
|
|
|
if (ModData != null && ModData.CursorProvider != null)
|
|
{
|
|
if (HideCursor)
|
|
Cursor.SetCursor(null);
|
|
else
|
|
{
|
|
Cursor.SetCursor(Ui.Root.GetCursorOuter(Viewport.LastMousePos) ?? "default");
|
|
Cursor.Render(Renderer);
|
|
}
|
|
}
|
|
}
|
|
|
|
using (new PerfSample("render_flip"))
|
|
Renderer.EndFrame(new DefaultInputHandler(OrderManager.World));
|
|
|
|
if (takeScreenshot)
|
|
{
|
|
takeScreenshot = false;
|
|
TakeScreenshotInner();
|
|
}
|
|
}
|
|
|
|
PerfHistory.Items["render"].Tick();
|
|
PerfHistory.Items["batches"].Tick();
|
|
PerfHistory.Items["render_world"].Tick();
|
|
PerfHistory.Items["render_widgets"].Tick();
|
|
PerfHistory.Items["render_flip"].Tick();
|
|
PerfHistory.Items["terrain_lighting"].Tick();
|
|
}
|
|
|
|
static void Loop()
|
|
{
|
|
// The game loop mainly does two things: logic updates and
|
|
// drawing on the screen.
|
|
// ---
|
|
// We ideally want the logic to run every 'Timestep' ms and
|
|
// rendering to be done at 'MaxFramerate', so 1000 / MaxFramerate ms.
|
|
// Any additional free time is used in 'Sleep' so we don't
|
|
// consume more CPU/GPU resources than necessary.
|
|
// ---
|
|
// In case logic or rendering takes more time than the ideal
|
|
// and we're getting behind, we can skip rendering some frames
|
|
// but there's a fail-safe minimum FPS to make sure the screen
|
|
// gets updated at least that often.
|
|
// ---
|
|
// TODO: Separate world/UI rendering
|
|
// It would be nice to separate the world rendering from the UI rendering
|
|
// so that we can update the UI more often than the world. This would
|
|
// help make the game playable (mouse/controls) even in low world
|
|
// framerates.
|
|
// It's not possible at the moment because the render buffer is cleared
|
|
// before rendering and we don't keep the last rendered world buffer.
|
|
|
|
// When the logic has fallen behind by this much, skip the pending
|
|
// updates and start fresh.
|
|
// For example, if we want to update logic every 10 ms but each loop
|
|
// temporarily takes 100 ms, the 'nextLogic' timestamp will be too low
|
|
// and the current timestamp ('now') will have moved on. Even if the
|
|
// update time returns to normal, it will take a long time to catch up
|
|
// (if ever).
|
|
// This also means that the 'logicInterval' cannot be longer than this
|
|
// value.
|
|
const int MaxLogicTicksBehind = 250;
|
|
|
|
// Try to maintain at least this many FPS during replays, even if it slows down logic.
|
|
// However, if the user has enabled a framerate limit that is even lower
|
|
// than this, then that limit will be used.
|
|
const int MinReplayFps = 10;
|
|
|
|
// Timestamps for when the next logic and rendering should run
|
|
var nextLogic = RunTime;
|
|
var nextRender = RunTime;
|
|
var forcedNextRender = RunTime;
|
|
var renderBeforeNextTick = false;
|
|
|
|
while (state == RunStatus.Running)
|
|
{
|
|
var logicInterval = Ui.Timestep;
|
|
var logicWorld = worldRenderer?.World;
|
|
|
|
// ReplayTimestep = 0 means the replay is paused: we need to keep logicInterval as UI.Timestep to avoid breakage
|
|
if (logicWorld != null && !(logicWorld.IsReplay && logicWorld.ReplayTimestep == 0))
|
|
logicInterval = logicWorld.IsLoadingGameSave ? 1 :
|
|
logicWorld.IsReplay ? logicWorld.ReplayTimestep :
|
|
logicWorld.Timestep;
|
|
|
|
// Ideal time between screen updates
|
|
var maxFramerate = Settings.Graphics.CapFramerate ? Settings.Graphics.MaxFramerate.Clamp(1, 1000) : 1000;
|
|
var renderInterval = 1000 / maxFramerate;
|
|
|
|
// Tick as fast as possible while restoring game saves, capping rendering at 5 FPS
|
|
if (OrderManager.World != null && OrderManager.World.IsLoadingGameSave)
|
|
{
|
|
logicInterval = 1;
|
|
renderInterval = 200;
|
|
}
|
|
|
|
var now = RunTime;
|
|
|
|
// If the logic has fallen behind too much, skip it and catch up
|
|
if (now - nextLogic > MaxLogicTicksBehind)
|
|
nextLogic = now;
|
|
|
|
// When's the next update (logic or render)
|
|
var nextUpdate = Math.Min(nextLogic, nextRender);
|
|
if (now >= nextUpdate)
|
|
{
|
|
var forceRender = renderBeforeNextTick || now >= forcedNextRender;
|
|
|
|
if (now >= nextLogic && !renderBeforeNextTick)
|
|
{
|
|
nextLogic += logicInterval;
|
|
|
|
LogicTick();
|
|
|
|
// Force at least one render per tick during regular gameplay
|
|
if (OrderManager.World != null && !OrderManager.World.IsLoadingGameSave && !OrderManager.World.IsReplay)
|
|
renderBeforeNextTick = true;
|
|
}
|
|
|
|
var haveSomeTimeUntilNextLogic = now < nextLogic;
|
|
var isTimeToRender = now >= nextRender;
|
|
|
|
if ((isTimeToRender && haveSomeTimeUntilNextLogic) || forceRender)
|
|
{
|
|
nextRender = now + renderInterval;
|
|
|
|
// Pick the minimum allowed FPS (the lower between 'minReplayFPS'
|
|
// and the user's max frame rate) and convert it to maximum time
|
|
// allowed between screen updates.
|
|
// We do this before rendering to include the time rendering takes
|
|
// in this interval.
|
|
var maxRenderInterval = Math.Max(1000 / MinReplayFps, renderInterval);
|
|
forcedNextRender = now + maxRenderInterval;
|
|
|
|
RenderTick();
|
|
renderBeforeNextTick = false;
|
|
}
|
|
}
|
|
else
|
|
Thread.Sleep((int)(nextUpdate - now));
|
|
}
|
|
}
|
|
|
|
static RunStatus Run()
|
|
{
|
|
if (Settings.Graphics.MaxFramerate < 1)
|
|
{
|
|
Settings.Graphics.MaxFramerate = new GraphicSettings().MaxFramerate;
|
|
Settings.Graphics.CapFramerate = false;
|
|
}
|
|
|
|
try
|
|
{
|
|
Loop();
|
|
}
|
|
finally
|
|
{
|
|
// Ensure that the active replay is properly saved
|
|
OrderManager?.Dispose();
|
|
}
|
|
|
|
worldRenderer?.Dispose();
|
|
ModData.Dispose();
|
|
ChromeProvider.Deinitialize();
|
|
|
|
Sound.Dispose();
|
|
Renderer.Dispose();
|
|
|
|
OnQuit();
|
|
|
|
return state;
|
|
}
|
|
|
|
public static void Exit()
|
|
{
|
|
state = RunStatus.Success;
|
|
}
|
|
|
|
public static void AddSystemLine(string text)
|
|
{
|
|
AddSystemLine(systemMessageLabel, text);
|
|
}
|
|
|
|
public static void AddSystemLine(string name, string text)
|
|
{
|
|
OrderManager.AddChatLine(name, systemMessageColor, text, systemMessageColor);
|
|
}
|
|
|
|
public static void AddChatLine(string name, Color nameColor, string text)
|
|
{
|
|
OrderManager.AddChatLine(name, nameColor, text, chatMessageColor);
|
|
}
|
|
|
|
public static void Debug(string s, params object[] args)
|
|
{
|
|
AddSystemLine("Debug", string.Format(s, args));
|
|
}
|
|
|
|
public static void Disconnect()
|
|
{
|
|
OrderManager.World?.TraitDict.PrintReport();
|
|
|
|
OrderManager.Dispose();
|
|
CloseServer();
|
|
JoinLocal();
|
|
}
|
|
|
|
public static void CloseServer()
|
|
{
|
|
server?.Shutdown();
|
|
}
|
|
|
|
public static T CreateObject<T>(string name)
|
|
{
|
|
return ModData.ObjectCreator.CreateObject<T>(name);
|
|
}
|
|
|
|
public static ConnectionTarget CreateServer(ServerSettings settings)
|
|
{
|
|
var endpoints = new List<IPEndPoint>
|
|
{
|
|
new IPEndPoint(IPAddress.IPv6Any, settings.ListenPort),
|
|
new IPEndPoint(IPAddress.Any, settings.ListenPort)
|
|
};
|
|
server = new Server.Server(endpoints, settings, ModData, ServerType.Multiplayer);
|
|
|
|
return server.GetEndpointForLocalConnection();
|
|
}
|
|
|
|
public static ConnectionTarget CreateLocalServer(string map)
|
|
{
|
|
var settings = new ServerSettings()
|
|
{
|
|
Name = "Skirmish Game",
|
|
Map = map,
|
|
AdvertiseOnline = false
|
|
};
|
|
|
|
// Always connect to local games using the same loopback connection
|
|
// Exposing multiple endpoints introduces a race condition on the client's PlayerIndex (sometimes 0, sometimes 1)
|
|
// This would break the Restart button, which relies on the PlayerIndex always being the same for local servers
|
|
var endpoints = new List<IPEndPoint>
|
|
{
|
|
new IPEndPoint(IPAddress.Loopback, 0)
|
|
};
|
|
server = new Server.Server(endpoints, settings, ModData, ServerType.Local);
|
|
|
|
return server.GetEndpointForLocalConnection();
|
|
}
|
|
|
|
public static bool IsCurrentWorld(World world)
|
|
{
|
|
return OrderManager != null && OrderManager.World == world && !world.Disposing;
|
|
}
|
|
|
|
public static bool SetClipboardText(string text)
|
|
{
|
|
return Renderer.Window.SetClipboardText(text);
|
|
}
|
|
|
|
public static void BenchmarkMode(string prefix)
|
|
{
|
|
benchmark = new Benchmark(prefix);
|
|
}
|
|
|
|
public static void LoadMap(string launchMap)
|
|
{
|
|
var orders = new List<Order>
|
|
{
|
|
Order.Command("option gamespeed {0}".F("default")),
|
|
Order.Command("state {0}".F(Session.ClientState.Ready))
|
|
};
|
|
|
|
var path = Platform.ResolvePath(launchMap);
|
|
var map = ModData.MapCache.SingleOrDefault(m => m.Uid == launchMap) ??
|
|
ModData.MapCache.SingleOrDefault(m => m.Package.Name == path);
|
|
|
|
if (map == null)
|
|
throw new InvalidOperationException("Could not find map '{0}'.".F(launchMap));
|
|
|
|
CreateAndStartLocalServer(map.Uid, orders);
|
|
}
|
|
|
|
public static void FinishBenchmark()
|
|
{
|
|
if (benchmark != null)
|
|
{
|
|
benchmark.Write();
|
|
Exit();
|
|
}
|
|
}
|
|
}
|
|
}
|