Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Proposal: WIP: Use type-safe ids #1707

Open
wants to merge 1 commit into
base: master
Choose a base branch
from
Open

Proposal: WIP: Use type-safe ids #1707

wants to merge 1 commit into from

Conversation

selaux
Copy link
Member

@selaux selaux commented Nov 15, 2022

This introduces a type-safe ID for Items. It's a struct with a single field, so it should serialize the same as the underlying type, making it usable in save games and such.

Why?

Currently we always use the underlying type (in this case UINT16) or a type alias. This has the disadvantage that the type checker does not check whether what you are using is actually an itemid and not just another UINT16 (or even other types that are automatically converted to UINT16). It also encourages things like iterating over item ids using ++ and strict item id bounds such as MAX_WEAPONS, which limits moddablility (what if I want to have more weapons in my mod).

Continuing the refactor

This would require other entities such as weapons to use the TypedId<> template (see Types.h) as well and refactoring the places where .inner or ItemId(x) is used to use iterators.

Implications

Most places using .inner() and ItemId(x) should be considered technical debt and should be replaced, e.g. by iterators.

Happy for some feedback on this one.

@lynxlynxlynx
Copy link
Member

Wouldn't it be cleaner to use an enum type? No need for an extra class, while still providing the extra strictness and arbitrary size.

@momoko-h
Copy link
Contributor

Yes, I also don't quite see the advantage of this approach over simply changing ITEMDEFINE to an enum class that inherits from UINT16. Since ITEMDEFINE looks fairly ugly you could also change the name, of course. You would have to change every occurrence of JAR, etc. to ItemId::JAR or add constexprs for them, but at least that's more idiomatic than JAR.inner().

One other thing I noticed: we really should give OBJECTTYPE a constructor that simply calls CreateItem(). OBJECTTYPE o; CreateItem(HMX, 100, &o); is so old school C.

@selaux
Copy link
Member Author

selaux commented Nov 15, 2022

I disagree. An enum class implies that there is a known number of known variants. I would argue we currently only have the known number and that also only holds as long as we don't allow adding any additional items in mods. What the actual item id points to is known only after loading the items.json. There is no guarantee what ItemId::Jar would point to at runtime. My general standpoint is we shouldn't have any ITEMDEFINEs in C++ at all, although that is more of a long term goal 😉.

@lynxlynxlynx
Copy link
Member

I wouldn't get stuck on the fact that it's an enum, since it'd clearly be used as a type. Just like nobody would find the underlying ugliness of your class approach, unless they looked specifically. ;P
And we don't actually need to care what underlying values are used for dynamic instances. Nor for the several hardcoded cases if we put some annoying work into it. But these two things are the same for both approaches anyway.

@@ -2114,7 +2114,7 @@ static void AddNewItemToSelectedMercsInventory(BOOLEAN fCreate)
if( !(GCM->getItem(gpSelected->pDetailedPlacement->Inv[ gbMercSlotTypes[ gbCurrSelect ] ].usItem)->isAmmo()) )
gpSelected->pDetailedPlacement->Inv[ gbMercSlotTypes[ gbCurrSelect ] ].bStatus[0] = (INT8)(80 + Random( 21 ));

if( gusMercsNewItemIndex )
if( gusMercsNewItemIndex != ItemId(NOTHING) )
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You already defined NOTHING as an ItemId.

}


static void InitBobbyRayUsedInventory(void)
{
UINT16 i;
ItemId i;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'd use the opportunity to move this into the for init-statement, here and in the other places.

@@ -1359,7 +1359,7 @@ static void LoadSoldierStructure(HWFILE const f, UINT32 savegame_version, bool s
if (!active) continue;

//Read in the saved soldier info into a Temp structure
SOLDIERTYPE SavedSoldierInfo;
SOLDIERTYPE SavedSoldierInfo = {};
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

SOLDIERTYPE already has a default constructor because of the ST::strings and other stuff it contains, so this does nothing useful.

@@ -2533,7 +2533,7 @@ static UINT8 GetUnusedMercProfileID(void)

static void CreateTempPlayerMerc(void)
{
SOLDIERCREATE_STRUCT MercCreateStruct;
SOLDIERCREATE_STRUCT MercCreateStruct = {};
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The line below is now not needed anymore.

@@ -560,7 +560,7 @@ void QuestDebugScreenInit()

gItemListBox.usItemDisplayedOnTopOfList = 1;
gItemListBox.usStartIndex = 1;
gItemListBox.usMaxArrayIndex = MAXITEMS;
gItemListBox.usMaxArrayIndex = MAXITEMS.inner();
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do you really want MAXITEMS to be an ItemId? size_t looks like a better choice to me.

@@ -38,7 +38,7 @@ void ExtractObject(DataReader& d, OBJECTTYPE* const o)
EXTR_U8(d, o->ubGunAmmoType)
EXTR_U8(d, o->ubGunShotsLeft)
EXTR_SKIP(d, 1)
EXTR_U16(d, o->usGunAmmoItem)
o->usGunAmmoItem = d.read<ItemId>();
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You're doing this often enough that adding EXTR_ItemId and INJ_ItemId macros are probably worthwhile

@@ -259,7 +259,7 @@ void LoadWorldItemsFromMap(HWFILE const f)
{
// Add all of the items to the world indirectly through AddItemToPool, but
// only if the chance associated with them succeed.
WORLDITEM wi;
WORLDITEM wi = {};
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The very next line overwrites this initialization.

// as the type Inner an can be explicitly converted from / to it.
template <typename For, typename Inner> struct TypedId {
public:
constexpr TypedId() : m_id(0) {}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If you add something like using TID = TypedId<For, Inner>; here you don't have to repeat it all the time.

return m_id != rhs.m_id;
}

TypedId<For, Inner> operator++(int i) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Drop the i after int.

}

TypedId<For, Inner> operator++(int i) {
auto copy = TypedId<For, Inner>(this->m_id);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Or just auto copy = *this ?

@magras
Copy link
Contributor

magras commented Aug 8, 2023

An enum class implies that there is a known number of known variants.

I know I am just a passing stranger, but that's not true. There are a bunch of counterexamples from the standard:

  1. std::byte:
    enum class byte : unsigned char {};
    
  2. std::align_val_t:
    enum class align_val_t : size_t {};
    
  3. Handle example:
    enum class Handle : uint32_t { Invalid = 0 };
    

The last one demonstrates that the committee sees scoped enums as opaque type aliases for integral types with associated constants.

@selaux
Copy link
Member Author

selaux commented Aug 8, 2023

Interesting. I might have another look at that then.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

None yet

4 participants