Console support
This commit is contained in:
parent
0a17a0e6fd
commit
c14ceff074
File diff suppressed because it is too large
Load Diff
|
@ -14,20 +14,9 @@ namespace DllRewrite
|
|||
{
|
||||
MethodPatcher mp = new MethodPatcher();
|
||||
AssemblyDefinition StardewValley = mp.InsertModHooks();
|
||||
TypeDefinition typeModHooksObject = StardewValley.MainModule.GetType("StardewValley.ModHooks");
|
||||
|
||||
TypeDefinition typeObject = StardewValley.MainModule.GetType("StardewValley.Object");
|
||||
//foreach (MethodDefinition method in typeObject.Methods) {
|
||||
// if(!method.IsConstructor && method.HasBody)
|
||||
// {
|
||||
// var processor = method.Body.GetILProcessor();
|
||||
// var hook = typeModHooksObject.Methods.FirstOrDefault(m => m.Name == "OnObject_xxx");
|
||||
// var newInstruction = processor.Create(OpCodes.Callvirt, hook);
|
||||
// var firstInstruction = method.Body.Instructions[0];
|
||||
// processor.InsertBefore(firstInstruction, newInstruction);
|
||||
// }
|
||||
//}
|
||||
StardewValley.Write("./StardewValley.dll");
|
||||
//AssemblyDefinition MonoFramework = mp.InsertMonoHooks();
|
||||
//MonoFramework.Write("./MonoGame.Framework.dll");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -0,0 +1,118 @@
|
|||
using System.Collections.Generic;
|
||||
using System.Reflection;
|
||||
using Microsoft.Xna.Framework;
|
||||
using Microsoft.Xna.Framework.Graphics;
|
||||
using Microsoft.Xna.Framework.Input;
|
||||
using StardewModdingAPI.Framework;
|
||||
using StardewModdingAPI.Internal.ConsoleWriting;
|
||||
using StardewValley;
|
||||
using StardewValley.Menus;
|
||||
|
||||
namespace StardewModdingAPI
|
||||
{
|
||||
class GameConsole : IClickableMenu
|
||||
{
|
||||
public static GameConsole Instance;
|
||||
public bool IsVisible;
|
||||
|
||||
private readonly LinkedList<KeyValuePair<ConsoleLogLevel, string>> _consoleMessageQueue = new LinkedList<KeyValuePair<ConsoleLogLevel, string>>();
|
||||
private readonly TextBox Textbox;
|
||||
private Rectangle TextboxBounds;
|
||||
|
||||
private SpriteFont _smallFont;
|
||||
|
||||
internal GameConsole()
|
||||
{
|
||||
Instance = this;
|
||||
this.IsVisible = true;
|
||||
this.Textbox = new TextBox(null, null, Game1.dialogueFont, Game1.textColor)
|
||||
{
|
||||
X = 0,
|
||||
Y = 0,
|
||||
Width = 1280,
|
||||
Height = 320
|
||||
};
|
||||
this.TextboxBounds = new Rectangle(this.Textbox.X, this.Textbox.Y, this.Textbox.Width, this.Textbox.Height);
|
||||
}
|
||||
|
||||
internal void InitContent(LocalizedContentManager content)
|
||||
{
|
||||
this._smallFont = content.Load<SpriteFont>(@"Fonts\SmallFont");
|
||||
}
|
||||
|
||||
public void Show()
|
||||
{
|
||||
Game1.activeClickableMenu = this;
|
||||
this.IsVisible = true;
|
||||
}
|
||||
|
||||
public override void receiveLeftClick(int x, int y, bool playSound = true)
|
||||
{
|
||||
if (this.TextboxBounds.Contains(x, y))
|
||||
{
|
||||
this.Textbox.OnEnterPressed += sender => { SGame.instance.CommandQueue.Enqueue(sender.Text); this.Textbox.Text = ""; };
|
||||
Game1.keyboardDispatcher.Subscriber = this.Textbox;
|
||||
typeof(TextBox).GetMethod("ShowAndroidKeyboard", BindingFlags.NonPublic | BindingFlags.Instance)?.Invoke(this.Textbox, new object[] { });
|
||||
}
|
||||
else
|
||||
{
|
||||
Game1.activeClickableMenu = null;
|
||||
this.IsVisible = false;
|
||||
}
|
||||
}
|
||||
|
||||
public void WriteLine(string consoleMessage, ConsoleLogLevel level)
|
||||
{
|
||||
lock (this._consoleMessageQueue)
|
||||
{
|
||||
this._consoleMessageQueue.AddFirst(new KeyValuePair<ConsoleLogLevel, string>(level, consoleMessage));
|
||||
if (this._consoleMessageQueue.Count > 2000)
|
||||
{
|
||||
this._consoleMessageQueue.RemoveLast();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public override void draw(SpriteBatch spriteBatch)
|
||||
{
|
||||
Vector2 size = this._smallFont.MeasureString("aA");
|
||||
float y = Game1.game1.screen.Height - size.Y * 2;
|
||||
lock (this._consoleMessageQueue)
|
||||
{
|
||||
foreach (var log in this._consoleMessageQueue)
|
||||
{
|
||||
string text = log.Value;
|
||||
switch (log.Key)
|
||||
{
|
||||
case ConsoleLogLevel.Critical:
|
||||
case ConsoleLogLevel.Error:
|
||||
spriteBatch.DrawString(this._smallFont, text, new Vector2(16, y), Color.Red);
|
||||
break;
|
||||
case ConsoleLogLevel.Alert:
|
||||
case ConsoleLogLevel.Warn:
|
||||
spriteBatch.DrawString(this._smallFont, text, new Vector2(16, y), Color.Orange);
|
||||
break;
|
||||
case ConsoleLogLevel.Info:
|
||||
case ConsoleLogLevel.Success:
|
||||
spriteBatch.DrawString(this._smallFont, text, new Vector2(16, y), Color.AntiqueWhite);
|
||||
break;
|
||||
case ConsoleLogLevel.Debug:
|
||||
case ConsoleLogLevel.Trace:
|
||||
spriteBatch.DrawString(this._smallFont, text, new Vector2(16, y), Color.LightGray);
|
||||
break;
|
||||
default:
|
||||
spriteBatch.DrawString(this._smallFont, text, new Vector2(16, y), Color.LightGray);
|
||||
break;
|
||||
}
|
||||
|
||||
size = this._smallFont.MeasureString(text);
|
||||
if (y < 0)
|
||||
{
|
||||
break;
|
||||
}
|
||||
y -= size.Y;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -426,6 +426,7 @@
|
|||
<Compile Include="Newtonsoft.Json\Utilities\TypeExtensions.cs" />
|
||||
<Compile Include="Newtonsoft.Json\Utilities\ValidationUtils.cs" />
|
||||
<Compile Include="Newtonsoft.Json\WriteState.cs" />
|
||||
<Compile Include="GameConsole.cs" />
|
||||
<Compile Include="Options\ModOptionsCheckbox.cs" />
|
||||
<Compile Include="Options\ModOptionsSlider.cs" />
|
||||
<Compile Include="Properties\AssemblyInfo.cs" />
|
||||
|
|
|
@ -5,6 +5,7 @@ using StardewModdingAPI.Framework;
|
|||
using System.Threading;
|
||||
using Microsoft.Xna.Framework.Graphics;
|
||||
using System.IO;
|
||||
using StardewModdingAPI;
|
||||
using StardewValley.Menus;
|
||||
using StardewValley.Buildings;
|
||||
using StardewValley.Objects;
|
||||
|
@ -21,9 +22,16 @@ namespace SMDroid
|
|||
/// <summary>SMAPI's content manager.</summary>
|
||||
private ContentCoordinator ContentCore { get; set; }
|
||||
|
||||
public static bool ContextInitialize = true;
|
||||
|
||||
public static ModEntry Instance;
|
||||
|
||||
public static bool IsHalt = false;
|
||||
|
||||
public ModEntry()
|
||||
{
|
||||
Instance = this;
|
||||
new GameConsole();
|
||||
this.core = new SCore(Path.Combine(Android.OS.Environment.ExternalStorageDirectory.Path, "SMDroid/Mods"), false);
|
||||
}
|
||||
public override bool OnGame1_CreateContentManager_Prefix(Game1 game1, IServiceProvider serviceProvider, string rootDirectory, ref LocalizedContentManager __result)
|
||||
|
@ -35,7 +43,8 @@ namespace SMDroid
|
|||
this.ContentCore = new ContentCoordinator(serviceProvider, rootDirectory, Thread.CurrentThread.CurrentUICulture, SGame.ConstructorHack.Monitor, SGame.ConstructorHack.Reflection, SGame.ConstructorHack.JsonHelper, SGame.OnLoadingFirstAsset ?? SGame.ConstructorHack?.OnLoadingFirstAsset);
|
||||
this.NextContentManagerIsMain = true;
|
||||
__result = this.ContentCore.CreateGameContentManager("Game1._temporaryContent");
|
||||
this.core.RunInteractively(this.ContentCore);
|
||||
ContextInitialize = true;
|
||||
this.core.RunInteractively(this.ContentCore, __result);
|
||||
return false;
|
||||
}
|
||||
// Game1.content initialising from LoadContent
|
||||
|
@ -43,6 +52,8 @@ namespace SMDroid
|
|||
{
|
||||
this.NextContentManagerIsMain = false;
|
||||
__result = this.ContentCore.MainContentManager;
|
||||
GameConsole.Instance.InitContent(__result);
|
||||
ContextInitialize = false;
|
||||
return false;
|
||||
}
|
||||
|
||||
|
|
|
@ -62,7 +62,7 @@ namespace StardewModdingAPI.Framework.ContentManagers
|
|||
public override T Load<T>(string assetName, LanguageCode language)
|
||||
{
|
||||
// raise first-load callback
|
||||
if (GameContentManager.IsFirstLoad)
|
||||
if (!SMDroid.ModEntry.ContextInitialize && GameContentManager.IsFirstLoad)
|
||||
{
|
||||
GameContentManager.IsFirstLoad = false;
|
||||
this.OnLoadingFirstAsset();
|
||||
|
|
|
@ -146,6 +146,10 @@ namespace StardewModdingAPI.Framework
|
|||
this.ConsoleWriter.WriteLine(consoleMessage, level);
|
||||
});
|
||||
}
|
||||
else if (this.ShowTraceInConsole || level != ConsoleLogLevel.Trace)
|
||||
{
|
||||
GameConsole.Instance.WriteLine(consoleMessage, level);
|
||||
}
|
||||
|
||||
// write to log file
|
||||
this.LogFile.WriteLine(fullMessage);
|
||||
|
|
|
@ -31,11 +31,5 @@ namespace StardewModdingAPI.Framework.RewriteFacades
|
|||
{
|
||||
warpFarmer(locationName, tileX, tileY, facingDirectionAfterWarp, false, true, false);
|
||||
}
|
||||
|
||||
[SuppressMessage("ReSharper", "CS0109", Justification = "The 'new' modifier applies when compiled on Windows.")]
|
||||
public static new void warpFarmer(string locationName, int tileX, int tileY, int facingDirectionAfterWarp, bool isStructure)
|
||||
{
|
||||
warpFarmer(locationName, tileX, tileY, facingDirectionAfterWarp, isStructure, true, false);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -11,10 +11,12 @@ using System.Security;
|
|||
using System.Text;
|
||||
using System.Text.RegularExpressions;
|
||||
using System.Threading;
|
||||
using Microsoft.Xna.Framework.Graphics;
|
||||
#if SMAPI_FOR_WINDOWS
|
||||
using System.Windows.Forms;
|
||||
#endif
|
||||
using Newtonsoft.Json;
|
||||
using SMDroid;
|
||||
using StardewModdingAPI.Events;
|
||||
using StardewModdingAPI.Framework.Events;
|
||||
using StardewModdingAPI.Framework.Exceptions;
|
||||
|
@ -159,6 +161,7 @@ namespace StardewModdingAPI.Framework
|
|||
|
||||
// init logging
|
||||
this.Monitor.Log($"SMAPI {Constants.ApiVersion} with Stardew Valley {Constants.GameVersion} on {EnvironmentUtility.GetFriendlyPlatformName(Constants.Platform)}", LogLevel.Info);
|
||||
this.Monitor.Log($"SMDroid 1.4.0 for Stardew Valley Android release {MainActivity.instance.GetBuild()}", LogLevel.Info);
|
||||
this.Monitor.Log($"Mods go here: {modsPath}", LogLevel.Info);
|
||||
if (modsPath != Constants.DefaultModsPath)
|
||||
this.Monitor.Log("(Using custom --mods-path argument.)", LogLevel.Trace);
|
||||
|
@ -181,7 +184,6 @@ namespace StardewModdingAPI.Framework
|
|||
// add more leniant assembly resolvers
|
||||
AppDomain.CurrentDomain.AssemblyResolve += (sender, e) => AssemblyLoader.ResolveAssembly(e.Name);
|
||||
SGame.ConstructorHack = new SGameConstructorHack(this.Monitor, this.Reflection, this.Toolkit.JsonHelper, this.InitialiseBeforeFirstAssetLoaded);
|
||||
|
||||
// validate platform
|
||||
//#if SMAPI_FOR_WINDOWS
|
||||
// if (Constants.Platform != Platform.Windows)
|
||||
|
@ -202,7 +204,7 @@ namespace StardewModdingAPI.Framework
|
|||
|
||||
/// <summary>Launch SMAPI.</summary>
|
||||
[HandleProcessCorruptedStateExceptions, SecurityCritical] // let try..catch handle corrupted state exceptions
|
||||
public void RunInteractively(ContentCoordinator contentCore)
|
||||
public void RunInteractively(ContentCoordinator contentCore, LocalizedContentManager contentManager)
|
||||
{
|
||||
// initialise SMAPI
|
||||
try
|
||||
|
@ -244,7 +246,6 @@ namespace StardewModdingAPI.Framework
|
|||
new ObjectErrorPatch(),
|
||||
new LoadForNewGamePatch(this.Reflection, this.GameInstance.OnLoadStageChanged)
|
||||
);
|
||||
|
||||
//// add exit handler
|
||||
//new Thread(() =>
|
||||
//{
|
||||
|
@ -309,8 +310,8 @@ namespace StardewModdingAPI.Framework
|
|||
this.Monitor.Log($"You have SMAPI for developers, so the console will be much more verbose. You can disable developer mode by installing the non-developer version of SMAPI, or by editing {Constants.ApiConfigPath}.", LogLevel.Info);
|
||||
if (!this.Settings.CheckForUpdates)
|
||||
this.Monitor.Log($"You configured SMAPI to not check for updates. Running an old version of SMAPI is not recommended. You can enable update checks by reinstalling SMAPI or editing {Constants.ApiConfigPath}.", LogLevel.Warn);
|
||||
if (!this.Monitor.WriteToConsole)
|
||||
this.Monitor.Log("Writing to the terminal is disabled because the --no-terminal argument was received. This usually means launching the terminal failed.", LogLevel.Warn);
|
||||
//if (!this.Monitor.WriteToConsole)
|
||||
// this.Monitor.Log("Writing to the terminal is disabled because the --no-terminal argument was received. This usually means launching the terminal failed.", LogLevel.Warn);
|
||||
this.Monitor.VerboseLog("Verbose logging enabled.");
|
||||
|
||||
// update window titles
|
||||
|
@ -365,46 +366,51 @@ namespace StardewModdingAPI.Framework
|
|||
return;
|
||||
}
|
||||
|
||||
// load mod data
|
||||
ModToolkit toolkit = new ModToolkit();
|
||||
ModDatabase modDatabase = toolkit.GetModDatabase(Constants.ApiMetadataPath);
|
||||
|
||||
// load mods
|
||||
this.GameInstance.IsSuspended = true;
|
||||
new Thread(() =>
|
||||
{
|
||||
this.Monitor.Log("Loading mod metadata...", LogLevel.Trace);
|
||||
ModResolver resolver = new ModResolver();
|
||||
|
||||
// load manifests
|
||||
IModMetadata[] mods = resolver.ReadManifests(toolkit, this.ModsPath, modDatabase).ToArray();
|
||||
|
||||
// filter out ignored mods
|
||||
foreach (IModMetadata mod in mods.Where(p => p.IsIgnored))
|
||||
this.Monitor.Log($" Skipped {mod.RelativeDirectoryPath} (folder name starts with a dot).", LogLevel.Trace);
|
||||
mods = mods.Where(p => !p.IsIgnored).ToArray();
|
||||
// load mod data
|
||||
ModToolkit toolkit = new ModToolkit();
|
||||
ModDatabase modDatabase = toolkit.GetModDatabase(Constants.ApiMetadataPath);
|
||||
|
||||
// load mods
|
||||
resolver.ValidateManifests(mods, Constants.ApiVersion, toolkit.GetUpdateUrl);
|
||||
mods = resolver.ProcessDependencies(mods, modDatabase).ToArray();
|
||||
this.LoadMods(mods, this.Toolkit.JsonHelper, this.ContentCore, modDatabase);
|
||||
|
||||
// write metadata file
|
||||
if (this.Settings.DumpMetadata)
|
||||
{
|
||||
ModFolderExport export = new ModFolderExport
|
||||
this.Monitor.Log("Loading mod metadata...", LogLevel.Trace);
|
||||
ModResolver resolver = new ModResolver();
|
||||
|
||||
// load manifests
|
||||
IModMetadata[] mods = resolver.ReadManifests(toolkit, this.ModsPath, modDatabase).ToArray();
|
||||
|
||||
// filter out ignored mods
|
||||
foreach (IModMetadata mod in mods.Where(p => p.IsIgnored))
|
||||
this.Monitor.Log($" Skipped {mod.RelativeDirectoryPath} (folder name starts with a dot).", LogLevel.Trace);
|
||||
mods = mods.Where(p => !p.IsIgnored).ToArray();
|
||||
|
||||
// load mods
|
||||
resolver.ValidateManifests(mods, Constants.ApiVersion, toolkit.GetUpdateUrl);
|
||||
mods = resolver.ProcessDependencies(mods, modDatabase).ToArray();
|
||||
this.LoadMods(mods, this.Toolkit.JsonHelper, this.ContentCore, modDatabase);
|
||||
|
||||
// write metadata file
|
||||
if (this.Settings.DumpMetadata)
|
||||
{
|
||||
Exported = DateTime.UtcNow.ToString("O"),
|
||||
ApiVersion = Constants.ApiVersion.ToString(),
|
||||
GameVersion = Constants.GameVersion.ToString(),
|
||||
ModFolderPath = this.ModsPath,
|
||||
Mods = mods
|
||||
};
|
||||
this.Toolkit.JsonHelper.WriteJsonFile(Path.Combine(Constants.LogDir, $"{Constants.LogNamePrefix}metadata-dump.json"), export);
|
||||
ModFolderExport export = new ModFolderExport
|
||||
{
|
||||
Exported = DateTime.UtcNow.ToString("O"),
|
||||
ApiVersion = Constants.ApiVersion.ToString(),
|
||||
GameVersion = Constants.GameVersion.ToString(),
|
||||
ModFolderPath = this.ModsPath,
|
||||
Mods = mods
|
||||
};
|
||||
this.Toolkit.JsonHelper.WriteJsonFile(Path.Combine(Constants.LogDir, $"{Constants.LogNamePrefix}metadata-dump.json"), export);
|
||||
}
|
||||
|
||||
// check for updates
|
||||
//this.CheckForUpdatesAsync(mods);
|
||||
}
|
||||
|
||||
// check for updates
|
||||
//this.CheckForUpdatesAsync(mods);
|
||||
}
|
||||
|
||||
GameConsole.Instance.IsVisible = false;
|
||||
this.GameInstance.IsSuspended = false;
|
||||
}).Start();
|
||||
// update window titles
|
||||
//int modsLoaded = this.ModRegistry.GetAll().Count();
|
||||
//Game1.game1.Window.Title = $"Stardew Valley {Constants.GameVersion} - running SMAPI {Constants.ApiVersion} with {modsLoaded} mods";
|
||||
|
|
File diff suppressed because it is too large
Load Diff
|
@ -53,9 +53,9 @@ namespace StardewModdingAPI.Framework.StateTracking
|
|||
/// <summary>Construct an instance.</summary>
|
||||
/// <param name="locations">The game's list of locations.</param>
|
||||
/// <param name="activeMineLocations">The game's list of active mine locations.</param>
|
||||
public WorldLocationsTracker(List<GameLocation> locations, IList<MineShaft> activeMineLocations)
|
||||
public WorldLocationsTracker(ObservableCollection<GameLocation> locations, IList<MineShaft> activeMineLocations)
|
||||
{
|
||||
this.LocationListWatcher = WatcherFactory.ForReferenceList(locations);
|
||||
this.LocationListWatcher = WatcherFactory.ForObservableCollection(locations);
|
||||
this.MineLocationListWatcher = WatcherFactory.ForReferenceList(activeMineLocations);
|
||||
}
|
||||
|
||||
|
|
|
@ -65,7 +65,7 @@ namespace StardewModdingAPI.Framework
|
|||
this.WindowSizeWatcher = WatcherFactory.ForEquatable(() => new Point(Game1.viewport.Width, Game1.viewport.Height));
|
||||
this.TimeWatcher = WatcherFactory.ForEquatable(() => Game1.timeOfDay);
|
||||
this.ActiveMenuWatcher = WatcherFactory.ForReference(() => Game1.activeClickableMenu);
|
||||
this.LocationsWatcher = new WorldLocationsTracker(Game1.locations, MineShaft.activeMines);
|
||||
this.LocationsWatcher = new WorldLocationsTracker((ObservableCollection<GameLocation>) Game1.locations, MineShaft.activeMines);
|
||||
this.LocaleWatcher = WatcherFactory.ForGenericEquality(() => LocalizedContentManager.CurrentLanguageCode);
|
||||
this.Watchers.AddRange(new IWatcher[]
|
||||
{
|
||||
|
|
|
@ -37,8 +37,6 @@ namespace StardewModdingAPI.Metadata
|
|||
// rewrite for Stardew Valley 1.3
|
||||
yield return new StaticFieldToConstantRewriter<int>(typeof(Game1), "tileSize", Game1.tileSize);
|
||||
|
||||
yield return new TypeReferenceRewriter("System.Collections.Generic.IList`1<StardewValley.GameLocation>", typeof(List<GameLocation>));
|
||||
|
||||
yield return new TypeReferenceRewriter("System.Collections.Generic.IList`1<StardewValley.Menus.IClickableMenu>", typeof(List<IClickableMenu>));
|
||||
|
||||
yield return new FieldToPropertyRewriter(typeof(Game1), "player");
|
||||
|
@ -61,11 +59,11 @@ namespace StardewModdingAPI.Metadata
|
|||
|
||||
yield return new FieldToPropertyRewriter(typeof(Game1), "isDebrisWeather");
|
||||
|
||||
yield return new MethodParentRewriter(typeof(IClickableMenu), typeof(IClickableMenuMethods), onlyIfPlatformChanged: true);
|
||||
yield return new MethodParentRewriter(typeof(IClickableMenu), typeof(IClickableMenuMethods));
|
||||
|
||||
yield return new MethodParentRewriter(typeof(Game1), typeof(Game1Methods), onlyIfPlatformChanged: true);
|
||||
yield return new MethodParentRewriter(typeof(Game1), typeof(Game1Methods));
|
||||
|
||||
yield return new MethodParentRewriter(typeof(Farmer), typeof(FarmerMethods), onlyIfPlatformChanged: true);
|
||||
yield return new MethodParentRewriter(typeof(Farmer), typeof(FarmerMethods));
|
||||
|
||||
/****
|
||||
** detect mod issues
|
||||
|
|
|
@ -76,7 +76,7 @@ namespace StardewModdingAPI.Patches
|
|||
if (LoadForNewGamePatch.IsCreating)
|
||||
{
|
||||
// raise CreatedBasicInfo after locations are cleared twice
|
||||
List<GameLocation> locations = Game1.locations;
|
||||
IList<GameLocation> locations = Game1.locations;
|
||||
//locations.CollectionChanged += LoadForNewGamePatch.OnLocationListChanged;
|
||||
}
|
||||
|
||||
|
@ -90,7 +90,7 @@ namespace StardewModdingAPI.Patches
|
|||
if (LoadForNewGamePatch.IsCreating)
|
||||
{
|
||||
// clean up
|
||||
List<GameLocation> locations = Game1.locations;
|
||||
IList<GameLocation> locations = Game1.locations;
|
||||
//locations.CollectionChanged -= LoadForNewGamePatch.OnLocationListChanged;
|
||||
|
||||
// raise stage changed
|
||||
|
|
Loading…
Reference in New Issue