mirror of
https://github.com/smartcmd/MinecraftConsoles.git
synced 2026-08-20 09:57:09 +00:00
added screenshot functionality
alpha is broken tho
This commit is contained in:
parent
43d520f692
commit
e87fb36bc4
|
|
@ -862,6 +862,7 @@ enum EControllerActions
|
||||||
MINECRAFT_ACTION_INVENTORY,
|
MINECRAFT_ACTION_INVENTORY,
|
||||||
MINECRAFT_ACTION_PAUSEMENU,
|
MINECRAFT_ACTION_PAUSEMENU,
|
||||||
MINECRAFT_ACTION_DROP,
|
MINECRAFT_ACTION_DROP,
|
||||||
|
MINECRAFT_ACTION_DROP_ALL,
|
||||||
MINECRAFT_ACTION_SNEAK_TOGGLE,
|
MINECRAFT_ACTION_SNEAK_TOGGLE,
|
||||||
MINECRAFT_ACTION_CRAFTING,
|
MINECRAFT_ACTION_CRAFTING,
|
||||||
MINECRAFT_ACTION_RENDER_THIRD_PERSON,
|
MINECRAFT_ACTION_RENDER_THIRD_PERSON,
|
||||||
|
|
|
||||||
|
|
@ -20727,6 +20727,7 @@ xcopy /q /y /i /s /e $(ProjectDir)Durango\CU $(LayoutDir)Image\Loose\CU</Comman
|
||||||
</ClInclude>
|
</ClInclude>
|
||||||
<ClInclude Include="Rect2i.h" />
|
<ClInclude Include="Rect2i.h" />
|
||||||
<ClInclude Include="ResourceLocation.h" />
|
<ClInclude Include="ResourceLocation.h" />
|
||||||
|
<ClInclude Include="ScreenshotManager.h" />
|
||||||
<ClInclude Include="ServerCommandDispatcher.h" />
|
<ClInclude Include="ServerCommandDispatcher.h" />
|
||||||
<ClInclude Include="ServerConnection.h" />
|
<ClInclude Include="ServerConnection.h" />
|
||||||
<ClInclude Include="ServerInterface.h" />
|
<ClInclude Include="ServerInterface.h" />
|
||||||
|
|
@ -37951,6 +37952,7 @@ xcopy /q /y /i /s /e $(ProjectDir)Durango\CU $(LayoutDir)Image\Loose\CU</Comman
|
||||||
<ClCompile Include="RedDustParticle.cpp" />
|
<ClCompile Include="RedDustParticle.cpp" />
|
||||||
<ClCompile Include="RenameWorldScreen.cpp" />
|
<ClCompile Include="RenameWorldScreen.cpp" />
|
||||||
<ClCompile Include="Screen.cpp" />
|
<ClCompile Include="Screen.cpp" />
|
||||||
|
<ClCompile Include="ScreenshotManager.cpp" />
|
||||||
<ClCompile Include="ScreenSizeCalculator.cpp" />
|
<ClCompile Include="ScreenSizeCalculator.cpp" />
|
||||||
<ClCompile Include="ScrolledSelectionList.cpp" />
|
<ClCompile Include="ScrolledSelectionList.cpp" />
|
||||||
<ClCompile Include="SelectWorldScreen.cpp" />
|
<ClCompile Include="SelectWorldScreen.cpp" />
|
||||||
|
|
|
||||||
|
|
@ -3793,6 +3793,9 @@
|
||||||
<ClInclude Include="..\include\lce_filesystem\lce_filesystem.h">
|
<ClInclude Include="..\include\lce_filesystem\lce_filesystem.h">
|
||||||
<Filter>Header Files</Filter>
|
<Filter>Header Files</Filter>
|
||||||
</ClInclude>
|
</ClInclude>
|
||||||
|
<ClInclude Include="ScreenshotManager.h">
|
||||||
|
<Filter>Windows64\Source Files</Filter>
|
||||||
|
</ClInclude>
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<ClCompile Include="stdafx.cpp">
|
<ClCompile Include="stdafx.cpp">
|
||||||
|
|
@ -5949,6 +5952,9 @@
|
||||||
<ClCompile Include="..\include\lce_filesystem\lce_filesystem.cpp">
|
<ClCompile Include="..\include\lce_filesystem\lce_filesystem.cpp">
|
||||||
<Filter>include\lce_filesystem</Filter>
|
<Filter>include\lce_filesystem</Filter>
|
||||||
</ClCompile>
|
</ClCompile>
|
||||||
|
<ClCompile Include="ScreenshotManager.cpp">
|
||||||
|
<Filter>Windows64\Source Files</Filter>
|
||||||
|
</ClCompile>
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<Library Include="Xbox\4JLibs\libs\4J_Render_d.lib">
|
<Library Include="Xbox\4JLibs\libs\4J_Render_d.lib">
|
||||||
|
|
|
||||||
|
|
@ -1459,6 +1459,7 @@ void Minecraft::run_middle()
|
||||||
if(InputManager.ButtonPressed(i, ACTION_MENU_GTC_PAUSE)) localplayers[i]->ullButtonsPressed|=1LL<<ACTION_MENU_GTC_PAUSE;
|
if(InputManager.ButtonPressed(i, ACTION_MENU_GTC_PAUSE)) localplayers[i]->ullButtonsPressed|=1LL<<ACTION_MENU_GTC_PAUSE;
|
||||||
#endif
|
#endif
|
||||||
if(InputManager.ButtonPressed(i, MINECRAFT_ACTION_DROP)) localplayers[i]->ullButtonsPressed|=1LL<<MINECRAFT_ACTION_DROP;
|
if(InputManager.ButtonPressed(i, MINECRAFT_ACTION_DROP)) localplayers[i]->ullButtonsPressed|=1LL<<MINECRAFT_ACTION_DROP;
|
||||||
|
if (InputManager.ButtonPressed(i, MINECRAFT_ACTION_DROP_ALL)) localplayers[i]->ullButtonsPressed |= 1LL << MINECRAFT_ACTION_DROP_ALL;
|
||||||
|
|
||||||
// 4J-PB - If we're flying, the sneak needs to be held on to go down
|
// 4J-PB - If we're flying, the sneak needs to be held on to go down
|
||||||
if(localplayers[i]->abilities.flying)
|
if(localplayers[i]->abilities.flying)
|
||||||
|
|
@ -1509,17 +1510,18 @@ void Minecraft::run_middle()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if(g_KBMInput.IsKeyPressed(KeyboardMouseInput::KEY_DROP))
|
if (g_KBMInput.IsKeyPressed(KeyboardMouseInput::KEY_DROP)) {
|
||||||
localplayers[i]->ullButtonsPressed|=1LL<<MINECRAFT_ACTION_DROP;
|
localplayers[i]->ullButtonsPressed |= 1LL << MINECRAFT_ACTION_DROP;
|
||||||
|
|
||||||
if(g_KBMInput.IsKeyPressed(KeyboardMouseInput::KEY_CRAFTING) || g_KBMInput.IsKeyPressed(KeyboardMouseInput::KEY_CRAFTING_ALT))
|
|
||||||
{
|
|
||||||
if((ui.IsSceneInStack(i, eUIScene_Crafting2x2Menu) || ui.IsSceneInStack(i, eUIScene_Crafting3x3Menu) || ui.IsSceneInStack(i, eUIScene_CreativeMenu) || isClosableByEitherKey) && !isEditing)
|
|
||||||
{
|
|
||||||
ui.CloseUIScenes(i);
|
|
||||||
}
|
}
|
||||||
else
|
|
||||||
{
|
if (g_KBMInput.IsKeyPressed(KeyboardMouseInput::KEY_CRAFTING) || g_KBMInput.IsKeyPressed(KeyboardMouseInput::KEY_CRAFTING_ALT)) {
|
||||||
|
if((ui.IsSceneInStack(i, eUIScene_Crafting2x2Menu)
|
||||||
|
|| ui.IsSceneInStack(i, eUIScene_Crafting3x3Menu)
|
||||||
|
|| ui.IsSceneInStack(i, eUIScene_CreativeMenu)
|
||||||
|
|| isClosableByEitherKey)
|
||||||
|
&& !isEditing) {
|
||||||
|
ui.CloseUIScenes(i);
|
||||||
|
} else {
|
||||||
localplayers[i]->ullButtonsPressed|=1LL<<MINECRAFT_ACTION_CRAFTING;
|
localplayers[i]->ullButtonsPressed|=1LL<<MINECRAFT_ACTION_CRAFTING;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
208
Minecraft.Client/ScreenshotManager.cpp
Normal file
208
Minecraft.Client/ScreenshotManager.cpp
Normal file
|
|
@ -0,0 +1,208 @@
|
||||||
|
#include "stdafx.h"
|
||||||
|
#include "ScreenshotManager.h"
|
||||||
|
|
||||||
|
#include <d3d11.h>
|
||||||
|
#include <dxgi.h>
|
||||||
|
#include <wincodec.h>
|
||||||
|
#include <shlobj.h>
|
||||||
|
#include <chrono>
|
||||||
|
#include <iomanip>
|
||||||
|
#include <sstream>
|
||||||
|
#include <vector>
|
||||||
|
|
||||||
|
// link WIC
|
||||||
|
#pragma comment(lib, "windowscodecs.lib")
|
||||||
|
|
||||||
|
// defined in Windows64_Minecraft.cpp
|
||||||
|
extern IDXGISwapChain* g_pSwapChain;
|
||||||
|
extern ID3D11Device* g_pd3dDevice;
|
||||||
|
extern ID3D11DeviceContext* g_pImmediateContext;
|
||||||
|
extern int g_rScreenWidth;
|
||||||
|
extern int g_rScreenHeight;
|
||||||
|
|
||||||
|
// make sure the output foldler exists
|
||||||
|
bool ScreenshotManager::ensureScreenshotsFolder(std::wstring& outPath) {
|
||||||
|
wchar_t exePath[MAX_PATH] = {};
|
||||||
|
if (!GetModuleFileNameW(nullptr, exePath, _countof(exePath))) return false;
|
||||||
|
wchar_t* lastSlash = wcsrchr(exePath, L'\\');
|
||||||
|
if (lastSlash) *(lastSlash + 1) = L'\0';
|
||||||
|
|
||||||
|
std::wstring folder = exePath;
|
||||||
|
folder += L"screenshots";
|
||||||
|
// create if doesn't exist
|
||||||
|
DWORD attrib = GetFileAttributesW(folder.c_str());
|
||||||
|
if (attrib == INVALID_FILE_ATTRIBUTES || !(attrib & FILE_ATTRIBUTE_DIRECTORY)) {
|
||||||
|
if (!CreateDirectoryW(folder.c_str(), nullptr)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
outPath = folder + L"\\";
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// create a formatted name with the current timestamp
|
||||||
|
std::wstring ScreenshotManager::makeTimestampedFilename() {
|
||||||
|
auto now = std::chrono::system_clock::now();
|
||||||
|
auto t = std::chrono::system_clock::to_time_t(now);
|
||||||
|
std::wstringstream ss;
|
||||||
|
ss << L"screenshot_";
|
||||||
|
std::tm tm{};
|
||||||
|
localtime_s(&tm, &t);
|
||||||
|
ss << std::put_time(&tm, L"%Y-%m-%d_%H-%M-%S") << ".png";
|
||||||
|
return ss.str();
|
||||||
|
}
|
||||||
|
|
||||||
|
// takes a screenshot of the current frame
|
||||||
|
bool ScreenshotManager::takeScreenshot(const std::wstring& path) {
|
||||||
|
if (!g_pSwapChain || !g_pd3dDevice || !g_pImmediateContext) return false;
|
||||||
|
|
||||||
|
HRESULT hr = S_OK;
|
||||||
|
ID3D11Texture2D* pBack = nullptr;
|
||||||
|
hr = g_pSwapChain->GetBuffer(0, __uuidof(ID3D11Texture2D), (void**) &pBack);
|
||||||
|
if (FAILED(hr) || !pBack) return false;
|
||||||
|
|
||||||
|
D3D11_TEXTURE2D_DESC desc;
|
||||||
|
pBack->GetDesc(&desc);
|
||||||
|
|
||||||
|
D3D11_TEXTURE2D_DESC stagingDesc = desc;
|
||||||
|
stagingDesc.CPUAccessFlags = D3D11_CPU_ACCESS_READ;
|
||||||
|
stagingDesc.Usage = D3D11_USAGE_STAGING;
|
||||||
|
stagingDesc.BindFlags = 0;
|
||||||
|
stagingDesc.MiscFlags = 0;
|
||||||
|
stagingDesc.SampleDesc.Count = 1;
|
||||||
|
stagingDesc.SampleDesc.Quality = 0;
|
||||||
|
ID3D11Texture2D* pStaging = nullptr;
|
||||||
|
hr = g_pd3dDevice->CreateTexture2D(&stagingDesc, nullptr, &pStaging);
|
||||||
|
|
||||||
|
if (FAILED(hr) || !pStaging) {
|
||||||
|
pBack->Release();
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
g_pImmediateContext->CopyResource(pStaging, pBack);
|
||||||
|
|
||||||
|
// map and read
|
||||||
|
D3D11_MAPPED_SUBRESOURCE mapped;
|
||||||
|
hr = g_pImmediateContext->Map(pStaging, 0, D3D11_MAP_READ, 0, &mapped);
|
||||||
|
if (FAILED(hr)) {
|
||||||
|
pStaging->Release();
|
||||||
|
pBack->Release();
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// prepare WIC
|
||||||
|
IWICImagingFactory* pFactory = nullptr;
|
||||||
|
|
||||||
|
hr = CoInitializeEx(nullptr, COINIT_MULTITHREADED);
|
||||||
|
if (FAILED(hr) && hr != RPC_E_CHANGED_MODE) {
|
||||||
|
g_pImmediateContext->Unmap(pStaging, 0);
|
||||||
|
pStaging->Release();
|
||||||
|
pBack->Release();
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
hr = CoCreateInstance(CLSID_WICImagingFactory, nullptr, CLSCTX_INPROC_SERVER, IID_PPV_ARGS(&pFactory));
|
||||||
|
if (FAILED(hr)) {
|
||||||
|
g_pImmediateContext->Unmap(pStaging, 0);
|
||||||
|
pStaging->Release();
|
||||||
|
pBack->Release();
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
std::wstring outFolder;
|
||||||
|
if (!ensureScreenshotsFolder(outFolder)) {
|
||||||
|
pFactory->Release();
|
||||||
|
g_pImmediateContext->Unmap(pStaging, 0);
|
||||||
|
pStaging->Release();
|
||||||
|
pBack->Release();
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
std::wstring fileName = path;
|
||||||
|
if (fileName.empty()) {
|
||||||
|
fileName = outFolder + makeTimestampedFilename();
|
||||||
|
} else {
|
||||||
|
if (fileName.find(L":\\") == std::wstring::npos) {
|
||||||
|
fileName = outFolder + fileName;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
GUID wicFormat = GUID_WICPixelFormat32bppBGRA;
|
||||||
|
|
||||||
|
// we swizzle R / B because WIC expects BGRA for some fucking reason
|
||||||
|
const UINT width = desc.Width;
|
||||||
|
const UINT height = desc.Height;
|
||||||
|
const UINT srcRowPitch = mapped.RowPitch;
|
||||||
|
const UINT dstStride = width * 4;
|
||||||
|
|
||||||
|
std::vector<BYTE> imageData(dstStride * height);
|
||||||
|
BYTE* dst = imageData.data();
|
||||||
|
BYTE* srcBase = reinterpret_cast<BYTE*>(mapped.pData);
|
||||||
|
for (UINT y = 0; y < height; ++y) {
|
||||||
|
BYTE* src = srcBase + y * srcRowPitch;
|
||||||
|
BYTE* row = dst + y * dstStride;
|
||||||
|
for (UINT x = 0; x < width; ++x) {
|
||||||
|
BYTE r = src[x * 4 + 0];
|
||||||
|
BYTE g = src[x * 4 + 1];
|
||||||
|
BYTE b = src[x * 4 + 2];
|
||||||
|
BYTE a = src[x * 4 + 3];
|
||||||
|
row[x * 4 + 0] = b;
|
||||||
|
row[x * 4 + 1] = g;
|
||||||
|
row[x * 4 + 2] = r;
|
||||||
|
row[x * 4 + 3] = a;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// create WIC bitmap from memory
|
||||||
|
IWICBitmap* pBitmap = nullptr;
|
||||||
|
hr = pFactory->CreateBitmapFromMemory(width, height, wicFormat, dstStride, static_cast<UINT>(imageData.size()), imageData.data(), &pBitmap);
|
||||||
|
if (FAILED(hr)) { pFactory->Release(); g_pImmediateContext->Unmap(pStaging, 0); pStaging->Release(); pBack->Release(); return false; }
|
||||||
|
|
||||||
|
// create encoder
|
||||||
|
IWICStream* pStream = nullptr;
|
||||||
|
IWICBitmapEncoder* pEncoder = nullptr;
|
||||||
|
hr = pFactory->CreateStream(&pStream);
|
||||||
|
if (FAILED(hr)) { pBitmap->Release(); pFactory->Release(); g_pImmediateContext->Unmap(pStaging, 0); pStaging->Release(); pBack->Release(); return false; }
|
||||||
|
|
||||||
|
hr = pStream->InitializeFromFilename(fileName.c_str(), GENERIC_WRITE);
|
||||||
|
if (FAILED(hr)) { pStream->Release(); pBitmap->Release(); pFactory->Release(); g_pImmediateContext->Unmap(pStaging, 0); pStaging->Release(); pBack->Release(); return false; }
|
||||||
|
|
||||||
|
hr = pFactory->CreateEncoder(GUID_ContainerFormatPng, nullptr, &pEncoder);
|
||||||
|
if (FAILED(hr)) { pStream->Release(); pBitmap->Release(); pFactory->Release(); g_pImmediateContext->Unmap(pStaging, 0); pStaging->Release(); pBack->Release(); return false; }
|
||||||
|
|
||||||
|
hr = pEncoder->Initialize(pStream, WICBitmapEncoderNoCache);
|
||||||
|
if (FAILED(hr)) { pEncoder->Release(); pStream->Release(); pBitmap->Release(); pFactory->Release(); g_pImmediateContext->Unmap(pStaging, 0); pStaging->Release(); pBack->Release(); return false; }
|
||||||
|
|
||||||
|
IWICBitmapFrameEncode* pFrame = nullptr;
|
||||||
|
IPropertyBag2* pProps = nullptr;
|
||||||
|
hr = pEncoder->CreateNewFrame(&pFrame, &pProps);
|
||||||
|
if (FAILED(hr)) { pEncoder->Release(); pStream->Release(); pBitmap->Release(); pFactory->Release(); g_pImmediateContext->Unmap(pStaging, 0); pStaging->Release(); pBack->Release(); return false; }
|
||||||
|
|
||||||
|
hr = pFrame->Initialize(pProps);
|
||||||
|
if (FAILED(hr)) { pFrame->Release(); if (pProps) pProps->Release(); pEncoder->Release(); pStream->Release(); pBitmap->Release(); pFactory->Release(); g_pImmediateContext->Unmap(pStaging, 0); pStaging->Release(); pBack->Release(); return false; }
|
||||||
|
|
||||||
|
hr = pFrame->SetSize(width, height);
|
||||||
|
if (FAILED(hr)) { pFrame->Release(); if (pProps) pProps->Release(); pEncoder->Release(); pStream->Release(); pBitmap->Release(); pFactory->Release(); g_pImmediateContext->Unmap(pStaging, 0); pStaging->Release(); pBack->Release(); return false; }
|
||||||
|
|
||||||
|
hr = pFrame->SetPixelFormat(&wicFormat); // prefer BGRA
|
||||||
|
hr = pFrame->WriteSource(pBitmap, nullptr);
|
||||||
|
hr = pFrame->Commit();
|
||||||
|
hr = pEncoder->Commit();
|
||||||
|
|
||||||
|
// cleanup
|
||||||
|
if (pProps) pProps->Release();
|
||||||
|
pFrame->Release();
|
||||||
|
pEncoder->Release();
|
||||||
|
pStream->Release();
|
||||||
|
pBitmap->Release();
|
||||||
|
pFactory->Release();
|
||||||
|
|
||||||
|
g_pImmediateContext->Unmap(pStaging, 0);
|
||||||
|
pStaging->Release();
|
||||||
|
pBack->Release();
|
||||||
|
|
||||||
|
CoUninitialize();
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
16
Minecraft.Client/ScreenshotManager.h
Normal file
16
Minecraft.Client/ScreenshotManager.h
Normal file
|
|
@ -0,0 +1,16 @@
|
||||||
|
#pragma once
|
||||||
|
|
||||||
|
class ScreenshotManager {
|
||||||
|
public:
|
||||||
|
static ScreenshotManager* getInstance() {
|
||||||
|
static ScreenshotManager instance;
|
||||||
|
return &instance;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool ensureScreenshotsFolder(std::wstring& outPath);
|
||||||
|
std::wstring makeTimestampedFilename();
|
||||||
|
bool takeScreenshot(const std::wstring& path = L"");
|
||||||
|
|
||||||
|
private:
|
||||||
|
ScreenshotManager() = default;
|
||||||
|
};
|
||||||
|
|
@ -48,6 +48,7 @@
|
||||||
#include "Network\WinsockNetLayer.h"
|
#include "Network\WinsockNetLayer.h"
|
||||||
#include "Windows64_Xuid.h"
|
#include "Windows64_Xuid.h"
|
||||||
#include "Common/UI/UI.h"
|
#include "Common/UI/UI.h"
|
||||||
|
#include "ScreenshotManager.h"
|
||||||
|
|
||||||
// Forward-declare the internal Renderer class and its global instance from 4J_Render_PC_d.lib.
|
// Forward-declare the internal Renderer class and its global instance from 4J_Render_PC_d.lib.
|
||||||
// C4JRender (RenderManager) is a stateless wrapper — all D3D state lives in InternalRenderManager.
|
// C4JRender (RenderManager) is a stateless wrapper — all D3D state lives in InternalRenderManager.
|
||||||
|
|
@ -1773,6 +1774,16 @@ int APIENTRY _tWinMain(_In_ HINSTANCE hInstance,
|
||||||
app.SetGameSettings(primaryPad, eGameSetting_DisplayHand, displayHud ? 0 : 1);
|
app.SetGameSettings(primaryPad, eGameSetting_DisplayHand, displayHud ? 0 : 1);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// F2 for screenshot - retucio
|
||||||
|
if (g_KBMInput.IsKeyPressed(KeyboardMouseInput::KEY_SCREENSHOT)) {
|
||||||
|
Minecraft* mc = Minecraft::GetInstance();
|
||||||
|
if (ScreenshotManager::getInstance()->takeScreenshot() && mc->player) {
|
||||||
|
mc->gui->addMessage(L"screenshot taken", -1, false);
|
||||||
|
} else if (mc->player) {
|
||||||
|
mc->gui->addMessage(L"failed to take screenshot", -1, false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// F3 toggles onscreen debug info
|
// F3 toggles onscreen debug info
|
||||||
if (g_KBMInput.IsKeyPressed(KeyboardMouseInput::KEY_DEBUG_INFO))
|
if (g_KBMInput.IsKeyPressed(KeyboardMouseInput::KEY_DEBUG_INFO))
|
||||||
{
|
{
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue