From ed67aedabf8383447bd7f541b139f3c3e00bf536 Mon Sep 17 00:00:00 2001 From: Peter Nelson Date: Tue, 14 May 2024 17:29:59 +0100 Subject: [PATCH] Feature: Allow manually placing town buildings in scenario editor. House picker is accessed from the Landscaping toolbar as there is no town toolbar. Once placed these houses behave like any other and can be removed by players and towns. Uses the unified picker system, so also supports used/saved favourites. As town building don't have class labels, town zones are use to imitate them. --- src/house.h | 2 + src/lang/english.txt | 14 ++ src/newgrf_house.cpp | 9 ++ src/terraform_gui.cpp | 1 + src/toolbar_gui.cpp | 23 +-- src/town.h | 1 + src/town_cmd.cpp | 5 + src/town_gui.cpp | 321 ++++++++++++++++++++++++++++++++++++++++++ src/window_type.h | 6 + 9 files changed, 374 insertions(+), 8 deletions(-) diff --git a/src/house.h b/src/house.h index 63e0344c71..04cddb5c2d 100644 --- a/src/house.h +++ b/src/house.h @@ -136,4 +136,6 @@ inline HouseID GetTranslatedHouseID(HouseID hid) return hs->grf_prop.override == INVALID_HOUSE_ID ? hid : hs->grf_prop.override; } +void ShowBuildHousePicker(struct Window *); + #endif /* HOUSE_H */ diff --git a/src/lang/english.txt b/src/lang/english.txt index 12eed0068a..6d64e76672 100644 --- a/src/lang/english.txt +++ b/src/lang/english.txt @@ -437,6 +437,9 @@ STR_SCENEDIT_FILE_MENU_QUIT_EDITOR :Abandon scenari STR_SCENEDIT_FILE_MENU_SEPARATOR : STR_SCENEDIT_FILE_MENU_QUIT :Exit +STR_SCENEDIT_TOWN_MENU_BUILD_TOWN :Generate towns +STR_SCENEDIT_TOWN_MENU_PACE_HOUSE :Place houses + # Settings menu ###length 16 STR_SETTINGS_MENU_GAME_OPTIONS :Game options @@ -2814,6 +2817,16 @@ STR_PICKER_ROADSTOP_TRUCK_CLASS_TOOLTIP :Select a lorry STR_PICKER_ROADSTOP_TRUCK_TYPE_TOOLTIP :Select a lorry station type to build. Ctrl+Click to add or remove in saved items STR_PICKER_OBJECT_CLASS_TOOLTIP :Select an object class to display STR_PICKER_OBJECT_TYPE_TOOLTIP :Select an object type to build. Ctrl+Click to add or remove in saved items. Ctrl+Click+Drag to select the area diagonally. Also press Shift to show cost estimate only +STR_PICKER_HOUSE_CLASS_TOOLTIP :Select a town zone to display +STR_PICKER_HOUSE_TYPE_TOOLTIP :Select a house type to build. Ctrl+Click to add or remove in saved items + +STR_HOUSE_PICKER_CAPTION :House Selection + +STR_HOUSE_PICKER_CLASS_ZONE1 :Edge +STR_HOUSE_PICKER_CLASS_ZONE2 :Outskirts +STR_HOUSE_PICKER_CLASS_ZONE3 :Outer Suburbs +STR_HOUSE_PICKER_CLASS_ZONE4 :Inner Suburbs +STR_HOUSE_PICKER_CLASS_ZONE5 :Town centre STR_STATION_CLASS_DFLT :Default STR_STATION_CLASS_DFLT_STATION :Default station @@ -5001,6 +5014,7 @@ STR_ERROR_NO_SPACE_FOR_TOWN :{WHITE}... ther STR_ERROR_ROAD_WORKS_IN_PROGRESS :{WHITE}Road works in progress STR_ERROR_TOWN_CAN_T_DELETE :{WHITE}Can't delete this town...{}A station or depot is referring to the town or a town owned tile can't be removed STR_ERROR_STATUE_NO_SUITABLE_PLACE :{WHITE}... there is no suitable place for a statue in the centre of this town +STR_ERROR_CAN_T_BUILD_HOUSE :{WHITE}Can't build house... # Industry related errors STR_ERROR_TOO_MANY_INDUSTRIES :{WHITE}... too many industries diff --git a/src/newgrf_house.cpp b/src/newgrf_house.cpp index 4f98c088b1..cd90684a53 100644 --- a/src/newgrf_house.cpp +++ b/src/newgrf_house.cpp @@ -172,6 +172,15 @@ void InitializeBuildingCounts() } } +/** + * Get read-only span of total HouseID building counts. + * @return span of HouseID building counts. + */ +std::span GetBuildingHouseIDCounts() +{ + return _building_counts.id_count; +} + /** * IncreaseBuildingCount() * Increase the count of a building when it has been added by a town. diff --git a/src/terraform_gui.cpp b/src/terraform_gui.cpp index 417c7fa427..df1d442742 100644 --- a/src/terraform_gui.cpp +++ b/src/terraform_gui.cpp @@ -12,6 +12,7 @@ #include "clear_map.h" #include "company_func.h" #include "company_base.h" +#include "house.h" #include "gui.h" #include "window_gui.h" #include "window_func.h" diff --git a/src/toolbar_gui.cpp b/src/toolbar_gui.cpp index 1f71b10671..77a5b265b9 100644 --- a/src/toolbar_gui.cpp +++ b/src/toolbar_gui.cpp @@ -16,6 +16,7 @@ #include "dropdown_type.h" #include "dropdown_func.h" #include "dropdown_common_type.h" +#include "house.h" #include "vehicle_gui.h" #include "rail_gui.h" #include "road.h" @@ -1211,12 +1212,18 @@ static CallBackFunction ToolbarScenGenLand(Window *w) return CBF_NONE; } - -static CallBackFunction ToolbarScenGenTown(Window *w) +static CallBackFunction ToolbarScenGenTownClick(Window *w) { - w->HandleButtonClick(WID_TE_TOWN_GENERATE); - if (_settings_client.sound.click_beep) SndPlayFx(SND_15_BEEP); - ShowFoundTownWindow(); + PopupMainToolbarMenu(w, WID_TE_TOWN_GENERATE, {STR_SCENEDIT_TOWN_MENU_BUILD_TOWN, STR_SCENEDIT_TOWN_MENU_PACE_HOUSE}); + return CBF_NONE; +} + +static CallBackFunction ToolbarScenGenTown(int index) +{ + switch (index) { + case 0: ShowFoundTownWindow(); break; + case 1: ShowBuildHousePicker(nullptr); break; + } return CBF_NONE; } @@ -2223,7 +2230,7 @@ static MenuClickedProc * const _scen_toolbar_dropdown_procs[] = { nullptr, // 9 nullptr, // 10 nullptr, // 11 - nullptr, // 12 + ToolbarScenGenTown, // 12 nullptr, // 13 ToolbarScenBuildRoad, // 14 ToolbarScenBuildTram, // 15 @@ -2249,7 +2256,7 @@ static ToolbarButtonProc * const _scen_toolbar_button_procs[] = { ToolbarZoomInClick, ToolbarZoomOutClick, ToolbarScenGenLand, - ToolbarScenGenTown, + ToolbarScenGenTownClick, ToolbarScenGenIndustry, ToolbarScenBuildRoadClick, ToolbarScenBuildTramClick, @@ -2376,7 +2383,7 @@ struct ScenarioEditorToolbarWindow : Window { case MTEHK_SETTINGS: ShowGameOptions(); break; case MTEHK_SAVEGAME: MenuClickSaveLoad(); break; case MTEHK_GENLAND: ToolbarScenGenLand(this); break; - case MTEHK_GENTOWN: ToolbarScenGenTown(this); break; + case MTEHK_GENTOWN: ToolbarScenGenTownClick(this); break; case MTEHK_GENINDUSTRY: ToolbarScenGenIndustry(this); break; case MTEHK_BUILD_ROAD: ToolbarScenBuildRoadClick(this); break; case MTEHK_BUILD_TRAM: ToolbarScenBuildTramClick(this); break; diff --git a/src/town.h b/src/town.h index 5034041136..ce930bb180 100644 --- a/src/town.h +++ b/src/town.h @@ -320,5 +320,6 @@ inline uint16_t TownTicksToGameTicks(uint16_t ticks) RoadType GetTownRoadType(); bool CheckTownRoadTypes(); +std::span GetTownDrawTileData(); #endif /* TOWN_H */ diff --git a/src/town_cmd.cpp b/src/town_cmd.cpp index 5a339161f7..192b369ca7 100644 --- a/src/town_cmd.cpp +++ b/src/town_cmd.cpp @@ -4066,3 +4066,8 @@ extern const TileTypeProcs _tile_type_town_procs = { GetFoundation_Town, // get_foundation_proc TerraformTile_Town, // terraform_tile_proc }; + +std::span GetTownDrawTileData() +{ + return _town_draw_tile_data; +} diff --git a/src/town_gui.cpp b/src/town_gui.cpp index a4d9f40761..0b8b39e5c6 100644 --- a/src/town_gui.cpp +++ b/src/town_gui.cpp @@ -12,6 +12,9 @@ #include "viewport_func.h" #include "error.h" #include "gui.h" +#include "house.h" +#include "newgrf_house.h" +#include "picker_gui.h" #include "command_func.h" #include "company_func.h" #include "company_base.h" @@ -1304,3 +1307,321 @@ void InitializeTownGui() { _town_local_authority_kdtree.Clear(); } + +/** + * Draw representation of a house tile for GUI purposes. + * @param x Position x of image. + * @param y Position y of image. + * @param spec House spec to draw. + * @param house_id House ID to draw. + * @param view The house's 'view'. + */ +void DrawNewHouseTileInGUI(int x, int y, const HouseSpec *spec, HouseID house_id, int view) +{ + HouseResolverObject object(house_id, INVALID_TILE, nullptr, CBID_NO_CALLBACK, 0, 0, true, view); + const SpriteGroup *group = object.Resolve(); + if (group == nullptr || group->type != SGT_TILELAYOUT) return; + + uint8_t stage = TOWN_HOUSE_COMPLETED; + const DrawTileSprites *dts = reinterpret_cast(group)->ProcessRegisters(&stage); + + PaletteID palette = GENERAL_SPRITE_COLOUR(spec->random_colour[0]); + if (HasBit(spec->callback_mask, CBM_HOUSE_COLOUR)) { + uint16_t callback = GetHouseCallback(CBID_HOUSE_COLOUR, 0, 0, house_id, nullptr, INVALID_TILE, true, view); + if (callback != CALLBACK_FAILED) { + /* If bit 14 is set, we should use a 2cc colour map, else use the callback value. */ + palette = HasBit(callback, 14) ? GB(callback, 0, 8) + SPR_2CCMAP_BASE : callback; + } + } + + SpriteID image = dts->ground.sprite; + PaletteID pal = dts->ground.pal; + + if (HasBit(image, SPRITE_MODIFIER_CUSTOM_SPRITE)) image += stage; + if (HasBit(pal, SPRITE_MODIFIER_CUSTOM_SPRITE)) pal += stage; + + if (GB(image, 0, SPRITE_WIDTH) != 0) { + DrawSprite(image, GroundSpritePaletteTransform(image, pal, palette), x, y); + } + + DrawNewGRFTileSeqInGUI(x, y, dts, stage, palette); +} + +/** + * Draw a house that does not exist. + * @param x Position x of image. + * @param y Position y of image. + * @param house_id House ID to draw. + * @param view The house's 'view'. + */ +void DrawHouseInGUI(int x, int y, HouseID house_id, int view) +{ + auto draw = [](int x, int y, HouseID house_id, int view) { + if (house_id >= NEW_HOUSE_OFFSET) { + /* Houses don't necessarily need new graphics. If they don't have a + * spritegroup associated with them, then the sprite for the substitute + * house id is drawn instead. */ + const HouseSpec *spec = HouseSpec::Get(house_id); + if (spec->grf_prop.spritegroup[0] != nullptr) { + DrawNewHouseTileInGUI(x, y, spec, house_id, view); + return; + } else { + house_id = HouseSpec::Get(house_id)->grf_prop.subst_id; + } + } + + /* Retrieve data from the draw town tile struct */ + const DrawBuildingsTileStruct &dcts = GetTownDrawTileData()[house_id << 4 | view << 2 | TOWN_HOUSE_COMPLETED]; + DrawSprite(dcts.ground.sprite, dcts.ground.pal, x, y); + + /* Add a house on top of the ground? */ + if (dcts.building.sprite != 0) { + Point pt = RemapCoords(dcts.subtile_x, dcts.subtile_y, 0); + DrawSprite(dcts.building.sprite, dcts.building.pal, x + UnScaleGUI(pt.x), y + UnScaleGUI(pt.y)); + } + }; + + /* Houses can have 1x1, 1x2, 2x1 and 2x2 layouts which are individual HouseIDs. For the GUI we need + * draw all of the tiles with appropriate positions. */ + int x_delta = ScaleGUITrad(TILE_PIXELS); + int y_delta = ScaleGUITrad(TILE_PIXELS / 2); + + const HouseSpec *hs = HouseSpec::Get(house_id); + if (hs->building_flags & TILE_SIZE_2x2) { + draw(x, y - y_delta - y_delta, house_id, view); // North corner. + draw(x + x_delta, y - y_delta, house_id + 1, view); // West corner. + draw(x - x_delta, y - y_delta, house_id + 2, view); // East corner. + draw(x, y, house_id + 3, view); // South corner. + } else if (hs->building_flags & TILE_SIZE_2x1) { + draw(x + x_delta / 2, y - y_delta, house_id, view); // North east tile. + draw(x - x_delta / 2, y, house_id + 1, view); // South west tile. + } else if (hs->building_flags & TILE_SIZE_1x2) { + draw(x - x_delta / 2, y - y_delta, house_id, view); // North west tile. + draw(x + x_delta / 2, y, house_id + 1, view); // South east tile. + } else { + draw(x, y, house_id, view); + } +} + + +class HousePickerCallbacks : public PickerCallbacks { +public: + HousePickerCallbacks() : PickerCallbacks("fav_houses") {} + + /** + * Set climate mask for filtering buildings from current landscape. + */ + void SetClimateMask() + { + switch (_settings_game.game_creation.landscape) { + case LT_TEMPERATE: climate_mask = HZ_TEMP; break; + case LT_ARCTIC: climate_mask = HZ_SUBARTC_ABOVE | HZ_SUBARTC_BELOW; break; + case LT_TROPIC: climate_mask = HZ_SUBTROPIC; break; + case LT_TOYLAND: climate_mask = HZ_TOYLND; break; + default: NOT_REACHED(); + } + } + + HouseZones climate_mask; + + static inline int sel_class; ///< Currently selected 'class'. + static inline int sel_type; ///< Currently selected HouseID. + static inline int sel_view; ///< Currently selected 'view'. This is not controllable as its based on random data. + + /* Houses do not have classes like NewGRFClass. We'll make up fake classes based on town zone + * availability instead. */ + static inline const std::array zone_names = { + STR_HOUSE_PICKER_CLASS_ZONE1, + STR_HOUSE_PICKER_CLASS_ZONE2, + STR_HOUSE_PICKER_CLASS_ZONE3, + STR_HOUSE_PICKER_CLASS_ZONE4, + STR_HOUSE_PICKER_CLASS_ZONE5, + }; + + StringID GetClassTooltip() const override { return STR_PICKER_HOUSE_CLASS_TOOLTIP; } + StringID GetTypeTooltip() const override { return STR_PICKER_HOUSE_TYPE_TOOLTIP; } + bool IsActive() const override { return true; } + + bool HasClassChoice() const override { return true; } + int GetClassCount() const override { return static_cast(zone_names.size()); } + + void Close([[maybe_unused]] int data) override { ResetObjectToPlace(); } + + int GetSelectedClass() const override { return HousePickerCallbacks::sel_class; } + void SetSelectedClass(int cls_id) const override { HousePickerCallbacks::sel_class = cls_id; } + + StringID GetClassName(int id) const override + { + if (id < GetClassCount()) return zone_names[id]; + return INVALID_STRING_ID; + } + + int GetTypeCount(int cls_id) const override + { + if (cls_id < GetClassCount()) return static_cast(HouseSpec::Specs().size()); + return 0; + } + + PickerItem GetPickerItem(int cls_id, int id) const override + { + const auto *spec = HouseSpec::Get(id); + if (spec->grf_prop.grffile == nullptr) return {0, spec->Index(), cls_id, id}; + return {spec->grf_prop.grffile->grfid, spec->grf_prop.local_id, cls_id, id}; + } + + int GetSelectedType() const override { return sel_type; } + void SetSelectedType(int id) const override { sel_type = id; } + + StringID GetTypeName(int cls_id, int id) const override + { + const HouseSpec *spec = HouseSpec::Get(id); + if (spec == nullptr) return INVALID_STRING_ID; + if (!spec->enabled) return INVALID_STRING_ID; + if ((spec->building_availability & climate_mask) == 0) return INVALID_STRING_ID; + if (!HasBit(spec->building_availability, cls_id)) return INVALID_STRING_ID; + for (int i = 0; i < cls_id; i++) { + /* Don't include if it's already included in an earlier zone. */ + if (HasBit(spec->building_availability, i)) return INVALID_STRING_ID; + } + + return spec->building_name; + } + + bool IsTypeAvailable(int, int id) const override + { + const HouseSpec *hs = HouseSpec::Get(id); + if (!hs->enabled) return false; + if (TimerGameCalendar::year < hs->min_year || TimerGameCalendar::year > hs->max_year) return false; + return true; + } + + void DrawType(int x, int y, int, int id) const override + { + DrawHouseInGUI(x, y, id, HousePickerCallbacks::sel_view); + } + + void FillUsedItems(std::set &items) override + { + auto id_count = GetBuildingHouseIDCounts(); + for (auto it = id_count.begin(); it != id_count.end(); ++it) { + if (*it == 0) continue; + HouseID house = static_cast(std::distance(id_count.begin(), it)); + const HouseSpec *hs = HouseSpec::Get(house); + int class_index = FindFirstBit(hs->building_availability & HZ_ZONALL); + items.insert({0, house, class_index, house}); + } + } + + std::set UpdateSavedItems(const std::set &src) override + { + if (src.empty()) return src; + + const auto specs = HouseSpec::Specs(); + std::set dst; + for (const auto &item : src) { + if (item.grfid == 0) { + dst.insert(item); + } else { + /* Search for spec by grfid and local index. */ + auto it = std::find_if(specs.begin(), specs.end(), [&item](const HouseSpec &spec) { return spec.grf_prop.grffile != nullptr && spec.grf_prop.grffile->grfid == item.grfid && spec.grf_prop.local_id == item.local_id; }); + if (it == specs.end()) { + /* Not preset, hide from UI. */ + dst.insert({item.grfid, item.local_id, -1, -1}); + } else { + int class_index = FindFirstBit(it->building_availability & HZ_ZONALL); + dst.insert( {item.grfid, item.local_id, class_index, it->Index()}); + } + } + } + + return dst; + } + + static HousePickerCallbacks instance; +}; +/* static */ HousePickerCallbacks HousePickerCallbacks::instance; + +struct BuildHouseWindow : public PickerWindow { + BuildHouseWindow(WindowDesc *desc, Window *parent) : PickerWindow(desc, parent, 0, HousePickerCallbacks::instance) + { + HousePickerCallbacks::instance.SetClimateMask(); + this->ConstructWindow(); + this->InvalidateData(); + } + + void UpdateSelectSize(const HouseSpec *spec) + { + if (spec == nullptr) { + SetTileSelectSize(1, 1); + ResetObjectToPlace(); + } else { + SetObjectToPlaceWnd(SPR_CURSOR_TOWN, PAL_NONE, HT_RECT | HT_DIAGONAL, this); + if (spec->building_flags & TILE_SIZE_2x2) { + SetTileSelectSize(2, 2); + } else if (spec->building_flags & TILE_SIZE_2x1) { + SetTileSelectSize(2, 1); + } else if (spec->building_flags & TILE_SIZE_1x2) { + SetTileSelectSize(1, 2); + } else if (spec->building_flags & TILE_SIZE_1x1) { + SetTileSelectSize(1, 1); + } + } + } + + void OnInvalidateData(int data = 0, bool gui_scope = true) override + { + this->PickerWindow::OnInvalidateData(data, gui_scope); + if (!gui_scope) return; + + if ((data & PickerWindow::PFI_POSITION) != 0) { + const HouseSpec *spec = HouseSpec::Get(HousePickerCallbacks::sel_type); + UpdateSelectSize(spec); + } + } + + void OnPlaceObject([[maybe_unused]] Point pt, TileIndex tile) override + { + const HouseSpec *spec = HouseSpec::Get(HousePickerCallbacks::sel_type); + Command::Post(STR_ERROR_CAN_T_BUILD_HOUSE, CcPlaySound_CONSTRUCTION_OTHER, tile, spec->Index()); + } + + IntervalTimer view_refresh_interval = {std::chrono::milliseconds(2500), [this](auto) { + /* There are four different 'views' that are random based on house tile position. As this is not + * user-controllable, instead we automatically cycle through them. */ + HousePickerCallbacks::sel_view = (HousePickerCallbacks::sel_view + 1) % 4; + this->SetDirty(); + }}; + + static inline HotkeyList hotkeys{"buildhouse", { + Hotkey('F', "focus_filter_box", PCWHK_FOCUS_FILTER_BOX), + }}; +}; + +/** Nested widget definition for the build NewGRF rail waypoint window */ +static constexpr NWidgetPart _nested_build_house_widgets[] = { + NWidget(NWID_HORIZONTAL), + NWidget(WWT_CLOSEBOX, COLOUR_DARK_GREEN), + NWidget(WWT_CAPTION, COLOUR_DARK_GREEN), SetDataTip(STR_HOUSE_PICKER_CAPTION, STR_TOOLTIP_WINDOW_TITLE_DRAG_THIS), + NWidget(WWT_SHADEBOX, COLOUR_DARK_GREEN), + NWidget(WWT_DEFSIZEBOX, COLOUR_DARK_GREEN), + NWidget(WWT_STICKYBOX, COLOUR_DARK_GREEN), + EndContainer(), + NWidget(NWID_HORIZONTAL), + NWidgetFunction(MakePickerClassWidgets), + NWidgetFunction(MakePickerTypeWidgets), + EndContainer(), +}; + +static WindowDesc _build_house_desc( + WDP_AUTO, "build_house", 0, 0, + WC_BUILD_HOUSE, WC_BUILD_TOOLBAR, + WDF_CONSTRUCTION, + std::begin(_nested_build_house_widgets), std::end(_nested_build_house_widgets), + &BuildHouseWindow::hotkeys +); + +void ShowBuildHousePicker(Window *parent) +{ + if (BringWindowToFrontById(WC_BUILD_HOUSE, 0)) return; + new BuildHouseWindow(&_build_house_desc, parent); +} diff --git a/src/window_type.h b/src/window_type.h index a677c34cdd..0896d5ff6f 100644 --- a/src/window_type.h +++ b/src/window_type.h @@ -375,6 +375,12 @@ enum WindowClass { */ WC_BUILD_OBJECT, + /** + * Build house; %Window numbers: + * - 0 = #BuildHouseWidgets + */ + WC_BUILD_HOUSE, + /** * Build vehicle; %Window numbers: * - #VehicleType = #BuildVehicleWidgets