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

Per-object machines, minimally functional fuel consumption #61

Merged

Conversation

z-adams
Copy link
Contributor

@z-adams z-adams commented Jan 24, 2021

This one makes up for the last PR, now you see why I split it into two :)

At a high level, the changes primarily add the ability for machines to be owned by non-part objects. The new PrototypeObject::m_machineIndices array allows a PrototypeObject to keep track of the machines it owns. When a PrototypeObject is created in AssetImporter::proto_add_obj_recurse(), its machines are loaded by AssetImporter::load_machines(). This function takes a reference to the ProtoObject's index array, and the parent PrototypePart's machine array. PrototypeMachines are added to the PrototypePart's master list, and the indices of the machines populate the PrototypeObject's index array. When a part is instantiated in SysVehicle::activate_sat(), the SysVehicle::part_instantiate() function creates an array of objects which track the relationships between the new object ActiveEnts and the PrototypePart's machine array, since the PrototypePart and PrototypeObject information is discarded once the entities have been loaded into the scene. A new function SysVehicle::part_instantiate_machines() takes this array, and uses it to add the appropriate machines to the various entities in the scene. This deferred action is necessary because machines may depend on the entire part's subhierarchy existing; the example given in the code is how MachineRocket needs the plume effect node to exist when it's instantiated.

The rest of the stuff is just a quick way of adding functional fuel consumption to MachineRocket. It's handled by an ugly and hardcoded solution right now (hardcoded tank ID, storing tank entity ID as MachineRocket member). This will change in the next PR, which generalized everything with a new "Pipe" wire type. If it's unacceptable to do the hardcoding (it currently renders the "moon" scene vessels immobile) I can include the next few commits in this PR as well, but it's already a large amount of code and making the PR 70% larger is going to suck.

The current functionality is basically that the 10m^3 fuselage tank is used to store a generic "fuel" which is drained by the rocket engines. When the fuel is depleted the engines cease to provide thrust, although the plumes keep working. The next PR will smooth out this proof of concept into a generalized system.

Comment on lines 485 to 499
if (node.extras.Has("machines"))
{
load_machines(node.extras, part.get_machines(), obj.m_machineIndices);
}
Copy link
Contributor Author

Choose a reason for hiding this comment

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

When each object is loaded, its machine configs are loaded here

Comment on lines 148 to 150
machineIndexArray.push_back(machineArray.size());
machineArray.emplace_back(std::move(machine));
Copy link
Contributor Author

Choose a reason for hiding this comment

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

PrototypeMachines are added to the PrototypePart's machine array, and the indices are stored in the PrototypeObject's machineIndexArray

Copy link
Member

Choose a reason for hiding this comment

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

Make sure to reserve space in the vector before calling emplace in a loop.

Note though that if this function is being called in a loop, don't reserve a whole bunch, instead figure out a way to get the total size the vect needs to be, and reserve once at the top of the stack.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

For the machineIndexArray, that's easy (and I may actually just change that parameter to a return value). For the machineArray, it's not so easy. There's not really any way to know how many machines will be in the part's hierarchy unless we add a second recursion pass just to pre-allocate machine storage. I think it's probably best to just let std::vector do its thing and then maybe shrink it to fit afterwards, unless you think the allocations are really such a big deal that it's worth doing a pre-pass to figure out how much space it needs. Otherwise, I'll just leave a comment on line 149 for now that pre-allocation would be nice, and if we ever find ourselves needing to do multiple passes over the config for some reason, we can add this at that point.

Comment on lines 139 to 150
/* Deferred machine instantiation
*
* Since machines may depend on the existence of other objects in the
* part's hierarchy, machine instantiation must be deferred until the
* entire hierarchy exists. For instance, a rocket engine's root
* "MachineRocket" machine may depend on the existence of a child node
* to receive an "ACompExhaustPlume" component that handles graphical
* effects related to the rocket machine, but that child node would not
* exist in time for the MachineRocket to be instantiated alongside
* the root part entity.
*/
part_instantiate_machines(partEntity, machineMapping, proto, partBp);
Copy link
Contributor Author

Choose a reason for hiding this comment

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

After the part and objects have all been instantiated, the machines are instantiated

Copy link
Contributor

Choose a reason for hiding this comment

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

This sounds good. I recently de-spaghettified SysVehicle in my own branch, making lots of functions static like in my other PRs. There will be merge conflict for sure soon.

Comment on lines 177 to 178
ActiveEnt part_instantiate(PrototypePart& part, BlueprintPart& blueprint,
ActiveEnt rootParent, std::vector<MachineDef>& machineDefinitions);
Copy link
Contributor Author

Choose a reason for hiding this comment

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

I meant to have this return a tuple and use structured bindings instead of the machineDefinitions output parameter; I'll do it tomorrow

DependRes<ShipResourceType> fuel = pkg.get<ShipResourceType>("fuel");
if (!fuel.empty())
{
ActiveEnt fuelSource = static_cast<ActiveEnt>(13);
Copy link
Contributor Author

Choose a reason for hiding this comment

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

So this is pretty egregious, but generalizing this stuff is going to take this PR from +500 lines to +700, so perhaps this is acceptable for the time being

Copy link
Contributor

Choose a reason for hiding this comment

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

what even is this doing? Should this be a search for connected MachineContainers?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I'm just manually assigning the feeding tank to the rocket.

Yes, it should be searching, but since any tank anywhere on the ship can be attached to any rocket anywhere on the ship, it needs a bit of an involved solution. I guess I could give it a config value for the time being and read that, it would just push the hard-codedness to the part config files. I will fix this in the next PR or two, but if you want to change it for now let me know how you think we should handle it. I can include my subsequent commits in this branch too, it'll just make the PR twice as bloated.

float m_massRateFraction;
osp::active::ActiveEnt m_sourceEnt;
};
using fuel_list_t = std::vector<ResourceInput>;
Copy link
Contributor Author

Choose a reason for hiding this comment

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

This arrangement kind of sucks, and will be changed in the next PR or two when the connections are automated/generalized

Copy link
Contributor

@Capital-Asterisk Capital-Asterisk left a comment

Choose a reason for hiding this comment

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

The biggest problem here is that I'm cringing at my own wiring code, but 10x more.

I also find it quite problematic how SysMachineRocket does the fuel management.

In issue #56 I talk about some 'node' system. Instead of storing states in WireOutput, it should be stored in a separate node component. This can do more than just logic.

All connected fuel containers and consumers can be connected to a common node using this theoretical wiring system, where it acts more like a pressure system instead. When a consumer needs fuel, it writes how much fuel it needs to the node, like negative pressure. This notifies connected fuel tanks which, will try to fill the void and set the node's pressure back to 0.

All connected tanks are known, and requirements from multiple consumers are combined, making it really simple for a system to determine how much fuel to take from each container.

Also consider pumping fuel between tanks, and fuel pipes that connect two different nodes which is making my head hurt right now.

src/adera/Machines/Rocket.cpp Outdated Show resolved Hide resolved
src/adera/Machines/Rocket.cpp Outdated Show resolved Hide resolved
float massFlow = massFlowRate * m_scene.get_time_delta_fixed();
uint64_t required = resource.m_type->resource_quantity(massFlow);
MachineContainer* src = m_scene.reg_try_get<MachineContainer>(resource.m_sourceEnt);
uint64_t consumed = src->request_contents(required);
Copy link
Contributor

Choose a reason for hiding this comment

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

I'm not liking how this is writing data into another machine. Where is m_massRateFraction being set from? What happens if one of the tanks are empty?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Good point; it should calculate the mass flow required before withdrawing anything from the tanks. This should probably be combined with the earlier step where I check for fuel availability. I should probably test all this with a bipropellant rocket to make sure it works in general. We can discuss having a node intermediate between fuel sources/sinks elsewhere, I do like that idea.

As for m_massRateFraction, MachineRocket stores a vector m_resourceLines which lists all the types of fuel demanded by the engine, and the by-mass mixture ratio of each one. massFlowRate is the total fuel mass flow rate from earlier, multiplied by the mass mixture ratio to get the mass flow rate of the current resource being iterated over.

DependRes<ShipResourceType> fuel = pkg.get<ShipResourceType>("fuel");
if (!fuel.empty())
{
ActiveEnt fuelSource = static_cast<ActiveEnt>(13);
Copy link
Contributor

Choose a reason for hiding this comment

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

what even is this doing? Should this be a search for connected MachineContainers?


using namespace osp;
using namespace osp::active;
using namespace adera::active::machines;

/* ShipResourceType */

double ShipResourceType::resource_volume(uint64_t quantity) const
Copy link
Contributor

Choose a reason for hiding this comment

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

Many of these functions look constexpr-able. I remember a generalized 2pow bitshift function being mentioned somewhere.

Copy link
Member

Choose a reason for hiding this comment

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

i agree that if you're always using a multiple of two, you could create a constexpr bitshift function for that.

src/adera/ShipResources.cpp Outdated Show resolved Hide resolved
Comment on lines 139 to 150
/* Deferred machine instantiation
*
* Since machines may depend on the existence of other objects in the
* part's hierarchy, machine instantiation must be deferred until the
* entire hierarchy exists. For instance, a rocket engine's root
* "MachineRocket" machine may depend on the existence of a child node
* to receive an "ACompExhaustPlume" component that handles graphical
* effects related to the rocket machine, but that child node would not
* exist in time for the MachineRocket to be instantiated alongside
* the root part entity.
*/
part_instantiate_machines(partEntity, machineMapping, proto, partBp);
Copy link
Contributor

Choose a reason for hiding this comment

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

This sounds good. I recently de-spaghettified SysVehicle in my own branch, making lots of functions static like in my other PRs. There will be merge conflict for sure soon.

src/osp/Active/SysVehicle.cpp Outdated Show resolved Hide resolved
{
inputs.push_back(&resource.m_lineIn);
}
return inputs;*/
Copy link
Member

Choose a reason for hiding this comment

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

prefer

#if 0
#endif 

for commenting out code.

Copy link
Member

Choose a reason for hiding this comment

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

or #if false

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Really? Why?

Copy link
Member

Choose a reason for hiding this comment

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

Much easier for a lot of code editors to handle gracefully, in addition to not having weird behavior when it comes to nested comments.

inline MachineRocket::MachineRocket(float thrust)
: Machine(true)
, m_thrust(thrust)
inline MachineRocket::MachineRocket(Parameters params, fuel_list_t& resources)
Copy link
Member

Choose a reason for hiding this comment

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

don't take by l-value reference if you are going to unconditionally std::move. Just take the parameter by value in that case.

uint64_t ShipResourceType::resource_capacity(double volume) const
{
double units = volume / m_volume;
double quantaPerUnit = std::pow(2.0, m_quanta);
Copy link
Member

Choose a reason for hiding this comment

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

if you're always turning m_quanta into 2^m_quanta, why even have m_quanta?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

It's tied to the number of bits used to represent the base unit of resource. You could make an enum or something instead but you can't just pick any old quantaPerUnit.

I will concede though that the language is really confusing in all of this stuff. "Quanta" and "unit" are both super ambiguous words in this context; "quanta" might have to change so that it's easier to differentiate the "unit" 1 and the arbitrary unit chosen by the user.

Copy link
Contributor

Choose a reason for hiding this comment

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

Hmm, now that I think of it, m_quanta is there to increase the precision of the other parameters. If m_quanta is decremented, then the mass, volume, and density can be divided by two to compensate. Though it is pretty useful to describe resources as "1.0m^3 of LOX is 1141.0kg, the game specifically divides this into 65536 units."

m_quanta can just be stored as some unsigned int instead, and is enforced to be a power of two.

A better name might be long but more obvious one: "quantaPerOneUnit"

Copy link
Contributor

@Capital-Asterisk Capital-Asterisk left a comment

Choose a reason for hiding this comment

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

I'd say it's best to wait until the new wiring system is implemented before making any major changes, so I'll approve this for now.

The ability for sub-part objects to own their own machines has a number
of benefits and will facilitate future features like integral RCS
thrusters, or realistic resource mass distribution. Upon import, parts
are now recursively scanned for machines and PrototypeMachines are
stored in PrototypeObjects instead of PrototypePart. BlueprintObjects
also store BlueprintMachines, which are used for unique configuration
options, e.g. specifying the type or quantity of fuel in a tank,
choosing whether a piece of equipment is retracted or extended
initially, etc.

This commit also sets up a bare minimum fuel consumption situation where
all container machines are automatically filled with a generic "fuel",
and all rockets are automatically connected to the large fuel tank (the
vessel is currently searched for the first non-empty fuel tank).
Eventually some sort of blueprint system will allow for the programmatic
connecting of fuel tank to engines. The engine configuration parameters
(currently just max thrust and specific impulse) are read in and used to
compute the fuel mass burn rate.
@z-adams z-adams merged commit 84f78b3 into TheOpenSpaceProgram:master Jan 30, 2021
@z-adams z-adams mentioned this pull request Feb 12, 2021
@Capital-Asterisk Capital-Asterisk mentioned this pull request Apr 7, 2021
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

3 participants