History Viewer: Added option to export a save state
This commit is contained in:
parent
accd3e7a0a
commit
10a7928f52
15 changed files with 99 additions and 26 deletions
|
@ -7,6 +7,7 @@
|
|||
#include "NotificationManager.h"
|
||||
#include "RomData.h"
|
||||
#include "MovieRecorder.h"
|
||||
#include "SaveStateManager.h"
|
||||
|
||||
HistoryViewer::HistoryViewer(shared_ptr<Console> console)
|
||||
{
|
||||
|
@ -80,6 +81,23 @@ void HistoryViewer::SeekTo(uint32_t seekPosition)
|
|||
}
|
||||
}
|
||||
|
||||
bool HistoryViewer::CreateSaveState(string outputFile, uint32_t position)
|
||||
{
|
||||
if(position < _history.size()) {
|
||||
std::stringstream stateData;
|
||||
_console->GetSaveStateManager()->GetSaveStateHeader(stateData);
|
||||
_history[position].GetStateData(stateData);
|
||||
|
||||
ofstream output(outputFile, ios::binary);
|
||||
if(output) {
|
||||
output << stateData.rdbuf();
|
||||
output.close();
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
bool HistoryViewer::SaveMovie(string movieFile, uint32_t startPosition, uint32_t endPosition)
|
||||
{
|
||||
//Take a savestate to be able to restore it after generating the movie file
|
||||
|
|
|
@ -27,6 +27,7 @@ public:
|
|||
uint32_t GetPosition();
|
||||
void SeekTo(uint32_t seekPosition);
|
||||
|
||||
bool CreateSaveState(string outputFile, uint32_t position);
|
||||
bool SaveMovie(string movieFile, uint32_t startPosition, uint32_t endPosition);
|
||||
|
||||
void ResumeGameplay(shared_ptr<Console> console, uint32_t resumePosition);
|
||||
|
|
|
@ -607,6 +607,7 @@
|
|||
<Control ID="mnuFile">File</Control>
|
||||
<Control ID="mnuImportMovie">Import Movie</Control>
|
||||
<Control ID="mnuExportMovie">Export Movie</Control>
|
||||
<Control ID="mnuCreateSaveState">Create Save State</Control>
|
||||
<Control ID="mnuResumeGameplay">Resume Gameplay</Control>
|
||||
<Control ID="mnuClose">Close</Control>
|
||||
</Form>
|
||||
|
@ -734,6 +735,7 @@
|
|||
|
||||
<Message ID="MovieSegment">Segment #{0}</Message>
|
||||
<Message ID="MovieSaveError">An error occurred while trying to save the movie file.</Message>
|
||||
<Message ID="FileSaveError">An error occurred while trying to save the file.</Message>
|
||||
|
||||
<Message ID="ClearHistory">Esborra el registre</Message>
|
||||
<Message ID="LastFolderUsed">Darrera carpeta utilitzada</Message>
|
||||
|
|
|
@ -617,6 +617,7 @@
|
|||
<Control ID="mnuFile">File</Control>
|
||||
<Control ID="mnuImportMovie">Import Movie</Control>
|
||||
<Control ID="mnuExportMovie">Export Movie</Control>
|
||||
<Control ID="mnuCreateSaveState">Create Save State</Control>
|
||||
<Control ID="mnuResumeGameplay">Resume Gameplay</Control>
|
||||
<Control ID="mnuClose">Close</Control>
|
||||
</Form>
|
||||
|
@ -764,6 +765,7 @@
|
|||
|
||||
<Message ID="MovieSegment">Segment #{0}</Message>
|
||||
<Message ID="MovieSaveError">An error occurred while trying to save the movie file.</Message>
|
||||
<Message ID="FileSaveError">An error occurred while trying to save the file.</Message>
|
||||
|
||||
<Message ID="ClearHistory">Clear History</Message>
|
||||
<Message ID="LastFolderUsed">Last Folder Used</Message>
|
||||
|
|
|
@ -605,6 +605,7 @@
|
|||
<Control ID="mnuFile">Archivo</Control>
|
||||
<Control ID="mnuImportMovie">Importar película</Control>
|
||||
<Control ID="mnuExportMovie">Exportar película</Control>
|
||||
<Control ID="mnuCreateSaveState">Create Save State</Control>
|
||||
<Control ID="mnuResumeGameplay">Continuar el juego</Control>
|
||||
<Control ID="mnuClose">Cerrar</Control>
|
||||
</Form>
|
||||
|
@ -751,6 +752,7 @@
|
|||
|
||||
<Message ID="MovieSegment">Parte #{0}</Message>
|
||||
<Message ID="MovieSaveError">Se produjo un error al intentar guardar el archivo de película.</Message>
|
||||
<Message ID="FileSaveError">An error occurred while trying to save the file.</Message>
|
||||
|
||||
<Message ID="ClearHistory">Limpiar historial</Message>
|
||||
<Message ID="LastFolderUsed">Última carpeta usada</Message>
|
||||
|
|
|
@ -617,6 +617,7 @@
|
|||
<Control ID="mnuFile">Fichier</Control>
|
||||
<Control ID="mnuImportMovie">Importer un film</Control>
|
||||
<Control ID="mnuExportMovie">Exporter un film</Control>
|
||||
<Control ID="mnuCreateSaveState">Créer une sauvegarde d'état</Control>
|
||||
<Control ID="mnuResumeGameplay">Reprendre le jeu</Control>
|
||||
<Control ID="mnuClose">Fermer</Control>
|
||||
</Form>
|
||||
|
@ -764,6 +765,7 @@
|
|||
|
||||
<Message ID="MovieSegment">Segment #{0}</Message>
|
||||
<Message ID="MovieSaveError">Une erreur est survenue pendant la sauvegarde du film.</Message>
|
||||
<Message ID="FileSaveError">Une erreur est survenue pendant la sauvegarde du fichier.</Message>
|
||||
|
||||
<Message ID="ClearHistory">Effacer l'historique</Message>
|
||||
<Message ID="LastFolderUsed">Dernier dossier utilisé</Message>
|
||||
|
|
|
@ -607,6 +607,7 @@
|
|||
<Control ID="mnuFile">ファイル</Control>
|
||||
<Control ID="mnuImportMovie">動画をインポートする</Control>
|
||||
<Control ID="mnuExportMovie">動画をエクスポートする</Control>
|
||||
<Control ID="mnuCreateSaveState">クイックセーブを作る</Control>
|
||||
<Control ID="mnuResumeGameplay">ここからゲームを再開する</Control>
|
||||
<Control ID="mnuClose">終了</Control>
|
||||
</Form>
|
||||
|
@ -754,6 +755,7 @@
|
|||
|
||||
<Message ID="MovieSegment">パート {0}</Message>
|
||||
<Message ID="MovieSaveError">動画エクスポートは失敗しました。</Message>
|
||||
<Message ID="FileSaveError">ファイルの保存に失敗しました。</Message>
|
||||
|
||||
<Message ID="ClearHistory">履歴を消去</Message>
|
||||
<Message ID="LastFolderUsed">最後に使用したフォルダ</Message>
|
||||
|
|
|
@ -617,6 +617,7 @@
|
|||
<Control ID="mnuFile">Arquivo</Control>
|
||||
<Control ID="mnuImportMovie">Importar vídeo</Control>
|
||||
<Control ID="mnuExportMovie">Exportar vídeo</Control>
|
||||
<Control ID="mnuCreateSaveState">Create Save State</Control>
|
||||
<Control ID="mnuResumeGameplay">Continuar jogo</Control>
|
||||
<Control ID="mnuClose">Fechar</Control>
|
||||
</Form>
|
||||
|
@ -764,7 +765,8 @@
|
|||
|
||||
<Message ID="MovieSegment">Segmento #{0}</Message>
|
||||
<Message ID="MovieSaveError">Ocorreu um erro ao tentar salvar o vídeo.</Message>
|
||||
|
||||
<Message ID="FileSaveError">An error occurred while trying to save the file.</Message>
|
||||
|
||||
<Message ID="ClearHistory">Limpar histórico</Message>
|
||||
<Message ID="LastFolderUsed">Última pasta usada</Message>
|
||||
|
||||
|
|
|
@ -605,6 +605,7 @@
|
|||
<Control ID="mnuFile">File</Control>
|
||||
<Control ID="mnuImportMovie">Import Movie</Control>
|
||||
<Control ID="mnuExportMovie">Export Movie</Control>
|
||||
<Control ID="mnuCreateSaveState">Create Save State</Control>
|
||||
<Control ID="mnuResumeGameplay">Resume Gameplay</Control>
|
||||
<Control ID="mnuClose">Close</Control>
|
||||
</Form>
|
||||
|
@ -752,6 +753,7 @@
|
|||
|
||||
<Message ID="MovieSegment">Segment #{0}</Message>
|
||||
<Message ID="MovieSaveError">An error occurred while trying to save the movie file.</Message>
|
||||
<Message ID="FileSaveError">An error occurred while trying to save the file.</Message>
|
||||
|
||||
<Message ID="ClearHistory">Clear History</Message>
|
||||
<Message ID="LastFolderUsed">Last Folder Used</Message>
|
||||
|
|
|
@ -605,6 +605,7 @@
|
|||
<Control ID="mnuFile">Файл</Control>
|
||||
<Control ID="mnuImportMovie">Імпортувати відео</Control>
|
||||
<Control ID="mnuExportMovie">Експортувати вiдео</Control>
|
||||
<Control ID="mnuCreateSaveState">Create Save State</Control>
|
||||
<Control ID="mnuResumeGameplay">Відновити геймплей</Control>
|
||||
<Control ID="mnuClose">Закрити</Control>
|
||||
</Form>
|
||||
|
@ -752,6 +753,7 @@
|
|||
|
||||
<Message ID="MovieSegment">Сегмент #{0}</Message>
|
||||
<Message ID="MovieSaveError">Сталася помилка під час спроби зберегти відео файл.</Message>
|
||||
<Message ID="FileSaveError">An error occurred while trying to save the file.</Message>
|
||||
|
||||
<Message ID="ClearHistory">Очистити історію</Message>
|
||||
<Message ID="LastFolderUsed">Використовувана остання папка</Message>
|
||||
|
|
|
@ -609,6 +609,7 @@
|
|||
<Control ID="mnuFile">文件</Control>
|
||||
<Control ID="mnuImportMovie">导入影片</Control>
|
||||
<Control ID="mnuExportMovie">导出影片</Control>
|
||||
<Control ID="mnuCreateSaveState">Create Save State</Control>
|
||||
<Control ID="mnuResumeGameplay">继续播放</Control>
|
||||
<Control ID="mnuClose">关闭</Control>
|
||||
</Form>
|
||||
|
@ -755,6 +756,8 @@
|
|||
<Message ID="SaveToFile">保存到文件...</Message>
|
||||
<Message ID="MovieSegment">第 #{0} 段</Message>
|
||||
<Message ID="MovieSaveError">尝试保存电影文件时发生错误.</Message>
|
||||
<Message ID="FileSaveError">An error occurred while trying to save the file.</Message>
|
||||
|
||||
<Message ID="ClearHistory">清除历史</Message>
|
||||
<Message ID="LastFolderUsed">上次使用的文件夹</Message>
|
||||
|
||||
|
|
45
GUI.NET/Forms/frmHistoryViewer.Designer.cs
generated
45
GUI.NET/Forms/frmHistoryViewer.Designer.cs
generated
|
@ -33,6 +33,7 @@
|
|||
this.btnPausePlay = new System.Windows.Forms.Button();
|
||||
this.lblPosition = new System.Windows.Forms.Label();
|
||||
this.pnlRenderer = new System.Windows.Forms.Panel();
|
||||
this.picNsfIcon = new System.Windows.Forms.PictureBox();
|
||||
this.tlpRenderer = new System.Windows.Forms.TableLayoutPanel();
|
||||
this.ctrlRenderer = new System.Windows.Forms.Panel();
|
||||
this.tableLayoutPanel3 = new System.Windows.Forms.TableLayoutPanel();
|
||||
|
@ -44,18 +45,18 @@
|
|||
this.mnuImportMovie = new System.Windows.Forms.ToolStripMenuItem();
|
||||
this.mnuExportMovie = new System.Windows.Forms.ToolStripMenuItem();
|
||||
this.toolStripMenuItem1 = new System.Windows.Forms.ToolStripSeparator();
|
||||
this.mnuCreateSaveState = new System.Windows.Forms.ToolStripMenuItem();
|
||||
this.mnuResumeGameplay = new System.Windows.Forms.ToolStripMenuItem();
|
||||
this.toolStripMenuItem2 = new System.Windows.Forms.ToolStripSeparator();
|
||||
this.mnuClose = new System.Windows.Forms.ToolStripMenuItem();
|
||||
this.picNsfIcon = new System.Windows.Forms.PictureBox();
|
||||
this.tableLayoutPanel1.SuspendLayout();
|
||||
((System.ComponentModel.ISupportInitialize)(this.trkPosition)).BeginInit();
|
||||
this.pnlRenderer.SuspendLayout();
|
||||
((System.ComponentModel.ISupportInitialize)(this.picNsfIcon)).BeginInit();
|
||||
this.tlpRenderer.SuspendLayout();
|
||||
this.tableLayoutPanel3.SuspendLayout();
|
||||
((System.ComponentModel.ISupportInitialize)(this.trkVolume)).BeginInit();
|
||||
this.menuStrip2.SuspendLayout();
|
||||
((System.ComponentModel.ISupportInitialize)(this.picNsfIcon)).BeginInit();
|
||||
this.SuspendLayout();
|
||||
//
|
||||
// tableLayoutPanel1
|
||||
|
@ -126,6 +127,19 @@
|
|||
this.pnlRenderer.Size = new System.Drawing.Size(551, 397);
|
||||
this.pnlRenderer.TabIndex = 0;
|
||||
//
|
||||
// picNsfIcon
|
||||
//
|
||||
this.picNsfIcon.Anchor = System.Windows.Forms.AnchorStyles.None;
|
||||
this.picNsfIcon.BackgroundImage = global::Mesen.GUI.Properties.Resources.NsfBackground;
|
||||
this.picNsfIcon.BackgroundImageLayout = System.Windows.Forms.ImageLayout.Zoom;
|
||||
this.picNsfIcon.Location = new System.Drawing.Point(199, 152);
|
||||
this.picNsfIcon.Margin = new System.Windows.Forms.Padding(0);
|
||||
this.picNsfIcon.MaximumSize = new System.Drawing.Size(500, 90);
|
||||
this.picNsfIcon.Name = "picNsfIcon";
|
||||
this.picNsfIcon.Size = new System.Drawing.Size(150, 90);
|
||||
this.picNsfIcon.TabIndex = 6;
|
||||
this.picNsfIcon.TabStop = false;
|
||||
//
|
||||
// tlpRenderer
|
||||
//
|
||||
this.tlpRenderer.ColumnCount = 1;
|
||||
|
@ -212,13 +226,14 @@
|
|||
this.mnuImportMovie,
|
||||
this.mnuExportMovie,
|
||||
this.toolStripMenuItem1,
|
||||
this.mnuCreateSaveState,
|
||||
this.mnuResumeGameplay,
|
||||
this.toolStripMenuItem2,
|
||||
this.mnuClose});
|
||||
this.mnuFile.Name = "mnuFile";
|
||||
this.mnuFile.Size = new System.Drawing.Size(37, 20);
|
||||
this.mnuFile.Text = "File";
|
||||
this.mnuFile.DropDownOpening += new System.EventHandler(this.fileToolStripMenuItem_DropDownOpening);
|
||||
this.mnuFile.DropDownOpening += new System.EventHandler(this.mnuFile_DropDownOpening);
|
||||
//
|
||||
// mnuImportMovie
|
||||
//
|
||||
|
@ -239,6 +254,14 @@
|
|||
this.toolStripMenuItem1.Name = "toolStripMenuItem1";
|
||||
this.toolStripMenuItem1.Size = new System.Drawing.Size(168, 6);
|
||||
//
|
||||
// mnuCreateSaveState
|
||||
//
|
||||
this.mnuCreateSaveState.Image = global::Mesen.GUI.Properties.Resources.Floppy;
|
||||
this.mnuCreateSaveState.Name = "mnuCreateSaveState";
|
||||
this.mnuCreateSaveState.Size = new System.Drawing.Size(171, 22);
|
||||
this.mnuCreateSaveState.Text = "Create Save State";
|
||||
this.mnuCreateSaveState.Click += new System.EventHandler(this.mnuCreateSaveState_Click);
|
||||
//
|
||||
// mnuResumeGameplay
|
||||
//
|
||||
this.mnuResumeGameplay.Image = global::Mesen.GUI.Properties.Resources.Play;
|
||||
|
@ -260,19 +283,6 @@
|
|||
this.mnuClose.Text = "Close";
|
||||
this.mnuClose.Click += new System.EventHandler(this.mnuClose_Click);
|
||||
//
|
||||
// picNsfIcon
|
||||
//
|
||||
this.picNsfIcon.Anchor = System.Windows.Forms.AnchorStyles.None;
|
||||
this.picNsfIcon.BackgroundImage = global::Mesen.GUI.Properties.Resources.NsfBackground;
|
||||
this.picNsfIcon.BackgroundImageLayout = System.Windows.Forms.ImageLayout.Zoom;
|
||||
this.picNsfIcon.Location = new System.Drawing.Point(199, 152);
|
||||
this.picNsfIcon.Margin = new System.Windows.Forms.Padding(0);
|
||||
this.picNsfIcon.MaximumSize = new System.Drawing.Size(500, 90);
|
||||
this.picNsfIcon.Name = "picNsfIcon";
|
||||
this.picNsfIcon.Size = new System.Drawing.Size(150, 90);
|
||||
this.picNsfIcon.TabIndex = 6;
|
||||
this.picNsfIcon.TabStop = false;
|
||||
//
|
||||
// frmHistoryViewer
|
||||
//
|
||||
this.AutoScaleDimensions = new System.Drawing.SizeF(6F, 13F);
|
||||
|
@ -287,13 +297,13 @@
|
|||
this.tableLayoutPanel1.PerformLayout();
|
||||
((System.ComponentModel.ISupportInitialize)(this.trkPosition)).EndInit();
|
||||
this.pnlRenderer.ResumeLayout(false);
|
||||
((System.ComponentModel.ISupportInitialize)(this.picNsfIcon)).EndInit();
|
||||
this.tlpRenderer.ResumeLayout(false);
|
||||
this.tableLayoutPanel3.ResumeLayout(false);
|
||||
this.tableLayoutPanel3.PerformLayout();
|
||||
((System.ComponentModel.ISupportInitialize)(this.trkVolume)).EndInit();
|
||||
this.menuStrip2.ResumeLayout(false);
|
||||
this.menuStrip2.PerformLayout();
|
||||
((System.ComponentModel.ISupportInitialize)(this.picNsfIcon)).EndInit();
|
||||
this.ResumeLayout(false);
|
||||
this.PerformLayout();
|
||||
|
||||
|
@ -321,5 +331,6 @@
|
|||
private System.Windows.Forms.Label lblVolume;
|
||||
private System.Windows.Forms.TrackBar trkVolume;
|
||||
private System.Windows.Forms.PictureBox picNsfIcon;
|
||||
private System.Windows.Forms.ToolStripMenuItem mnuCreateSaveState;
|
||||
}
|
||||
}
|
|
@ -167,7 +167,7 @@ namespace Mesen.GUI.Forms
|
|||
InteropEmu.HistoryViewerResumeGameplay(InteropEmu.HistoryViewerGetPosition());
|
||||
}
|
||||
|
||||
private void fileToolStripMenuItem_DropDownOpening(object sender, EventArgs e)
|
||||
private void mnuFile_DropDownOpening(object sender, EventArgs e)
|
||||
{
|
||||
mnuExportMovie.DropDownItems.Clear();
|
||||
|
||||
|
@ -186,13 +186,14 @@ namespace Mesen.GUI.Forms
|
|||
string segmentName = ResourceHelper.GetMessage("MovieSegment", (mnuExportMovie.DropDownItems.Count + 1).ToString());
|
||||
ToolStripMenuItem item = new ToolStripMenuItem(segmentName + ", " + start.ToString() + " - " + end.ToString());
|
||||
item.Click += (s, evt) => {
|
||||
SaveFileDialog sfd = new SaveFileDialog();
|
||||
sfd.SetFilter(ResourceHelper.GetMessage("FilterMovie"));
|
||||
sfd.InitialDirectory = ConfigManager.MovieFolder;
|
||||
sfd.FileName = InteropEmu.GetRomInfo().GetRomName() + ".mmo";
|
||||
if(sfd.ShowDialog() == DialogResult.OK) {
|
||||
if(!InteropEmu.HistoryViewerSaveMovie(sfd.FileName, segStart, segEnd)) {
|
||||
MesenMsgBox.Show("MovieSaveError", MessageBoxButtons.OK, MessageBoxIcon.Error);
|
||||
using(SaveFileDialog sfd = new SaveFileDialog()) {
|
||||
sfd.SetFilter(ResourceHelper.GetMessage("FilterMovie"));
|
||||
sfd.InitialDirectory = ConfigManager.MovieFolder;
|
||||
sfd.FileName = InteropEmu.GetRomInfo().GetRomName() + ".mmo";
|
||||
if(sfd.ShowDialog() == DialogResult.OK) {
|
||||
if(!InteropEmu.HistoryViewerSaveMovie(sfd.FileName, segStart, segEnd)) {
|
||||
MesenMsgBox.Show("MovieSaveError", MessageBoxButtons.OK, MessageBoxIcon.Error);
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
@ -204,6 +205,20 @@ namespace Mesen.GUI.Forms
|
|||
mnuImportMovie.Visible = false;
|
||||
mnuExportMovie.Enabled = mnuExportMovie.HasDropDownItems && !_isNsf;
|
||||
}
|
||||
|
||||
private void mnuCreateSaveState_Click(object sender, EventArgs e)
|
||||
{
|
||||
using(SaveFileDialog sfd = new SaveFileDialog()) {
|
||||
sfd.SetFilter(ResourceHelper.GetMessage("FilterSavestate"));
|
||||
sfd.InitialDirectory = ConfigManager.SaveStateFolder;
|
||||
sfd.FileName = InteropEmu.GetRomInfo().GetRomName() + ".mst";
|
||||
if(sfd.ShowDialog() == DialogResult.OK) {
|
||||
if(!InteropEmu.HistoryViewerCreateSaveState(sfd.FileName, InteropEmu.HistoryViewerGetPosition())) {
|
||||
MesenMsgBox.Show("FileSaveError", MessageBoxButtons.OK, MessageBoxIcon.Error);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void btnPausePlay_Click(object sender, EventArgs e)
|
||||
{
|
||||
|
|
|
@ -33,6 +33,7 @@ namespace Mesen.GUI
|
|||
[DllImport(DLLPath)] public static extern void HistoryViewerStop();
|
||||
[DllImport(DLLPath)] public static extern UInt32 HistoryViewerGetHistoryLength();
|
||||
[DllImport(DLLPath)] [return: MarshalAs(UnmanagedType.I1)] public static extern bool HistoryViewerSaveMovie([MarshalAs(UnmanagedType.CustomMarshaler, MarshalTypeRef = typeof(UTF8Marshaler))]string movieFile, UInt32 startPosition, UInt32 endPosition);
|
||||
[DllImport(DLLPath)] [return: MarshalAs(UnmanagedType.I1)] public static extern bool HistoryViewerCreateSaveState([MarshalAs(UnmanagedType.CustomMarshaler, MarshalTypeRef = typeof(UTF8Marshaler))]string outfileFile, UInt32 position);
|
||||
[DllImport(DLLPath)] public static extern void HistoryViewerSetPosition(UInt32 seekPosition);
|
||||
[DllImport(DLLPath)] public static extern void HistoryViewerResumeGameplay(UInt32 seekPosition);
|
||||
[DllImport(DLLPath)] public static extern UInt32 HistoryViewerGetPosition();
|
||||
|
|
|
@ -250,6 +250,14 @@ namespace InteropEmu {
|
|||
}
|
||||
}
|
||||
|
||||
DllExport bool __stdcall HistoryViewerCreateSaveState(const char* outputFile, uint32_t position)
|
||||
{
|
||||
if(_historyConsole) {
|
||||
return _historyConsole->GetHistoryViewer()->CreateSaveState(outputFile, position);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
DllExport bool __stdcall HistoryViewerSaveMovie(const char* movieFile, uint32_t startPosition, uint32_t endPosition)
|
||||
{
|
||||
if(_historyConsole) {
|
||||
|
|
Loading…
Add table
Reference in a new issue