From a9e3458a3b93649f62919566c3ffacf27f16332c Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Thu, 29 Mar 2018 00:39:25 -0400 Subject: add success/error banner to log parser page --- src/SMAPI.Web/Views/LogParser/Index.cshtml | 36 ++++++++++++++++++------ src/SMAPI.Web/wwwroot/Content/css/log-parser.css | 20 +++++++++++-- src/SMAPI.Web/wwwroot/Content/js/log-parser.js | 4 ++- 3 files changed, 48 insertions(+), 12 deletions(-) (limited to 'src') diff --git a/src/SMAPI.Web/Views/LogParser/Index.cshtml b/src/SMAPI.Web/Views/LogParser/Index.cshtml index 7213e286..c909b203 100644 --- a/src/SMAPI.Web/Views/LogParser/Index.cshtml +++ b/src/SMAPI.Web/Views/LogParser/Index.cshtml @@ -44,11 +44,36 @@ ** Intro *********@

This page lets you upload, view, and share a SMAPI log to help troubleshoot mod issues.

- @if (Model.ParsedLog?.IsValid == true) { -

Parsed log

+ +} +else if (Model.ParsedLog?.IsValid == false) +{ + +} +else +{ + +} + +@********* +** Parsed log +*********@ +@if (Model.ParsedLog?.IsValid == true) +{ +

Log info

@@ -148,12 +173,6 @@ } else if (Model.ParsedLog?.IsValid == false) { -

Parsed log

-
-

We couldn't parse that file, but you can still share the link.

-

Error details: @Model.ParsedLog.Error

-
-

Raw log

@Model.ParsedLog.RawText
} @@ -166,7 +185,6 @@ else if (Model.ParsedLog?.IsValid == false)
  • Find your SMAPI log file (not the console text).
  • Drag the file onto the textbox below (or paste the text in).
  • Click Parse.
  • -
  • Share the URL of the new page.
  • diff --git a/src/SMAPI.Web/wwwroot/Content/css/log-parser.css b/src/SMAPI.Web/wwwroot/Content/css/log-parser.css index cbf09ffe..9d604072 100644 --- a/src/SMAPI.Web/wwwroot/Content/css/log-parser.css +++ b/src/SMAPI.Web/wwwroot/Content/css/log-parser.css @@ -20,18 +20,34 @@ caption { font-family: monospace; } -#upload-button { +input#upload-button { background: #ccf; border: 1px solid #000088; } -#upload-button { +input#upload-button { background: #eef; } /********* ** Log metadata & filters *********/ +.banner { + border: 2px solid gray; + border-radius: 5px; + padding: 1em; +} + +.banner.success { + border-color: green; + background: #CFC; +} + +.banner.error { + border-color: red; + background: #FCC; +} + #metadata, #mods, #filters { font-weight: bold; border-bottom: 1px dashed #888888; diff --git a/src/SMAPI.Web/wwwroot/Content/js/log-parser.js b/src/SMAPI.Web/wwwroot/Content/js/log-parser.js index 38a75a80..c4a35e96 100644 --- a/src/SMAPI.Web/wwwroot/Content/js/log-parser.js +++ b/src/SMAPI.Web/wwwroot/Content/js/log-parser.js @@ -92,7 +92,9 @@ smapi.logParser = function (data, sectionUrl) { *********/ var error = $("#error"); - $("#upload-button").on("click", function() { + $("#upload-button").on("click", function(e) { + e.preventDefault(); + $("#input").val(""); $("#popup-upload").fadeIn(); }); -- cgit From d49eb880115fefae406edfdf38fbff54acf8ba44 Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Thu, 29 Mar 2018 00:43:31 -0400 Subject: show game path on log parser page instead of mods path --- src/SMAPI.Web/Framework/LogParsing/LogParser.cs | 1 + src/SMAPI.Web/Framework/LogParsing/Models/ParsedLog.cs | 3 +++ src/SMAPI.Web/Views/LogParser/Index.cshtml | 4 ++-- 3 files changed, 6 insertions(+), 2 deletions(-) (limited to 'src') diff --git a/src/SMAPI.Web/Framework/LogParsing/LogParser.cs b/src/SMAPI.Web/Framework/LogParsing/LogParser.cs index 9e44f163..f49fb05c 100644 --- a/src/SMAPI.Web/Framework/LogParsing/LogParser.cs +++ b/src/SMAPI.Web/Framework/LogParsing/LogParser.cs @@ -135,6 +135,7 @@ namespace StardewModdingAPI.Web.Framework.LogParsing { Match match = this.ModPathPattern.Match(message.Text); log.ModPath = match.Groups["path"].Value; + log.GamePath = new FileInfo(log.ModPath).Directory.FullName; } // log UTC timestamp line diff --git a/src/SMAPI.Web/Framework/LogParsing/Models/ParsedLog.cs b/src/SMAPI.Web/Framework/LogParsing/Models/ParsedLog.cs index a82b6a1b..87b20eb0 100644 --- a/src/SMAPI.Web/Framework/LogParsing/Models/ParsedLog.cs +++ b/src/SMAPI.Web/Framework/LogParsing/Models/ParsedLog.cs @@ -32,6 +32,9 @@ namespace StardewModdingAPI.Web.Framework.LogParsing.Models /// The player's operating system. public string OperatingSystem { get; set; } + /// The game install path. + public string GamePath { get; set; } + /// The mod folder path. public string ModPath { get; set; } diff --git a/src/SMAPI.Web/Views/LogParser/Index.cshtml b/src/SMAPI.Web/Views/LogParser/Index.cshtml index c909b203..7103e9a1 100644 --- a/src/SMAPI.Web/Views/LogParser/Index.cshtml +++ b/src/SMAPI.Web/Views/LogParser/Index.cshtml @@ -90,8 +90,8 @@ else
    - - + + -- cgit From 4cd77225837b541bd6032539aa0895bade95181f Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Thu, 29 Mar 2018 00:51:23 -0400 Subject: tweak metadata formatting --- src/SMAPI.Web/Views/LogParser/Index.cshtml | 18 +++++++----------- src/SMAPI.Web/wwwroot/Content/css/log-parser.css | 20 +++++++++++++++----- 2 files changed, 22 insertions(+), 16 deletions(-) (limited to 'src') diff --git a/src/SMAPI.Web/Views/LogParser/Index.cshtml b/src/SMAPI.Web/Views/LogParser/Index.cshtml index 7103e9a1..b739ebbb 100644 --- a/src/SMAPI.Web/Views/LogParser/Index.cshtml +++ b/src/SMAPI.Web/Views/LogParser/Index.cshtml @@ -78,23 +78,19 @@ else
    Game info:
    @Model.ParsedLog.OperatingSystem
    Mods path:@Model.ParsedLog.ModPathGame path:@Model.ParsedLog.GamePath
    Log started:
    - - - - - - + + - - + + - + - +
    Game info:
    SMAPI version:@Model.ParsedLog.ApiVersion
    Game version:@Model.ParsedLog.GameVersionStardew Valley:@Model.ParsedLog.GameVersion on @Model.ParsedLog.OperatingSystem
    Platform:@Model.ParsedLog.OperatingSystemSMAPI:@Model.ParsedLog.ApiVersion
    Game path:Folder: @Model.ParsedLog.GamePath
    Log started:Log started: @Model.ParsedLog.Timestamp.UtcDateTime.ToString("yyyy-MM-dd HH:mm") UTC ({{localTimeStarted}} your time)
    @@ -111,7 +107,7 @@ else - @mod.Name + @mod.Name @if (contentPacks != null && contentPacks.TryGetValue(mod.Name, out LogModInfo[] contentPackList)) {
    diff --git a/src/SMAPI.Web/wwwroot/Content/css/log-parser.css b/src/SMAPI.Web/wwwroot/Content/css/log-parser.css index 9d604072..789274e2 100644 --- a/src/SMAPI.Web/wwwroot/Content/css/log-parser.css +++ b/src/SMAPI.Web/wwwroot/Content/css/log-parser.css @@ -29,8 +29,12 @@ input#upload-button { background: #eef; } +table caption { + font-weight: bold; +} + /********* -** Log metadata & filters +** Result banner *********/ .banner { border: 2px solid gray; @@ -48,15 +52,20 @@ input#upload-button { background: #FCC; } +/********* +** Log metadata & filters +*********/ #metadata, #mods, #filters { - font-weight: bold; border-bottom: 1px dashed #888888; - padding-bottom: 10px; margin-bottom: 5px; } -table#metadata, -table#mods { +#metadata th { + text-align: right; + padding-right: 0.7em; +} + +table#metadata, table#mods { border: 1px solid #000000; background: #ffffff; border-radius: 5px; @@ -131,6 +140,7 @@ table#mods { #filters { margin: 1em 0 0 0; padding: 0; + font-weight: bold; } #filters span { -- cgit From db0c88dbaf4af8622cb9b88fab7ea10d715fd7f6 Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Thu, 29 Mar 2018 19:17:44 -0400 Subject: move version closer to mod name in log parser --- src/SMAPI.Web/Views/LogParser/Index.cshtml | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) (limited to 'src') diff --git a/src/SMAPI.Web/Views/LogParser/Index.cshtml b/src/SMAPI.Web/Views/LogParser/Index.cshtml index b739ebbb..2d1c1b44 100644 --- a/src/SMAPI.Web/Views/LogParser/Index.cshtml +++ b/src/SMAPI.Web/Views/LogParser/Index.cshtml @@ -107,7 +107,7 @@ else - @mod.Name + @mod.Name @mod.Version @if (contentPacks != null && contentPacks.TryGetValue(mod.Name, out LogModInfo[] contentPackList)) {
    @@ -118,7 +118,6 @@ else
    } - @mod.Version @mod.Author @if (mod.Errors == 0) { -- cgit From c3555c74f558cae5041c882fba0377d9fa2894be Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Thu, 29 Mar 2018 20:27:43 -0400 Subject: update for Stardew Valley 1.2.0.20 (#453) --- src/SMAPI/Constants.cs | 2 +- src/SMAPI/Framework/ContentCore.cs | 20 +++++- src/SMAPI/Framework/SGame.cs | 138 ++++++++++++++++++++++--------------- 3 files changed, 101 insertions(+), 59 deletions(-) (limited to 'src') diff --git a/src/SMAPI/Constants.cs b/src/SMAPI/Constants.cs index 6270186a..72b9c78e 100644 --- a/src/SMAPI/Constants.cs +++ b/src/SMAPI/Constants.cs @@ -186,7 +186,7 @@ namespace StardewModdingAPI { string prefix = #if STARDEW_VALLEY_1_3 - new string(Game1.player.name.Value.Where(char.IsLetterOrDigit).ToArray()); + new string(Game1.player.Name.Where(char.IsLetterOrDigit).ToArray()); #else new string(Game1.player.name.Where(char.IsLetterOrDigit).ToArray()); #endif diff --git a/src/SMAPI/Framework/ContentCore.cs b/src/SMAPI/Framework/ContentCore.cs index 3c7e7b5a..43357553 100644 --- a/src/SMAPI/Framework/ContentCore.cs +++ b/src/SMAPI/Framework/ContentCore.cs @@ -44,6 +44,11 @@ namespace StardewModdingAPI.Framework /// The underlying asset cache. private readonly ContentCache Cache; +#if STARDEW_VALLEY_1_3 + /// A lookup which indicates whether the asset is localisable (i.e. the filename contains the locale), if previously loaded. + private readonly IDictionary IsLocalisableLookup; +#endif + /// The locale codes used in asset keys indexed by enum value. private readonly IDictionary Locales; @@ -106,6 +111,9 @@ namespace StardewModdingAPI.Framework this.CoreAssets = new CoreAssetPropagator(this.NormaliseAssetName, reflection); this.Locales = this.GetKeyLocales(reflection); this.LanguageCodes = this.Locales.ToDictionary(p => p.Value, p => p.Key, StringComparer.InvariantCultureIgnoreCase); +#if STARDEW_VALLEY_1_3 + this.IsLocalisableLookup = reflection.GetField>(this.Content, "_localizedAsset").GetValue(); +#endif } /// Get a new content manager which defers loading to the content core. @@ -512,8 +520,18 @@ namespace StardewModdingAPI.Framework /// The normalised asset name. private bool IsNormalisedKeyLoaded(string normalisedAssetName) { - return this.Cache.ContainsKey(normalisedAssetName) +#if STARDEW_VALLEY_1_3 + if (!this.IsLocalisableLookup.TryGetValue(normalisedAssetName, out bool localisable)) + return false; + + return localisable + ? this.Cache.ContainsKey($"{normalisedAssetName}.{this.Locales[this.Content.GetCurrentLanguage()]}") + : this.Cache.ContainsKey(normalisedAssetName); +#else + return + this.Cache.ContainsKey(normalisedAssetName) || this.Cache.ContainsKey($"{normalisedAssetName}.{this.Locales[this.Content.GetCurrentLanguage()]}"); // translated asset +#endif } /// Track that a content manager loaded an asset. diff --git a/src/SMAPI/Framework/SGame.cs b/src/SMAPI/Framework/SGame.cs index 47bc40e6..ae702711 100644 --- a/src/SMAPI/Framework/SGame.cs +++ b/src/SMAPI/Framework/SGame.cs @@ -4,6 +4,7 @@ using System.Collections.Generic; using System.Diagnostics; using System.Diagnostics.CodeAnalysis; using System.Linq; +using System.Text; using System.Threading; using System.Threading.Tasks; using Microsoft.Xna.Framework; @@ -25,6 +26,8 @@ using StardewValley.Tools; using xTile.Dimensions; #if !STARDEW_VALLEY_1_3 using xTile.Layers; +#else +using SFarmer = StardewValley.Farmer; #endif namespace StardewModdingAPI.Framework @@ -162,6 +165,7 @@ namespace StardewModdingAPI.Framework private readonly Action drawDialogueBox = () => SGame.Reflection.GetMethod(SGame.Instance, nameof(drawDialogueBox)).Invoke(); #if STARDEW_VALLEY_1_3 private readonly Action drawOverlays = spriteBatch => SGame.Reflection.GetMethod(SGame.Instance, nameof(SGame.drawOverlays)).Invoke(spriteBatch); + private static StringBuilder _debugStringBuilder => SGame.Reflection.GetField(typeof(Game1), nameof(_debugStringBuilder)).GetValue(); #endif private readonly Action renderScreenBuffer = () => SGame.Reflection.GetMethod(SGame.Instance, nameof(renderScreenBuffer)).Invoke(); // ReSharper restore ArrangeStaticMemberQualifier, ArrangeThisQualifier, InconsistentNaming @@ -727,6 +731,12 @@ namespace StardewModdingAPI.Framework this.RaisePostRender(); Game1.spriteBatch.End(); } + if (Game1.overlayMenu != null) + { + Game1.spriteBatch.Begin(SpriteSortMode.Deferred, BlendState.AlphaBlend, SamplerState.PointClamp, (DepthStencilState)null, (RasterizerState)null); + Game1.overlayMenu.draw(Game1.spriteBatch); + Game1.spriteBatch.End(); + } //base.Draw(gameTime); this.renderScreenBuffer(); } @@ -845,7 +855,7 @@ namespace StardewModdingAPI.Framework int widthOfString = SpriteText.getWidthOfString(str3); int height = 64; int x = 64; - int y = Game1.graphics.GraphicsDevice.Viewport.TitleSafeArea.Bottom - height; + int y = Game1.graphics.GraphicsDevice.Viewport.GetTitleSafeArea().Bottom - height; SpriteText.drawString(Game1.spriteBatch, s, x, y, 999999, widthOfString, height, 1f, 0.88f, false, 0, str3, -1); Game1.spriteBatch.End(); if ((double)Game1.options.zoomLevel != 1.0) @@ -857,6 +867,7 @@ namespace StardewModdingAPI.Framework Game1.spriteBatch.End(); } this.drawOverlays(Game1.spriteBatch); + //base.Draw(gameTime); } else { @@ -956,6 +967,27 @@ namespace StardewModdingAPI.Framework } } } + foreach (SFarmer farmer in Game1.currentLocation.getFarmers()) + { + if (!(bool)((NetFieldBase)farmer.swimming) && !farmer.isRidingHorse() && (Game1.currentLocation == null || !Game1.currentLocation.shouldShadowBeDrawnAboveBuildingsLayer(farmer.getTileLocation()))) + { + SpriteBatch spriteBatch = Game1.spriteBatch; + Texture2D shadowTexture = Game1.shadowTexture; + Vector2 local = Game1.GlobalToLocal(farmer.Position + new Vector2(32f, 24f)); + Microsoft.Xna.Framework.Rectangle? sourceRectangle = new Microsoft.Xna.Framework.Rectangle?(Game1.shadowTexture.Bounds); + Color white = Color.White; + double num2 = 0.0; + Microsoft.Xna.Framework.Rectangle bounds2 = Game1.shadowTexture.Bounds; + double x = (double)bounds2.Center.X; + bounds2 = Game1.shadowTexture.Bounds; + double y = (double)bounds2.Center.Y; + Vector2 origin = new Vector2((float)x, (float)y); + double num3 = 4.0 - (!farmer.running && !farmer.UsingTool || farmer.FarmerSprite.currentAnimationIndex <= 1 ? 0.0 : (double)Math.Abs(FarmerRenderer.featureYOffsetPerFrame[farmer.FarmerSprite.CurrentFrame]) * 0.5); + int num4 = 0; + double num5 = 0.0; + spriteBatch.Draw(shadowTexture, local, sourceRectangle, white, (float)num2, origin, (float)num3, (SpriteEffects)num4, (float)num5); + } + } Game1.currentLocation.Map.GetLayer("Buildings").Draw(Game1.mapDisplayDevice, Game1.viewport, Location.Origin, false, 4); Game1.mapDisplayDevice.EndScene(); Game1.spriteBatch.End(); @@ -976,6 +1008,27 @@ namespace StardewModdingAPI.Framework Game1.spriteBatch.Draw(Game1.shadowTexture, Game1.GlobalToLocal(Game1.viewport, actor.Position + new Vector2((float)(actor.Sprite.SpriteWidth * 4) / 2f, (float)(actor.GetBoundingBox().Height + (actor.IsMonster ? 0 : 12)))), new Microsoft.Xna.Framework.Rectangle?(Game1.shadowTexture.Bounds), Color.White, 0.0f, new Vector2((float)Game1.shadowTexture.Bounds.Center.X, (float)Game1.shadowTexture.Bounds.Center.Y), (float)(4.0 + (double)actor.yJumpOffset / 40.0) * (float)((NetFieldBase)actor.scale), SpriteEffects.None, Math.Max(0.0f, (float)actor.getStandingY() / 10000f) - 1E-06f); } } + foreach (SFarmer farmer in Game1.currentLocation.getFarmers()) + { + if (!(bool)((NetFieldBase)farmer.swimming) && !farmer.isRidingHorse() && (Game1.currentLocation != null && Game1.currentLocation.shouldShadowBeDrawnAboveBuildingsLayer(farmer.getTileLocation()))) + { + SpriteBatch spriteBatch = Game1.spriteBatch; + Texture2D shadowTexture = Game1.shadowTexture; + Vector2 local = Game1.GlobalToLocal(farmer.Position + new Vector2(32f, 24f)); + Microsoft.Xna.Framework.Rectangle? sourceRectangle = new Microsoft.Xna.Framework.Rectangle?(Game1.shadowTexture.Bounds); + Color white = Color.White; + double num2 = 0.0; + Microsoft.Xna.Framework.Rectangle bounds2 = Game1.shadowTexture.Bounds; + double x = (double)bounds2.Center.X; + bounds2 = Game1.shadowTexture.Bounds; + double y = (double)bounds2.Center.Y; + Vector2 origin = new Vector2((float)x, (float)y); + double num3 = 4.0 - (!farmer.running && !farmer.UsingTool || farmer.FarmerSprite.currentAnimationIndex <= 1 ? 0.0 : (double)Math.Abs(FarmerRenderer.featureYOffsetPerFrame[farmer.FarmerSprite.CurrentFrame]) * 0.5); + int num4 = 0; + double num5 = 0.0; + spriteBatch.Draw(shadowTexture, local, sourceRectangle, white, (float)num2, origin, (float)num3, (SpriteEffects)num4, (float)num5); + } + } if ((Game1.eventUp || Game1.killScreen) && (!Game1.killScreen && Game1.currentLocation.currentEvent != null)) Game1.currentLocation.currentEvent.draw(Game1.spriteBatch); if (Game1.player.currentUpgrade != null && Game1.player.currentUpgrade.daysLeftTillUpgradeDone <= 3 && Game1.currentLocation.Name.Equals("Farm")) @@ -1054,7 +1107,6 @@ namespace StardewModdingAPI.Framework Game1.spriteBatch.Draw(Game1.rainTexture, Game1.rainDrops[index].position, new Microsoft.Xna.Framework.Rectangle?(Game1.getSourceRectForStandardTileSheet(Game1.rainTexture, Game1.rainDrops[index].frame, -1, -1)), Color.White); } Game1.spriteBatch.End(); - //base.Draw(gameTime); Game1.spriteBatch.Begin(SpriteSortMode.FrontToBack, BlendState.AlphaBlend, SamplerState.PointClamp, (DepthStencilState)null, (RasterizerState)null); if (Game1.eventUp && Game1.currentLocation.currentEvent != null) { @@ -1155,30 +1207,8 @@ namespace StardewModdingAPI.Framework this.drawDialogueBox(); if (Game1.progressBar) { - SpriteBatch spriteBatch1 = Game1.spriteBatch; - Texture2D fadeToBlackRect = Game1.fadeToBlackRect; - viewport1 = Game1.graphics.GraphicsDevice.Viewport; - int x1 = (viewport1.TitleSafeArea.Width - Game1.dialogueWidth) / 2; - viewport1 = Game1.graphics.GraphicsDevice.Viewport; - Microsoft.Xna.Framework.Rectangle titleSafeArea = viewport1.TitleSafeArea; - int y1 = titleSafeArea.Bottom - 128; - int dialogueWidth = Game1.dialogueWidth; - int height1 = 32; - Microsoft.Xna.Framework.Rectangle destinationRectangle1 = new Microsoft.Xna.Framework.Rectangle(x1, y1, dialogueWidth, height1); - Color lightGray = Color.LightGray; - spriteBatch1.Draw(fadeToBlackRect, destinationRectangle1, lightGray); - SpriteBatch spriteBatch2 = Game1.spriteBatch; - Texture2D staminaRect = Game1.staminaRect; - viewport1 = Game1.graphics.GraphicsDevice.Viewport; - int x2 = (viewport1.TitleSafeArea.Width - Game1.dialogueWidth) / 2; - viewport1 = Game1.graphics.GraphicsDevice.Viewport; - titleSafeArea = viewport1.TitleSafeArea; - int y2 = titleSafeArea.Bottom - 128; - int width = (int)((double)Game1.pauseAccumulator / (double)Game1.pauseTime * (double)Game1.dialogueWidth); - int height2 = 32; - Microsoft.Xna.Framework.Rectangle destinationRectangle2 = new Microsoft.Xna.Framework.Rectangle(x2, y2, width, height2); - Color dimGray = Color.DimGray; - spriteBatch2.Draw(staminaRect, destinationRectangle2, dimGray); + Game1.spriteBatch.Draw(Game1.fadeToBlackRect, new Microsoft.Xna.Framework.Rectangle((Game1.graphics.GraphicsDevice.Viewport.GetTitleSafeArea().Width - Game1.dialogueWidth) / 2, Game1.graphics.GraphicsDevice.Viewport.GetTitleSafeArea().Bottom - 128, Game1.dialogueWidth, 32), Color.LightGray); + Game1.spriteBatch.Draw(Game1.staminaRect, new Microsoft.Xna.Framework.Rectangle((Game1.graphics.GraphicsDevice.Viewport.GetTitleSafeArea().Width - Game1.dialogueWidth) / 2, Game1.graphics.GraphicsDevice.Viewport.GetTitleSafeArea().Bottom - 128, (int)((double)Game1.pauseAccumulator / (double)Game1.pauseTime * (double)Game1.dialogueWidth), 32), Color.DimGray); } if (Game1.eventUp && (Game1.currentLocation != null && Game1.currentLocation.currentEvent != null)) Game1.currentLocation.currentEvent.drawAfterMap(Game1.spriteBatch); @@ -1219,38 +1249,31 @@ namespace StardewModdingAPI.Framework overlayTempSprite.draw(Game1.spriteBatch, true, 0, 0, 1f); if (Game1.debugMode) { - SpriteBatch spriteBatch = Game1.spriteBatch; - SpriteFont smallFont = Game1.smallFont; - object[] objArray = new object[10]; - int index = 0; - string str; - if (!Game1.panMode) - str = "player: " + (object)(Game1.player.getStandingX() / 64) + ", " + (object)(Game1.player.getStandingY() / 64); + StringBuilder debugStringBuilder = SGame._debugStringBuilder; + debugStringBuilder.Clear(); + if (Game1.panMode) + { + debugStringBuilder.Append((Game1.getOldMouseX() + Game1.viewport.X) / 64); + debugStringBuilder.Append(","); + debugStringBuilder.Append((Game1.getOldMouseY() + Game1.viewport.Y) / 64); + } else - str = ((Game1.getOldMouseX() + Game1.viewport.X) / 64).ToString() + "," + (object)((Game1.getOldMouseY() + Game1.viewport.Y) / 64); - objArray[index] = (object)str; - objArray[1] = (object)" mouseTransparency: "; - objArray[2] = (object)Game1.mouseCursorTransparency; - objArray[3] = (object)" mousePosition: "; - objArray[4] = (object)Game1.getMouseX(); - objArray[5] = (object)","; - objArray[6] = (object)Game1.getMouseY(); - objArray[7] = (object)Environment.NewLine; - objArray[8] = (object)"debugOutput: "; - objArray[9] = (object)Game1.debugOutput; - string text = string.Concat(objArray); - Viewport viewport2 = this.GraphicsDevice.Viewport; - double x = (double)viewport2.TitleSafeArea.X; - viewport2 = this.GraphicsDevice.Viewport; - double y = (double)viewport2.TitleSafeArea.Y; - Vector2 position = new Vector2((float)x, (float)y); - Color red = Color.Red; - double num2 = 0.0; - Vector2 zero = Vector2.Zero; - double num3 = 1.0; - int num4 = 0; - double num5 = 0.99999988079071; - spriteBatch.DrawString(smallFont, text, position, red, (float)num2, zero, (float)num3, (SpriteEffects)num4, (float)num5); + { + debugStringBuilder.Append("player: "); + debugStringBuilder.Append(Game1.player.getStandingX() / 64); + debugStringBuilder.Append(", "); + debugStringBuilder.Append(Game1.player.getStandingY() / 64); + } + debugStringBuilder.Append(" mouseTransparency: "); + debugStringBuilder.Append(Game1.mouseCursorTransparency); + debugStringBuilder.Append(" mousePosition: "); + debugStringBuilder.Append(Game1.getMouseX()); + debugStringBuilder.Append(","); + debugStringBuilder.Append(Game1.getMouseY()); + debugStringBuilder.Append(Environment.NewLine); + debugStringBuilder.Append("debugOutput: "); + debugStringBuilder.Append(Game1.debugOutput); + Game1.spriteBatch.DrawString(Game1.smallFont, debugStringBuilder, new Vector2((float)this.GraphicsDevice.Viewport.GetTitleSafeArea().X, (float)(this.GraphicsDevice.Viewport.GetTitleSafeArea().Y + Game1.smallFont.LineSpacing * 8)), Color.Red, 0.0f, Vector2.Zero, 1f, SpriteEffects.None, 0.9999999f); } if (Game1.showKeyHelp) Game1.spriteBatch.DrawString(Game1.smallFont, Game1.keyHelpString, new Vector2(64f, (float)(Game1.viewport.Height - 64 - (Game1.dialogueUp ? 192 + (Game1.isQuestion ? Game1.questionChoices.Count * 64 : 0) : 0)) - Game1.smallFont.MeasureString(Game1.keyHelpString).Y), Color.LightGray, 0.0f, Vector2.Zero, 1f, SpriteEffects.None, 0.9999999f); @@ -1279,6 +1302,7 @@ namespace StardewModdingAPI.Framework Game1.spriteBatch.End(); this.drawOverlays(Game1.spriteBatch); this.renderScreenBuffer(); + //base.Draw(gameTime); } } } -- cgit From 30e89b3a3373d3a73f62e7297ac27db8de70246b Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Fri, 30 Mar 2018 22:51:34 -0400 Subject: fix mods not being loaded if an optional dependency is installed but skipped --- src/SMAPI/Framework/ModLoading/ModResolver.cs | 1 + 1 file changed, 1 insertion(+) (limited to 'src') diff --git a/src/SMAPI/Framework/ModLoading/ModResolver.cs b/src/SMAPI/Framework/ModLoading/ModResolver.cs index f878a1b9..a9896278 100644 --- a/src/SMAPI/Framework/ModLoading/ModResolver.cs +++ b/src/SMAPI/Framework/ModLoading/ModResolver.cs @@ -352,6 +352,7 @@ namespace StardewModdingAPI.Framework.ModLoading { // sorted successfully case ModDependencyStatus.Sorted: + case ModDependencyStatus.Failed when !dependency.IsRequired: // ignore failed optional dependency break; // failed, which means this mod can't be loaded either -- cgit From 22965604bfa5858a089d842173cdebe6aaed0ed8 Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Mon, 9 Apr 2018 19:30:17 -0400 Subject: add support for build message URLs (#471) --- src/SMAPI.ModBuildConfig/build/smapi.targets | 2 +- .../Framework/RewriteRules/RedirectToUrlRule.cs | 20 ++++++++------------ src/SMAPI.Web/Startup.cs | 1 + 3 files changed, 10 insertions(+), 13 deletions(-) (limited to 'src') diff --git a/src/SMAPI.ModBuildConfig/build/smapi.targets b/src/SMAPI.ModBuildConfig/build/smapi.targets index d2e37101..a177840c 100644 --- a/src/SMAPI.ModBuildConfig/build/smapi.targets +++ b/src/SMAPI.ModBuildConfig/build/smapi.targets @@ -142,7 +142,7 @@ - + diff --git a/src/SMAPI.Web/Framework/RewriteRules/RedirectToUrlRule.cs b/src/SMAPI.Web/Framework/RewriteRules/RedirectToUrlRule.cs index 0719e311..4bae0b4c 100644 --- a/src/SMAPI.Web/Framework/RewriteRules/RedirectToUrlRule.cs +++ b/src/SMAPI.Web/Framework/RewriteRules/RedirectToUrlRule.cs @@ -12,11 +12,8 @@ namespace StardewModdingAPI.Web.Framework.RewriteRules /********* ** Properties *********/ - /// A predicate which indicates when the rule should be applied. - private readonly Func ShouldRewrite; - - /// The new URL to which to redirect. - private readonly string NewUrl; + /// Get the new URL to which to redirect (or null to skip). + private readonly Func NewUrl; /********* @@ -27,8 +24,7 @@ namespace StardewModdingAPI.Web.Framework.RewriteRules /// The new URL to which to redirect. public RedirectToUrlRule(Func shouldRewrite, string url) { - this.ShouldRewrite = shouldRewrite ?? (req => true); - this.NewUrl = url; + this.NewUrl = req => shouldRewrite(req) ? url : null; } /// Construct an instance. @@ -37,8 +33,7 @@ namespace StardewModdingAPI.Web.Framework.RewriteRules public RedirectToUrlRule(string pathRegex, string url) { Regex regex = new Regex(pathRegex, RegexOptions.IgnoreCase | RegexOptions.Compiled); - this.ShouldRewrite = req => req.Path.HasValue && regex.IsMatch(req.Path.Value); - this.NewUrl = url; + this.NewUrl = req => req.Path.HasValue ? regex.Replace(req.Path.Value, url) : null; } /// Applies the rule. Implementations of ApplyRule should set the value for (defaults to RuleResult.ContinueRules). @@ -47,14 +42,15 @@ namespace StardewModdingAPI.Web.Framework.RewriteRules { HttpRequest request = context.HttpContext.Request; - // check condition - if (!this.ShouldRewrite(request)) + // check rewrite + string newUrl = this.NewUrl(request); + if (newUrl == null || newUrl == request.Path.Value) return; // redirect request HttpResponse response = context.HttpContext.Response; response.StatusCode = (int)HttpStatusCode.Redirect; - response.Headers["Location"] = this.NewUrl; + response.Headers["Location"] = newUrl; context.Result = RuleResult.EndResponse; } } diff --git a/src/SMAPI.Web/Startup.cs b/src/SMAPI.Web/Startup.cs index 47102e5c..6c7ccecd 100644 --- a/src/SMAPI.Web/Startup.cs +++ b/src/SMAPI.Web/Startup.cs @@ -155,6 +155,7 @@ namespace StardewModdingAPI.Web // shortcut redirects redirects.Add(new RedirectToUrlRule(@"^/compat\.?$", "https://stardewvalleywiki.com/Modding:SMAPI_compatibility")); redirects.Add(new RedirectToUrlRule(@"^/docs\.?$", "https://stardewvalleywiki.com/Modding:Index")); + redirects.Add(new RedirectToUrlRule(@"^/buildmsg(?:/?(.*))$", "https://github.com/Pathoschild/SMAPI/blob/develop/docs/mod-build-config.md#$1")); // redirect legacy canimod.com URLs var wikiRedirects = new Dictionary -- cgit From f52f7ca36f2ecf3d4478c5bc1e9cd95e5ff53929 Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Mon, 9 Apr 2018 19:32:00 -0400 Subject: add mod code analyzers to detect implicit net field conversion issues (#471) --- .../Framework/DiagnosticResult.cs | 88 ++++++++ .../Framework/DiagnosticVerifier.Helper.cs | 171 ++++++++++++++ .../Framework/DiagnosticVerifier.cs | 248 +++++++++++++++++++++ .../SMAPI.ModBuildConfig.Analyzer.Tests.csproj | 18 ++ .../UnitTests.cs | 118 ++++++++++ .../ImplicitNetFieldCastAnalyzer.cs | 107 +++++++++ .../Properties/AssemblyInfo.cs | 4 + ...tardewModdingAPI.ModBuildConfig.Analyzer.csproj | 23 ++ .../tools/install.ps1 | 58 +++++ .../tools/uninstall.ps1 | 65 ++++++ .../StardewModdingAPI.ModBuildConfig.csproj | 1 + src/SMAPI.ModBuildConfig/package.nuspec | 24 +- src/SMAPI.Tests/StardewModdingAPI.Tests.csproj | 3 + src/SMAPI.sln | 33 +++ 14 files changed, 940 insertions(+), 21 deletions(-) create mode 100644 src/SMAPI.ModBuildConfig.Analyzer.Tests/Framework/DiagnosticResult.cs create mode 100644 src/SMAPI.ModBuildConfig.Analyzer.Tests/Framework/DiagnosticVerifier.Helper.cs create mode 100644 src/SMAPI.ModBuildConfig.Analyzer.Tests/Framework/DiagnosticVerifier.cs create mode 100644 src/SMAPI.ModBuildConfig.Analyzer.Tests/SMAPI.ModBuildConfig.Analyzer.Tests.csproj create mode 100644 src/SMAPI.ModBuildConfig.Analyzer.Tests/UnitTests.cs create mode 100644 src/SMAPI.ModBuildConfig.Analyzer/ImplicitNetFieldCastAnalyzer.cs create mode 100644 src/SMAPI.ModBuildConfig.Analyzer/Properties/AssemblyInfo.cs create mode 100644 src/SMAPI.ModBuildConfig.Analyzer/StardewModdingAPI.ModBuildConfig.Analyzer.csproj create mode 100644 src/SMAPI.ModBuildConfig.Analyzer/tools/install.ps1 create mode 100644 src/SMAPI.ModBuildConfig.Analyzer/tools/uninstall.ps1 (limited to 'src') diff --git a/src/SMAPI.ModBuildConfig.Analyzer.Tests/Framework/DiagnosticResult.cs b/src/SMAPI.ModBuildConfig.Analyzer.Tests/Framework/DiagnosticResult.cs new file mode 100644 index 00000000..896c2cb8 --- /dev/null +++ b/src/SMAPI.ModBuildConfig.Analyzer.Tests/Framework/DiagnosticResult.cs @@ -0,0 +1,88 @@ +// +using Microsoft.CodeAnalysis; +using System; + +namespace SMAPI.ModBuildConfig.Analyzer.Tests.Framework +{ + /// + /// Location where the diagnostic appears, as determined by path, line number, and column number. + /// + public struct DiagnosticResultLocation + { + public DiagnosticResultLocation(string path, int line, int column) + { + if (line < -1) + { + throw new ArgumentOutOfRangeException(nameof(line), "line must be >= -1"); + } + + if (column < -1) + { + throw new ArgumentOutOfRangeException(nameof(column), "column must be >= -1"); + } + + this.Path = path; + this.Line = line; + this.Column = column; + } + + public string Path { get; } + public int Line { get; } + public int Column { get; } + } + + /// + /// Struct that stores information about a Diagnostic appearing in a source + /// + public struct DiagnosticResult + { + private DiagnosticResultLocation[] locations; + + public DiagnosticResultLocation[] Locations + { + get + { + if (this.locations == null) + { + this.locations = new DiagnosticResultLocation[] { }; + } + return this.locations; + } + + set + { + this.locations = value; + } + } + + public DiagnosticSeverity Severity { get; set; } + + public string Id { get; set; } + + public string Message { get; set; } + + public string Path + { + get + { + return this.Locations.Length > 0 ? this.Locations[0].Path : ""; + } + } + + public int Line + { + get + { + return this.Locations.Length > 0 ? this.Locations[0].Line : -1; + } + } + + public int Column + { + get + { + return this.Locations.Length > 0 ? this.Locations[0].Column : -1; + } + } + } +} diff --git a/src/SMAPI.ModBuildConfig.Analyzer.Tests/Framework/DiagnosticVerifier.Helper.cs b/src/SMAPI.ModBuildConfig.Analyzer.Tests/Framework/DiagnosticVerifier.Helper.cs new file mode 100644 index 00000000..d33455fc --- /dev/null +++ b/src/SMAPI.ModBuildConfig.Analyzer.Tests/Framework/DiagnosticVerifier.Helper.cs @@ -0,0 +1,171 @@ +// +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.CSharp; +using Microsoft.CodeAnalysis.Diagnostics; +using Microsoft.CodeAnalysis.Text; +using System; +using System.Collections.Generic; +using System.Collections.Immutable; +using System.Linq; + +namespace SMAPI.ModBuildConfig.Analyzer.Tests.Framework +{ + /// + /// Class for turning strings into documents and getting the diagnostics on them + /// All methods are static + /// + public abstract partial class DiagnosticVerifier + { + private static readonly MetadataReference CorlibReference = MetadataReference.CreateFromFile(typeof(object).Assembly.Location); + private static readonly MetadataReference SystemCoreReference = MetadataReference.CreateFromFile(typeof(Enumerable).Assembly.Location); + private static readonly MetadataReference CSharpSymbolsReference = MetadataReference.CreateFromFile(typeof(CSharpCompilation).Assembly.Location); + private static readonly MetadataReference CodeAnalysisReference = MetadataReference.CreateFromFile(typeof(Compilation).Assembly.Location); + + internal static string DefaultFilePathPrefix = "Test"; + internal static string CSharpDefaultFileExt = "cs"; + internal static string VisualBasicDefaultExt = "vb"; + internal static string TestProjectName = "TestProject"; + + #region Get Diagnostics + + /// + /// Given classes in the form of strings, their language, and an IDiagnosticAnalyzer to apply to it, return the diagnostics found in the string after converting it to a document. + /// + /// Classes in the form of strings + /// The language the source classes are in + /// The analyzer to be run on the sources + /// An IEnumerable of Diagnostics that surfaced in the source code, sorted by Location + private static Diagnostic[] GetSortedDiagnostics(string[] sources, string language, DiagnosticAnalyzer analyzer) + { + return GetSortedDiagnosticsFromDocuments(analyzer, GetDocuments(sources, language)); + } + + /// + /// Given an analyzer and a document to apply it to, run the analyzer and gather an array of diagnostics found in it. + /// The returned diagnostics are then ordered by location in the source document. + /// + /// The analyzer to run on the documents + /// The Documents that the analyzer will be run on + /// An IEnumerable of Diagnostics that surfaced in the source code, sorted by Location + protected static Diagnostic[] GetSortedDiagnosticsFromDocuments(DiagnosticAnalyzer analyzer, Document[] documents) + { + var projects = new HashSet(); + foreach (var document in documents) + { + projects.Add(document.Project); + } + + var diagnostics = new List(); + foreach (var project in projects) + { + var compilationWithAnalyzers = project.GetCompilationAsync().Result.WithAnalyzers(ImmutableArray.Create(analyzer)); + var diags = compilationWithAnalyzers.GetAnalyzerDiagnosticsAsync().Result; + foreach (var diag in diags) + { + if (diag.Location == Location.None || diag.Location.IsInMetadata) + { + diagnostics.Add(diag); + } + else + { + for (int i = 0; i < documents.Length; i++) + { + var document = documents[i]; + var tree = document.GetSyntaxTreeAsync().Result; + if (tree == diag.Location.SourceTree) + { + diagnostics.Add(diag); + } + } + } + } + } + + var results = SortDiagnostics(diagnostics); + diagnostics.Clear(); + return results; + } + + /// + /// Sort diagnostics by location in source document + /// + /// The list of Diagnostics to be sorted + /// An IEnumerable containing the Diagnostics in order of Location + private static Diagnostic[] SortDiagnostics(IEnumerable diagnostics) + { + return diagnostics.OrderBy(d => d.Location.SourceSpan.Start).ToArray(); + } + + #endregion + + #region Set up compilation and documents + /// + /// Given an array of strings as sources and a language, turn them into a project and return the documents and spans of it. + /// + /// Classes in the form of strings + /// The language the source code is in + /// A Tuple containing the Documents produced from the sources and their TextSpans if relevant + private static Document[] GetDocuments(string[] sources, string language) + { + if (language != LanguageNames.CSharp && language != LanguageNames.VisualBasic) + { + throw new ArgumentException("Unsupported Language"); + } + + var project = CreateProject(sources, language); + var documents = project.Documents.ToArray(); + + if (sources.Length != documents.Length) + { + throw new InvalidOperationException("Amount of sources did not match amount of Documents created"); + } + + return documents; + } + + /// + /// Create a Document from a string through creating a project that contains it. + /// + /// Classes in the form of a string + /// The language the source code is in + /// A Document created from the source string + protected static Document CreateDocument(string source, string language = LanguageNames.CSharp) + { + return CreateProject(new[] { source }, language).Documents.First(); + } + + /// + /// Create a project using the inputted strings as sources. + /// + /// Classes in the form of strings + /// The language the source code is in + /// A Project created out of the Documents created from the source strings + private static Project CreateProject(string[] sources, string language = LanguageNames.CSharp) + { + string fileNamePrefix = DefaultFilePathPrefix; + string fileExt = language == LanguageNames.CSharp ? CSharpDefaultFileExt : VisualBasicDefaultExt; + + var projectId = ProjectId.CreateNewId(debugName: TestProjectName); + + var solution = new AdhocWorkspace() + .CurrentSolution + .AddProject(projectId, TestProjectName, TestProjectName, language) + .AddMetadataReference(projectId, CorlibReference) + .AddMetadataReference(projectId, SystemCoreReference) + .AddMetadataReference(projectId, CSharpSymbolsReference) + .AddMetadataReference(projectId, CodeAnalysisReference); + + int count = 0; + foreach (var source in sources) + { + var newFileName = fileNamePrefix + count + "." + fileExt; + var documentId = DocumentId.CreateNewId(projectId, debugName: newFileName); + solution = solution.AddDocument(documentId, newFileName, SourceText.From(source)); + count++; + } + return solution.GetProject(projectId); + } + #endregion + } +} + diff --git a/src/SMAPI.ModBuildConfig.Analyzer.Tests/Framework/DiagnosticVerifier.cs b/src/SMAPI.ModBuildConfig.Analyzer.Tests/Framework/DiagnosticVerifier.cs new file mode 100644 index 00000000..edaaabd4 --- /dev/null +++ b/src/SMAPI.ModBuildConfig.Analyzer.Tests/Framework/DiagnosticVerifier.cs @@ -0,0 +1,248 @@ +// +using System.Collections.Generic; +using System.Linq; +using System.Text; +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.Diagnostics; +using NUnit.Framework; + +namespace SMAPI.ModBuildConfig.Analyzer.Tests.Framework +{ + /// + /// Superclass of all Unit Tests for DiagnosticAnalyzers + /// + public abstract partial class DiagnosticVerifier + { + #region To be implemented by Test classes + /// + /// Get the CSharp analyzer being tested - to be implemented in non-abstract class + /// + protected virtual DiagnosticAnalyzer GetCSharpDiagnosticAnalyzer() + { + return null; + } + + /// + /// Get the Visual Basic analyzer being tested (C#) - to be implemented in non-abstract class + /// + protected virtual DiagnosticAnalyzer GetBasicDiagnosticAnalyzer() + { + return null; + } + #endregion + + #region Verifier wrappers + + /// + /// Called to test a C# DiagnosticAnalyzer when applied on the single inputted string as a source + /// Note: input a DiagnosticResult for each Diagnostic expected + /// + /// A class in the form of a string to run the analyzer on + /// DiagnosticResults that should appear after the analyzer is run on the source + protected void VerifyCSharpDiagnostic(string source, params DiagnosticResult[] expected) + { + VerifyDiagnostics(new[] { source }, LanguageNames.CSharp, GetCSharpDiagnosticAnalyzer(), expected); + } + + /// + /// Called to test a C# DiagnosticAnalyzer when applied on the inputted strings as a source + /// Note: input a DiagnosticResult for each Diagnostic expected + /// + /// An array of strings to create source documents from to run the analyzers on + /// DiagnosticResults that should appear after the analyzer is run on the sources + protected void VerifyCSharpDiagnostic(string[] sources, params DiagnosticResult[] expected) + { + VerifyDiagnostics(sources, LanguageNames.CSharp, GetCSharpDiagnosticAnalyzer(), expected); + } + + /// + /// General method that gets a collection of actual diagnostics found in the source after the analyzer is run, + /// then verifies each of them. + /// + /// An array of strings to create source documents from to run the analyzers on + /// The language of the classes represented by the source strings + /// The analyzer to be run on the source code + /// DiagnosticResults that should appear after the analyzer is run on the sources + private void VerifyDiagnostics(string[] sources, string language, DiagnosticAnalyzer analyzer, params DiagnosticResult[] expected) + { + var diagnostics = GetSortedDiagnostics(sources, language, analyzer); + VerifyDiagnosticResults(diagnostics, analyzer, expected); + } + + #endregion + + #region Actual comparisons and verifications + /// + /// Checks each of the actual Diagnostics found and compares them with the corresponding DiagnosticResult in the array of expected results. + /// Diagnostics are considered equal only if the DiagnosticResultLocation, Id, Severity, and Message of the DiagnosticResult match the actual diagnostic. + /// + /// The Diagnostics found by the compiler after running the analyzer on the source code + /// The analyzer that was being run on the sources + /// Diagnostic Results that should have appeared in the code + private static void VerifyDiagnosticResults(IEnumerable actualResults, DiagnosticAnalyzer analyzer, params DiagnosticResult[] expectedResults) + { + int expectedCount = expectedResults.Count(); + int actualCount = actualResults.Count(); + + if (expectedCount != actualCount) + { + string diagnosticsOutput = actualResults.Any() ? FormatDiagnostics(analyzer, actualResults.ToArray()) : " NONE."; + + Assert.IsTrue(false, + string.Format("Mismatch between number of diagnostics returned, expected \"{0}\" actual \"{1}\"\r\n\r\nDiagnostics:\r\n{2}\r\n", expectedCount, actualCount, diagnosticsOutput)); + } + + for (int i = 0; i < expectedResults.Length; i++) + { + var actual = actualResults.ElementAt(i); + var expected = expectedResults[i]; + + if (expected.Line == -1 && expected.Column == -1) + { + if (actual.Location != Location.None) + { + Assert.IsTrue(false, + string.Format("Expected:\nA project diagnostic with No location\nActual:\n{0}", + FormatDiagnostics(analyzer, actual))); + } + } + else + { + VerifyDiagnosticLocation(analyzer, actual, actual.Location, expected.Locations.First()); + var additionalLocations = actual.AdditionalLocations.ToArray(); + + if (additionalLocations.Length != expected.Locations.Length - 1) + { + Assert.IsTrue(false, + string.Format("Expected {0} additional locations but got {1} for Diagnostic:\r\n {2}\r\n", + expected.Locations.Length - 1, additionalLocations.Length, + FormatDiagnostics(analyzer, actual))); + } + + for (int j = 0; j < additionalLocations.Length; ++j) + { + VerifyDiagnosticLocation(analyzer, actual, additionalLocations[j], expected.Locations[j + 1]); + } + } + + if (actual.Id != expected.Id) + { + Assert.IsTrue(false, + string.Format("Expected diagnostic id to be \"{0}\" was \"{1}\"\r\n\r\nDiagnostic:\r\n {2}\r\n", + expected.Id, actual.Id, FormatDiagnostics(analyzer, actual))); + } + + if (actual.Severity != expected.Severity) + { + Assert.IsTrue(false, + string.Format("Expected diagnostic severity to be \"{0}\" was \"{1}\"\r\n\r\nDiagnostic:\r\n {2}\r\n", + expected.Severity, actual.Severity, FormatDiagnostics(analyzer, actual))); + } + + if (actual.GetMessage() != expected.Message) + { + Assert.IsTrue(false, + string.Format("Expected diagnostic message to be \"{0}\" was \"{1}\"\r\n\r\nDiagnostic:\r\n {2}\r\n", + expected.Message, actual.GetMessage(), FormatDiagnostics(analyzer, actual))); + } + } + } + + /// + /// Helper method to VerifyDiagnosticResult that checks the location of a diagnostic and compares it with the location in the expected DiagnosticResult. + /// + /// The analyzer that was being run on the sources + /// The diagnostic that was found in the code + /// The Location of the Diagnostic found in the code + /// The DiagnosticResultLocation that should have been found + private static void VerifyDiagnosticLocation(DiagnosticAnalyzer analyzer, Diagnostic diagnostic, Location actual, DiagnosticResultLocation expected) + { + var actualSpan = actual.GetLineSpan(); + + Assert.IsTrue(actualSpan.Path == expected.Path || (actualSpan.Path != null && actualSpan.Path.Contains("Test0.") && expected.Path.Contains("Test.")), + string.Format("Expected diagnostic to be in file \"{0}\" was actually in file \"{1}\"\r\n\r\nDiagnostic:\r\n {2}\r\n", + expected.Path, actualSpan.Path, FormatDiagnostics(analyzer, diagnostic))); + + var actualLinePosition = actualSpan.StartLinePosition; + + // Only check line position if there is an actual line in the real diagnostic + if (actualLinePosition.Line > 0) + { + if (actualLinePosition.Line + 1 != expected.Line) + { + Assert.IsTrue(false, + string.Format("Expected diagnostic to be on line \"{0}\" was actually on line \"{1}\"\r\n\r\nDiagnostic:\r\n {2}\r\n", + expected.Line, actualLinePosition.Line + 1, FormatDiagnostics(analyzer, diagnostic))); + } + } + + // Only check column position if there is an actual column position in the real diagnostic + if (actualLinePosition.Character > 0) + { + if (actualLinePosition.Character + 1 != expected.Column) + { + Assert.IsTrue(false, + string.Format("Expected diagnostic to start at column \"{0}\" was actually at column \"{1}\"\r\n\r\nDiagnostic:\r\n {2}\r\n", + expected.Column, actualLinePosition.Character + 1, FormatDiagnostics(analyzer, diagnostic))); + } + } + } + #endregion + + #region Formatting Diagnostics + /// + /// Helper method to format a Diagnostic into an easily readable string + /// + /// The analyzer that this verifier tests + /// The Diagnostics to be formatted + /// The Diagnostics formatted as a string + private static string FormatDiagnostics(DiagnosticAnalyzer analyzer, params Diagnostic[] diagnostics) + { + var builder