Page 1 of 1

"Noob" prototype "type checking" question

Posted: Sun Jan 26, 2025 9:58 pm
by aaron311
Sorry. Bit of a concept question here, definitely of the 'noob' variety -

So, despite having written a mod for Factorio I still struggle a lot with the concept of prototypes in the game and what that actually "means" (my mod didn't deal with too much custom prototype stuff, etc. hence why I think I was able to 'sneak by').


In a new mod I'm working on, I want to basically do an "instance of" check:

Code: Select all

for (thisEntity : list of entities) {
   if (thisEntity.prototype instanceof "whatever") {
      -- do stuff
   }
}
I'm not sure the correct way to pull such a basic type check off. Granted, I could try prototype.name == "type-name-string" but there are challenges with that, I think:

1. Maybe "whatever" is an abstract prototype built into the game and I don't care to hard code a list of all possible concrete 'names' using that abstract prototype?
- Granted, it looks like the inheritance tree is available in the game's API so maybe you could iterate over all subclass names and check .name == entry for each entry? Seems ugly though...

2. What happens if another mod deepcopy's an existing prototype (that I care about), adds a custom thing with the same/similar behavior, etc. And maybe I still want my logic to try to handle that "inheriting" prototype? I assume anything that's deepcopy()'d still has a duplicate of the original prototype's Lua APIs in the entity's .prototype property?

What's the proper way to handle an 'instanceof' type check like this? Or (more likely) where is my understanding of prototype trees/inheritance incorrect?

Thanks! :-D

Re: "Noob" prototype "type checking" question

Posted: Mon Jan 27, 2025 10:14 am
by Pi-C
You can define new prototypes in the data stage. They must be based on an existing type/class of things, where each type/class has certain properties that you can use (see here for a list of all prototype types).

There may be different prototypes of each prototype type/class. So the easiest way to create a new prototype is something like this:

Code: Select all

my_lamp = table.deepcopy(data.raw.lamp["small-lamp"])
my_lamp.name = "my_new_lamp"
…
data:extend({my_lamp})
In the control stage, players or mods can then create instances of your new lamp prototype (placing an item, blueprint, using LuaSurface::create_entity() etc.). All of these entities share the same properties because they are based on the same prototype.

If you want to make your mod more general, you don't have to depend on prototype name, but you can check all different prototypes of the same type/class: just check for entity.type instead of entity.name. (In a hurry now, have an appointment. More later, if necessary …)

Re: "Noob" prototype "type checking" question

Posted: Mon Jan 27, 2025 9:09 pm
by aaron311
Gotcha. So in the case where a mod is extending an existing prototype, checking .type (instead of .name) would return the same Lua *Prototype class of the 'base' object despite it potentially having a different prototype name due to deepcopy+extend? Is that it? Or is it possible to modify .type when copying/extending such that the value is unreliable?

Re: "Noob" prototype "type checking" question

Posted: Mon Jan 27, 2025 10:49 pm
by s6x
I get the sense that you might be assuming that the Factorio prototype system is a lot more "object oriented" than it really is, which could be why you're struggling. A prototype in Factorio is really more just a clump of data telling the game engine what it is. On the C++ side, there's a whole class hierarchy of course, but that's not really exposed to the Lua API. You can't add new types. The type must be something off a hardcoded list the game engine recognizes, and the name can be just about anything. Each prototype is independent and there's no real concept of inheritance or anything like that. A deepcopy is just that, a copy. To the game engine, it's a completely separate thing.

So to try to answer your question, if you want to check if an entity is a crafting machine, you can check its type, because that's a type that is defined. If you want to check if an entity is an assembling machine 2, you can check its name. If you want to check if it's an assembling machine 2 or was derived from an assembling machine 2 via deepcopy... there's no real way to do that generically. The best you can do is check for traits that make it "look like" the thing you wanted, like supporting certain crafting categories or whatever.

Re: "Noob" prototype "type checking" question

Posted: Mon Jan 27, 2025 11:15 pm
by aaron311
s6x wrote: Mon Jan 27, 2025 10:49 pm I get the sense that you might be assuming that the Factorio prototype system is a lot more "object oriented" than it really is, which could be why you're struggling. A prototype in Factorio is really more just a clump of data telling the game engine what it is. On the C++ side, there's a whole class hierarchy of course, but that's not really exposed to the Lua API. You can't add new types. The type must be something off a hardcoded list the game engine recognizes, and the name can be just about anything. Each prototype is independent and there's no real concept of inheritance or anything like that. A deepcopy is just that, a copy. To the game engine, it's a completely separate thing.
Yes! Exactly -- this is the heart of my question, thank you.

Now, I DID/DO already understand that the Lua "types" aren't truly an inheritance hierarchy under the table, but I speak from a practical perspective about what a particular Lua script "cares about" and "sees". Game scripts dealing with prototype objects (or pretty much anything for that matter) at runtime only care about how something *behaves* and attributes it has. In such a case, a deepcopy()'d prototype will result in LuaEntity objects with a .prototype member that behaves exactly as entities - created from the original prototype - will have for their own .prototype member. At least I assume this is correct?

So in a sense, the behavior of any custom prototype is 'inherited' (albeit an abuse of the term). So, since a Lua script would interact with both .prototype attributes in the "same way" what I was thinking is you would want to do a 'type check' not based on .name but based on something else which would indicate the underlying "source" of the prototype object in Lua.

s6x wrote: Mon Jan 27, 2025 10:49 pm So to try to answer your question, if you want to check if an entity is a crafting machine, you can check its type, because that's a type that is defined. If you want to check if an entity is an assembling machine 2, you can check its name. If you want to check if it's an assembling machine 2 or was derived from an assembling machine 2 via deepcopy... there's no real way to do that generically. The best you can do is check for traits that make it "look like" the thing you wanted, like supporting certain crafting categories or whatever.
Yeah, so in my specific use case, I wanted to read back certain attributes from LuaEntity.prototype.<attr> that aren't supported for all prototypes but only one of them (or anything copied from it). So in this case, you're saying the way to do this is not by checking any kind of "type" attribute (sounds like there isn't one) but instead just check for the desired attribute's presence in the LuaEntity.prototype table reference? I had tried simply reading it and checking for 'nil' but that resulted in an error reading a nonexistent attribute, which then made me start thinking I needed a 'type check'. But probably I just need to check if the table contains the key?

Re: "Noob" prototype "type checking" question

Posted: Tue Jan 28, 2025 12:08 am
by s6x
aaron311 wrote: Mon Jan 27, 2025 11:15 pm So in this case, you're saying the way to do this is not by checking any kind of "type" attribute (sounds like there isn't one) but instead just check for the desired attribute's presence in the LuaEntity.prototype table reference?
Right. Sometimes the presence of the attribute is enough, other times you will have to look at the value of the attribute. As an example, there's no real way to detect that a certain furnace prototype is a modified copy of ("derived from" in a less-strict sense of the term) an electric furnace's prototype. But if its type is "furnace", and one of its crafting_categories is "smelting", and it has an electric_energy_source_prototype, then it's probably safe to assume it's either a copy of an electric furnace or something that acts enough like an electric furnace you probably want to treat it like one.

Re: "Noob" prototype "type checking" question

Posted: Tue Jan 28, 2025 2:06 am
by DaveMcW
The Factorio engine does not support object-oriented programming.

You can't extend an entity. And you can't use "instance-of" with user-defined entities (because user-defined classes are impossible).

Lua does allow you to use object-oriented patterns, but it only works in your own mod and can't interact with other mods or the built-in classes.

Re: "Noob" prototype "type checking" question

Posted: Tue Jan 28, 2025 2:58 am
by aaron311
Got it, thanks for the help!

Re: "Noob" prototype "type checking" question

Posted: Wed Jan 29, 2025 4:04 am
by aaron311
Update - ahh, yes, now I remember why I started down this path. It's because attempting to access a property that doesn't apply to an entity will trigger an exception rather than returning nil. THAT'S why I was trying to detect the type proactively and only THEN read properties I knew would exist.

I take it I'm doomed to having to iterate over pairs() for each attribute I want to conditionally detect? Seems horribly slow/inefficient but I don't see any other way to do it. Am I missing something?

I don't think there's any O(1) "contains key" operation in Lua (it's usually just table[key] ~= nil in the standard language but this is effectively 'removed' from the language for in-game objects due to this design...which don't get me wrong, I understand why, it just breaks my goal here).

Maybe there's a 'read or get default' utility function, but I don't think so? (Checking the forum history for this topic there is quite a bit of very opinionated resistance to adding any sort of helper function that would read or default to nil.)

Re: "Noob" prototype "type checking" question

Posted: Wed Jan 29, 2025 6:19 am
by Muche
It doesn't seem to me there is a way around knowing the type of an object before trying to do anything with it?
For example, what does entity.get_inventory(4) mean for a random object you are trying to access?
For a player it's ammo, for an assembler it's modules.
If you don't know object's type, even getting an empty inventory (instead of an error/nil) is meaningless.

Re: "Noob" prototype "type checking" question

Posted: Wed Jan 29, 2025 11:23 am
by Xorimuth
2. What happens if another mod deepcopy's an existing prototype (that I care about), adds a custom thing with the same/similar behavior, etc. And maybe I still want my logic to try to handle that "inheriting" prototype? I assume anything that's deepcopy()'d still has a duplicate of the original prototype's Lua APIs in the entity's .prototype property?
The entity's .prototype property returns a LuaEntityPrototype (https://lua-api.factorio.com/latest/cla ... otype.html), which has a static list of properties. If a mod adds a value to a prototype during data stage (e.g. `data.raw["transport-belt"]["fast-transport-belt"].my_custom_property = 5), then that value will be completely ignored by the engine, and will not be accessible at runtime through `.prototype`. (In fact, the engine will output a warning that a prototype property was not used
aaron311 wrote: Wed Jan 29, 2025 4:04 am Update - ahh, yes, now I remember why I started down this path. It's because attempting to access a property that doesn't apply to an entity will trigger an exception rather than returning nil. THAT'S why I was trying to detect the type proactively and only THEN read properties I knew would exist.

I take it I'm doomed to having to iterate over pairs() for each attribute I want to conditionally detect? Seems horribly slow/inefficient but I don't see any other way to do it. Am I missing something?

I don't think there's any O(1) "contains key" operation in Lua (it's usually just table[key] ~= nil in the standard language but this is effectively 'removed' from the language for in-game objects due to this design...which don't get me wrong, I understand why, it just breaks my goal here).

Maybe there's a 'read or get default' utility function, but I don't think so? (Checking the forum history for this topic there is quite a bit of very opinionated resistance to adding any sort of helper function that would read or default to nil.)
Indeed, LuaObjects (i.e. all factorio objects returned from the API like LuaEntity, LuaEntityPrototype, etc) generally error rather than just returning nil if you try to access something which isn't a valid option for that type. Like reading prototype.max_count_of_owned_units will error unless `prototype.type` is "unit-spawner".

I don't think pairs() will work for what you want either, using pairs() on a LuaObject will not iterate through all valid properties, it will generally do nothing. (I think for some things, it can be used, like for a LuaInventory it will iterate through each slot: https://lua-api.factorio.com/latest/cla ... x_operator)

The 'correct' solution to your problem is indeed to first check `type`, and then only check the attributes are valid for that type. I'm curious what it is you are actually trying to do, I suspect an XY problem may be present. It is very rare for mods to need to deal with an arbitrary LuaEntityPrototype and read complex data from it.

------

That said, there is technically a way to do a "contains key" operation even on LuaObjects, using `pcall` which is basically Lua's version of a try/catch structure. This is considered bad practice and should only be used if you are confident that what you are using it for can't be done in any other way. This is because it can easily hide other errors/bugs, and generally attempting to continue after errors will lead to inconsistent state, which can cause confusing bug reports/behaviour rather than just an error message which clearly points to the culprit.
Anyway, I have used pcall to check if a LuaObject contains an attribute once before:
https://github.com/tburrows13/Spidertro ... #L194-L200
I wrote an explanation for why I did it, and since it exposed a deficiency in the API, I should also have made a bug report (or mod API request) for it too. I don't remember whether I actually did or not, and either way, I can probably remove the pcall in 2.0 anyway since the API around logistics requests has changed quite a bit since 1.1.

Re: "Noob" prototype "type checking" question

Posted: Wed Jan 29, 2025 1:23 pm
by aaron311
Hi @Xorimuth,

Thank you for your helpful and insightful commentary.


Xorimuth wrote: Wed Jan 29, 2025 11:23 am I don't think pairs() will work for what you want either, using pairs() on a LuaObject will not iterate through all valid properties, it will generally do nothing. (I think for some things, it can be used, like for a LuaInventory it will iterate through each slot: https://lua-api.factorio.com/latest/cla ... x_operator)
Ah right, good point (and thank you for the reminder)! I seem to remember now that pairs() does not work when I tried it years ago. This is why I adopted the whole mentality of check types before I tried to do anything with them.


Xorimuth wrote: Wed Jan 29, 2025 11:23 am The 'correct' solution to your problem is indeed to first check `type`, and then only check the attributes are valid for that type.
Exactly! I agree. This is what lead to my original question in this thread (regarding the appropriate idiom to use for type checking).


Xorimuth wrote: Wed Jan 29, 2025 11:23 am I'm curious what it is you are actually trying to do, I suspect an XY problem may be present. It is very rare for mods to need to deal with an arbitrary LuaEntityPrototype and read complex data from it.
I'm actually wanting to read LuaEntity attributes. I was trying to use the .prototype attribute to determine what properties of LuaEntity are available, etc.

To recap, my original question was effectively "What is the appropriate and reliable way to check if a prototype 'behaves like' a candidate target prototype of interest that my code knows about (I had previously used 'inherits from' as the term to describe this, although I admit no real inheritance is in play)"

Notably prototype.name is NOT the way (IMO) to check this since ideally code could handle any new prototype that was deepcopy()'d + extend()'d into the game's system with an alternate prototype name. Suppose (for example) the built-in prototype is "duck" and the entity has .feather_color attribute. Then a mod comes and deepcopy()'s and extend()'s the "duck" prototype to be a "radioactive-duck". From a Lua script perspective, all LuaEntities from this new type radioactive-duck prototype still have a .feather_color attribute. But the prototype name is now different. But it's still a duck at least from the stance that any Lua code could ever really care about ("if it walks like a duck, quacks like a duck, then ... it's a duck"). Thus, checking prototype name for equality is (at least as far as I can tell) insufficient to understand the nature of the thing you're dealing with, right?


Going back to this:
Xorimuth wrote: Wed Jan 29, 2025 11:23 am The 'correct' solution to your problem is indeed to first check `type`, and then only check the attributes are valid for that type.
What jumps out at me here is you aren't actually suggesting checking PrototypeBase.name but rather PrototypeBase.type? From reading the API docs it wasn't clear to me the difference. Is .type *guaranteed* to be a built-in game prototype string where .name can be customized by mods? Is that what's going on?

In other words, if I check .type rather than .name does it solve my "quacks like a duck" problem above? :D


Xorimuth wrote: Wed Jan 29, 2025 11:23 am That said, there is technically a way to do a "contains key" operation even on LuaObjects, using `pcall` which is basically Lua's version of a try/catch structure. This is considered bad practice and should only be used if you are confident that what you are using it for can't be done in any other way. This is because it can easily hide other errors/bugs, and generally attempting to continue after errors will lead to inconsistent state, which can cause confusing bug reports/behaviour rather than just an error message which clearly points to the culprit.
Anyway, I have used pcall to check if a LuaObject contains an attribute once before:
https://github.com/tburrows13/Spidertro ... #L194-L200
I wrote an explanation for why I did it, and since it exposed a deficiency in the API, I should also have made a bug report (or mod API request) for it too. I don't remember whether I actually did or not, and either way, I can probably remove the pcall in 2.0 anyway since the API around logistics requests has changed quite a bit since 1.1.
For sure! This is great insight and a fun example. But as you point out, I 100% agree using pcall to catch/hide the exception like this would only ever be done in last resort. I can't imagine this type of thing is very performant.

Re: "Noob" prototype "type checking" question

Posted: Wed Jan 29, 2025 3:13 pm
by Xorimuth
aaron311 wrote: Wed Jan 29, 2025 1:23 pm Notably prototype.name is NOT the way (IMO) to check this since ideally code could handle any new prototype that was deepcopy()'d + extend()'d into the game's system with an alternate prototype name. Suppose (for example) the built-in prototype is "duck"
You'd need to be more precise. Is it of type "duck" or of name "duck"? (Let's assume both, which is quite common, e.g. the tier 1 transport belt has name="transport-belt", type="transport-belt").
aaron311 wrote: Wed Jan 29, 2025 1:23 pmand the entity has .feather_color attribute. Then a mod comes and deepcopy()'s and extend()'s the "duck" prototype to be a "radioactive-duck".
The mod could set `name = "radioactive-duck"` but the type would still be "duck". If the mod sets `type = "radioactive-duck"` then it would fail because that isn't a valid type. You can see all valid types here: https://lua-api.factorio.com/latest/prototypes.html (their names in lua are in the small gray text, e.g. 'active-defense-equipment').

A prototype can have any name, but its type must be from the list of types implemented by the game engine.
From a Lua script perspective, all LuaEntities from this new type radioactive-duck prototype still have a .feather_color attribute. But the prototype name is now different. But it's still a duck at least from the stance that any Lua code could ever really care about ("if it walks like a duck, quacks like a duck, then ... it's a duck"). Thus, checking prototype name for equality is (at least as far as I can tell) insufficient to understand the nature of the thing you're dealing with, right?
Yes, the name doesn't tell you anything about what the entity can do, but its type tells you everything. Some types inherit behaviour from other types, you can see that list here: https://lua-api.factorio.com/latest/aux ... -tree.html.

So, at runtime, if you have a LuaEntity and you want to know whether you can do X to it, then reading https://lua-api.factorio.com/latest/cla ... .html#type will allow you to know. You want to call get_spider_legs() on it? Well the docs say "Can only be used if this is SpiderVehicle" so you shouldn't do it unless you've checked `if my_entity.type == "spider-vehicle" then`.

What if you have a LuaEntity and you want to call damage() on it? It says "Can only be used if this is EntityWithHealth". Well, you check it's type, but that returns "accumulator". You can check the aforementioned inheritance tree and see that type "accumulator" is in fact inherited from EntityWithHealth so calling damage() on this LuaEntity would be valid. (For this case, a better method to check this would be to read my_entity.is_entity_with_health.
Is .type *guaranteed* to be a built-in game prototype string where .name can be customized by mods? Is that what's going on?
In other words, if I check .type rather than .name does it solve my "quacks like a duck" problem above?
Yes, yes, and yes. See the data-stage docs for type which says "For a list of all possible types, see the prototype overview.

If you'd like to discuss further, you may find it easier to do that in the #mod-dev-discussion channel in the discord server :)

Re: "Noob" prototype "type checking" question

Posted: Thu Jan 30, 2025 12:30 am
by aaron311
Got it! Thanks. I think I wasn't properly interpreting the description of the 'type' property which is what confused me initially.