From a46a3a97f3fe8360bb0e693922f7d925986a2f23 Mon Sep 17 00:00:00 2001 From: Peter Nelson Date: Tue, 24 Jun 2025 07:59:49 +0100 Subject: [PATCH] Change: New company face definition system and UI. (#14319) Bits used by company faces are now defined by a variable system instead of being hardcoded, allowing future expansion. The four face types covering gender and skin colour are now separate face styles with their own definitions. --- src/company_cmd.cpp | 212 +++++++-- src/company_cmd.h | 2 +- src/company_func.h | 2 +- src/company_gui.cpp | 638 ++++++++++----------------- src/company_manager_face.h | 328 ++++++-------- src/company_type.h | 7 +- src/error_gui.cpp | 1 + src/lang/english.txt | 34 +- src/newgrf.cpp | 3 + src/news_type.h | 3 +- src/saveload/afterload.cpp | 23 +- src/saveload/company_sl.cpp | 99 +++-- src/saveload/oldloader_sl.cpp | 6 +- src/saveload/saveload.h | 2 + src/script/api/script_company.cpp | 22 +- src/table/CMakeLists.txt | 1 + src/table/company_face.h | 98 ++++ src/table/settings/misc_settings.ini | 8 +- src/widgets/company_widget.h | 59 +-- 19 files changed, 785 insertions(+), 763 deletions(-) create mode 100644 src/table/company_face.h diff --git a/src/company_cmd.cpp b/src/company_cmd.cpp index 6399a34cc1..3f48937794 100644 --- a/src/company_cmd.cpp +++ b/src/company_cmd.cpp @@ -28,6 +28,7 @@ #include "sound_func.h" #include "rail.h" #include "core/pool_func.hpp" +#include "core/string_consumer.hpp" #include "settings_func.h" #include "vehicle_base.h" #include "vehicle_func.h" @@ -44,6 +45,7 @@ #include "widgets/statusbar_widget.h" #include "table/strings.h" +#include "table/company_face.h" #include "safeguards.h" @@ -53,7 +55,7 @@ void UpdateObjectColours(const Company *c); CompanyID _local_company; ///< Company controlled by the human player at this client. Can also be #COMPANY_SPECTATOR. CompanyID _current_company; ///< Company currently doing an action. TypedIndexContainer, CompanyID> _company_colours; ///< NOSAVE: can be determined from company structs. -CompanyManagerFace _company_manager_face; ///< for company manager face storage in openttd.cfg +std::string _company_manager_face; ///< for company manager face storage in openttd.cfg uint _cur_company_tick_index; ///< used to generate a name for one company that doesn't have a name yet per tick CompanyPool _company_pool("Company"); ///< Pool of companies. @@ -181,24 +183,12 @@ void DrawCompanyIcon(CompanyID c, int x, int y) */ static bool IsValidCompanyManagerFace(CompanyManagerFace cmf) { - if (!AreCompanyManagerFaceBitsValid(cmf, CMFV_GEN_ETHN, GE_WM)) return false; + if (cmf.style >= GetNumCompanyManagerFaceStyles()) return false; - GenderEthnicity ge = (GenderEthnicity)GetCompanyManagerFaceBits(cmf, CMFV_GEN_ETHN, GE_WM); - bool has_moustache = !HasBit(ge, GENDER_FEMALE) && GetCompanyManagerFaceBits(cmf, CMFV_HAS_MOUSTACHE, ge) != 0; - bool has_tie_earring = !HasBit(ge, GENDER_FEMALE) || GetCompanyManagerFaceBits(cmf, CMFV_HAS_TIE_EARRING, ge) != 0; - bool has_glasses = GetCompanyManagerFaceBits(cmf, CMFV_HAS_GLASSES, ge) != 0; - - if (!AreCompanyManagerFaceBitsValid(cmf, CMFV_EYE_COLOUR, ge)) return false; - for (CompanyManagerFaceVariable cmfv = CMFV_CHEEKS; cmfv < CMFV_END; cmfv++) { - switch (cmfv) { - case CMFV_MOUSTACHE: if (!has_moustache) continue; break; - case CMFV_LIPS: - case CMFV_NOSE: if (has_moustache) continue; break; - case CMFV_TIE_EARRING: if (!has_tie_earring) continue; break; - case CMFV_GLASSES: if (!has_glasses) continue; break; - default: break; - } - if (!AreCompanyManagerFaceBitsValid(cmf, cmfv, ge)) return false; + /* Test if each enabled part is valid. */ + FaceVars vars = GetCompanyManagerFaceVars(cmf.style); + for (uint var : SetBitIterator(GetActiveFaceVars(cmf, vars))) { + if (!vars[var].IsValid(cmf)) return false; } return true; @@ -633,11 +623,15 @@ Company *DoStartupNewCompany(bool is_ai, CompanyID company = CompanyID::Invalid( /* If starting a player company in singleplayer and a favorite company manager face is selected, choose it. Otherwise, use a random face. * In a network game, we'll choose the favorite face later in CmdCompanyCtrl to sync it to all clients. */ - if (_company_manager_face != 0 && !is_ai && !_networking) { - c->face = _company_manager_face; - } else { - RandomCompanyManagerFaceBits(c->face, (GenderEthnicity)Random(), false, _random); + bool randomise_face = true; + if (!_company_manager_face.empty() && !is_ai && !_networking) { + auto cmf = ParseCompanyManagerFaceCode(_company_manager_face); + if (cmf.has_value()) { + randomise_face = false; + c->face = std::move(*cmf); + } } + if (randomise_face) RandomiseCompanyManagerFace(c->face, _random); SetDefaultCompanySettings(c->index); ClearEnginesHiddenFlagOfCompany(c->index); @@ -926,7 +920,12 @@ CommandCost CmdCompanyCtrl(DoCommandFlags flags, CompanyCtrlAction cca, CompanyI * its configuration and we are currently in the execution of a command, we have * to circumvent the normal ::Post logic for commands and just send the command. */ - if (_company_manager_face != 0) Command::SendNet(STR_NULL, c->index, _company_manager_face); + if (!_company_manager_face.empty()) { + auto cmf = ParseCompanyManagerFaceCode(_company_manager_face); + if (cmf.has_value()) { + Command::SendNet(STR_NULL, c->index, cmf->bits, cmf->style); + } + } /* Now that we have a new company, broadcast our company settings to * all clients so everything is in sync */ @@ -1049,15 +1048,20 @@ CommandCost CmdCompanyAllowListCtrl(DoCommandFlags flags, CompanyAllowListCtrlAc /** * Change the company manager's face. * @param flags operation to perform - * @param cmf face bitmasked + * @param bits The bits of company manager face. + * @param style The style of the company manager face. * @return the cost of this operation or an error */ -CommandCost CmdSetCompanyManagerFace(DoCommandFlags flags, CompanyManagerFace cmf) +CommandCost CmdSetCompanyManagerFace(DoCommandFlags flags, uint32_t bits, uint style) { - if (!IsValidCompanyManagerFace(cmf)) return CMD_ERROR; + CompanyManagerFace tmp_face{style, bits, {}}; + if (!IsValidCompanyManagerFace(tmp_face)) return CMD_ERROR; if (flags.Test(DoCommandFlag::Execute)) { - Company::Get(_current_company)->face = cmf; + CompanyManagerFace &cmf = Company::Get(_current_company)->face; + SetCompanyManagerFaceStyle(cmf, style); + cmf.bits = tmp_face.bits; + MarkWholeScreenDirty(); } return CommandCost(); @@ -1375,3 +1379,157 @@ CompanyID GetFirstPlayableCompanyID() return CompanyID::Begin(); } + +static std::vector _faces; ///< All company manager face styles. + +/** + * Reset company manager face styles to default. + */ +void ResetFaces() +{ + _faces.clear(); + _faces.assign(std::begin(_original_faces), std::end(_original_faces)); +} + +/** + * Get the number of company manager face styles. + * @return Number of face styles. + */ +uint GetNumCompanyManagerFaceStyles() +{ + return static_cast(std::size(_faces)); +} + +/** + * Get the definition of a company manager face style. + * @param style_index Face style to get. + * @return Definition of face style. + */ +const FaceSpec *GetCompanyManagerFaceSpec(uint style_index) +{ + if (style_index < GetNumCompanyManagerFaceStyles()) return &_faces[style_index]; + return nullptr; +} + +/** + * Find a company manager face style by label. + * @param label Label to find. + * @return Index of face style if label is found, otherwise std::nullopt. + */ +std::optional FindCompanyManagerFaceLabel(std::string_view label) +{ + auto it = std::ranges::find(_faces, label, &FaceSpec::label); + if (it == std::end(_faces)) return std::nullopt; + + return static_cast(std::distance(std::begin(_faces), it)); +} + +/** + * Get the face variables for a face style. + * @param style_index Face style to get variables for. + * @return Variables for the face style. + */ +FaceVars GetCompanyManagerFaceVars(uint style) +{ + const FaceSpec *spec = GetCompanyManagerFaceSpec(style); + if (spec == nullptr) return {}; + return spec->GetFaceVars(); +} + +/** + * Set a company face style. + * Changes both the style index and the label. + * @param cmf The CompanyManagerFace to change. + * @param style The style to set. + */ +void SetCompanyManagerFaceStyle(CompanyManagerFace &cmf, uint style) +{ + const FaceSpec *spec = GetCompanyManagerFaceSpec(style); + assert(spec != nullptr); + + cmf.style = style; + cmf.style_label = spec->label; +} + +/** + * Completely randomise a company manager face, including style. + * @note randomizer should passed be appropriate for server-side or client-side usage. + * @param cmf The CompanyManagerFace to randomise. + * @param randomizer The randomizer to use. + */ +void RandomiseCompanyManagerFace(CompanyManagerFace &cmf, Randomizer &randomizer) +{ + SetCompanyManagerFaceStyle(cmf, randomizer.Next(GetNumCompanyManagerFaceStyles())); + RandomiseCompanyManagerFaceBits(cmf, GetCompanyManagerFaceVars(cmf.style), randomizer); +} + +/** + * Mask company manager face bits to ensure they are all within range. + * @note Does not update the CompanyManagerFace itself. Unused bits are cleared. + * @param cmf The CompanyManagerFace. + * @param style The face variables. + * @return The masked face bits. + */ +uint32_t MaskCompanyManagerFaceBits(const CompanyManagerFace &cmf, FaceVars vars) +{ + CompanyManagerFace face{}; + + for (auto var : SetBitIterator(GetActiveFaceVars(cmf, vars))) { + vars[var].SetBits(face, vars[var].GetBits(cmf)); + } + + return face.bits; +} + +/** + * Get a face code representation of a company manager face. + * @param cmf The company manager face. + * @return String containing face code. + */ +std::string FormatCompanyManagerFaceCode(const CompanyManagerFace &cmf) +{ + uint32_t masked_face_bits = MaskCompanyManagerFaceBits(cmf, GetCompanyManagerFaceVars(cmf.style)); + return fmt::format("{}:{}", cmf.style_label, masked_face_bits); +} + +/** + * Parse a face code into a company manager face. + * @param str Face code to parse. + * @return Company manager face, or std::nullopt if it could not be parsed. + */ +std::optional ParseCompanyManagerFaceCode(std::string_view str) +{ + if (str.empty()) return std::nullopt; + + CompanyManagerFace cmf; + StringConsumer consumer{str}; + if (consumer.FindChar(':') != StringConsumer::npos) { + auto label = consumer.ReadUntilChar(':', StringConsumer::SKIP_ONE_SEPARATOR); + + /* Read numeric part and ensure it's valid. */ + auto bits = consumer.TryReadIntegerBase(10, true); + if (!bits.has_value() || consumer.AnyBytesLeft()) return std::nullopt; + + /* Ensure style laberl is valid. */ + auto style = FindCompanyManagerFaceLabel(label); + if (!style.has_value()) return std::nullopt; + + SetCompanyManagerFaceStyle(cmf, *style); + cmf.bits = *bits; + } else { + /* No ':' included, treat as numeric-only. This allows old-style codes to be entered. */ + auto bits = ParseInteger(str, 10, true); + if (!bits.has_value()) return std::nullopt; + + /* Old codes use bits 0..1 to represent face style. These map directly to the default face styles. */ + SetCompanyManagerFaceStyle(cmf, GB(*bits, 0, 2)); + cmf.bits = *bits; + } + + /* Force the face bits to be valid. */ + FaceVars vars = GetCompanyManagerFaceVars(cmf.style); + ScaleAllCompanyManagerFaceBits(cmf, vars); + cmf.bits = MaskCompanyManagerFaceBits(cmf, vars); + + return cmf; +} diff --git a/src/company_cmd.h b/src/company_cmd.h index 8359d5e07f..eb30f9b361 100644 --- a/src/company_cmd.h +++ b/src/company_cmd.h @@ -22,7 +22,7 @@ CommandCost CmdCompanyAllowListCtrl(DoCommandFlags flags, CompanyAllowListCtrlAc CommandCost CmdGiveMoney(DoCommandFlags flags, Money money, CompanyID dest_company); CommandCost CmdRenameCompany(DoCommandFlags flags, const std::string &text); CommandCost CmdRenamePresident(DoCommandFlags flags, const std::string &text); -CommandCost CmdSetCompanyManagerFace(DoCommandFlags flags, CompanyManagerFace cmf); +CommandCost CmdSetCompanyManagerFace(DoCommandFlags flags, uint style, uint32_t bits); CommandCost CmdSetCompanyColour(DoCommandFlags flags, LiveryScheme scheme, bool primary, Colours colour); DEF_CMD_TRAIT(CMD_COMPANY_CTRL, CmdCompanyCtrl, CommandFlags({CommandFlag::Spectator, CommandFlag::ClientID, CommandFlag::NoEst}), CMDT_SERVER_SETTING) diff --git a/src/company_func.h b/src/company_func.h index deddcd4b60..3a68556f37 100644 --- a/src/company_func.h +++ b/src/company_func.h @@ -37,7 +37,7 @@ extern CompanyID _local_company; extern CompanyID _current_company; extern TypedIndexContainer, CompanyID> _company_colours; -extern CompanyManagerFace _company_manager_face; +extern std::string _company_manager_face; PaletteID GetCompanyPalette(CompanyID company); /** diff --git a/src/company_gui.cpp b/src/company_gui.cpp index 29f48a8957..9debd59c5b 100644 --- a/src/company_gui.cpp +++ b/src/company_gui.cpp @@ -11,6 +11,7 @@ #include "currency.h" #include "error.h" #include "gui.h" +#include "settings_gui.h" #include "window_gui.h" #include "textbuf_gui.h" #include "viewport_func.h" @@ -1116,45 +1117,46 @@ void ShowCompanyLiveryWindow(CompanyID company, GroupID group) * @param colour the (background) colour of the gradient * @param r position to draw the face */ -void DrawCompanyManagerFace(CompanyManagerFace cmf, Colours colour, const Rect &r) +void DrawCompanyManagerFace(const CompanyManagerFace &cmf, Colours colour, const Rect &r) { - GenderEthnicity ge = (GenderEthnicity)GetCompanyManagerFaceBits(cmf, CMFV_GEN_ETHN, GE_WM); - /* Determine offset from centre of drawing rect. */ Dimension d = GetSpriteSize(SPR_GRADIENT); int x = CentreBounds(r.left, r.right, d.width); int y = CentreBounds(r.top, r.bottom, d.height); - bool has_moustache = !HasBit(ge, GENDER_FEMALE) && GetCompanyManagerFaceBits(cmf, CMFV_HAS_MOUSTACHE, ge) != 0; - bool has_tie_earring = !HasBit(ge, GENDER_FEMALE) || GetCompanyManagerFaceBits(cmf, CMFV_HAS_TIE_EARRING, ge) != 0; - bool has_glasses = GetCompanyManagerFaceBits(cmf, CMFV_HAS_GLASSES, ge) != 0; - PaletteID pal; + FaceVars vars = GetCompanyManagerFaceVars(cmf.style); - /* Modify eye colour palette only if 2 or more valid values exist */ - if (_cmf_info[CMFV_EYE_COLOUR].valid_values[ge] < 2) { - pal = PAL_NONE; - } else { - switch (GetCompanyManagerFaceBits(cmf, CMFV_EYE_COLOUR, ge)) { + /* First determine which parts are enabled. */ + uint64_t active_vars = GetActiveFaceVars(cmf, vars); + + std::unordered_map palettes; + + /* Second, get palettes. */ + for (auto var : SetBitIterator(active_vars)) { + if (vars[var].type != FaceVarType::Palette) continue; + + PaletteID pal = PAL_NONE; + switch (vars[var].GetBits(cmf)) { default: NOT_REACHED(); case 0: pal = PALETTE_TO_BROWN; break; case 1: pal = PALETTE_TO_BLUE; break; case 2: pal = PALETTE_TO_GREEN; break; } + for (uint8_t affected_var : SetBitIterator(std::get(vars[var].data))) { + palettes[affected_var] = pal; + } } /* Draw the gradient (background) */ DrawSprite(SPR_GRADIENT, GetColourPalette(colour), x, y); - for (CompanyManagerFaceVariable cmfv = CMFV_CHEEKS; cmfv < CMFV_END; cmfv++) { - switch (cmfv) { - case CMFV_MOUSTACHE: if (!has_moustache) continue; break; - case CMFV_LIPS: - case CMFV_NOSE: if (has_moustache) continue; break; - case CMFV_TIE_EARRING: if (!has_tie_earring) continue; break; - case CMFV_GLASSES: if (!has_glasses) continue; break; - default: break; - } - DrawSprite(GetCompanyManagerFaceSprite(cmf, cmfv, ge), (cmfv == CMFV_EYEBROWS) ? pal : PAL_NONE, x, y); + /* Thirdly, draw sprites. */ + for (auto var : SetBitIterator(active_vars)) { + if (vars[var].type != FaceVarType::Sprite) continue; + + auto it = palettes.find(var); + PaletteID pal = (it == std::end(palettes)) ? PAL_NONE : it->second; + DrawSprite(vars[var].GetSprite(cmf), pal, x, y); } } @@ -1165,153 +1167,40 @@ static constexpr NWidgetPart _nested_select_company_manager_face_widgets[] = { NWidget(WWT_CAPTION, COLOUR_GREY, WID_SCMF_CAPTION), SetStringTip(STR_FACE_CAPTION, STR_TOOLTIP_WINDOW_TITLE_DRAG_THIS), NWidget(WWT_IMGBTN, COLOUR_GREY, WID_SCMF_TOGGLE_LARGE_SMALL), SetSpriteTip(SPR_LARGE_SMALL_WINDOW, STR_FACE_ADVANCED_TOOLTIP), SetAspect(WidgetDimensions::ASPECT_TOGGLE_SIZE), EndContainer(), - NWidget(WWT_PANEL, COLOUR_GREY, WID_SCMF_SELECT_FACE), - NWidget(NWID_HORIZONTAL), SetPIP(0, WidgetDimensions::unscaled.hsep_normal, 0), SetPadding(2), + NWidget(NWID_HORIZONTAL), + NWidget(WWT_PANEL, COLOUR_GREY, WID_SCMF_SELECT_FACE), /* Left side */ - NWidget(NWID_VERTICAL), SetPIP(0, WidgetDimensions::unscaled.vsep_normal, 0), - NWidget(NWID_HORIZONTAL), SetPIPRatio(1, 0, 1), - NWidget(WWT_EMPTY, INVALID_COLOUR, WID_SCMF_FACE), SetMinimalSize(92, 119), SetFill(1, 0), - EndContainer(), + NWidget(NWID_VERTICAL), SetPIP(0, WidgetDimensions::unscaled.vsep_normal, 0), SetPadding(4), + NWidget(WWT_EMPTY, INVALID_COLOUR, WID_SCMF_FACE), SetMinimalSize(92, 119), SetFill(1, 0), NWidget(WWT_PUSHTXTBTN, COLOUR_GREY, WID_SCMF_RANDOM_NEW_FACE), SetFill(1, 0), SetStringTip(STR_FACE_NEW_FACE_BUTTON, STR_FACE_NEW_FACE_TOOLTIP), + NWidget(WWT_PUSHTXTBTN, COLOUR_GREY, WID_SCMF_TOGGLE_LARGE_SMALL_BUTTON), SetFill(1, 0), SetStringTip(STR_FACE_ADVANCED, STR_FACE_ADVANCED_TOOLTIP), NWidget(NWID_SELECTION, INVALID_COLOUR, WID_SCMF_SEL_LOADSAVE), // Load/number/save buttons under the portrait in the advanced view. - NWidget(NWID_VERTICAL), SetPIP(0, 0, 0), SetPIPRatio(1, 0, 1), + NWidget(NWID_VERTICAL), + NWidget(NWID_SPACER), SetFill(1, 1), SetResize(0, 1), NWidget(WWT_PUSHTXTBTN, COLOUR_GREY, WID_SCMF_LOAD), SetFill(1, 0), SetStringTip(STR_FACE_LOAD, STR_FACE_LOAD_TOOLTIP), NWidget(WWT_PUSHTXTBTN, COLOUR_GREY, WID_SCMF_FACECODE), SetFill(1, 0), SetStringTip(STR_FACE_FACECODE, STR_FACE_FACECODE_TOOLTIP), NWidget(WWT_PUSHTXTBTN, COLOUR_GREY, WID_SCMF_SAVE), SetFill(1, 0), SetStringTip(STR_FACE_SAVE, STR_FACE_SAVE_TOOLTIP), EndContainer(), EndContainer(), EndContainer(), - /* Right side */ - NWidget(NWID_VERTICAL), SetPIP(0, WidgetDimensions::unscaled.vsep_normal, 0), - NWidget(WWT_PUSHTXTBTN, COLOUR_GREY, WID_SCMF_TOGGLE_LARGE_SMALL_BUTTON), SetFill(1, 0), SetStringTip(STR_FACE_ADVANCED, STR_FACE_ADVANCED_TOOLTIP), - NWidget(NWID_SELECTION, INVALID_COLOUR, WID_SCMF_SEL_MALEFEMALE), // Simple male/female face setting. - NWidget(NWID_VERTICAL), SetPIPRatio(1, 0, 1), - NWidget(WWT_TEXTBTN, COLOUR_GREY, WID_SCMF_MALE), SetFill(1, 0), SetStringTip(STR_FACE_MALE_BUTTON, STR_FACE_MALE_TOOLTIP), - NWidget(WWT_TEXTBTN, COLOUR_GREY, WID_SCMF_FEMALE), SetFill(1, 0), SetStringTip(STR_FACE_FEMALE_BUTTON, STR_FACE_FEMALE_TOOLTIP), - EndContainer(), - EndContainer(), - NWidget(NWID_SELECTION, INVALID_COLOUR, WID_SCMF_SEL_PARTS), // Advanced face parts setting. - NWidget(NWID_VERTICAL), SetPIP(0, WidgetDimensions::unscaled.vsep_normal, 0), - NWidget(NWID_HORIZONTAL, NWidContainerFlag::EqualSize), - NWidget(WWT_TEXTBTN, COLOUR_GREY, WID_SCMF_MALE2), SetFill(1, 0), SetStringTip(STR_FACE_MALE_BUTTON, STR_FACE_MALE_TOOLTIP), - NWidget(WWT_TEXTBTN, COLOUR_GREY, WID_SCMF_FEMALE2), SetFill(1, 0), SetStringTip(STR_FACE_FEMALE_BUTTON, STR_FACE_FEMALE_TOOLTIP), - EndContainer(), - NWidget(NWID_HORIZONTAL, NWidContainerFlag::EqualSize), - NWidget(WWT_TEXTBTN, COLOUR_GREY, WID_SCMF_ETHNICITY_EUR), SetFill(1, 0), SetStringTip(STR_FACE_EUROPEAN, STR_FACE_EUROPEAN_TOOLTIP), - NWidget(WWT_TEXTBTN, COLOUR_GREY, WID_SCMF_ETHNICITY_AFR), SetFill(1, 0), SetStringTip(STR_FACE_AFRICAN, STR_FACE_AFRICAN_TOOLTIP), - EndContainer(), - NWidget(NWID_VERTICAL), - NWidget(NWID_HORIZONTAL), SetPIP(0, WidgetDimensions::unscaled.hsep_normal, 0), - NWidget(WWT_TEXT, INVALID_COLOUR, WID_SCMF_HAS_MOUSTACHE_EARRING_TEXT), SetFill(1, 0), - SetStringTip(STR_FACE_EYECOLOUR), SetTextStyle(TC_GOLD), SetAlignment(SA_VERT_CENTER | SA_RIGHT), - NWidget(WWT_PUSHTXTBTN, COLOUR_GREY, WID_SCMF_HAS_MOUSTACHE_EARRING), SetToolTip(STR_FACE_MOUSTACHE_EARRING_TOOLTIP), SetTextStyle(TC_WHITE), - EndContainer(), - NWidget(NWID_HORIZONTAL), SetPIP(0, WidgetDimensions::unscaled.hsep_normal, 0), - NWidget(WWT_TEXT, INVALID_COLOUR, WID_SCMF_HAS_GLASSES_TEXT), SetFill(1, 0), - SetStringTip(STR_FACE_GLASSES), SetTextStyle(TC_GOLD), SetAlignment(SA_VERT_CENTER | SA_RIGHT), - NWidget(WWT_PUSHTXTBTN, COLOUR_GREY, WID_SCMF_HAS_GLASSES), SetToolTip(STR_FACE_GLASSES_TOOLTIP), SetTextStyle(TC_WHITE), - EndContainer(), - EndContainer(), - NWidget(NWID_VERTICAL), - NWidget(NWID_HORIZONTAL), SetPIP(0, WidgetDimensions::unscaled.hsep_normal, 0), - NWidget(WWT_TEXT, INVALID_COLOUR, WID_SCMF_HAIR_TEXT), SetFill(1, 0), - SetStringTip(STR_FACE_HAIR), SetTextStyle(TC_GOLD), SetAlignment(SA_VERT_CENTER | SA_RIGHT), - NWidget(NWID_HORIZONTAL), - NWidget(WWT_PUSHARROWBTN, COLOUR_GREY, WID_SCMF_HAIR_L), SetArrowWidgetTypeTip(AWV_DECREASE, STR_FACE_HAIR_TOOLTIP), - NWidget(WWT_PUSHTXTBTN, COLOUR_GREY, WID_SCMF_HAIR), SetToolTip(STR_FACE_HAIR_TOOLTIP), SetTextStyle(TC_WHITE), - NWidget(WWT_PUSHARROWBTN, COLOUR_GREY, WID_SCMF_HAIR_R), SetArrowWidgetTypeTip(AWV_INCREASE, STR_FACE_HAIR_TOOLTIP), - EndContainer(), - EndContainer(), - NWidget(NWID_HORIZONTAL), SetPIP(0, WidgetDimensions::unscaled.hsep_normal, 0), - NWidget(WWT_TEXT, INVALID_COLOUR, WID_SCMF_EYEBROWS_TEXT), SetFill(1, 0), - SetStringTip(STR_FACE_EYEBROWS), SetTextStyle(TC_GOLD), SetAlignment(SA_VERT_CENTER | SA_RIGHT), - NWidget(NWID_HORIZONTAL), - NWidget(WWT_PUSHARROWBTN, COLOUR_GREY, WID_SCMF_EYEBROWS_L), SetArrowWidgetTypeTip(AWV_DECREASE, STR_FACE_EYEBROWS_TOOLTIP), - NWidget(WWT_PUSHTXTBTN, COLOUR_GREY, WID_SCMF_EYEBROWS), SetToolTip(STR_FACE_EYEBROWS_TOOLTIP), SetTextStyle(TC_WHITE), - NWidget(WWT_PUSHARROWBTN, COLOUR_GREY, WID_SCMF_EYEBROWS_R), SetArrowWidgetTypeTip(AWV_INCREASE, STR_FACE_EYEBROWS_TOOLTIP), - EndContainer(), - EndContainer(), - NWidget(NWID_HORIZONTAL), SetPIP(0, WidgetDimensions::unscaled.hsep_normal, 0), - NWidget(WWT_TEXT, INVALID_COLOUR, WID_SCMF_EYECOLOUR_TEXT), SetFill(1, 0), - SetStringTip(STR_FACE_EYECOLOUR), SetTextStyle(TC_GOLD), SetAlignment(SA_VERT_CENTER | SA_RIGHT), - NWidget(NWID_HORIZONTAL), - NWidget(WWT_PUSHARROWBTN, COLOUR_GREY, WID_SCMF_EYECOLOUR_L), SetArrowWidgetTypeTip(AWV_DECREASE, STR_FACE_EYECOLOUR_TOOLTIP), - NWidget(WWT_PUSHTXTBTN, COLOUR_GREY, WID_SCMF_EYECOLOUR), SetToolTip(STR_FACE_EYECOLOUR_TOOLTIP), SetTextStyle(TC_WHITE), - NWidget(WWT_PUSHARROWBTN, COLOUR_GREY, WID_SCMF_EYECOLOUR_R), SetArrowWidgetTypeTip(AWV_INCREASE, STR_FACE_EYECOLOUR_TOOLTIP), - EndContainer(), - EndContainer(), - NWidget(NWID_HORIZONTAL), SetPIP(0, WidgetDimensions::unscaled.hsep_normal, 0), - NWidget(WWT_TEXT, INVALID_COLOUR, WID_SCMF_GLASSES_TEXT), SetFill(1, 0), - SetStringTip(STR_FACE_GLASSES), SetTextStyle(TC_GOLD), SetAlignment(SA_VERT_CENTER | SA_RIGHT), - NWidget(NWID_HORIZONTAL), - NWidget(WWT_PUSHARROWBTN, COLOUR_GREY, WID_SCMF_GLASSES_L), SetArrowWidgetTypeTip(AWV_DECREASE, STR_FACE_GLASSES_TOOLTIP_2), - NWidget(WWT_PUSHTXTBTN, COLOUR_GREY, WID_SCMF_GLASSES), SetToolTip(STR_FACE_GLASSES_TOOLTIP_2), SetTextStyle(TC_WHITE), - NWidget(WWT_PUSHARROWBTN, COLOUR_GREY, WID_SCMF_GLASSES_R), SetArrowWidgetTypeTip(AWV_INCREASE, STR_FACE_GLASSES_TOOLTIP_2), - EndContainer(), - EndContainer(), - NWidget(NWID_HORIZONTAL), SetPIP(0, WidgetDimensions::unscaled.hsep_normal, 0), - NWidget(WWT_TEXT, INVALID_COLOUR, WID_SCMF_NOSE_TEXT), SetFill(1, 0), - SetStringTip(STR_FACE_NOSE), SetTextStyle(TC_GOLD), SetAlignment(SA_VERT_CENTER | SA_RIGHT), - NWidget(NWID_HORIZONTAL), - NWidget(WWT_PUSHARROWBTN, COLOUR_GREY, WID_SCMF_NOSE_L), SetArrowWidgetTypeTip(AWV_DECREASE, STR_FACE_NOSE_TOOLTIP), - NWidget(WWT_PUSHTXTBTN, COLOUR_GREY, WID_SCMF_NOSE), SetToolTip(STR_FACE_NOSE_TOOLTIP), SetTextStyle(TC_WHITE), - NWidget(WWT_PUSHARROWBTN, COLOUR_GREY, WID_SCMF_NOSE_R), SetArrowWidgetTypeTip(AWV_INCREASE, STR_FACE_NOSE_TOOLTIP), - EndContainer(), - EndContainer(), - NWidget(NWID_HORIZONTAL), SetPIP(0, WidgetDimensions::unscaled.hsep_normal, 0), - NWidget(WWT_TEXT, INVALID_COLOUR, WID_SCMF_LIPS_MOUSTACHE_TEXT), SetFill(1, 0), - SetStringTip(STR_FACE_MOUSTACHE), SetTextStyle(TC_GOLD), SetAlignment(SA_VERT_CENTER | SA_RIGHT), - NWidget(NWID_HORIZONTAL), - NWidget(WWT_PUSHARROWBTN, COLOUR_GREY, WID_SCMF_LIPS_MOUSTACHE_L), SetArrowWidgetTypeTip(AWV_DECREASE, STR_FACE_LIPS_MOUSTACHE_TOOLTIP), - NWidget(WWT_PUSHTXTBTN, COLOUR_GREY, WID_SCMF_LIPS_MOUSTACHE), SetToolTip(STR_FACE_LIPS_MOUSTACHE_TOOLTIP), SetTextStyle(TC_WHITE), - NWidget(WWT_PUSHARROWBTN, COLOUR_GREY, WID_SCMF_LIPS_MOUSTACHE_R), SetArrowWidgetTypeTip(AWV_INCREASE, STR_FACE_LIPS_MOUSTACHE_TOOLTIP), - EndContainer(), - EndContainer(), - NWidget(NWID_HORIZONTAL), SetPIP(0, WidgetDimensions::unscaled.hsep_normal, 0), - NWidget(WWT_TEXT, INVALID_COLOUR, WID_SCMF_CHIN_TEXT), SetFill(1, 0), - SetStringTip(STR_FACE_CHIN), SetTextStyle(TC_GOLD), SetAlignment(SA_VERT_CENTER | SA_RIGHT), - NWidget(NWID_HORIZONTAL), - NWidget(WWT_PUSHARROWBTN, COLOUR_GREY, WID_SCMF_CHIN_L), SetArrowWidgetTypeTip(AWV_DECREASE, STR_FACE_CHIN_TOOLTIP), - NWidget(WWT_PUSHTXTBTN, COLOUR_GREY, WID_SCMF_CHIN), SetToolTip(STR_FACE_CHIN_TOOLTIP), SetTextStyle(TC_WHITE), - NWidget(WWT_PUSHARROWBTN, COLOUR_GREY, WID_SCMF_CHIN_R), SetArrowWidgetTypeTip(AWV_INCREASE, STR_FACE_CHIN_TOOLTIP), - EndContainer(), - EndContainer(), - NWidget(NWID_HORIZONTAL), SetPIP(0, WidgetDimensions::unscaled.hsep_normal, 0), - NWidget(WWT_TEXT, INVALID_COLOUR, WID_SCMF_JACKET_TEXT), SetFill(1, 0), - SetStringTip(STR_FACE_JACKET), SetTextStyle(TC_GOLD), SetAlignment(SA_VERT_CENTER | SA_RIGHT), - NWidget(NWID_HORIZONTAL), - NWidget(WWT_PUSHARROWBTN, COLOUR_GREY, WID_SCMF_JACKET_L), SetArrowWidgetTypeTip(AWV_DECREASE, STR_FACE_JACKET_TOOLTIP), - NWidget(WWT_PUSHTXTBTN, COLOUR_GREY, WID_SCMF_JACKET), SetToolTip(STR_FACE_JACKET_TOOLTIP), SetTextStyle(TC_WHITE), - NWidget(WWT_PUSHARROWBTN, COLOUR_GREY, WID_SCMF_JACKET_R), SetArrowWidgetTypeTip(AWV_INCREASE, STR_FACE_JACKET_TOOLTIP), - EndContainer(), - EndContainer(), - NWidget(NWID_HORIZONTAL), SetPIP(0, WidgetDimensions::unscaled.hsep_normal, 0), - NWidget(WWT_TEXT, INVALID_COLOUR, WID_SCMF_COLLAR_TEXT), SetFill(1, 0), - SetStringTip(STR_FACE_COLLAR), SetTextStyle(TC_GOLD), SetAlignment(SA_VERT_CENTER | SA_RIGHT), - NWidget(NWID_HORIZONTAL), - NWidget(WWT_PUSHARROWBTN, COLOUR_GREY, WID_SCMF_COLLAR_L), SetArrowWidgetTypeTip(AWV_DECREASE, STR_FACE_COLLAR_TOOLTIP), - NWidget(WWT_PUSHTXTBTN, COLOUR_GREY, WID_SCMF_COLLAR), SetToolTip(STR_FACE_COLLAR_TOOLTIP), SetTextStyle(TC_WHITE), - NWidget(WWT_PUSHARROWBTN, COLOUR_GREY, WID_SCMF_COLLAR_R), SetArrowWidgetTypeTip(AWV_INCREASE, STR_FACE_COLLAR_TOOLTIP), - EndContainer(), - EndContainer(), - NWidget(NWID_HORIZONTAL), SetPIP(0, WidgetDimensions::unscaled.hsep_normal, 0), - NWidget(WWT_TEXT, INVALID_COLOUR, WID_SCMF_TIE_EARRING_TEXT), SetFill(1, 0), - SetStringTip(STR_FACE_EARRING), SetTextStyle(TC_GOLD), SetAlignment(SA_VERT_CENTER | SA_RIGHT), - NWidget(NWID_HORIZONTAL), - NWidget(WWT_PUSHARROWBTN, COLOUR_GREY, WID_SCMF_TIE_EARRING_L), SetArrowWidgetTypeTip(AWV_DECREASE, STR_FACE_TIE_EARRING_TOOLTIP), - NWidget(WWT_PUSHTXTBTN, COLOUR_GREY, WID_SCMF_TIE_EARRING), SetToolTip(STR_FACE_TIE_EARRING_TOOLTIP), SetTextStyle(TC_WHITE), - NWidget(WWT_PUSHARROWBTN, COLOUR_GREY, WID_SCMF_TIE_EARRING_R), SetArrowWidgetTypeTip(AWV_INCREASE, STR_FACE_TIE_EARRING_TOOLTIP), - EndContainer(), - EndContainer(), - EndContainer(), - EndContainer(), + EndContainer(), + /* Right side */ + NWidget(NWID_SELECTION, INVALID_COLOUR, WID_SCMF_SEL_PARTS), // Advanced face parts setting. + NWidget(NWID_VERTICAL), + NWidget(WWT_MATRIX, COLOUR_GREY, WID_SCMF_STYLE), SetResize(1, 0), SetFill(1, 0), SetMatrixDataTip(1, 1), + NWidget(NWID_HORIZONTAL), + NWidget(WWT_MATRIX, COLOUR_GREY, WID_SCMF_PARTS), SetResize(1, 1), SetFill(1, 1), SetMatrixDataTip(1, 0), SetScrollbar(WID_SCMF_PARTS_SCROLLBAR), + NWidget(NWID_VSCROLLBAR, COLOUR_GREY, WID_SCMF_PARTS_SCROLLBAR), EndContainer(), EndContainer(), EndContainer(), EndContainer(), NWidget(NWID_HORIZONTAL, NWidContainerFlag::EqualSize), - NWidget(WWT_PUSHTXTBTN, COLOUR_GREY, WID_SCMF_CANCEL), SetFill(1, 0), SetStringTip(STR_BUTTON_CANCEL, STR_FACE_CANCEL_TOOLTIP), - NWidget(WWT_PUSHTXTBTN, COLOUR_GREY, WID_SCMF_ACCEPT), SetFill(1, 0), SetStringTip(STR_BUTTON_OK, STR_FACE_OK_TOOLTIP), + NWidget(WWT_PUSHTXTBTN, COLOUR_GREY, WID_SCMF_CANCEL), SetFill(1, 0), SetResize(1, 0), SetStringTip(STR_BUTTON_CANCEL, STR_FACE_CANCEL_TOOLTIP), + NWidget(WWT_PUSHTXTBTN, COLOUR_GREY, WID_SCMF_ACCEPT), SetFill(1, 0), SetResize(1, 0), SetStringTip(STR_BUTTON_OK, STR_FACE_OK_TOOLTIP), + NWidget(NWID_SELECTION, INVALID_COLOUR, WID_SCMF_SEL_RESIZE), + NWidget(WWT_RESIZEBOX, COLOUR_GREY), + EndContainer(), EndContainer(), }; @@ -1320,43 +1209,34 @@ class SelectCompanyManagerFaceWindow : public Window { CompanyManagerFace face{}; ///< company manager face bits bool advanced = false; ///< advanced company manager face selection window + uint selected_var = UINT_MAX; ///< Currently selected face variable. `UINT_MAX` for none, `UINT_MAX - 1` means style is clicked instead. + uint click_state = 0; ///< Click state on selected face variable. + int line_height = 0; ///< Height of each face variable row. - GenderEthnicity ge{}; ///< Gender and ethnicity. - bool is_female = false; ///< Female face. - bool is_moust_male = false; ///< Male face with a moustache. - - Dimension yesno_dim{}; ///< Dimension of a yes/no button of a part in the advanced face window. - Dimension number_dim{}; ///< Dimension of a number widget of a part in the advanced face window. + std::vector face_vars; ///< Visible face variables. /** - * Get the string for the value of face control buttons. - * - * @param widget_index index of this widget in the window - * @param stringid formatting string for the button. - * @param val the value which will be displayed - * @param is_bool_widget is it a bool button + * Make face bits valid and update visible face variables. */ - std::string GetFaceString(WidgetID widget_index, uint8_t val, bool is_bool_widget) const - { - const NWidgetCore *nwi_widget = this->GetWidget(widget_index); - if (nwi_widget->IsDisabled()) return {}; - - /* If it a bool button write yes or no. */ - if (is_bool_widget) return GetString((val != 0) ? STR_FACE_YES : STR_FACE_NO); - - /* Else write the value + 1. */ - return GetString(STR_JUST_INT, val + 1); - } - void UpdateData() { - this->ge = (GenderEthnicity)GB(this->face, _cmf_info[CMFV_GEN_ETHN].offset, _cmf_info[CMFV_GEN_ETHN].length); // get the gender and ethnicity - this->is_female = HasBit(this->ge, GENDER_FEMALE); // get the gender: 0 == male and 1 == female - this->is_moust_male = !is_female && GetCompanyManagerFaceBits(this->face, CMFV_HAS_MOUSTACHE, this->ge) != 0; // is a male face with moustache + FaceVars vars = GetCompanyManagerFaceVars(this->face.style); + ScaleAllCompanyManagerFaceBits(this->face, vars); - this->GetWidget(WID_SCMF_HAS_MOUSTACHE_EARRING_TEXT)->SetString(this->is_female ? STR_FACE_EARRING : STR_FACE_MOUSTACHE); - this->GetWidget(WID_SCMF_TIE_EARRING_TEXT)->SetString(this->is_female ? STR_FACE_EARRING : STR_FACE_TIE); - this->GetWidget(WID_SCMF_LIPS_MOUSTACHE_TEXT)->SetString(this->is_moust_male ? STR_FACE_MOUSTACHE : STR_FACE_LIPS); + uint64_t active_vars = GetActiveFaceVars(this->face, vars); + /* Exclude active parts which have no string. */ + for (auto var : SetBitIterator(active_vars)) { + if (vars[var].name == STR_NULL) ClrBit(active_vars, var); + } + + /* Rebuild the sorted list of face variable pointers. */ + this->face_vars.clear(); + for (auto var : SetBitIterator(active_vars)) { + this->face_vars.emplace_back(&vars[var]); + } + std::ranges::sort(this->face_vars, std::less{}, &FaceVar::position); + + this->GetScrollbar(WID_SCMF_PARTS_SCROLLBAR)->SetCount(std::size(this->face_vars)); } public: @@ -1372,16 +1252,20 @@ public: this->UpdateData(); } + void OnInit() override + { + this->line_height = std::max(SETTING_BUTTON_HEIGHT, GetCharacterHeight(FS_NORMAL)) + WidgetDimensions::scaled.matrix.Vertical(); + } + /** * Select planes to display to the user with the #NWID_SELECTION widgets #WID_SCMF_SEL_LOADSAVE, #WID_SCMF_SEL_MALEFEMALE, and #WID_SCMF_SEL_PARTS. * @param advanced Display advanced face management window. */ void SelectDisplayPlanes(bool advanced) { - this->GetWidget(WID_SCMF_SEL_LOADSAVE)->SetDisplayedPlane(advanced ? 0 : SZSP_NONE); + this->GetWidget(WID_SCMF_SEL_LOADSAVE)->SetDisplayedPlane(advanced ? 0 : SZSP_HORIZONTAL); this->GetWidget(WID_SCMF_SEL_PARTS)->SetDisplayedPlane(advanced ? 0 : SZSP_NONE); - this->GetWidget(WID_SCMF_SEL_MALEFEMALE)->SetDisplayedPlane(advanced ? SZSP_NONE : 0); - this->GetWidget(WID_SCMF_RANDOM_NEW_FACE)->SetString(advanced ? STR_FACE_RANDOM : STR_FACE_NEW_FACE_BUTTON); + this->GetWidget(WID_SCMF_SEL_RESIZE)->SetDisplayedPlane(advanced ? 0 : SZSP_NONE); NWidgetCore *wi = this->GetWidget(WID_SCMF_TOGGLE_LARGE_SMALL_BUTTON); if (advanced) { @@ -1391,185 +1275,99 @@ public: } } - void OnInit() override + static StringID GetLongestString(StringID a, StringID b) { - /* Size of the boolean yes/no button. */ - Dimension yesno_dim = maxdim(GetStringBoundingBox(STR_FACE_YES), GetStringBoundingBox(STR_FACE_NO)); - yesno_dim.width += WidgetDimensions::scaled.framerect.Horizontal(); - yesno_dim.height += WidgetDimensions::scaled.framerect.Vertical(); - /* Size of the number button + arrows. */ - Dimension number_dim = {0, 0}; - for (int val = 1; val <= 12; val++) { - number_dim = maxdim(number_dim, GetStringBoundingBox(GetString(STR_JUST_INT, val))); - } - uint arrows_width = GetSpriteSize(SPR_ARROW_LEFT).width + GetSpriteSize(SPR_ARROW_RIGHT).width + 2 * (WidgetDimensions::scaled.imgbtn.Horizontal()); - number_dim.width += WidgetDimensions::scaled.framerect.Horizontal() + arrows_width; - number_dim.height += WidgetDimensions::scaled.framerect.Vertical(); - /* Compute width of both buttons. */ - yesno_dim.width = std::max(yesno_dim.width, number_dim.width); - number_dim.width = yesno_dim.width - arrows_width; + return GetStringBoundingBox(a).width > GetStringBoundingBox(b).width ? a : b; + } - this->yesno_dim = yesno_dim; - this->number_dim = number_dim; + static uint GetMaximumFacePartsWidth() + { + StringID yes_no = GetLongestString(STR_FACE_YES, STR_FACE_NO); + + uint width = 0; + for (uint style_index = 0; style_index != GetNumCompanyManagerFaceStyles(); ++style_index) { + FaceVars vars = GetCompanyManagerFaceVars(style_index); + for (const auto &info : vars) { + if (info.name == STR_NULL) continue; + if (info.type == FaceVarType::Toggle) { + width = std::max(width, GetStringBoundingBox(GetString(STR_FACE_SETTING_TOGGLE, info.name, yes_no)).width); + } else { + uint64_t max_digits = GetParamMaxValue(info.valid_values); + width = std::max(width, GetStringBoundingBox(GetString(STR_FACE_SETTING_NUMERIC, info.name, max_digits, max_digits)).width); + } + } + } + + /* Include width of button and spacing. */ + width += SETTING_BUTTON_WIDTH + WidgetDimensions::scaled.hsep_wide + WidgetDimensions::scaled.frametext.Horizontal(); + return width; } void UpdateWidgetSize(WidgetID widget, Dimension &size, [[maybe_unused]] const Dimension &padding, [[maybe_unused]] Dimension &fill, [[maybe_unused]] Dimension &resize) override { switch (widget) { - case WID_SCMF_HAS_MOUSTACHE_EARRING_TEXT: - size = maxdim(size, GetStringBoundingBox(STR_FACE_EARRING)); - size = maxdim(size, GetStringBoundingBox(STR_FACE_MOUSTACHE)); - break; - - case WID_SCMF_TIE_EARRING_TEXT: - size = maxdim(size, GetStringBoundingBox(STR_FACE_EARRING)); - size = maxdim(size, GetStringBoundingBox(STR_FACE_TIE)); - break; - - case WID_SCMF_LIPS_MOUSTACHE_TEXT: - size = maxdim(size, GetStringBoundingBox(STR_FACE_LIPS)); - size = maxdim(size, GetStringBoundingBox(STR_FACE_MOUSTACHE)); - break; - case WID_SCMF_FACE: size = maxdim(size, GetScaledSpriteSize(SPR_GRADIENT)); break; - case WID_SCMF_HAS_MOUSTACHE_EARRING: - case WID_SCMF_HAS_GLASSES: - size = this->yesno_dim; + case WID_SCMF_STYLE: + size.height = this->line_height; break; - case WID_SCMF_EYECOLOUR: - case WID_SCMF_CHIN: - case WID_SCMF_EYEBROWS: - case WID_SCMF_LIPS_MOUSTACHE: - case WID_SCMF_NOSE: - case WID_SCMF_HAIR: - case WID_SCMF_JACKET: - case WID_SCMF_COLLAR: - case WID_SCMF_TIE_EARRING: - case WID_SCMF_GLASSES: - size = this->number_dim; + case WID_SCMF_PARTS: + fill.height = resize.height = this->line_height; + size.width = GetMaximumFacePartsWidth(); + size.height = resize.height * 5; break; } } - void OnPaint() override - { - /* lower the non-selected gender button */ - this->SetWidgetsLoweredState(!this->is_female, WID_SCMF_MALE, WID_SCMF_MALE2); - this->SetWidgetsLoweredState( this->is_female, WID_SCMF_FEMALE, WID_SCMF_FEMALE2); - - /* advanced company manager face selection window */ - - /* lower the non-selected ethnicity button */ - this->SetWidgetLoweredState(WID_SCMF_ETHNICITY_EUR, !HasBit(this->ge, ETHNICITY_BLACK)); - this->SetWidgetLoweredState(WID_SCMF_ETHNICITY_AFR, HasBit(this->ge, ETHNICITY_BLACK)); - - - /* Disable dynamically the widgets which CompanyManagerFaceVariable has less than 2 options - * (or in other words you haven't any choice). - * If the widgets depend on a HAS-variable and this is false the widgets will be disabled, too. */ - - /* Eye colour buttons */ - this->SetWidgetsDisabledState(_cmf_info[CMFV_EYE_COLOUR].valid_values[this->ge] < 2, - WID_SCMF_EYECOLOUR, WID_SCMF_EYECOLOUR_L, WID_SCMF_EYECOLOUR_R); - - /* Chin buttons */ - this->SetWidgetsDisabledState(_cmf_info[CMFV_CHIN].valid_values[this->ge] < 2, - WID_SCMF_CHIN, WID_SCMF_CHIN_L, WID_SCMF_CHIN_R); - - /* Eyebrows buttons */ - this->SetWidgetsDisabledState(_cmf_info[CMFV_EYEBROWS].valid_values[this->ge] < 2, - WID_SCMF_EYEBROWS, WID_SCMF_EYEBROWS_L, WID_SCMF_EYEBROWS_R); - - /* Lips or (if it a male face with a moustache) moustache buttons */ - this->SetWidgetsDisabledState(_cmf_info[this->is_moust_male ? CMFV_MOUSTACHE : CMFV_LIPS].valid_values[this->ge] < 2, - WID_SCMF_LIPS_MOUSTACHE, WID_SCMF_LIPS_MOUSTACHE_L, WID_SCMF_LIPS_MOUSTACHE_R); - - /* Nose buttons | male faces with moustache haven't any nose options */ - this->SetWidgetsDisabledState(_cmf_info[CMFV_NOSE].valid_values[this->ge] < 2 || this->is_moust_male, - WID_SCMF_NOSE, WID_SCMF_NOSE_L, WID_SCMF_NOSE_R); - - /* Hair buttons */ - this->SetWidgetsDisabledState(_cmf_info[CMFV_HAIR].valid_values[this->ge] < 2, - WID_SCMF_HAIR, WID_SCMF_HAIR_L, WID_SCMF_HAIR_R); - - /* Jacket buttons */ - this->SetWidgetsDisabledState(_cmf_info[CMFV_JACKET].valid_values[this->ge] < 2, - WID_SCMF_JACKET, WID_SCMF_JACKET_L, WID_SCMF_JACKET_R); - - /* Collar buttons */ - this->SetWidgetsDisabledState(_cmf_info[CMFV_COLLAR].valid_values[this->ge] < 2, - WID_SCMF_COLLAR, WID_SCMF_COLLAR_L, WID_SCMF_COLLAR_R); - - /* Tie/earring buttons | female faces without earring haven't any earring options */ - this->SetWidgetsDisabledState(_cmf_info[CMFV_TIE_EARRING].valid_values[this->ge] < 2 || - (this->is_female && GetCompanyManagerFaceBits(this->face, CMFV_HAS_TIE_EARRING, this->ge) == 0), - WID_SCMF_TIE_EARRING, WID_SCMF_TIE_EARRING_L, WID_SCMF_TIE_EARRING_R); - - /* Glasses buttons | faces without glasses haven't any glasses options */ - this->SetWidgetsDisabledState(_cmf_info[CMFV_GLASSES].valid_values[this->ge] < 2 || GetCompanyManagerFaceBits(this->face, CMFV_HAS_GLASSES, this->ge) == 0, - WID_SCMF_GLASSES, WID_SCMF_GLASSES_L, WID_SCMF_GLASSES_R); - - this->DrawWidgets(); - } - - std::string GetWidgetString(WidgetID widget, StringID stringid) const override - { - switch (widget) { - case WID_SCMF_HAS_MOUSTACHE_EARRING: { - CompanyManagerFaceVariable facepart = this->is_female ? CMFV_HAS_TIE_EARRING : CMFV_HAS_MOUSTACHE; - return this->GetFaceString(widget, GetCompanyManagerFaceBits(this->face, facepart, this->ge), true); - } - - case WID_SCMF_TIE_EARRING: - return this->GetFaceString(widget, GetCompanyManagerFaceBits(this->face, CMFV_TIE_EARRING, this->ge), false); - - case WID_SCMF_LIPS_MOUSTACHE: { - CompanyManagerFaceVariable facepart = this->is_moust_male ? CMFV_MOUSTACHE : CMFV_LIPS; - return this->GetFaceString(widget, GetCompanyManagerFaceBits(this->face, facepart, this->ge), false); - } - - case WID_SCMF_HAS_GLASSES: - return this->GetFaceString(widget, GetCompanyManagerFaceBits(this->face, CMFV_HAS_GLASSES, this->ge), true ); - - case WID_SCMF_HAIR: - return this->GetFaceString(widget, GetCompanyManagerFaceBits(this->face, CMFV_HAIR, this->ge), false); - - case WID_SCMF_EYEBROWS: - return this->GetFaceString(widget, GetCompanyManagerFaceBits(this->face, CMFV_EYEBROWS, this->ge), false); - - case WID_SCMF_EYECOLOUR: - return this->GetFaceString(widget, GetCompanyManagerFaceBits(this->face, CMFV_EYE_COLOUR, this->ge), false); - - case WID_SCMF_GLASSES: - return this->GetFaceString(widget, GetCompanyManagerFaceBits(this->face, CMFV_GLASSES, this->ge), false); - - case WID_SCMF_NOSE: - return this->GetFaceString(widget, GetCompanyManagerFaceBits(this->face, CMFV_NOSE, this->ge), false); - - case WID_SCMF_CHIN: - return this->GetFaceString(widget, GetCompanyManagerFaceBits(this->face, CMFV_CHIN, this->ge), false); - - case WID_SCMF_JACKET: - return this->GetFaceString(widget, GetCompanyManagerFaceBits(this->face, CMFV_JACKET, this->ge), false); - - case WID_SCMF_COLLAR: - return this->GetFaceString(widget, GetCompanyManagerFaceBits(this->face, CMFV_COLLAR, this->ge), false); - - default: - return this->Window::GetWidgetString(widget, stringid); - } - } - void DrawWidget(const Rect &r, WidgetID widget) const override { switch (widget) { case WID_SCMF_FACE: DrawCompanyManagerFace(this->face, Company::Get(this->window_number)->colour, r); break; + + case WID_SCMF_STYLE: { + Rect ir = r.Shrink(WidgetDimensions::scaled.frametext, RectPadding::zero).WithHeight(this->line_height); + bool rtl = _current_text_dir == TD_RTL; + + Rect br = ir.CentreTo(ir.Width(), SETTING_BUTTON_HEIGHT).WithWidth(SETTING_BUTTON_WIDTH, rtl); + Rect tr = ir.Shrink(RectPadding::zero, WidgetDimensions::scaled.matrix).CentreTo(ir.Width(), GetCharacterHeight(FS_NORMAL)).Indent(SETTING_BUTTON_WIDTH + WidgetDimensions::scaled.hsep_wide, rtl); + + DrawArrowButtons(br.left, br.top, COLOUR_YELLOW, this->selected_var == UINT_MAX - 1 ? this->click_state : 0, true, true); + DrawString(tr, GetString(STR_FACE_SETTING_NUMERIC, STR_FACE_STYLE, this->face.style + 1, GetNumCompanyManagerFaceStyles()), TC_WHITE); + break; + } + + case WID_SCMF_PARTS: { + Rect ir = r.Shrink(WidgetDimensions::scaled.frametext, RectPadding::zero).WithHeight(this->line_height); + bool rtl = _current_text_dir == TD_RTL; + + FaceVars vars = GetCompanyManagerFaceVars(this->face.style); + + auto [first, last] = this->GetScrollbar(WID_SCMF_PARTS_SCROLLBAR)->GetVisibleRangeIterators(this->face_vars); + for (auto it = first; it != last; ++it) { + const uint8_t var = static_cast(*it - vars.data()); + const FaceVar &facevar = **it; + + Rect br = ir.CentreTo(ir.Width(), SETTING_BUTTON_HEIGHT).WithWidth(SETTING_BUTTON_WIDTH, rtl); + Rect tr = ir.Shrink(RectPadding::zero, WidgetDimensions::scaled.matrix).CentreTo(ir.Width(), GetCharacterHeight(FS_NORMAL)).Indent(SETTING_BUTTON_WIDTH + WidgetDimensions::scaled.hsep_wide, rtl); + + uint val = vars[var].GetBits(this->face); + if (facevar.type == FaceVarType::Toggle) { + DrawBoolButton(br.left, br.top, COLOUR_YELLOW, COLOUR_GREY, val == 1, true); + DrawString(tr, GetString(STR_FACE_SETTING_TOGGLE, facevar.name, val == 1 ? STR_FACE_YES : STR_FACE_NO), TC_WHITE); + } else { + DrawArrowButtons(br.left, br.top, COLOUR_YELLOW, this->selected_var == var ? this->click_state : 0, true, true); + DrawString(tr, GetString(STR_FACE_SETTING_NUMERIC, facevar.name, val + 1, facevar.valid_values), TC_WHITE); + } + + ir = ir.Translate(0, this->line_height); + } + break; + } } } @@ -1586,7 +1384,7 @@ public: /* OK button */ case WID_SCMF_ACCEPT: - Command::Post(this->face); + Command::Post(this->face.bits, this->face.style); [[fallthrough]]; /* Cancel button */ @@ -1595,101 +1393,119 @@ public: break; /* Load button */ - case WID_SCMF_LOAD: - this->face = _company_manager_face; - ScaleAllCompanyManagerFaceBits(this->face); + case WID_SCMF_LOAD: { + auto cmf = ParseCompanyManagerFaceCode(_company_manager_face); + if (cmf.has_value()) this->face = *cmf; ShowErrorMessage(GetEncodedString(STR_FACE_LOAD_DONE), {}, WL_INFO); this->UpdateData(); this->SetDirty(); break; + } /* 'Company manager face number' button, view and/or set company manager face number */ case WID_SCMF_FACECODE: - ShowQueryString(GetString(STR_JUST_INT, this->face), STR_FACE_FACECODE_CAPTION, 10 + 1, this, CS_NUMERAL, {}); + ShowQueryString(FormatCompanyManagerFaceCode(this->face), STR_FACE_FACECODE_CAPTION, 128, this, CS_ALPHANUMERAL, {}); break; /* Save button */ case WID_SCMF_SAVE: - _company_manager_face = this->face; + _company_manager_face = FormatCompanyManagerFaceCode(this->face); ShowErrorMessage(GetEncodedString(STR_FACE_SAVE_DONE), {}, WL_INFO); break; - /* Toggle gender (male/female) button */ - case WID_SCMF_MALE: - case WID_SCMF_FEMALE: - case WID_SCMF_MALE2: - case WID_SCMF_FEMALE2: - SetCompanyManagerFaceBits(this->face, CMFV_GENDER, this->ge, (widget == WID_SCMF_FEMALE || widget == WID_SCMF_FEMALE2)); - ScaleAllCompanyManagerFaceBits(this->face); - this->UpdateData(); - this->SetDirty(); - break; - /* Randomize face button */ case WID_SCMF_RANDOM_NEW_FACE: - RandomCompanyManagerFaceBits(this->face, this->ge, this->advanced, _interactive_random); + RandomiseCompanyManagerFace(this->face, _interactive_random); this->UpdateData(); this->SetDirty(); break; - /* Toggle ethnicity (european/african) button */ - case WID_SCMF_ETHNICITY_EUR: - case WID_SCMF_ETHNICITY_AFR: - SetCompanyManagerFaceBits(this->face, CMFV_ETHNICITY, this->ge, widget - WID_SCMF_ETHNICITY_EUR); - ScaleAllCompanyManagerFaceBits(this->face); - this->UpdateData(); - this->SetDirty(); - break; + case WID_SCMF_STYLE: { + bool rtl = _current_text_dir == TD_RTL; + Rect ir = this->GetWidget(widget)->GetCurrentRect().Shrink(WidgetDimensions::scaled.frametext, RectPadding::zero); + Rect br = ir.WithWidth(SETTING_BUTTON_WIDTH, rtl); - default: - /* Here all buttons from WID_SCMF_HAS_MOUSTACHE_EARRING to WID_SCMF_GLASSES_R are handled. - * First it checks which CompanyManagerFaceVariable is being changed, and then either - * a: invert the value for boolean variables, or - * b: it checks inside of IncreaseCompanyManagerFaceBits() if a left (_L) butten is pressed and then decrease else increase the variable */ - if (widget >= WID_SCMF_HAS_MOUSTACHE_EARRING && widget <= WID_SCMF_GLASSES_R) { - CompanyManagerFaceVariable cmfv; // which CompanyManagerFaceVariable shall be edited - - if (widget < WID_SCMF_EYECOLOUR_L) { // Bool buttons - switch (widget - WID_SCMF_HAS_MOUSTACHE_EARRING) { - default: NOT_REACHED(); - case 0: cmfv = this->is_female ? CMFV_HAS_TIE_EARRING : CMFV_HAS_MOUSTACHE; break; // Has earring/moustache button - case 1: cmfv = CMFV_HAS_GLASSES; break; // Has glasses button - } - SetCompanyManagerFaceBits(this->face, cmfv, this->ge, !GetCompanyManagerFaceBits(this->face, cmfv, this->ge)); - ScaleAllCompanyManagerFaceBits(this->face); - } else { // Value buttons - switch ((widget - WID_SCMF_EYECOLOUR_L) / 3) { - default: NOT_REACHED(); - case 0: cmfv = CMFV_EYE_COLOUR; break; // Eye colour buttons - case 1: cmfv = CMFV_CHIN; break; // Chin buttons - case 2: cmfv = CMFV_EYEBROWS; break; // Eyebrows buttons - case 3: cmfv = this->is_moust_male ? CMFV_MOUSTACHE : CMFV_LIPS; break; // Moustache or lips buttons - case 4: cmfv = CMFV_NOSE; break; // Nose buttons - case 5: cmfv = CMFV_HAIR; break; // Hair buttons - case 6: cmfv = CMFV_JACKET; break; // Jacket buttons - case 7: cmfv = CMFV_COLLAR; break; // Collar buttons - case 8: cmfv = CMFV_TIE_EARRING; break; // Tie/earring buttons - case 9: cmfv = CMFV_GLASSES; break; // Glasses buttons - } - /* 0 == left (_L), 1 == middle or 2 == right (_R) - button click */ - IncreaseCompanyManagerFaceBits(this->face, cmfv, this->ge, (((widget - WID_SCMF_EYECOLOUR_L) % 3) != 0) ? 1 : -1); - } - this->UpdateData(); - this->SetDirty(); + uint num_styles = GetNumCompanyManagerFaceStyles(); + this->selected_var = UINT_MAX - 1; + if (IsInsideBS(pt.x, br.left, SETTING_BUTTON_WIDTH / 2)) { + SetCompanyManagerFaceStyle(this->face, (this->face.style + num_styles - 1) % num_styles); + this->click_state = 1; + } else if (IsInsideBS(pt.x, br.left + SETTING_BUTTON_WIDTH / 2, SETTING_BUTTON_WIDTH / 2)) { + SetCompanyManagerFaceStyle(this->face, (this->face.style + 1) % num_styles); + this->click_state = 2; } + + this->UpdateData(); + this->SetTimeout(); + this->SetDirty(); break; + } + + case WID_SCMF_PARTS: { + bool rtl = _current_text_dir == TD_RTL; + Rect ir = this->GetWidget(widget)->GetCurrentRect().Shrink(WidgetDimensions::scaled.frametext, RectPadding::zero); + Rect br = ir.WithWidth(SETTING_BUTTON_WIDTH, rtl); + + this->selected_var = UINT_MAX; + + FaceVars vars = GetCompanyManagerFaceVars(this->face.style); + auto it = this->GetScrollbar(WID_SCMF_PARTS_SCROLLBAR)->GetScrolledItemFromWidget(this->face_vars, pt.y, this, widget, 0, this->line_height); + if (it == std::end(this->face_vars)) break; + + this->selected_var = static_cast(*it - vars.data()); + const auto &facevar = **it; + + if (facevar.type == FaceVarType::Toggle) { + if (!IsInsideBS(pt.x, br.left, SETTING_BUTTON_WIDTH)) break; + facevar.ChangeBits(this->face, 1); + this->UpdateData(); + } else { + if (IsInsideBS(pt.x, br.left, SETTING_BUTTON_WIDTH / 2)) { + facevar.ChangeBits(this->face, -1); + this->click_state = 1; + } else if (IsInsideBS(pt.x, br.left + SETTING_BUTTON_WIDTH / 2, SETTING_BUTTON_WIDTH / 2)) { + facevar.ChangeBits(this->face, 1); + this->click_state = 2; + } else { + break; + } + } + + this->SetTimeout(); + this->SetDirty(); + break; + } } } + void OnResize() override + { + if (auto *wid = this->GetWidget(WID_SCMF_PARTS); wid != nullptr) { + /* Workaround for automatic widget sizing ignoring resize steps. Manually ensure parts matrix is a + * multiple of its resize step. This trick only works here as the window itself is not resizable. */ + if (wid->UpdateVerticalSize((wid->current_y + wid->resize_y - 1) / wid->resize_y * wid->resize_y)) { + this->ReInit(); + return; + } + } + + this->GetScrollbar(WID_SCMF_PARTS_SCROLLBAR)->SetCapacityFromWidget(this, WID_SCMF_PARTS); + } + + void OnTimeout() override + { + this->click_state = 0; + this->selected_var = UINT_MAX; + this->SetDirty(); + } + void OnQueryTextFinished(std::optional str) override { if (!str.has_value()) return; - /* Set a new company manager face number */ - if (!str->empty()) { - auto val = ParseInteger(*str, 10, true); - if (!val.has_value()) return; - this->face = *val; - ScaleAllCompanyManagerFaceBits(this->face); + /* Parse new company manager face number */ + auto cmf = ParseCompanyManagerFaceCode(*str); + if (cmf.has_value()) { + this->face = *cmf; ShowErrorMessage(GetEncodedString(STR_FACE_FACECODE_SET), {}, WL_INFO); this->UpdateData(); this->SetDirty(); diff --git a/src/company_manager_face.h b/src/company_manager_face.h index 02544cf4a2..663da09a11 100644 --- a/src/company_manager_face.h +++ b/src/company_manager_face.h @@ -12,158 +12,142 @@ #include "core/random_func.hpp" #include "core/bitmath_func.hpp" -#include "table/sprites.h" +#include "strings_type.h" #include "company_type.h" +#include "gfx_type.h" -/** The gender/race combinations that we have faces for */ -enum GenderEthnicity : uint8_t { - GENDER_FEMALE = 0, ///< This bit set means a female, otherwise male - ETHNICITY_BLACK = 1, ///< This bit set means black, otherwise white +#include "table/strings.h" - GE_WM = 0, ///< A male of Caucasian origin (white) - GE_WF = 1 << GENDER_FEMALE, ///< A female of Caucasian origin (white) - GE_BM = 1 << ETHNICITY_BLACK, ///< A male of African origin (black) - GE_BF = 1 << ETHNICITY_BLACK | 1 << GENDER_FEMALE, ///< A female of African origin (black) - GE_END, +enum class FaceVarType : uint8_t { + Sprite, + Palette, + Toggle, }; -DECLARE_ENUM_AS_BIT_SET(GenderEthnicity) ///< See GenderRace as a bitset - -/** Bitgroups of the CompanyManagerFace variable */ -enum CompanyManagerFaceVariable : uint8_t { - CMFV_GENDER, - CMFV_ETHNICITY, - CMFV_GEN_ETHN, - CMFV_HAS_MOUSTACHE, - CMFV_HAS_TIE_EARRING, - CMFV_HAS_GLASSES, - CMFV_EYE_COLOUR, - CMFV_CHEEKS, - CMFV_CHIN, - CMFV_EYEBROWS, - CMFV_MOUSTACHE, - CMFV_LIPS, - CMFV_NOSE, - CMFV_HAIR, - CMFV_COLLAR, - CMFV_JACKET, - CMFV_TIE_EARRING, - CMFV_GLASSES, - CMFV_END, -}; -DECLARE_INCREMENT_DECREMENT_OPERATORS(CompanyManagerFaceVariable) /** Information about the valid values of CompanyManagerFace bitgroups as well as the sprites to draw */ -struct CompanyManagerFaceBitsInfo { - uint8_t offset; ///< Offset in bits into the CompanyManagerFace - uint8_t length; ///< Number of bits used in the CompanyManagerFace - uint8_t valid_values[GE_END]; ///< The number of valid values per gender/ethnicity - SpriteID first_sprite[GE_END]; ///< The first sprite per gender/ethnicity -}; +struct FaceVar { + FaceVarType type; + uint8_t position; ///< Position in UI. + uint8_t offset; ///< Offset in bits into the CompanyManagerFace + uint8_t length; ///< Number of bits used in the CompanyManagerFace + uint8_t valid_values; ///< The number of valid values + std::variant> data; ///< The first sprite + StringID name = STR_NULL; -/** Lookup table for indices into the CompanyManagerFace, valid ranges and sprites */ -static const CompanyManagerFaceBitsInfo _cmf_info[] = { - /* Index off len WM WF BM BF WM WF BM BF - * CMFV_GENDER */ { 0, 1, { 2, 2, 2, 2 }, { 0, 0, 0, 0 } }, ///< 0 = male, 1 = female - /* CMFV_ETHNICITY */ { 1, 2, { 2, 2, 2, 2 }, { 0, 0, 0, 0 } }, ///< 0 = (Western-)Caucasian, 1 = African(-American)/Black - /* CMFV_GEN_ETHN */ { 0, 3, { 4, 4, 4, 4 }, { 0, 0, 0, 0 } }, ///< Shortcut to get/set gender _and_ ethnicity - /* CMFV_HAS_MOUSTACHE */ { 3, 1, { 2, 0, 2, 0 }, { 0, 0, 0, 0 } }, ///< Females do not have a moustache - /* CMFV_HAS_TIE_EARRING */ { 3, 1, { 0, 2, 0, 2 }, { 0, 0, 0, 0 } }, ///< Draw the earring for females or not. For males the tie is always drawn. - /* CMFV_HAS_GLASSES */ { 4, 1, { 2, 2, 2, 2 }, { 0, 0, 0, 0 } }, ///< Whether to draw glasses or not - /* CMFV_EYE_COLOUR */ { 5, 2, { 3, 3, 1, 1 }, { 0, 0, 0, 0 } }, ///< Palette modification - /* CMFV_CHEEKS */ { 0, 0, { 1, 1, 1, 1 }, { 0x325, 0x326, 0x390, 0x3B0 } }, ///< Cheeks are only indexed by their gender/ethnicity - /* CMFV_CHIN */ { 7, 2, { 4, 1, 2, 2 }, { 0x327, 0x327, 0x391, 0x3B1 } }, - /* CMFV_EYEBROWS */ { 9, 4, { 12, 16, 11, 16 }, { 0x32B, 0x337, 0x39A, 0x3B8 } }, - /* CMFV_MOUSTACHE */ { 13, 2, { 3, 0, 3, 0 }, { 0x367, 0, 0x397, 0 } }, ///< Depends on CMFV_HAS_MOUSTACHE - /* CMFV_LIPS */ { 13, 4, { 12, 10, 9, 9 }, { 0x35B, 0x351, 0x3A5, 0x3C8 } }, ///< Depends on !CMFV_HAS_MOUSTACHE - /* CMFV_NOSE */ { 17, 3, { 8, 4, 4, 5 }, { 0x349, 0x34C, 0x393, 0x3B3 } }, ///< Depends on !CMFV_HAS_MOUSTACHE - /* CMFV_HAIR */ { 20, 4, { 9, 5, 5, 5 }, { 0x382, 0x38B, 0x3D4, 0x3D9 } }, - /* CMFV_COLLAR */ { 26, 2, { 4, 4, 4, 4 }, { 0x36E, 0x37B, 0x36E, 0x37B } }, - /* CMFV_JACKET */ { 24, 2, { 3, 3, 3, 3 }, { 0x36B, 0x378, 0x36B, 0x378 } }, - /* CMFV_TIE_EARRING */ { 28, 3, { 6, 3, 6, 3 }, { 0x372, 0x37F, 0x372, 0x3D1 } }, ///< Depends on CMFV_HAS_TIE_EARRING - /* CMFV_GLASSES */ { 31, 1, { 2, 2, 2, 2 }, { 0x347, 0x347, 0x3AE, 0x3AE } } ///< Depends on CMFV_HAS_GLASSES -}; -/** Make sure the table's size is right. */ -static_assert(lengthof(_cmf_info) == CMFV_END); - -/** - * Gets the company manager's face bits for the given company manager's face variable - * @param cmf the face to extract the bits from - * @param cmfv the face variable to get the data of - * @param ge the gender and ethnicity of the face - * @pre _cmf_info[cmfv].valid_values[ge] != 0 - * @return the requested bits - */ -inline uint GetCompanyManagerFaceBits(CompanyManagerFace cmf, CompanyManagerFaceVariable cmfv, [[maybe_unused]] GenderEthnicity ge) -{ - assert(_cmf_info[cmfv].valid_values[ge] != 0); - - return GB(cmf, _cmf_info[cmfv].offset, _cmf_info[cmfv].length); -} - -/** - * Sets the company manager's face bits for the given company manager's face variable - * @param cmf the face to write the bits to - * @param cmfv the face variable to write the data of - * @param ge the gender and ethnicity of the face - * @param val the new value - * @pre val < _cmf_info[cmfv].valid_values[ge] - */ -inline void SetCompanyManagerFaceBits(CompanyManagerFace &cmf, CompanyManagerFaceVariable cmfv, [[maybe_unused]] GenderEthnicity ge, uint val) -{ - assert(val < _cmf_info[cmfv].valid_values[ge]); - - SB(cmf, _cmf_info[cmfv].offset, _cmf_info[cmfv].length, val); -} - -/** - * Increase/Decrease the company manager's face variable by the given amount. - * The value wraps around to stay in the valid range. - * - * @param cmf the company manager face to write the bits to - * @param cmfv the company manager face variable to write the data of - * @param ge the gender and ethnicity of the company manager's face - * @param amount the amount which change the value - * - * @pre 0 <= val < _cmf_info[cmfv].valid_values[ge] - */ -inline void IncreaseCompanyManagerFaceBits(CompanyManagerFace &cmf, CompanyManagerFaceVariable cmfv, GenderEthnicity ge, int8_t amount) -{ - int8_t val = GetCompanyManagerFaceBits(cmf, cmfv, ge) + amount; // the new value for the cmfv - - /* scales the new value to the correct scope */ - while (val < 0) { - val += _cmf_info[cmfv].valid_values[ge]; + /** + * Gets the company manager's face bits. + * @param cmf The face to extract the bits from. + * @return the requested bits + */ + inline uint GetBits(const CompanyManagerFace &cmf) const + { + return GB(cmf.bits, this->offset, this->length); } - val %= _cmf_info[cmfv].valid_values[ge]; - SetCompanyManagerFaceBits(cmf, cmfv, ge, val); // save the new value -} + /** + * Sets the company manager's face bits. + * @param cmf The face to write the bits to. + * @param val The new value. + */ + inline void SetBits(CompanyManagerFace &cmf, uint val) const + { + SB(cmf.bits, this->offset, this->length, val); + } + + /** + * Increase/Decrease the company manager's face variable by the given amount. + * The value wraps around to stay in the valid range. + * @param cmf The face to write the bits to. + * @param amount the amount to change the value + */ + inline void ChangeBits(CompanyManagerFace &cmf, int8_t amount) const + { + int8_t val = this->GetBits(cmf) + amount; // the new value for the cmfv + + /* scales the new value to the correct scope */ + while (val < 0) { + val += this->valid_values; + } + val %= this->valid_values; + + this->SetBits(cmf, val); // save the new value + } + + /** + * Checks whether the company manager's face bits have a valid range + * @param cmf The face to check. + * @return true if and only if the bits are valid + */ + inline bool IsValid(const CompanyManagerFace &cmf) const + { + return GB(cmf.bits, this->offset, this->length) < this->valid_values; + } + + /** + * Scales a company manager's face bits variable to the correct scope + * @param vars The face variables of the face style. + * @pre val < (1U << length), i.e. val has a value of 0..2^(bits used for this variable)-1 + * @return the scaled value + */ + inline uint ScaleBits(uint val) const + { + assert(val < (1U << this->length)); + return (val * this->valid_values) >> this->length; + } + + /** + * Gets the sprite to draw. + * @param cmf The face to extract the data from + * @pre vars[var].type == FaceVarType::Sprite. + * @return sprite to draw + */ + inline SpriteID GetSprite(const CompanyManagerFace &cmf) const + { + assert(this->type == FaceVarType::Sprite); + return std::get(this->data) + this->GetBits(cmf); + } +}; + +using FaceVars = std::span; + +struct FaceSpec { + std::string label; + std::variant> face_vars; + + inline FaceVars GetFaceVars() const + { + struct visitor { + FaceVars operator()(FaceVars vars) const { return vars; } + FaceVars operator()(const std::vector &vars) const { return vars; } + }; + return std::visit(visitor{}, this->face_vars); + } +}; + +void ResetFaces(); +uint GetNumCompanyManagerFaceStyles(); +std::optional FindCompanyManagerFaceLabel(std::string_view label); +const FaceSpec *GetCompanyManagerFaceSpec(uint style_index); +FaceVars GetCompanyManagerFaceVars(uint style_index); /** - * Checks whether the company manager's face bits have a valid range - * @param cmf the face to extract the bits from - * @param cmfv the face variable to get the data of - * @param ge the gender and ethnicity of the face - * @return true if and only if the bits are valid + * Get a bitmask of currently active face variables. + * Face variables can be inactive due to toggles in the face variables. + * @param cmf The company manager face. + * @param vars The face variables of the face. + * @return Currently active face variables for the face. */ -inline bool AreCompanyManagerFaceBitsValid(CompanyManagerFace cmf, CompanyManagerFaceVariable cmfv, GenderEthnicity ge) +inline uint64_t GetActiveFaceVars(const CompanyManagerFace &cmf, FaceVars vars) { - return GB(cmf, _cmf_info[cmfv].offset, _cmf_info[cmfv].length) < _cmf_info[cmfv].valid_values[ge]; -} + uint64_t active_vars = (1ULL << std::size(vars)) - 1ULL; -/** - * Scales a company manager's face bits variable to the correct scope - * @param cmfv the face variable to write the data of - * @param ge the gender and ethnicity of the face - * @param val the to value to scale - * @pre val < (1U << _cmf_info[cmfv].length), i.e. val has a value of 0..2^(bits used for this variable)-1 - * @return the scaled value - */ -inline uint ScaleCompanyManagerFaceValue(CompanyManagerFaceVariable cmfv, GenderEthnicity ge, uint val) -{ - assert(val < (1U << _cmf_info[cmfv].length)); + for (const auto &info : vars) { + if (info.type != FaceVarType::Toggle) continue; + const auto &[off, on] = std::get>(info.data); + active_vars &= ~(HasBit(cmf.bits, info.offset) ? on : off); + } - return (val * _cmf_info[cmfv].valid_values[ge]) >> _cmf_info[cmfv].length; + return active_vars; } /** @@ -171,69 +155,31 @@ inline uint ScaleCompanyManagerFaceValue(CompanyManagerFaceVariable cmfv, Gender * * @param cmf the company manager's face to write the bits to */ -inline void ScaleAllCompanyManagerFaceBits(CompanyManagerFace &cmf) +inline void ScaleAllCompanyManagerFaceBits(CompanyManagerFace &cmf, FaceVars vars) { - IncreaseCompanyManagerFaceBits(cmf, CMFV_ETHNICITY, GE_WM, 0); // scales the ethnicity - - GenderEthnicity ge = (GenderEthnicity)GB(cmf, _cmf_info[CMFV_GEN_ETHN].offset, _cmf_info[CMFV_GEN_ETHN].length); // gender & ethnicity of the face - - /* Is a male face with moustache. Need to reduce CPU load in the loop. */ - bool is_moust_male = !HasBit(ge, GENDER_FEMALE) && GetCompanyManagerFaceBits(cmf, CMFV_HAS_MOUSTACHE, ge) != 0; - - for (CompanyManagerFaceVariable cmfv = CMFV_EYE_COLOUR; cmfv < CMFV_END; cmfv++) { // scales all other variables - - /* The moustache variable will be scaled only if it is a male face with has a moustache */ - if (cmfv != CMFV_MOUSTACHE || is_moust_male) { - IncreaseCompanyManagerFaceBits(cmf, cmfv, ge, 0); - } + for (auto var : SetBitIterator(GetActiveFaceVars(cmf, vars))) { + vars[var].ChangeBits(cmf, 0); } } /** - * Make a random new face. - * If it is for the advanced company manager's face window then the new face have the same gender - * and ethnicity as the old one, else the gender is equal and the ethnicity is random. - * - * @param cmf the company manager's face to write the bits to - * @param ge the gender and ethnicity of the old company manager's face - * @param adv if it for the advanced company manager's face window - * @param randomizer the source of random to use for creating the manager face - * - * @pre scale 'ge' to a valid gender/ethnicity combination + * Make a random new face without changing the face style. + * @param cmf The company manager's face to write the bits to + * @param vars The face variables. + * @param randomizer The source of random to use for creating the manager face */ -inline void RandomCompanyManagerFaceBits(CompanyManagerFace &cmf, GenderEthnicity ge, bool adv, Randomizer &randomizer) +inline void RandomiseCompanyManagerFaceBits(CompanyManagerFace &cmf, FaceVars vars, Randomizer &randomizer) { - cmf = randomizer.Next(); // random all company manager's face bits - - /* scale ge: 0 == GE_WM, 1 == GE_WF, 2 == GE_BM, 3 == GE_BF (and maybe in future: ...) */ - ge = (GenderEthnicity)((uint)ge % GE_END); - - /* set the gender (and ethnicity) for the new company manager's face */ - if (adv) { - SetCompanyManagerFaceBits(cmf, CMFV_GEN_ETHN, ge, ge); - } else { - SetCompanyManagerFaceBits(cmf, CMFV_GENDER, ge, HasBit(ge, GENDER_FEMALE)); - } - - /* scales all company manager's face bits to the correct scope */ - ScaleAllCompanyManagerFaceBits(cmf); + cmf.bits = randomizer.Next(); + ScaleAllCompanyManagerFaceBits(cmf, vars); } -/** - * Gets the sprite to draw for the given company manager's face variable - * @param cmf the face to extract the data from - * @param cmfv the face variable to get the sprite of - * @param ge the gender and ethnicity of the face - * @pre _cmf_info[cmfv].valid_values[ge] != 0 - * @return sprite to draw - */ -inline SpriteID GetCompanyManagerFaceSprite(CompanyManagerFace cmf, CompanyManagerFaceVariable cmfv, GenderEthnicity ge) -{ - assert(_cmf_info[cmfv].valid_values[ge] != 0); +void SetCompanyManagerFaceStyle(CompanyManagerFace &cmf, uint style); +void RandomiseCompanyManagerFace(CompanyManagerFace &cmf, Randomizer &randomizer); +uint32_t MaskCompanyManagerFaceBits(const CompanyManagerFace &cmf, FaceVars vars); +std::string FormatCompanyManagerFaceCode(const CompanyManagerFace &cmf); +std::optional ParseCompanyManagerFaceCode(std::string_view str); - return _cmf_info[cmfv].first_sprite[ge] + GB(cmf, _cmf_info[cmfv].offset, _cmf_info[cmfv].length); -} - -void DrawCompanyManagerFace(CompanyManagerFace face, Colours colour, const Rect &r); +void DrawCompanyManagerFace(const CompanyManagerFace &cmf, Colours colour, const Rect &r); #endif /* COMPANY_MANAGER_FACE_H */ diff --git a/src/company_type.h b/src/company_type.h index 17ce117399..fe984a245d 100644 --- a/src/company_type.h +++ b/src/company_type.h @@ -49,7 +49,12 @@ public: }; struct Company; -typedef uint32_t CompanyManagerFace; ///< Company manager face bits, info see in company_manager_face.h + +struct CompanyManagerFace { + uint style = 0; ///< Company manager face style. + uint32_t bits = 0; ///< Company manager face bits, meaning is dependent on style. + std::string style_label; ///< Face style label. +}; /** The reason why the company was removed. */ enum CompanyRemoveReason : uint8_t { diff --git a/src/error_gui.cpp b/src/error_gui.cpp index b4b51cf549..182ac7229f 100644 --- a/src/error_gui.cpp +++ b/src/error_gui.cpp @@ -28,6 +28,7 @@ #include "widgets/error_widget.h" +#include "table/sprites.h" #include "table/strings.h" #include "safeguards.h" diff --git a/src/lang/english.txt b/src/lang/english.txt index 55c1cf1e30..30c6848c6c 100644 --- a/src/lang/english.txt +++ b/src/lang/english.txt @@ -2318,12 +2318,7 @@ STR_LIVERY_FREIGHT_TRAM :Freight Tram STR_FACE_CAPTION :{WHITE}Face Selection STR_FACE_CANCEL_TOOLTIP :{BLACK}Cancel new face selection STR_FACE_OK_TOOLTIP :{BLACK}Accept new face selection -STR_FACE_RANDOM :{BLACK}Randomise -STR_FACE_MALE_BUTTON :{BLACK}Male -STR_FACE_MALE_TOOLTIP :{BLACK}Select male faces -STR_FACE_FEMALE_BUTTON :{BLACK}Female -STR_FACE_FEMALE_TOOLTIP :{BLACK}Select female faces STR_FACE_NEW_FACE_BUTTON :{BLACK}New Face STR_FACE_NEW_FACE_TOOLTIP :{BLACK}Generate random new face STR_FACE_ADVANCED :{BLACK}Advanced @@ -2333,44 +2328,31 @@ STR_FACE_SIMPLE_TOOLTIP :{BLACK}Simple f STR_FACE_LOAD :{BLACK}Load STR_FACE_LOAD_TOOLTIP :{BLACK}Load favourite face STR_FACE_LOAD_DONE :{WHITE}Your favourite face has been loaded from the OpenTTD configuration file -STR_FACE_FACECODE :{BLACK}Player face no. -STR_FACE_FACECODE_TOOLTIP :{BLACK}View and/or set face number of the company president -STR_FACE_FACECODE_CAPTION :{WHITE}View and/or set president face number -STR_FACE_FACECODE_SET :{WHITE}New face number code has been set -STR_FACE_FACECODE_ERR :{WHITE}Couldn't set president face number - must be a number between 0 and 4,294,967,295! +STR_FACE_FACECODE :{BLACK}Player face code +STR_FACE_FACECODE_TOOLTIP :{BLACK}View and/or set face code of the company president +STR_FACE_FACECODE_CAPTION :{WHITE}View and/or set president face code +STR_FACE_FACECODE_SET :{WHITE}New president face has been set +STR_FACE_FACECODE_ERR :{WHITE}Couldn't set president face code - must be a valid label and number STR_FACE_SAVE :{BLACK}Save STR_FACE_SAVE_TOOLTIP :{BLACK}Save favourite face STR_FACE_SAVE_DONE :{WHITE}This face will be saved as your favourite in the OpenTTD configuration file -STR_FACE_EUROPEAN :{BLACK}European -STR_FACE_EUROPEAN_TOOLTIP :{BLACK}Select European faces -STR_FACE_AFRICAN :{BLACK}African -STR_FACE_AFRICAN_TOOLTIP :{BLACK}Select African faces +STR_FACE_SETTING_TOGGLE :{STRING} {ORANGE}{STRING} +STR_FACE_SETTING_NUMERIC :{STRING} {ORANGE}{NUM} / {NUM} STR_FACE_YES :Yes STR_FACE_NO :No -STR_FACE_MOUSTACHE_EARRING_TOOLTIP :{BLACK}Enable moustache or earring +STR_FACE_STYLE :Style: STR_FACE_HAIR :Hair: -STR_FACE_HAIR_TOOLTIP :{BLACK}Change hair STR_FACE_EYEBROWS :Eyebrows: -STR_FACE_EYEBROWS_TOOLTIP :{BLACK}Change eyebrows STR_FACE_EYECOLOUR :Eye colour: -STR_FACE_EYECOLOUR_TOOLTIP :{BLACK}Change eye colour STR_FACE_GLASSES :Glasses: -STR_FACE_GLASSES_TOOLTIP :{BLACK}Enable glasses -STR_FACE_GLASSES_TOOLTIP_2 :{BLACK}Change glasses STR_FACE_NOSE :Nose: -STR_FACE_NOSE_TOOLTIP :{BLACK}Change nose STR_FACE_LIPS :Lips: STR_FACE_MOUSTACHE :Moustache: -STR_FACE_LIPS_MOUSTACHE_TOOLTIP :{BLACK}Change lips or moustache STR_FACE_CHIN :Chin: -STR_FACE_CHIN_TOOLTIP :{BLACK}Change chin STR_FACE_JACKET :Jacket: -STR_FACE_JACKET_TOOLTIP :{BLACK}Change jacket STR_FACE_COLLAR :Collar: -STR_FACE_COLLAR_TOOLTIP :{BLACK}Change collar STR_FACE_TIE :Tie: STR_FACE_EARRING :Earring: -STR_FACE_TIE_EARRING_TOOLTIP :{BLACK}Change tie or earring # Matches ServerGameType ###length 3 diff --git a/src/newgrf.cpp b/src/newgrf.cpp index c1d9904ad2..6cb2919198 100644 --- a/src/newgrf.cpp +++ b/src/newgrf.cpp @@ -10,6 +10,7 @@ #include "stdafx.h" #include "core/backup_type.hpp" #include "core/container_func.hpp" +#include "company_manager_face.h" #include "debug.h" #include "fileio_func.h" #include "engine_func.h" @@ -463,6 +464,8 @@ void ResetNewGRFData() /* Reset canal sprite groups and flags */ _water_feature.fill({}); + ResetFaces(); + /* Reset the snowline table. */ ClearSnowLine(); diff --git a/src/news_type.h b/src/news_type.h index 87387a0f0b..11d932ced6 100644 --- a/src/news_type.h +++ b/src/news_type.h @@ -11,6 +11,7 @@ #define NEWS_TYPE_H #include "core/enum_type.hpp" +#include "company_type.h" #include "engine_type.h" #include "industry_type.h" #include "gfx_type.h" @@ -164,7 +165,7 @@ struct CompanyNewsInformation : NewsAllocatedData { std::string other_company_name; ///< The name of the company taking over this one StringID title; - uint32_t face; ///< The face of the president + CompanyManagerFace face; ///< The face of the president Colours colour; ///< The colour related to the company CompanyNewsInformation(StringID title, const struct Company *c, const struct Company *other = nullptr); diff --git a/src/saveload/afterload.cpp b/src/saveload/afterload.cpp index 4e6899ea36..da1386915f 100644 --- a/src/saveload/afterload.cpp +++ b/src/saveload/afterload.cpp @@ -1625,7 +1625,28 @@ bool AfterLoadGame() } } - if (IsSavegameVersionBefore(SLV_49)) for (Company *c : Company::Iterate()) c->face = ConvertFromOldCompanyManagerFace(c->face); + if (IsSavegameVersionBefore(SLV_49)) { + /* Perform conversion of very old face bits. */ + for (Company *c : Company::Iterate()) { + c->face = ConvertFromOldCompanyManagerFace(c->face.bits); + } + } else if (IsSavegameVersionBefore(SLV_FACE_STYLES)) { + /* Convert old gender and ethnicity bits to face style. */ + for (Company *c : Company::Iterate()) { + SetCompanyManagerFaceStyle(c->face, GB(c->face.bits, 0, 2)); + } + } else { + /* Look up each company face style by its label. */ + for (Company *c : Company::Iterate()) { + auto style = FindCompanyManagerFaceLabel(c->face.style_label); + if (style.has_value()) { + SetCompanyManagerFaceStyle(c->face, *style); + } else { + /* Style no longer exists, pick an entirely new face. */ + RandomiseCompanyManagerFace(c->face, _random); + } + } + } if (IsSavegameVersionBefore(SLV_52)) { for (auto t : Map::Iterate()) { diff --git a/src/saveload/company_sl.cpp b/src/saveload/company_sl.cpp index 810f977fdf..679b42bded 100644 --- a/src/saveload/company_sl.cpp +++ b/src/saveload/company_sl.cpp @@ -24,6 +24,20 @@ #include "../safeguards.h" +/** + * Search for a face variable by type and name. + * @param style Face style to find variable in. + * @param type Type of variable to look up. + * @param name Name (string) of variable to look up. + * @return Face variable if present, otherwise nullptr. + */ +static const FaceVar *FindFaceVar(FaceVars style, FaceVarType type, StringID name) +{ + auto it = std::ranges::find_if(style, [type, name](const FaceVar &facevar) { return facevar.type == type && facevar.name == name; }); + if (it != std::end(style)) return &*it; + return nullptr; +} + /** * Converts an old company manager's face format to the new company manager's face format * @@ -44,49 +58,65 @@ */ CompanyManagerFace ConvertFromOldCompanyManagerFace(uint32_t face) { - CompanyManagerFace cmf = 0; - GenderEthnicity ge = GE_WM; + CompanyManagerFace cmf{}; - if (HasBit(face, 31)) SetBit(ge, GENDER_FEMALE); - if (HasBit(face, 27) && (HasBit(face, 26) == HasBit(face, 19))) SetBit(ge, ETHNICITY_BLACK); + if (HasBit(face, 31)) cmf.style += 1; + if (HasBit(face, 27) && (HasBit(face, 26) == HasBit(face, 19))) cmf.style += 2; - SetCompanyManagerFaceBits(cmf, CMFV_GEN_ETHN, ge, ge); - SetCompanyManagerFaceBits(cmf, CMFV_HAS_GLASSES, ge, GB(face, 28, 3) <= 1); - SetCompanyManagerFaceBits(cmf, CMFV_EYE_COLOUR, ge, HasBit(ge, ETHNICITY_BLACK) ? 0 : ClampU(GB(face, 20, 3), 5, 7) - 5); - SetCompanyManagerFaceBits(cmf, CMFV_CHIN, ge, ScaleCompanyManagerFaceValue(CMFV_CHIN, ge, GB(face, 4, 2))); - SetCompanyManagerFaceBits(cmf, CMFV_EYEBROWS, ge, ScaleCompanyManagerFaceValue(CMFV_EYEBROWS, ge, GB(face, 6, 4))); - SetCompanyManagerFaceBits(cmf, CMFV_HAIR, ge, ScaleCompanyManagerFaceValue(CMFV_HAIR, ge, GB(face, 16, 4))); - SetCompanyManagerFaceBits(cmf, CMFV_JACKET, ge, ScaleCompanyManagerFaceValue(CMFV_JACKET, ge, GB(face, 20, 2))); - SetCompanyManagerFaceBits(cmf, CMFV_COLLAR, ge, ScaleCompanyManagerFaceValue(CMFV_COLLAR, ge, GB(face, 22, 2))); - SetCompanyManagerFaceBits(cmf, CMFV_GLASSES, ge, GB(face, 28, 1)); + const FaceSpec *spec = GetCompanyManagerFaceSpec(cmf.style); + FaceVars vars = spec->GetFaceVars(); + + cmf.style_label = spec->label; + + if (auto var = FindFaceVar(vars, FaceVarType::Toggle, STR_FACE_GLASSES); var != nullptr) var->SetBits(cmf, GB(face, 28, 3) <= 1); + if (auto var = FindFaceVar(vars, FaceVarType::Palette, STR_FACE_EYECOLOUR); var != nullptr) var->SetBits(cmf, ClampU(GB(face, 20, 3), 5, 7) - 5); + if (auto var = FindFaceVar(vars, FaceVarType::Sprite, STR_FACE_CHIN); var != nullptr) var->SetBits(cmf, var->ScaleBits(GB(face, 4, 2))); + if (auto var = FindFaceVar(vars, FaceVarType::Sprite, STR_FACE_EYEBROWS); var != nullptr) var->SetBits(cmf, var->ScaleBits(GB(face, 6, 4))); + if (auto var = FindFaceVar(vars, FaceVarType::Sprite, STR_FACE_HAIR); var != nullptr) var->SetBits(cmf, var->ScaleBits(GB(face, 16, 4))); + if (auto var = FindFaceVar(vars, FaceVarType::Sprite, STR_FACE_JACKET); var != nullptr) var->SetBits(cmf, var->ScaleBits(GB(face, 20, 2))); + if (auto var = FindFaceVar(vars, FaceVarType::Sprite, STR_FACE_COLLAR); var != nullptr) var->SetBits(cmf, var->ScaleBits(GB(face, 22, 2))); + if (auto var = FindFaceVar(vars, FaceVarType::Sprite, STR_FACE_GLASSES); var != nullptr) var->SetBits(cmf, GB(face, 28, 1)); uint lips = GB(face, 10, 4); - if (!HasBit(ge, GENDER_FEMALE) && lips < 4) { - SetCompanyManagerFaceBits(cmf, CMFV_HAS_MOUSTACHE, ge, true); - SetCompanyManagerFaceBits(cmf, CMFV_MOUSTACHE, ge, std::max(lips, 1U) - 1); + if (cmf.style != 1 && cmf.style != 3 && lips < 4) { + if (auto var = FindFaceVar(vars, FaceVarType::Toggle, STR_FACE_MOUSTACHE); var != nullptr) var->SetBits(cmf, true); + if (auto var = FindFaceVar(vars, FaceVarType::Sprite, STR_FACE_MOUSTACHE); var != nullptr) var->SetBits(cmf, std::max(lips, 1U) - 1); } else { - if (!HasBit(ge, GENDER_FEMALE)) { - lips = lips * 15 / 16; - lips -= 3; - if (HasBit(ge, ETHNICITY_BLACK) && lips > 8) lips = 0; - } else { - lips = ScaleCompanyManagerFaceValue(CMFV_LIPS, ge, lips); + if (auto var = FindFaceVar(vars, FaceVarType::Sprite, STR_FACE_LIPS); var != nullptr) { + if (cmf.style == 0 || cmf.style == 2) { + lips = lips * 15 / 16; + lips -= 3; + if (cmf.style == 2 && lips > 8) lips = 0; + } else { + lips = var->ScaleBits(lips); + } + var->SetBits(cmf, lips); } - SetCompanyManagerFaceBits(cmf, CMFV_LIPS, ge, lips); - uint nose = GB(face, 13, 3); - if (ge == GE_WF) { - nose = (nose * 3 >> 3) * 3 >> 2; // There is 'hole' in the nose sprites for females - } else { - nose = ScaleCompanyManagerFaceValue(CMFV_NOSE, ge, nose); + if (auto var = FindFaceVar(vars, FaceVarType::Sprite, STR_FACE_NOSE); var != nullptr) { + uint nose = GB(face, 13, 3); + if (cmf.style == 1) { + nose = (nose * 3 >> 3) * 3 >> 2; // There is 'hole' in the nose sprites for women + } else { + nose = var->ScaleBits(nose); + } + var->SetBits(cmf, nose); } - SetCompanyManagerFaceBits(cmf, CMFV_NOSE, ge, nose); } - uint tie_earring = GB(face, 24, 4); - if (!HasBit(ge, GENDER_FEMALE) || tie_earring < 3) { // Not all females have an earring - if (HasBit(ge, GENDER_FEMALE)) SetCompanyManagerFaceBits(cmf, CMFV_HAS_TIE_EARRING, ge, true); - SetCompanyManagerFaceBits(cmf, CMFV_TIE_EARRING, ge, HasBit(ge, GENDER_FEMALE) ? tie_earring : ScaleCompanyManagerFaceValue(CMFV_TIE_EARRING, ge, tie_earring / 2)); + if (auto var = FindFaceVar(vars, FaceVarType::Sprite, STR_FACE_TIE); var != nullptr) { + uint tie = GB(face, 24, 4); + var->SetBits(cmf, var->ScaleBits(tie / 2)); + } + + if (auto var = FindFaceVar(vars, FaceVarType::Sprite, STR_FACE_EARRING); var != nullptr) { + uint earring = GB(face, 24, 4); + if (earring < 3) { // Not all women have an earring + if (auto has_earring = FindFaceVar(vars, FaceVarType::Toggle, STR_FACE_EARRING)) { + has_earring->SetBits(cmf, true); + var->SetBits(cmf, earring); + } + } } return cmf; @@ -470,7 +500,8 @@ static const SaveLoad _company_desc[] = { SLE_CONDVECTOR(CompanyProperties, allow_list, SLE_STR, SLV_COMPANY_ALLOW_LIST, SLV_COMPANY_ALLOW_LIST_V2), SLEG_CONDSTRUCTLIST("allow_list", SlAllowListData, SLV_COMPANY_ALLOW_LIST_V2, SL_MAX_VERSION), - SLE_VAR(CompanyProperties, face, SLE_UINT32), + SLE_VARNAME(CompanyProperties, face.bits, "face", SLE_UINT32), + SLE_CONDSSTRNAME(CompanyProperties, face.style_label, "face_style", SLE_STR, SLV_FACE_STYLES, SL_MAX_VERSION), /* money was changed to a 64 bit field in savegame version 1. */ SLE_CONDVAR(CompanyProperties, money, SLE_VAR_I64 | SLE_FILE_I32, SL_MIN_VERSION, SLV_1), diff --git a/src/saveload/oldloader_sl.cpp b/src/saveload/oldloader_sl.cpp index 46585f0eab..8ba02478cc 100644 --- a/src/saveload/oldloader_sl.cpp +++ b/src/saveload/oldloader_sl.cpp @@ -939,7 +939,7 @@ static bool LoadOldCompanyEconomy(LoadgameState &ls, int) static const OldChunks _company_chunk[] = { OCL_VAR ( OC_UINT16, 1, &_old_string_id ), OCL_SVAR( OC_UINT32, Company, name_2 ), - OCL_SVAR( OC_UINT32, Company, face ), + OCL_SVAR( OC_UINT32, Company, face.bits ), OCL_VAR ( OC_UINT16, 1, &_old_string_id_2 ), OCL_SVAR( OC_UINT32, Company, president_name_2 ), @@ -992,9 +992,9 @@ static bool LoadOldCompany(LoadgameState &ls, int num) if (_savegame_type == SGT_TTO) { /* adjust manager's face */ - if (HasBit(c->face, 27) && GB(c->face, 26, 1) == GB(c->face, 19, 1)) { + if (HasBit(c->face.bits, 27) && GB(c->face.bits, 26, 1) == GB(c->face.bits, 19, 1)) { /* if face would be black in TTD, adjust tie colour and thereby face colour */ - ClrBit(c->face, 27); + ClrBit(c->face.bits, 27); } /* Company name */ diff --git a/src/saveload/saveload.h b/src/saveload/saveload.h index ec78153db1..7ab7c06742 100644 --- a/src/saveload/saveload.h +++ b/src/saveload/saveload.h @@ -403,6 +403,8 @@ enum SaveLoadVersion : uint16_t { SLV_FIX_SCC_ENCODED_NEGATIVE, ///< 353 PR#14049 Fix encoding of negative parameters. SLV_ORDERS_OWNED_BY_ORDERLIST, ///< 354 PR#13948 Orders stored in OrderList, pool removed. + SLV_FACE_STYLES, ///< 355 PR#14319 Addition of face styles, replacing gender and ethnicity. + SL_MAX_VERSION, ///< Highest possible saveload version }; diff --git a/src/script/api/script_company.cpp b/src/script/api/script_company.cpp index 9def32f0d8..3ad171f9d0 100644 --- a/src/script/api/script_company.cpp +++ b/src/script/api/script_company.cpp @@ -108,12 +108,18 @@ EnforcePrecondition(false, gender == GENDER_MALE || gender == GENDER_FEMALE); EnforcePrecondition(false, GetPresidentGender(ScriptCompany::COMPANY_SELF) != gender); - Randomizer &randomizer = ScriptObject::GetRandomizer(); - CompanyManagerFace cmf; - GenderEthnicity ge = (GenderEthnicity)((gender == GENDER_FEMALE ? (1 << ::GENDER_FEMALE) : 0) | (randomizer.Next() & (1 << ETHNICITY_BLACK))); - RandomCompanyManagerFaceBits(cmf, ge, false, randomizer); + assert(GetNumCompanyManagerFaceStyles() >= 2); /* At least two styles are needed to fake a gender. */ - return ScriptObject::Command::Do(cmf); + /* Company faces no longer have a defined gender, so pick a random face style instead. */ + Randomizer &randomizer = ScriptObject::GetRandomizer(); + CompanyManagerFace cmf{}; + do { + cmf.style = randomizer.Next(GetNumCompanyManagerFaceStyles()); + } while ((HasBit(cmf.style, 0) ? GENDER_FEMALE : GENDER_MALE) != gender); + + RandomiseCompanyManagerFaceBits(cmf, GetCompanyManagerFaceVars(cmf.style), randomizer); + + return ScriptObject::Command::Do(cmf.style, cmf.bits); } /* static */ ScriptCompany::Gender ScriptCompany::GetPresidentGender(ScriptCompany::CompanyID company) @@ -121,8 +127,10 @@ company = ResolveCompanyID(company); if (company == ScriptCompany::COMPANY_INVALID) return GENDER_INVALID; - GenderEthnicity ge = (GenderEthnicity)GetCompanyManagerFaceBits(Company::Get(ScriptCompany::FromScriptCompanyID(company))->face, CMFV_GEN_ETHN, GE_WM); - return HasBit(ge, ::GENDER_FEMALE) ? GENDER_FEMALE : GENDER_MALE; + /* Company faces no longer have a defined gender, so fake one based on the style index. This might not match + * the face appearance. */ + const auto &cmf = ::Company::Get(ScriptCompany::FromScriptCompanyID(company))->face; + return HasBit(cmf.style, 0) ? GENDER_FEMALE : GENDER_MALE; } /* static */ Money ScriptCompany::GetQuarterlyIncome(ScriptCompany::CompanyID company, SQInteger quarter) diff --git a/src/table/CMakeLists.txt b/src/table/CMakeLists.txt index 3a615135ef..6b25662a09 100644 --- a/src/table/CMakeLists.txt +++ b/src/table/CMakeLists.txt @@ -11,6 +11,7 @@ add_files( build_industry.h cargo_const.h clear_land.h + company_face.h control_codes.h elrail_data.h engines.h diff --git a/src/table/company_face.h b/src/table/company_face.h new file mode 100644 index 0000000000..e40e0c85d3 --- /dev/null +++ b/src/table/company_face.h @@ -0,0 +1,98 @@ +/* + * This file is part of OpenTTD. + * OpenTTD is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, version 2. + * OpenTTD is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. + * See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with OpenTTD. If not, see . + */ + +/** + * @file table/company_face.h + * This file contains all definitions for default company faces. + */ + +#ifndef TABLE_COMPANY_FACE_H +#define TABLE_COMPANY_FACE_H + +#include "../company_manager_face.h" + +/* Definitions for default face variables. + * Faces are drawn in the listed order, so sprite layers must be ordered + * according to how the face should be rendered. */ + +/** Variables of first masculine face. */ +static constexpr FaceVar _face_style_1[] = { + {FaceVarType::Toggle, 3, 3, 1, 2, std::pair{1ULL << 6, 1ULL << 7 | 1ULL << 8}, STR_FACE_MOUSTACHE}, + {FaceVarType::Toggle, 11, 4, 1, 2, std::pair{1ULL << 13, 0ULL}, STR_FACE_GLASSES}, + {FaceVarType::Palette, 2, 5, 2, 3, uint64_t{1ULL << 5}, STR_FACE_EYECOLOUR}, + {FaceVarType::Sprite, 0, 0, 0, 1, SpriteID{0x325}}, + {FaceVarType::Sprite, 7, 7, 2, 4, SpriteID{0x327}, STR_FACE_CHIN}, + {FaceVarType::Sprite, 1, 9, 4, 12, SpriteID{0x32B}, STR_FACE_EYEBROWS}, + {FaceVarType::Sprite, 4, 13, 2, 3, SpriteID{0x367}, STR_FACE_MOUSTACHE}, + {FaceVarType::Sprite, 6, 13, 4, 12, SpriteID{0x35B}, STR_FACE_LIPS}, + {FaceVarType::Sprite, 5, 17, 3, 8, SpriteID{0x349}, STR_FACE_NOSE}, + {FaceVarType::Sprite, 0, 20, 4, 9, SpriteID{0x382}, STR_FACE_HAIR}, + {FaceVarType::Sprite, 9, 26, 2, 4, SpriteID{0x36E}, STR_FACE_COLLAR}, + {FaceVarType::Sprite, 8, 24, 2, 3, SpriteID{0x36B}, STR_FACE_JACKET}, + {FaceVarType::Sprite, 10, 28, 3, 6, SpriteID{0x372}, STR_FACE_TIE}, + {FaceVarType::Sprite, 12, 31, 1, 2, SpriteID{0x347}, STR_FACE_GLASSES}, +}; + +/** Variables of first feminine face. */ +static constexpr FaceVar _face_style_2[] = { + {FaceVarType::Toggle, 9, 3, 1, 2, std::pair{1ULL << 11, 0}, STR_FACE_EARRING}, + {FaceVarType::Toggle, 7, 4, 1, 2, std::pair{1ULL << 12, 0}, STR_FACE_GLASSES}, + {FaceVarType::Palette, 2, 5, 2, 3, uint64_t{1ULL << 5}, STR_FACE_EYECOLOUR}, + {FaceVarType::Sprite, 0, 0, 0, 1, SpriteID{0x326}}, + {FaceVarType::Sprite, 0, 0, 0, 1, SpriteID{0x327}}, + {FaceVarType::Sprite, 1, 9, 4, 16, SpriteID{0x337}, STR_FACE_EYEBROWS}, + {FaceVarType::Sprite, 4, 13, 4, 10, SpriteID{0x351}, STR_FACE_LIPS}, + {FaceVarType::Sprite, 3, 17, 3, 4, SpriteID{0x34C}, STR_FACE_NOSE}, + {FaceVarType::Sprite, 0, 20, 4, 5, SpriteID{0x38B}, STR_FACE_HAIR}, + {FaceVarType::Sprite, 6, 26, 2, 4, SpriteID{0x37B}, STR_FACE_COLLAR}, + {FaceVarType::Sprite, 5, 24, 2, 3, SpriteID{0x378}, STR_FACE_JACKET}, + {FaceVarType::Sprite, 10, 28, 3, 3, SpriteID{0x37F}, STR_FACE_EARRING}, + {FaceVarType::Sprite, 8, 31, 1, 2, SpriteID{0x347}, STR_FACE_GLASSES}, +}; + +/** Variables of second masculine face. */ +static constexpr FaceVar _face_style_3[] = { + {FaceVarType::Toggle, 2, 3, 1, 2, std::pair{1ULL << 5, 1ULL << 6 | 1ULL << 7}, STR_FACE_MOUSTACHE}, + {FaceVarType::Toggle, 10, 4, 1, 2, std::pair{1ULL << 12, 0ULL}, STR_FACE_GLASSES}, + {FaceVarType::Sprite, 0, 0, 0, 1, SpriteID{0x390}}, + {FaceVarType::Sprite, 6, 7, 2, 2, SpriteID{0x391}, STR_FACE_CHIN}, + {FaceVarType::Sprite, 1, 9, 4, 11, SpriteID{0x39A}, STR_FACE_EYEBROWS}, + {FaceVarType::Sprite, 3, 13, 2, 3, SpriteID{0x397}, STR_FACE_MOUSTACHE}, + {FaceVarType::Sprite, 5, 13, 4, 9, SpriteID{0x3A5}, STR_FACE_LIPS}, + {FaceVarType::Sprite, 4, 17, 3, 4, SpriteID{0x393}, STR_FACE_NOSE}, + {FaceVarType::Sprite, 0, 20, 4, 5, SpriteID{0x3D4}, STR_FACE_HAIR}, + {FaceVarType::Sprite, 8, 26, 2, 4, SpriteID{0x36E}, STR_FACE_COLLAR}, + {FaceVarType::Sprite, 7, 24, 2, 3, SpriteID{0x36B}, STR_FACE_JACKET}, + {FaceVarType::Sprite, 9, 28, 3, 6, SpriteID{0x372}, STR_FACE_TIE}, + {FaceVarType::Sprite, 11, 31, 1, 2, SpriteID{0x3AE}, STR_FACE_GLASSES}, +}; + +/** Variables of second feminine face. */ +static constexpr FaceVar _face_style_4[] = { + {FaceVarType::Toggle, 9, 3, 1, 2, std::pair{1ULL << 10, 0ULL}, STR_FACE_EARRING}, + {FaceVarType::Toggle, 7, 4, 1, 2, std::pair{1ULL << 11, 0ULL}, STR_FACE_GLASSES}, + {FaceVarType::Sprite, 0, 0, 0, 1, SpriteID{0x3B0}}, + {FaceVarType::Sprite, 4, 7, 2, 2, SpriteID{0x3B1}, STR_FACE_CHIN}, + {FaceVarType::Sprite, 1, 9, 4, 16, SpriteID{0x3B8}, STR_FACE_EYEBROWS}, + {FaceVarType::Sprite, 3, 13, 4, 9, SpriteID{0x3C8}, STR_FACE_LIPS}, + {FaceVarType::Sprite, 2, 17, 3, 5, SpriteID{0x3B3}, STR_FACE_NOSE}, + {FaceVarType::Sprite, 0, 20, 4, 5, SpriteID{0x3D9}, STR_FACE_HAIR}, + {FaceVarType::Sprite, 6, 26, 2, 4, SpriteID{0x37B}, STR_FACE_COLLAR}, + {FaceVarType::Sprite, 5, 24, 2, 3, SpriteID{0x378}, STR_FACE_JACKET}, + {FaceVarType::Sprite, 10, 28, 3, 3, SpriteID{0x3D1}, STR_FACE_EARRING}, + {FaceVarType::Sprite, 8, 31, 1, 2, SpriteID{0x3AE}, STR_FACE_GLASSES}, +}; + +/** Original face styles. */ +static FaceSpec _original_faces[] = { + {"default/face1", _face_style_1}, + {"default/face2", _face_style_2}, + {"default/face3", _face_style_3}, + {"default/face4", _face_style_4}, +}; + +#endif /* TABLE_COMPANY_FACE_H */ diff --git a/src/table/settings/misc_settings.ini b/src/table/settings/misc_settings.ini index ff46d87f54..fd39ff102e 100644 --- a/src/table/settings/misc_settings.ini +++ b/src/table/settings/misc_settings.ini @@ -268,13 +268,11 @@ min = 1 max = 512 cat = SC_EXPERT -[SDTG_VAR] +[SDTG_SSTR] name = ""player_face"" -type = SLE_UINT32 +type = SLE_STR var = _company_manager_face -def = 0 -min = 0 -max = 0xFFFFFFFF +def = """" cat = SC_BASIC [SDTG_VAR] diff --git a/src/widgets/company_widget.h b/src/widgets/company_widget.h index 88605f60ef..2964b341c0 100644 --- a/src/widgets/company_widget.h +++ b/src/widgets/company_widget.h @@ -97,8 +97,6 @@ enum SelectCompanyLiveryWidgets : WidgetID { /** * Widgets of the #SelectCompanyManagerFaceWindow class. - * Do not change the order of the widgets from WID_SCMF_HAS_MOUSTACHE_EARRING to WID_SCMF_GLASSES_R, - * this order is needed for the WE_CLICK event of DrawFaceStringLabel(). */ enum SelectCompanyManagerFaceWidgets : WidgetID { WID_SCMF_CAPTION, ///< Caption of window. @@ -106,65 +104,18 @@ enum SelectCompanyManagerFaceWidgets : WidgetID { WID_SCMF_SELECT_FACE, ///< Select face. WID_SCMF_CANCEL, ///< Cancel. WID_SCMF_ACCEPT, ///< Accept. - WID_SCMF_MALE, ///< Male button in the simple view. - WID_SCMF_FEMALE, ///< Female button in the simple view. - WID_SCMF_MALE2, ///< Male button in the advanced view. - WID_SCMF_FEMALE2, ///< Female button in the advanced view. WID_SCMF_SEL_LOADSAVE, ///< Selection to display the load/save/number buttons in the advanced view. - WID_SCMF_SEL_MALEFEMALE, ///< Selection to display the male/female buttons in the simple view. - WID_SCMF_SEL_PARTS, ///< Selection to display the buttons for setting each part of the face in the advanced view. + WID_SCMF_SEL_PARTS, ///< Selection to display the buttons for setting each part of the face in the advanced view. + WID_SCMF_SEL_RESIZE, ///< Selection to display the resize button. WID_SCMF_RANDOM_NEW_FACE, ///< Create random new face. WID_SCMF_TOGGLE_LARGE_SMALL_BUTTON, ///< Toggle for large or small. WID_SCMF_FACE, ///< Current face. WID_SCMF_LOAD, ///< Load face. WID_SCMF_FACECODE, ///< Get the face code. WID_SCMF_SAVE, ///< Save face. - WID_SCMF_HAS_MOUSTACHE_EARRING_TEXT, ///< Text about moustache and earring. - WID_SCMF_TIE_EARRING_TEXT, ///< Text about tie and earring. - WID_SCMF_LIPS_MOUSTACHE_TEXT, ///< Text about lips and moustache. - WID_SCMF_HAS_GLASSES_TEXT, ///< Text about glasses. - WID_SCMF_HAIR_TEXT, ///< Text about hair. - WID_SCMF_EYEBROWS_TEXT, ///< Text about eyebrows. - WID_SCMF_EYECOLOUR_TEXT, ///< Text about eyecolour. - WID_SCMF_GLASSES_TEXT, ///< Text about glasses. - WID_SCMF_NOSE_TEXT, ///< Text about nose. - WID_SCMF_CHIN_TEXT, ///< Text about chin. - WID_SCMF_JACKET_TEXT, ///< Text about jacket. - WID_SCMF_COLLAR_TEXT, ///< Text about collar. - WID_SCMF_ETHNICITY_EUR, ///< Text about ethnicity european. - WID_SCMF_ETHNICITY_AFR, ///< Text about ethnicity african. - WID_SCMF_HAS_MOUSTACHE_EARRING, ///< Has moustache or earring. - WID_SCMF_HAS_GLASSES, ///< Has glasses. - WID_SCMF_EYECOLOUR_L, ///< Eyecolour left. - WID_SCMF_EYECOLOUR, ///< Eyecolour. - WID_SCMF_EYECOLOUR_R, ///< Eyecolour right. - WID_SCMF_CHIN_L, ///< Chin left. - WID_SCMF_CHIN, ///< Chin. - WID_SCMF_CHIN_R, ///< Chin right. - WID_SCMF_EYEBROWS_L, ///< Eyebrows left. - WID_SCMF_EYEBROWS, ///< Eyebrows. - WID_SCMF_EYEBROWS_R, ///< Eyebrows right. - WID_SCMF_LIPS_MOUSTACHE_L, ///< Lips / Moustache left. - WID_SCMF_LIPS_MOUSTACHE, ///< Lips / Moustache. - WID_SCMF_LIPS_MOUSTACHE_R, ///< Lips / Moustache right. - WID_SCMF_NOSE_L, ///< Nose left. - WID_SCMF_NOSE, ///< Nose. - WID_SCMF_NOSE_R, ///< Nose right. - WID_SCMF_HAIR_L, ///< Hair left. - WID_SCMF_HAIR, ///< Hair. - WID_SCMF_HAIR_R, ///< Hair right. - WID_SCMF_JACKET_L, ///< Jacket left. - WID_SCMF_JACKET, ///< Jacket. - WID_SCMF_JACKET_R, ///< Jacket right. - WID_SCMF_COLLAR_L, ///< Collar left. - WID_SCMF_COLLAR, ///< Collar. - WID_SCMF_COLLAR_R, ///< Collar right. - WID_SCMF_TIE_EARRING_L, ///< Tie / Earring left. - WID_SCMF_TIE_EARRING, ///< Tie / Earring. - WID_SCMF_TIE_EARRING_R, ///< Tie / Earring right. - WID_SCMF_GLASSES_L, ///< Glasses left. - WID_SCMF_GLASSES, ///< Glasses. - WID_SCMF_GLASSES_R, ///< Glasses right. + WID_SCMF_STYLE, ///< Style selector widget. + WID_SCMF_PARTS, ///< Face configuration parts widget. + WID_SCMF_PARTS_SCROLLBAR, ///< Scrollbar for configuration parts widget. }; /** Widgets of the #CompanyInfrastructureWindow class. */