mirror of
https://github.com/k4zmu2a/SpaceCadetPinball.git
synced 2024-11-22 08:50:18 +01:00
Fixed timer.
This commit is contained in:
parent
090beefd07
commit
b412563ee3
11 changed files with 185 additions and 115 deletions
Binary file not shown.
|
@ -158,6 +158,7 @@
|
|||
<ClInclude Include="DatParser.h" />
|
||||
<ClInclude Include="fullscrn.h" />
|
||||
<ClInclude Include="gdrv.h" />
|
||||
<ClInclude Include="high_score.h" />
|
||||
<ClInclude Include="loader.h" />
|
||||
<ClInclude Include="maths.h" />
|
||||
<ClInclude Include="memory.h" />
|
||||
|
@ -219,6 +220,7 @@
|
|||
<ClCompile Include="DatParser.cpp" />
|
||||
<ClCompile Include="fullscrn.cpp" />
|
||||
<ClCompile Include="gdrv.cpp" />
|
||||
<ClCompile Include="high_score.cpp" />
|
||||
<ClCompile Include="loader.cpp" />
|
||||
<ClCompile Include="maths.cpp" />
|
||||
<ClCompile Include="memory.cpp" />
|
||||
|
|
|
@ -210,6 +210,9 @@
|
|||
<ClInclude Include="TTextBoxMessage.h">
|
||||
<Filter>Header Files\TPinballComponent</Filter>
|
||||
</ClInclude>
|
||||
<ClInclude Include="high_score.h">
|
||||
<Filter>Header Files</Filter>
|
||||
</ClInclude>
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<ClCompile Include="pch.cpp">
|
||||
|
@ -386,6 +389,9 @@
|
|||
<ClCompile Include="TTextBoxMessage.cpp">
|
||||
<Filter>Source Files\TPinballComponent</Filter>
|
||||
</ClCompile>
|
||||
<ClCompile Include="high_score.cpp">
|
||||
<Filter>Source Files</Filter>
|
||||
</ClCompile>
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<Natvis Include="NatvisFile.natvis" />
|
||||
|
|
16
SpaceCadetPinball/high_score.cpp
Normal file
16
SpaceCadetPinball/high_score.cpp
Normal file
|
@ -0,0 +1,16 @@
|
|||
#include "pch.h"
|
||||
#include "high_score.h"
|
||||
|
||||
int high_score::read(CHAR* table, int* ptrToSmth)
|
||||
{
|
||||
return 0;
|
||||
}
|
||||
|
||||
int high_score::write(CHAR* table, int* ptrToSmth)
|
||||
{
|
||||
return 0;
|
||||
}
|
||||
|
||||
void high_score::show_high_score_dialog(CHAR* table)
|
||||
{
|
||||
}
|
8
SpaceCadetPinball/high_score.h
Normal file
8
SpaceCadetPinball/high_score.h
Normal file
|
@ -0,0 +1,8 @@
|
|||
#pragma once
|
||||
class high_score
|
||||
{
|
||||
public:
|
||||
static int read(CHAR* table, int* ptrToSmth);
|
||||
static int write(CHAR* table, int* ptrToSmth);
|
||||
static void show_high_score_dialog(CHAR* table);
|
||||
};
|
|
@ -53,14 +53,14 @@ void options::init(HMENU menuHandle)
|
|||
Options.LeftTableBumpKey = get_int(nullptr, "Left Table Bump key", Options.LeftTableBumpKey);
|
||||
Options.RightTableBumpKey = get_int(nullptr, "Right Table Bump key", Options.RightTableBumpKey);
|
||||
Options.BottomTableBumpKey = get_int(nullptr, "Bottom Table Bump key", Options.BottomTableBumpKey);
|
||||
menu_check(0xC9u, Options.Sounds);
|
||||
menu_check(Menu1_Sounds, Options.Sounds);
|
||||
Sound::Enable(0, 7, Options.Sounds);
|
||||
menu_check(0xCAu, Options.Music);
|
||||
menu_check(0x193u, Options.FullScreen);
|
||||
menu_check(0x198u, Options.Players == 1);
|
||||
menu_check(0x199u, Options.Players == 2);
|
||||
menu_check(0x19Au, Options.Players == 3);
|
||||
menu_check(0x19Bu, Options.Players == 4);
|
||||
menu_check(Menu1_Music, Options.Music);
|
||||
menu_check(Menu1_Full_Screen, Options.FullScreen);
|
||||
menu_check(Menu1_1Player, Options.Players == 1);
|
||||
menu_check(Menu1_2Players, Options.Players == 2);
|
||||
menu_check(Menu1_3Players, Options.Players == 3);
|
||||
menu_check(Menu1_4Players, Options.Players == 4);
|
||||
auto tmpBuf = memory::allocate(0x1F4u);
|
||||
if (tmpBuf)
|
||||
{
|
||||
|
@ -247,10 +247,10 @@ void options::toggle(UINT uIDCheckItem)
|
|||
if (uIDCheckItem > 407 && uIDCheckItem <= 411)
|
||||
{
|
||||
Options.Players = uIDCheckItem - 407;
|
||||
menu_check(0x198u, Options.Players == 1);
|
||||
menu_check(0x199u, Options.Players == 2);
|
||||
menu_check(0x19Au, Options.Players == 3);
|
||||
menu_check(0x19Bu, Options.Players == 4);
|
||||
menu_check(Menu1_1Player, Options.Players == 1);
|
||||
menu_check(Menu1_2Players, Options.Players == 2);
|
||||
menu_check(Menu1_3Players, Options.Players == 3);
|
||||
menu_check(Menu1_4Players, Options.Players == 4);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
#include "pch.h"
|
||||
#include "pb.h"
|
||||
|
||||
#include "high_score.h"
|
||||
#include "memory.h"
|
||||
#include "pinball.h"
|
||||
#include "proj.h"
|
||||
|
@ -11,12 +12,14 @@
|
|||
#include "options.h"
|
||||
#include "timer.h"
|
||||
#include "winmain.h"
|
||||
#include "resource.h"
|
||||
|
||||
TPinballTable* pb::MainTable = nullptr;
|
||||
datFileStruct* pb::record_table = nullptr;
|
||||
int pb::time_ticks = 0, pb::demo_mode = 0, pb::cheat_mode = 0, pb::game_mode = 2, pb::mode_countdown_, pb::
|
||||
ball_speed_limit;
|
||||
ball_speed_limit, pb::state;
|
||||
float pb::time_now, pb::time_next;
|
||||
char pb::highscore_table[32];
|
||||
|
||||
int pb::init()
|
||||
{
|
||||
|
@ -58,8 +61,7 @@ int pb::init()
|
|||
}
|
||||
|
||||
render::init(nullptr, zMin, zScaler, tableSize[0], tableSize[1]);
|
||||
gdrv::fill_bitmap(&render::vscreen, render::vscreen.Width, render::vscreen.Height, 0, 0,
|
||||
static_cast<char>(0xff)); // temp
|
||||
gdrv::fill_bitmap(&render::vscreen, render::vscreen.Width, render::vscreen.Height, 0, 0, '\xFF'); // temp
|
||||
gdrv::copy_bitmap(
|
||||
&render::vscreen,
|
||||
backgroundBmp->Width,
|
||||
|
@ -84,7 +86,7 @@ int pb::init()
|
|||
|
||||
MainTable = new TPinballTable();
|
||||
|
||||
//high_score_read(highscore_table, (int)&pb_state);
|
||||
high_score::read(highscore_table, &state);
|
||||
//v11 = *(float*)((char*)MainTable->ListP2.ListPtr->Array[0] + 154);
|
||||
//ball_speed_limit = v11 * 200.0;
|
||||
|
||||
|
@ -97,7 +99,7 @@ int pb::uninit()
|
|||
score::unload_msg_font();
|
||||
loader::unload();
|
||||
partman::unload_records(record_table);
|
||||
//high_score_write(highscore_table, (int)&pb_state);
|
||||
high_score::write(highscore_table, &state);
|
||||
if (MainTable)
|
||||
delete MainTable;
|
||||
MainTable = nullptr;
|
||||
|
@ -128,6 +130,52 @@ void pb::paint()
|
|||
|
||||
void pb::mode_change(int mode)
|
||||
{
|
||||
switch (mode)
|
||||
{
|
||||
case 1:
|
||||
if (demo_mode)
|
||||
{
|
||||
options::menu_set(Menu1_Launch_Ball, 0);
|
||||
options::menu_set(Menu1_High_Scores, 0);
|
||||
options::menu_check(Menu1_Demo, 1);
|
||||
if (MainTable)
|
||||
{
|
||||
/*v2 = MainTable->UnknownP48;
|
||||
if (v2)
|
||||
*(_BYTE*)(v2 + 5) = 1;*/
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
options::menu_set(Menu1_High_Scores, 1);
|
||||
options::menu_set(Menu1_Launch_Ball, 1);
|
||||
options::menu_check(Menu1_Demo, 0);
|
||||
if (MainTable)
|
||||
{
|
||||
/*v1 = MainTable->UnknownP48;
|
||||
if (v1)
|
||||
*(_BYTE*)(v1 + 5) = 0;*/
|
||||
}
|
||||
}
|
||||
break;
|
||||
case 2:
|
||||
options::menu_set(Menu1_Launch_Ball, 0);
|
||||
if (!demo_mode)
|
||||
{
|
||||
options::menu_set(Menu1_High_Scores, 1);
|
||||
options::menu_check(Menu1_Demo, 0);
|
||||
}
|
||||
if (MainTable && MainTable->LightGroup)
|
||||
MainTable->LightGroup->Message(29, 1.4f);
|
||||
break;
|
||||
case 3:
|
||||
case 4:
|
||||
options::menu_set(Menu1_Launch_Ball, 0);
|
||||
options::menu_set(Menu1_High_Scores, 0);
|
||||
mode_countdown_ = 5000;
|
||||
break;
|
||||
}
|
||||
game_mode = mode;
|
||||
}
|
||||
|
||||
void pb::toggle_demo()
|
||||
|
@ -151,8 +199,8 @@ void pb::replay_level(int demoMode)
|
|||
{
|
||||
demo_mode = demoMode;
|
||||
mode_change(1);
|
||||
//if (options::Options.Music)
|
||||
midi::play_pb_theme(0);
|
||||
if (options::Options.Music)
|
||||
midi::play_pb_theme(0);
|
||||
MainTable->Message(1014, static_cast<float>(options::Options.Players));
|
||||
}
|
||||
|
||||
|
@ -388,6 +436,7 @@ int pb::cheat_bump_rank()
|
|||
|
||||
void pb::launch_ball()
|
||||
{
|
||||
MainTable->Plunger->Message(1017, 0.0f);
|
||||
}
|
||||
|
||||
int pb::end_game()
|
||||
|
@ -397,4 +446,5 @@ int pb::end_game()
|
|||
|
||||
void pb::high_scores()
|
||||
{
|
||||
high_score::show_high_score_dialog(highscore_table);
|
||||
}
|
||||
|
|
|
@ -35,4 +35,6 @@ public:
|
|||
private :
|
||||
static int demo_mode, mode_countdown_;
|
||||
static float time_now, time_next;
|
||||
static char highscore_table[32];
|
||||
static int state;
|
||||
};
|
||||
|
|
|
@ -4,128 +4,124 @@
|
|||
#include "memory.h"
|
||||
#include "pb.h"
|
||||
|
||||
timer_struct timer::timerStruct{};
|
||||
int timer::set_count;
|
||||
int timer::SetCount;
|
||||
timer_struct* timer::ActiveList;
|
||||
int timer::MaxCount;
|
||||
int timer::Count;
|
||||
timer_struct* timer::FreeList;
|
||||
timer_struct* timer::TimerBuffer;
|
||||
|
||||
int timer::init(int count)
|
||||
{
|
||||
auto buf = (timer_sub_struct*)memory::allocate(sizeof(timer_sub_struct) * count);
|
||||
timerStruct.TimerMem = buf;
|
||||
auto buf = (timer_struct*)memory::allocate(sizeof(timer_struct) * count);
|
||||
TimerBuffer = buf;
|
||||
if (!buf)
|
||||
return 1;
|
||||
timerStruct.Count = 0;
|
||||
timerStruct.MaxCount = count;
|
||||
set_count = 1;
|
||||
Count = 0;
|
||||
MaxCount = count;
|
||||
SetCount = 1;
|
||||
|
||||
for (int index = 0; index < count - 1; index++)
|
||||
buf[index].NextTimer = &buf[index + 1];
|
||||
buf[count - 1].NextTimer = nullptr;
|
||||
|
||||
timerStruct.NextTimer = nullptr;
|
||||
timerStruct.LastTimer = buf;
|
||||
ActiveList = nullptr;
|
||||
FreeList = buf;
|
||||
return 0;
|
||||
}
|
||||
|
||||
void timer::uninit()
|
||||
{
|
||||
if (timerStruct.TimerMem)
|
||||
memory::free(timerStruct.TimerMem);
|
||||
timerStruct.TimerMem = nullptr;
|
||||
if (TimerBuffer)
|
||||
memory::free(TimerBuffer);
|
||||
TimerBuffer = nullptr;
|
||||
}
|
||||
|
||||
int timer::kill(int timerId)
|
||||
{
|
||||
timer_sub_struct* next = timerStruct.NextTimer;
|
||||
timer_struct* current = ActiveList;
|
||||
int index = 0;
|
||||
timer_sub_struct* current = nullptr;
|
||||
if (timerStruct.Count <= 0)
|
||||
timer_struct* prev = nullptr;
|
||||
if (Count <= 0)
|
||||
return 0;
|
||||
while (timerId != next->TimerId)
|
||||
while (timerId != current->TimerId)
|
||||
{
|
||||
++index;
|
||||
current = next;
|
||||
next = next->NextTimer;
|
||||
if (index >= timerStruct.Count)
|
||||
prev = current;
|
||||
current = current->NextTimer;
|
||||
if (index >= Count)
|
||||
return 0;
|
||||
}
|
||||
if (current)
|
||||
current->NextTimer = next->NextTimer;
|
||||
if (prev)
|
||||
prev->NextTimer = current->NextTimer;
|
||||
else
|
||||
timerStruct.NextTimer = next->NextTimer;
|
||||
ActiveList = current->NextTimer;
|
||||
|
||||
--timerStruct.Count;
|
||||
next->NextTimer = timerStruct.LastTimer;
|
||||
timerStruct.LastTimer = next;
|
||||
--Count;
|
||||
current->NextTimer = FreeList;
|
||||
FreeList = current;
|
||||
return timerId;
|
||||
}
|
||||
|
||||
int timer::set(float time, void* caller, void (* callback)(int, void*))
|
||||
{
|
||||
if (timerStruct.Count >= timerStruct.MaxCount)
|
||||
if (Count >= MaxCount)
|
||||
return 0;
|
||||
|
||||
/*timerStruct.LastTimer->NextTimer = nullptr;
|
||||
timerStruct.LastTimer = timerStruct.LastTimer->NextTimer;*/
|
||||
auto timer = FreeList;
|
||||
FreeList = timer->NextTimer;
|
||||
timer->NextTimer = nullptr;
|
||||
|
||||
auto lastNext = timerStruct.LastTimer->NextTimer;
|
||||
timerStruct.LastTimer->NextTimer = nullptr;
|
||||
timerStruct.LastTimer = lastNext;
|
||||
|
||||
auto prev = timerStruct.NextTimer;
|
||||
auto current = timerStruct.NextTimer;
|
||||
auto prev = ActiveList;
|
||||
auto current = ActiveList;
|
||||
|
||||
auto targetTime = pb::time_ticks + static_cast<int>(time * 1000.0f);
|
||||
for (int index = 0; index < timerStruct.Count && targetTime >= current->TargetTime; ++index)
|
||||
for (int index = 0; index < Count && targetTime >= current->TargetTime; ++index)
|
||||
{
|
||||
prev = current;
|
||||
current = current->NextTimer;
|
||||
}
|
||||
|
||||
auto last = timerStruct.LastTimer;
|
||||
if (current != prev)
|
||||
{
|
||||
timerStruct.LastTimer->NextTimer = prev->NextTimer;
|
||||
prev->NextTimer = last;
|
||||
timer->NextTimer = prev->NextTimer;
|
||||
prev->NextTimer = timer;
|
||||
}
|
||||
else
|
||||
{
|
||||
timerStruct.LastTimer->NextTimer = timerStruct.NextTimer;
|
||||
timerStruct.NextTimer = last;
|
||||
timer->NextTimer = ActiveList;
|
||||
ActiveList = timer;
|
||||
}
|
||||
|
||||
last->Caller = caller;
|
||||
last->Callback = callback;
|
||||
last->TimerId = set_count;
|
||||
last->TargetTime = targetTime;
|
||||
timerStruct.Count++;
|
||||
timer->Caller = caller;
|
||||
timer->Callback = callback;
|
||||
timer->TimerId = SetCount;
|
||||
timer->TargetTime = targetTime;
|
||||
Count++;
|
||||
|
||||
set_count++;
|
||||
if (set_count <= 0)
|
||||
set_count = 1;
|
||||
return last->TimerId;
|
||||
SetCount++;
|
||||
if (SetCount <= 0)
|
||||
SetCount = 1;
|
||||
return timer->TimerId;
|
||||
}
|
||||
|
||||
int timer::check()
|
||||
{
|
||||
timer_sub_struct curCopy{};
|
||||
timer_sub_struct* current = timerStruct.NextTimer;
|
||||
timer_struct* current = ActiveList;
|
||||
int index = 0;
|
||||
if (timerStruct.NextTimer)
|
||||
if (ActiveList)
|
||||
{
|
||||
while (pb::time_ticks >= current->TargetTime)
|
||||
{
|
||||
--timerStruct.Count;
|
||||
memcpy(&curCopy, current, sizeof curCopy);
|
||||
timer_sub_struct** nextPtr = ¤t->NextTimer;
|
||||
current = current->NextTimer;
|
||||
timerStruct.NextTimer = current;
|
||||
*nextPtr = timerStruct.LastTimer;
|
||||
timerStruct.LastTimer = current;
|
||||
if (curCopy.Callback != nullptr)
|
||||
{
|
||||
curCopy.Callback(curCopy.TimerId, curCopy.Caller);
|
||||
current = timerStruct.NextTimer;
|
||||
}
|
||||
--Count;
|
||||
// Advance active list, move current to free
|
||||
ActiveList = current->NextTimer;
|
||||
current->NextTimer = FreeList;
|
||||
FreeList = current;
|
||||
if (current->Callback != nullptr)
|
||||
current->Callback(current->TimerId, current->Caller);
|
||||
|
||||
current = ActiveList;
|
||||
++index;
|
||||
if (index > 1)
|
||||
break;
|
||||
|
@ -134,18 +130,14 @@ int timer::check()
|
|||
}
|
||||
while (current && pb::time_ticks >= current->TargetTime + 100)
|
||||
{
|
||||
--timerStruct.Count;
|
||||
memcpy(&curCopy, current, sizeof curCopy);
|
||||
timer_sub_struct** nextPtr = ¤t->NextTimer;
|
||||
current = current->NextTimer;
|
||||
timerStruct.NextTimer = current;
|
||||
*nextPtr = timerStruct.LastTimer;
|
||||
timerStruct.LastTimer = current;
|
||||
if (curCopy.Callback != nullptr)
|
||||
{
|
||||
curCopy.Callback(curCopy.TimerId, curCopy.Caller);
|
||||
current = timerStruct.NextTimer;
|
||||
}
|
||||
--Count;
|
||||
ActiveList = current->NextTimer;
|
||||
current->NextTimer = FreeList;
|
||||
FreeList = current;
|
||||
if (current->Callback != nullptr)
|
||||
current->Callback(current->TimerId, current->Caller);
|
||||
|
||||
current = ActiveList;
|
||||
++index;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,24 +1,14 @@
|
|||
#pragma once
|
||||
|
||||
struct __declspec(align(4)) timer_sub_struct
|
||||
struct __declspec(align(4)) timer_struct
|
||||
{
|
||||
int TargetTime;
|
||||
void* Caller;
|
||||
void (* Callback)(int, void*);
|
||||
timer_sub_struct* NextTimer;
|
||||
timer_struct* NextTimer;
|
||||
int TimerId;
|
||||
};
|
||||
|
||||
struct __declspec(align(4)) timer_struct
|
||||
{
|
||||
timer_sub_struct* NextTimer;
|
||||
int MaxCount;
|
||||
int Count;
|
||||
timer_sub_struct* LastTimer;
|
||||
timer_sub_struct* TimerMem;
|
||||
};
|
||||
|
||||
|
||||
class timer
|
||||
{
|
||||
public:
|
||||
|
@ -29,6 +19,10 @@ public:
|
|||
static int check();
|
||||
|
||||
private:
|
||||
static timer_struct timerStruct;
|
||||
static int set_count;
|
||||
static int SetCount;
|
||||
static timer_struct* ActiveList;
|
||||
static int MaxCount;
|
||||
static int Count;
|
||||
static timer_struct* FreeList;
|
||||
static timer_struct* TimerBuffer;
|
||||
};
|
||||
|
|
|
@ -131,7 +131,7 @@ int winmain::WinMain(HINSTANCE hInstance, HINSTANCE hPrevInstance, LPSTR lpCmdLi
|
|||
picce.dwSize = 8;
|
||||
picce.dwICC = 5885;
|
||||
InitCommonControlsEx(&picce);
|
||||
|
||||
|
||||
WNDCLASSA WndClass{};
|
||||
WndClass.style = 4104;
|
||||
WndClass.lpfnWndProc = message_handler;
|
||||
|
@ -167,7 +167,7 @@ int winmain::WinMain(HINSTANCE hInstance, HINSTANCE hPrevInstance, LPSTR lpCmdLi
|
|||
if (strstr(lpCmdLine, "-fullscreen"))
|
||||
{
|
||||
options::Options.FullScreen = 1;
|
||||
options::menu_check(0x193u, 1);
|
||||
options::menu_check(Menu1_Full_Screen, 1);
|
||||
}
|
||||
|
||||
ShowWindow(hwnd_frame, nShowCmd);
|
||||
|
@ -331,7 +331,7 @@ LRESULT CALLBACK winmain::message_handler(HWND hWnd, UINT Msg, WPARAM wParam, LP
|
|||
{
|
||||
activated = 0;
|
||||
fullscrn::activate(0);
|
||||
options::menu_check(0x193u, 0);
|
||||
options::menu_check(Menu1_Full_Screen, 0);
|
||||
options::Options.FullScreen = 0;
|
||||
SetThreadPriority(GetCurrentThread(), 0);
|
||||
Sound::Deactivate();
|
||||
|
@ -359,11 +359,11 @@ LRESULT CALLBACK winmain::message_handler(HWND hWnd, UINT Msg, WPARAM wParam, LP
|
|||
|
||||
auto voiceCount = options::get_int(nullptr, "Voices", 8);
|
||||
if (!Sound::Init(hinst, voiceCount, nullptr))
|
||||
options::menu_set(0xC9u, 0);
|
||||
options::menu_set(Menu1_Sounds, 0);
|
||||
Sound::Activate();
|
||||
|
||||
if (!pinball::quickFlag && !midi::music_init(hWnd))
|
||||
options::menu_set(0xCAu, 0);
|
||||
options::menu_set(Menu1_Music, 0);
|
||||
|
||||
if (pb::init())
|
||||
_exit(0);
|
||||
|
@ -432,7 +432,7 @@ LRESULT CALLBACK winmain::message_handler(HWND hWnd, UINT Msg, WPARAM wParam, LP
|
|||
if (fullscrn::displaychange())
|
||||
{
|
||||
options::Options.FullScreen = 0;
|
||||
options::menu_check(0x193u, 0);
|
||||
options::menu_check(Menu1_Full_Screen, 0);
|
||||
}
|
||||
return DefWindowProcA(hWnd, Msg, wParam, lParam);
|
||||
case WM_KEYUP:
|
||||
|
@ -640,7 +640,7 @@ LRESULT CALLBACK winmain::message_handler(HWND hWnd, UINT Msg, WPARAM wParam, LP
|
|||
if (wParam == 4 && options::Options.FullScreen)
|
||||
{
|
||||
options::Options.FullScreen = 0;
|
||||
options::menu_check(0x193u, 0);
|
||||
options::menu_check(Menu1_Full_Screen, 0);
|
||||
fullscrn::set_screen_mode(options::Options.FullScreen);
|
||||
}
|
||||
return DefWindowProcA(hWnd, Msg, wParam, lParam);
|
||||
|
|
Loading…
Reference in a new issue