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