OpenLoco/src/OpenLoco/Windows/StationList.cpp

735 lines
26 KiB
C++

#include "../CompanyManager.h"
#include "../Graphics/Colour.h"
#include "../Graphics/Gfx.h"
#include "../Graphics/ImageIds.h"
#include "../Input.h"
#include "../Interop/Interop.hpp"
#include "../Localisation/FormatArguments.hpp"
#include "../Localisation/StringIds.h"
#include "../Objects/CargoObject.h"
#include "../Objects/CompetitorObject.h"
#include "../Objects/InterfaceSkinObject.h"
#include "../Objects/ObjectManager.h"
#include "../OpenLoco.h"
#include "../StationManager.h"
#include "../TownManager.h"
#include "../Ui/Dropdown.h"
#include "../Ui/WindowManager.h"
#include "../Widget.h"
using namespace OpenLoco::Interop;
namespace OpenLoco::Ui::Windows::StationList
{
static const Ui::Size window_size = { 600, 197 };
static const Ui::Size max_dimensions = { 640, 1200 };
static const Ui::Size min_dimensions = { 192, 100 };
static const uint8_t rowHeight = 10; // CJK: 13
enum widx
{
frame = 0,
caption = 1,
close_button = 2,
panel = 3,
tab_all_stations,
tab_rail_stations,
tab_road_stations,
tab_airports,
tab_ship_ports,
company_select,
sort_name,
sort_status,
sort_total_waiting,
sort_accepts,
scrollview,
};
static Widget _widgets[] = {
makeWidget({ 0, 0 }, { 600, 197 }, WidgetType::frame, WindowColour::primary),
makeWidget({ 1, 1 }, { 598, 13 }, WidgetType::caption_24, WindowColour::primary, StringIds::stringid_all_stations),
makeWidget({ 585, 2 }, { 13, 13 }, WidgetType::wt_9, WindowColour::primary, ImageIds::close_button, StringIds::tooltip_close_window),
makeWidget({ 0, 41 }, { 600, 155 }, WidgetType::panel, WindowColour::secondary),
makeRemapWidget({ 3, 15 }, { 31, 27 }, WidgetType::wt_8, WindowColour::secondary, ImageIds::tab, StringIds::tooltip_all_stations),
makeRemapWidget({ 3, 15 }, { 31, 27 }, WidgetType::wt_8, WindowColour::secondary, ImageIds::tab, StringIds::tooltip_rail_stations),
makeRemapWidget({ 3, 15 }, { 31, 27 }, WidgetType::wt_8, WindowColour::secondary, ImageIds::tab, StringIds::tooltip_road_stations),
makeRemapWidget({ 3, 15 }, { 31, 27 }, WidgetType::wt_8, WindowColour::secondary, ImageIds::tab, StringIds::tooltip_airports),
makeRemapWidget({ 3, 15 }, { 31, 27 }, WidgetType::wt_8, WindowColour::secondary, ImageIds::tab, StringIds::tooltip_ship_ports),
makeWidget({ 0, 14 }, { 26, 26 }, WidgetType::wt_9, WindowColour::primary, StringIds::null, StringIds::tooltip_select_company),
makeWidget({ 4, 43 }, { 200, 12 }, WidgetType::wt_14, WindowColour::secondary, StringIds::null, StringIds::tooltip_sort_by_name),
makeWidget({ 204, 43 }, { 200, 12 }, WidgetType::wt_14, WindowColour::secondary, StringIds::null, StringIds::tooltip_sort_by_station_status),
makeWidget({ 404, 43 }, { 90, 12 }, WidgetType::wt_14, WindowColour::secondary, StringIds::null, StringIds::tooltip_sort_by_total_units_waiting),
makeWidget({ 494, 43 }, { 120, 12 }, WidgetType::wt_14, WindowColour::secondary, StringIds::null, StringIds::tooltip_sort_by_cargo_accepted),
makeWidget({ 3, 56 }, { 594, 126 }, WidgetType::scrollview, WindowColour::secondary, Scrollbars::vertical),
widgetEnd(),
};
static WindowEventList _events;
struct TabDetails
{
widx widgetIndex;
string_id windowTitleId;
uint32_t imageId;
uint16_t stationMask;
};
static TabDetails tabInformationByType[] = {
{ tab_all_stations, StringIds::stringid_all_stations, InterfaceSkin::ImageIds::all_stations, StationFlags::allModes },
{ tab_rail_stations, StringIds::stringid_rail_stations, InterfaceSkin::ImageIds::rail_stations, StationFlags::transportModeRail },
{ tab_road_stations, StringIds::stringid_road_stations, InterfaceSkin::ImageIds::road_stations, StationFlags::transportModeRoad },
{ tab_airports, StringIds::stringid_airports, InterfaceSkin::ImageIds::airports, StationFlags::transportModeAir },
{ tab_ship_ports, StringIds::stringid_ship_ports, InterfaceSkin::ImageIds::ship_ports, StationFlags::transportModeWater }
};
enum SortMode : uint16_t
{
Name,
Status,
TotalUnitsWaiting,
CargoAccepted,
};
loco_global<uint16_t[4], 0x112C826> _common_format_args;
static Ui::CursorId cursor(Window* window, int16_t widgetIdx, int16_t xPos, int16_t yPos, Ui::CursorId fallback);
static void draw(Ui::Window* window, Gfx::Context* context);
static void drawScroll(Ui::Window& window, Gfx::Context& context, const uint32_t scrollIndex);
static void event_08(Window* window);
static void event_09(Window* window);
static void getScrollSize(Ui::Window* window, uint32_t scrollIndex, uint16_t* scrollWidth, uint16_t* scrollHeight);
static void onDropdown(Ui::Window* window, WidgetIndex_t widgetIndex, int16_t itemIndex);
static void onMouseDown(Ui::Window* window, WidgetIndex_t widgetIndex);
static void onMouseUp(Ui::Window* window, WidgetIndex_t widgetIndex);
static void onScrollMouseDown(Ui::Window* window, int16_t x, int16_t y, uint8_t scroll_index);
static void onScrollMouseOver(Ui::Window* window, int16_t x, int16_t y, uint8_t scroll_index);
static void onUpdate(Window* window);
static void prepareDraw(Ui::Window* window);
static std::optional<FormatArguments> tooltip(Ui::Window* window, WidgetIndex_t widgetIndex);
static void initEvents()
{
_events.cursor = cursor;
_events.draw = draw;
_events.draw_scroll = drawScroll;
_events.event_08 = event_08;
_events.event_09 = event_09;
_events.get_scroll_size = getScrollSize;
_events.on_dropdown = onDropdown;
_events.on_mouse_down = onMouseDown;
_events.on_mouse_up = onMouseUp;
_events.on_update = onUpdate;
_events.scroll_mouse_down = onScrollMouseDown;
_events.scroll_mouse_over = onScrollMouseOver;
_events.prepare_draw = prepareDraw;
_events.tooltip = tooltip;
}
// 0x004910E8
static void refreshStationList(Window* window)
{
window->row_count = 0;
for (auto& station : StationManager::stations())
{
if (station.owner == window->number)
{
station.flags &= ~StationFlags::flag_4;
}
}
}
// 0x004911FD
static bool orderByName(const OpenLoco::Station& lhs, const OpenLoco::Station& rhs)
{
char lhsString[256] = { 0 };
StringManager::formatString(lhsString, lhs.name, (void*)&lhs.town);
char rhsString[256] = { 0 };
StringManager::formatString(rhsString, rhs.name, (void*)&rhs.town);
return strcmp(lhsString, rhsString) < 0;
}
// 0x00491281, 0x00491247
static bool orderByQuantity(const OpenLoco::Station& lhs, const OpenLoco::Station& rhs)
{
uint32_t lhsSum = 0;
for (const auto& cargo : lhs.cargo_stats)
{
lhsSum += cargo.quantity;
}
uint32_t rhsSum = 0;
for (const auto& cargo : rhs.cargo_stats)
{
rhsSum += cargo.quantity;
}
return rhsSum < lhsSum;
}
// 0x004912BB
static bool orderByAccepts(const OpenLoco::Station& lhs, const OpenLoco::Station& rhs)
{
char* ptr;
char lhsString[256] = { 0 };
ptr = &lhsString[0];
for (uint32_t cargoId = 0; cargoId < max_cargo_stats; cargoId++)
{
if (lhs.cargo_stats[cargoId].isAccepted())
{
ptr = StringManager::formatString(ptr, ObjectManager::get<CargoObject>(cargoId)->name);
}
}
char rhsString[256] = { 0 };
ptr = &rhsString[0];
for (uint32_t cargoId = 0; cargoId < max_cargo_stats; cargoId++)
{
if (rhs.cargo_stats[cargoId].isAccepted())
{
ptr = StringManager::formatString(ptr, ObjectManager::get<CargoObject>(cargoId)->name);
}
}
return strcmp(lhsString, rhsString) < 0;
}
// 0x004911FD, 0x00491247, 0x00491281, 0x004912BB
static bool getOrder(const SortMode mode, const OpenLoco::Station& lhs, const OpenLoco::Station& rhs)
{
switch (mode)
{
case SortMode::Name:
return orderByName(lhs, rhs);
case SortMode::Status:
case SortMode::TotalUnitsWaiting:
return orderByQuantity(lhs, rhs);
case SortMode::CargoAccepted:
return orderByAccepts(lhs, rhs);
}
return false;
}
// 0x0049111A
static void updateStationList(Window* window)
{
auto edi = -1;
for (auto& station : StationManager::stations())
{
if (station.owner != window->number)
continue;
if ((station.flags & StationFlags::flag_5) != 0)
continue;
const uint16_t mask = tabInformationByType[window->current_tab].stationMask;
if ((station.flags & mask) == 0)
continue;
if ((station.flags & StationFlags::flag_4) != 0)
continue;
if (edi == -1)
{
edi = station.id();
continue;
}
if (getOrder(SortMode(window->sort_mode), station, *StationManager::get(edi)))
{
edi = station.id();
}
}
if (edi != -1)
{
bool dl = false;
StationManager::get(edi)->flags |= StationFlags::flag_4;
auto ebp = window->row_count;
if (edi != window->row_info[ebp])
{
window->row_info[ebp] = edi;
dl = true;
}
window->row_count += 1;
if (window->row_count > window->var_83C)
{
window->var_83C = window->row_count;
dl = true;
}
if (dl)
{
window->invalidate();
}
}
else
{
if (window->var_83C != window->row_count)
{
window->var_83C = window->row_count;
window->invalidate();
}
refreshStationList(window);
}
}
// 0x00490F6C
Window* open(CompanyId_t companyId)
{
Window* window = WindowManager::bringToFront(WindowType::stationList, companyId);
if (window != nullptr)
{
if (Input::isToolActive(window->type, window->number))
Input::toolCancel();
// Still active?
window = WindowManager::bringToFront(WindowType::stationList, companyId);
}
if (window == nullptr)
{
// 0x00491010
window = WindowManager::createWindow(
WindowType::stationList,
window_size,
WindowFlags::flag_11,
&_events);
window->number = companyId;
window->owner = companyId;
window->current_tab = 0;
window->frame_no = 0;
window->sort_mode = 0;
window->var_83C = 0;
window->row_hover = -1;
refreshStationList(window);
window->min_width = min_dimensions.width;
window->min_height = min_dimensions.height;
window->max_width = max_dimensions.width;
window->max_height = max_dimensions.height;
window->flags |= WindowFlags::resizable;
auto interface = ObjectManager::get<InterfaceSkinObject>();
window->setColour(WindowColour::secondary, interface->colour_0A);
}
// TODO: only needs to be called once.
initEvents();
window->current_tab = 0;
window->invalidate();
window->widgets = _widgets;
window->enabled_widgets = (1 << close_button) | (1 << tab_all_stations) | (1 << tab_rail_stations) | (1 << tab_road_stations) | (1 << tab_airports) | (1 << tab_ship_ports) | (1 << company_select) | (1 << sort_name) | (1 << sort_status) | (1 << sort_total_waiting) | (1 << sort_accepts) | (1 << scrollview);
window->activated_widgets = 0;
window->holdable_widgets = 0;
window->callOnResize();
window->callPrepareDraw();
window->initScrollWidgets();
return window;
}
Window* open(CompanyId_t companyId, uint8_t type)
{
if (type > 4)
throw std::domain_error("Unexpected station type");
Window* station_list = open(companyId);
widx target = tabInformationByType[type].widgetIndex;
station_list->callOnMouseUp(target);
return station_list;
}
// 0x004919A4
static Ui::CursorId cursor(Window* window, int16_t widgetIdx, int16_t xPos, int16_t yPos, Ui::CursorId fallback)
{
if (widgetIdx != widx::scrollview)
return fallback;
uint16_t currentIndex = yPos / rowHeight;
if (currentIndex < window->var_83C && window->row_info[currentIndex] != -1)
return CursorId::handPointer;
return fallback;
}
// 0x0049196F
static void event_08(Window* window)
{
window->flags |= WindowFlags::not_scroll_view;
}
// 0x00491977
static void event_09(Window* window)
{
if ((window->flags & WindowFlags::not_scroll_view) == 0)
return;
if (window->row_hover == -1)
return;
window->row_hover = -1;
window->invalidate();
}
// 0x00491344
static void prepareDraw(Ui::Window* window)
{
// Reset active tab.
window->activated_widgets &= ~((1 << tab_all_stations) | (1 << tab_rail_stations) | (1 << tab_road_stations) | (1 << tab_airports) | (1 << tab_ship_ports));
window->activated_widgets |= (1ULL << tabInformationByType[window->current_tab].widgetIndex);
// Set company name.
auto company = CompanyManager::get(window->number);
*_common_format_args = company->name;
// Set window title.
window->widgets[widx::caption].text = tabInformationByType[window->current_tab].windowTitleId;
// Resize general window widgets.
window->widgets[widx::frame].right = window->width - 1;
window->widgets[widx::frame].bottom = window->height - 1;
window->widgets[widx::panel].right = window->width - 1;
window->widgets[widx::panel].bottom = window->height - 1;
window->widgets[widx::caption].right = window->width - 2;
window->widgets[widx::close_button].left = window->width - 15;
window->widgets[widx::close_button].right = window->width - 3;
window->widgets[widx::scrollview].right = window->width - 4;
window->widgets[widx::scrollview].bottom = window->height - 14;
// Reposition header buttons.
window->widgets[widx::sort_name].right = std::min(203, window->width - 4);
window->widgets[widx::sort_status].left = std::min(204, window->width - 4);
window->widgets[widx::sort_status].right = std::min(403, window->width - 4);
window->widgets[widx::sort_total_waiting].left = std::min(404, window->width - 4);
window->widgets[widx::sort_total_waiting].right = std::min(493, window->width - 4);
window->widgets[widx::sort_accepts].left = std::min(494, window->width - 4);
window->widgets[widx::sort_accepts].right = std::min(613, window->width - 4);
// Reposition company selection.
window->widgets[widx::company_select].left = window->width - 28;
window->widgets[widx::company_select].right = window->width - 3;
// Set header button captions.
window->widgets[widx::sort_name].text = window->sort_mode == SortMode::Name ? StringIds::table_header_name_desc : StringIds::table_header_name;
window->widgets[widx::sort_status].text = window->sort_mode == SortMode::Status ? StringIds::table_header_status_desc : StringIds::table_header_status;
window->widgets[widx::sort_total_waiting].text = window->sort_mode == SortMode::TotalUnitsWaiting ? StringIds::table_header_total_waiting_desc : StringIds::table_header_total_waiting;
window->widgets[widx::sort_accepts].text = window->sort_mode == SortMode::CargoAccepted ? StringIds::table_header_accepts_desc : StringIds::table_header_accepts;
// Reposition tabs (0x00491A39 / 0x00491A3F)
int16_t new_tab_x = window->widgets[widx::tab_all_stations].left;
int16_t tab_width = window->widgets[widx::tab_all_stations].right - new_tab_x;
for (auto& tabInfo : tabInformationByType)
{
if (window->isDisabled(tabInfo.widgetIndex))
continue;
Widget& tab = window->widgets[tabInfo.widgetIndex];
tab.left = new_tab_x;
new_tab_x += tab_width;
tab.right = new_tab_x++;
}
}
// 0x0049157F
static void drawScroll(Ui::Window& window, Gfx::Context& context, const uint32_t scrollIndex)
{
auto shade = Colour::getShade(window.getColour(WindowColour::secondary), 4);
Gfx::clearSingle(context, shade);
uint16_t yPos = 0;
for (uint16_t i = 0; i < window.var_83C; i++)
{
StationId_t stationId = window.row_info[i];
// Skip items outside of view, or irrelevant to the current filter.
if (yPos + rowHeight < context.y || yPos >= yPos + rowHeight + context.height || stationId == (uint16_t)-1)
{
yPos += rowHeight;
continue;
}
string_id text_colour_id = StringIds::black_stringid;
// Highlight selection.
if (stationId == window.row_hover)
{
Gfx::drawRect(&context, 0, yPos, window.width, rowHeight, 0x2000030);
text_colour_id = StringIds::wcolour2_stringid;
}
auto station = StationManager::get(stationId);
// First, draw the town name.
_common_format_args[0] = StringIds::stringid_stringid;
_common_format_args[1] = station->name;
_common_format_args[2] = station->town;
_common_format_args[3] = getTransportIconsFromStationFlags(station->flags);
Gfx::drawString_494BBF(context, 0, yPos, 198, Colour::black, text_colour_id, &*_common_format_args);
// Then the station's current status.
char* buffer = const_cast<char*>(StringManager::getString(StringIds::buffer_1250));
station->getStatusString(buffer);
_common_format_args[0] = StringIds::buffer_1250;
Gfx::drawString_494BBF(context, 200, yPos, 198, Colour::black, text_colour_id, &*_common_format_args);
// Total units waiting.
uint16_t totalUnits = 0;
for (const auto& stats : station->cargo_stats)
totalUnits += stats.quantity;
_common_format_args[0] = StringIds::num_units;
*(uint32_t*)&_common_format_args[1] = totalUnits;
Gfx::drawString_494BBF(context, 400, yPos, 88, Colour::black, text_colour_id, &*_common_format_args);
// And, finally, what goods the station accepts.
char* ptr = buffer;
*ptr = '\0';
for (uint32_t cargoId = 0; cargoId < max_cargo_stats; cargoId++)
{
auto& stats = station->cargo_stats[cargoId];
if (!stats.isAccepted())
continue;
if (*buffer != '\0')
ptr = StringManager::formatString(ptr, StringIds::unit_separator);
ptr = StringManager::formatString(ptr, ObjectManager::get<CargoObject>(cargoId)->name);
}
_common_format_args[0] = StringIds::buffer_1250;
Gfx::drawString_494BBF(context, 490, yPos, 118, Colour::black, text_colour_id, &*_common_format_args);
yPos += rowHeight;
}
}
// 00491A76
static void drawTabs(Ui::Window* window, Gfx::Context* context)
{
auto skin = ObjectManager::get<InterfaceSkinObject>();
auto companyColour = CompanyManager::getCompanyColour(window->number);
for (const auto& tab : tabInformationByType)
{
uint32_t image = Gfx::recolour(skin->img + tab.imageId, companyColour);
Widget::drawTab(window, context, image, tab.widgetIndex);
}
}
// 0x004914D8
static void draw(Ui::Window* window, Gfx::Context* context)
{
// Draw widgets and tabs.
window->draw(context);
drawTabs(window, context);
// Draw company owner image.
auto company = CompanyManager::get(window->number);
auto competitor = ObjectManager::get<CompetitorObject>(company->competitor_id);
uint32_t image = Gfx::recolour(competitor->images[company->owner_emotion], company->mainColours.primary);
uint16_t x = window->x + window->widgets[widx::company_select].left + 1;
uint16_t y = window->y + window->widgets[widx::company_select].top + 1;
Gfx::drawImage(context, x, y, image);
// TODO: locale-based pluralisation.
_common_format_args[0] = window->var_83C == 1 ? StringIds::status_num_stations_singular : StringIds::status_num_stations_plural;
_common_format_args[1] = window->var_83C;
// Draw number of stations.
auto origin = Ui::Point(window->x + 4, window->y + window->height - 12);
Gfx::drawString_494B3F(*context, &origin, Colour::black, StringIds::black_stringid, &*_common_format_args);
}
// 0x004917BB
static void onDropdown(Ui::Window* window, WidgetIndex_t widgetIndex, int16_t itemIndex)
{
if (widgetIndex != widx::company_select)
return;
if (itemIndex == -1)
return;
CompanyId_t companyId = Dropdown::getCompanyIdFromSelection(itemIndex);
// Try to find an open station list for this company.
auto companyWindow = WindowManager::bringToFront(WindowType::stationList, companyId);
if (companyWindow != nullptr)
return;
// If not, we'll turn this window into a window for the company selected.
auto company = CompanyManager::get(companyId);
if (company->name == StringIds::empty)
return;
window->number = companyId;
window->owner = companyId;
window->sort_mode = 0;
window->row_count = 0;
refreshStationList(window);
window->var_83C = 0;
window->row_hover = -1;
window->callOnResize();
window->callPrepareDraw();
window->initScrollWidgets();
window->invalidate();
}
// 0x004917B0
static void onMouseDown(Ui::Window* window, WidgetIndex_t widgetIndex)
{
if (widgetIndex == widx::company_select)
Dropdown::populateCompanySelect(window, &window->widgets[widgetIndex]);
}
// 0x00491785
static void onMouseUp(Ui::Window* window, WidgetIndex_t widgetIndex)
{
switch (widgetIndex)
{
case widx::close_button:
WindowManager::close(window);
break;
case tab_all_stations:
case tab_rail_stations:
case tab_road_stations:
case tab_airports:
case tab_ship_ports:
{
if (Input::isToolActive(window->type, window->number))
Input::toolCancel();
window->current_tab = widgetIndex - widx::tab_all_stations;
window->frame_no = 0;
window->invalidate();
window->var_83C = 0;
window->row_hover = -1;
refreshStationList(window);
window->callOnResize();
window->callPrepareDraw();
window->initScrollWidgets();
window->moveInsideScreenEdges();
break;
}
case sort_name:
case sort_status:
case sort_total_waiting:
case sort_accepts:
{
auto sort_mode = widgetIndex - widx::sort_name;
if (window->sort_mode == sort_mode)
return;
window->sort_mode = sort_mode;
window->invalidate();
window->var_83C = 0;
window->row_hover = -1;
refreshStationList(window);
break;
}
}
}
// 0x00491A0C
static void onScrollMouseDown(Ui::Window* window, int16_t x, int16_t y, uint8_t scroll_index)
{
uint16_t currentRow = y / rowHeight;
if (currentRow > window->var_83C)
return;
int16_t currentStation = window->row_info[currentRow];
if (currentStation == -1)
return;
Station::open(currentStation);
}
// 0x004919D1
static void onScrollMouseOver(Ui::Window* window, int16_t x, int16_t y, uint8_t scroll_index)
{
window->flags &= ~(WindowFlags::not_scroll_view);
uint16_t currentRow = y / rowHeight;
int16_t currentStation = -1;
if (currentRow < window->var_83C)
currentStation = window->row_info[currentRow];
if (currentStation == window->row_hover)
return;
window->row_hover = currentStation;
window->invalidate();
}
// 0x0049193F
static void onUpdate(Window* window)
{
window->frame_no++;
window->callPrepareDraw();
WindowManager::invalidateWidget(WindowType::stationList, window->number, window->current_tab + 4);
// Add three stations every tick.
updateStationList(window);
updateStationList(window);
updateStationList(window);
}
// 0x00491999
static void getScrollSize(Ui::Window* window, uint32_t scrollIndex, uint16_t* scrollWidth, uint16_t* scrollHeight)
{
*scrollHeight = rowHeight * window->var_83C;
}
// 0x00491841
static std::optional<FormatArguments> tooltip(Ui::Window* window, WidgetIndex_t widgetIndex)
{
FormatArguments args{};
args.push(StringIds::tooltip_scroll_station_list);
return args;
}
}