See the docs in this section for a pretty incomplete coverage of the game’s code.
It mentions “warping” quite a bit, which refers to the technique used for TAS.
This is the multi-page printable view of this section. Click here to print.
See the docs in this section for a pretty incomplete coverage of the game’s code.
It mentions “warping” quite a bit, which refers to the technique used for TAS.
The game is written with multiplayer in mind, and even single player gameplay uses the same stack as the multiplayer scenario, just with a single client.
The host of a game is always the server, and all players in the game (including the host) are clients.
Generally speaking, the clients are only responsible for rendering and input, whereas the server is responsible for the game logic. For example, when a player grabs an tomato from a crate, its client sends a message to the server that “I will pickup something from the crate”, and the server decides to spawn a tomato and put it in the chef’s hands, and synchronizes that information to the clients. Even if the player is the host, it goes through the same code path, except the networking part just calls local code.
The physics simulation is somewhat of an exception. Both the server and the client run physics simulation. I don’t yet understand how that works.
Each object during the gameplay that requires multiplayer synchronization has an entity ID. The entity IDs
are agreed upon at all times between the server and the clients. The bidirectional mapping between entity IDs
and their corresponding GameObjects are kept by EntitySerialisationRegistry
as static fields. The registry
also keeps track of some other properties.
EntitySerialisationRegistry
statically. This is a questionable
design decision, but in the face of Unity (which uses static objects all over the place), it doesn’t
stand out.Initially, when the level is first launched, the starting objects (such as counters, mixers, plates, chefs, etc.) are assigned entity IDs by iterating through the objects:
EntitySerialisationRegistry.SetupSynchronisation
which then calls LinkAllEntitiesToSynchronisationScripts()
.Each synchronized entity (i.e. GameObject) can contain multiple components, each of which may provide its own synchronization logic:
Workstation
and AttachStation
. The
Workstation
component corresponds to the functionality of a cutting board, whereas AttachStation
provides the ability to place anything on the counter (such as a plate). The two components interact: if
the attached object is a WorkableItem
, then the Workstation
may be used to “work on” the item, which
is the game’s way of saying, chopping it.SetupSynchronisation
is called, the server will attach ServerWorkstation
and
ServerAttachStation
to the same GameObject. A client (including the host’s) will attach
ClientWorkstation
and ClientAttachStation
.ServerWorkstation
and ClientWorkstation
work with each other to synchronize the state for
just that aspect of the cutting board object.Workstation
the
message type is WorkstationMessage
. The message contains all states represented by this component.An entity is spawned by specifying a spawner entity and the prefab to be spawned.
NetworkUtils.ServerSpawnPrefab(GameObject spawner, GameObject prefab)
.ServerPlateReturnStation.CreateStack
calls
NetworkUtils.ServerSpawnPrefab(this.gameObject, this.m_returnStation.m_stackPrefab)
, the latter being
the prefab for a dirty plate stack.NetworkUtils.RegisterSpawnablePrefab
. This
will add a SpawnableEntityCollection
onto the GameObject if it’s not already there, and add the prefab
to the list of spawnables. Inside NetworkUtils.ServerSpawnPrefab
, the SpawnableEntityCollection
is
requested from the spawner GameObject via the interface INetworkEntitySpawner
(whose only implementation
is SpawnableEntityCollection
).NetworkUtils.ServerSpawnPrefab
, the spawned object is automatically registered as a new
entity in the EntitySerialisationRegistry
, and it automatically calls
EntitySerialisationRegistry.StartSynchronisingEntry
on the resulting GameObject.ServerMessenger.SpawnEntity
to notify clients about the spawning of the
entity. This implies that StartSynchronisingEntry should not immediately emit any synchronization messages,
or else the message will just be dropped at the client side.PhysicalAttachment
, which generally means something
that has a container object that has a RigidBody
. If so, it additionally registers the container object
as a separate entity, and instead of calling ServerMessenger.SpawnEntity
, calls
ServerMessenger.SpawnPhysicalAttachment
instead - this is a separate network message type. This message
includes both entity IDs.ClientSynchronisationReceiver.OnSpawnPhysicalAttachmentMessageReceived
.All the message types and entity message types are registered in MultiplayerController.Awake
.
Most entities are synchronized when the corresponding server component calls SendServerData
, which is then
processed by the client side when it arrives.
Some entities are synchronized automatically by the entity system, if they override GetServerUpdate
. Periodically (once every few frames), the ServerSynchronisationScheduler.SynchroniseList
function will
send an EntitySynchronisationMessage
to synchronize all such entities to the clients.
These automatically synchronized entities are ServerWorldObjectSynchroniser
(for physics synchronization), ServerCookingHandler
, ServerIngredientContainer
, and ServerMixingHandler
.
Some people found this funny bug where you can make a mega-burger:
Example video: https://www.bilibili.com/video/BV1ZX4y157Ra/
Let’s see what happened.
ServerPreparationContainer
is the primary component of a burger bun; here’s the code that is invoked when trying to
place something onto it:
public bool CanHandlePlacement(ICarrier _carrier, Vector2 _directionXZ, PlacementContext _context)
{
// Get the thing the chef is holding (a plate of ingredients, in this case).
GameObject gameObject = _carrier.InspectCarriedItem();
// Gets the definition of the plate's contents. In this case it would be 3 separate items
// (cooked meat, cooked meat, pineapple). See below.
AssembledDefinitionNode[] orderDefinitionOfCarriedItem = this.GetOrderDefinitionOfCarriedItem(gameObject);
// This is the plate's ServerIngredientContainer component.
ServerIngredientContainer component = gameObject.GetComponent<ServerIngredientContainer>();
// It indeed is a plate.
Plate component2 = gameObject.GetComponent<Plate>();
// See below. Passes if individual ingredients are each eligible, and also container is not
// overfilled in capacity.
if (!this.CanAddOrderContents(orderDefinitionOfCarriedItem))
{
// Tray is the rectangular tray, not the case here.
Tray component3 = gameObject.GetComponent<Tray>();
if (component3 != null)
{
return true;
}
// If the check above fails, and the thing is not a plate, or its ingredients can't
// individually be inserted, then fail.
if (!(component2 != null) || !(component != null) || !this.CanTransferToContainer(component))
{
return false;
}
}
// Finally check if the thing we're inserting is a plate (true), or the two plating steps are the same (plating steps are like,
// cup vs plate vs tray).
return !(component2 != null) || !(this.m_preparationContainer.m_ingredientOrderNode.m_platingStep != component2.m_platingStep);
// To summarize, if the thing we're transferring from is a plate, we succeed if either:
// - The plates' contents are individually eligible for the target, and the target wouldn't be overfilled; OR
// - the plate's ServerIngredientContainer's individual elements are transferrable to the target (but without checking
// target's capacity!)
}
// ServerPreparationContainer::GetOrderDefinitionOfCarriedItem
private AssembledDefinitionNode[] GetOrderDefinitionOfCarriedItem(GameObject _carriedItem)
{
IBaseCookable cookingHandler = null;
// A plate is not cookable, so doesn't apply here.
ServerCookableContainer component = _carriedItem.GetComponent<ServerCookableContainer>();
if (component != null)
{
cookingHandler = component.GetCookingHandler();
}
return this.m_preparationContainer.GetOrderDefinitionOfCarriedItem(_carriedItem, this.m_itemContainer, cookingHandler);
}
// PreparationContainer::GetOrderDefinitionOfCarriedItem
public AssembledDefinitionNode[] GetOrderDefinitionOfCarriedItem(GameObject _carriedItem, IIngredientContents _itemContainer, IBaseCookable _cookingHandler)
{
AssembledDefinitionNode[] result = null;
if (_carriedItem.GetComponent<CookableContainer>() != null)
{
ClientIngredientContainer component = _carriedItem.GetComponent<ClientIngredientContainer>();
IContainerTransferBehaviour containerTransferBehaviour = _carriedItem.RequireInterface<IContainerTransferBehaviour>();
if (component.HasContents() && containerTransferBehaviour.CanTransferToContainer(_itemContainer))
{
CookableContainer component2 = _carriedItem.GetComponent<CookableContainer>();
ClientMixableContainer component3 = _carriedItem.GetComponent<ClientMixableContainer>();
AssembledDefinitionNode cookableMixableContents = null;
bool isMixed = false;
if (component3 != null)
{
cookableMixableContents = component3.GetOrderComposition();
isMixed = component3.GetMixingHandler().IsMixed();
}
CookedCompositeAssembledNode cookedCompositeAssembledNode = component2.GetOrderComposition(_itemContainer, _cookingHandler, cookableMixableContents, isMixed) as CookedCompositeAssembledNode;
cookedCompositeAssembledNode.m_composition = new AssembledDefinitionNode[]
{
component.GetContentsElement(0)
};
result = new AssembledDefinitionNode[]
{
cookedCompositeAssembledNode
};
}
}
else if (_carriedItem.GetComponent<IngredientPropertiesComponent>() != null)
{
IngredientPropertiesComponent component4 = _carriedItem.GetComponent<IngredientPropertiesComponent>();
result = new AssembledDefinitionNode[]
{
component4.GetOrderComposition()
};
}
// This case applies.
else if (_carriedItem.GetComponent<Plate>() != null)
{
ClientIngredientContainer component5 = _carriedItem.GetComponent<ClientIngredientContainer>();
result = component5.GetContents();
}
return result;
}
// ClientIngredientContainer::GetContents. Why Client and not Server? I have no idea.
public AssembledDefinitionNode[] GetContents()
{
return this.m_contents.ToArray();
}
// ServerPreparationContainer::CanAddOrderContents - can we add these things into the bun?
public bool CanAddOrderContents(AssembledDefinitionNode[] _contents)
{
if (_contents != null)
{
foreach (AssembledDefinitionNode toAdd in _contents)
{
// Check each individual ingredient.
if (!this.CanAddIngredient(toAdd))
{
return false;
}
}
// Then check the whole container.
// This does nothing more than checking the capacity.
return this.m_itemContainer.CanTakeContents(_contents);
}
return false;
}
// ServerPreparationContainer::CanAddIngredient
protected virtual bool CanAddIngredient(AssembledDefinitionNode _toAdd)
{
CompositeAssembledNode asOrderComposite = this.GetAsOrderComposite();
return asOrderComposite.CanAddOrderNode(_toAdd, true);
}
// CompositeAssembledNode::CanAddOrderNode
// Basically checks that each ingredient is allowable for this container,
// and that we don't already have too many (e.g. can't insert two lettuces).
public bool CanAddOrderNode(AssembledDefinitionNode _toAdd, bool _raw = false)
{
if (!_raw && this.m_freeObject != null && this.m_freeObject.RequestInterface<IHandleOrderModification>() != null)
{
IHandleOrderModification handleOrderModification = this.m_freeObject.RequestInterface<IHandleOrderModification>();
return handleOrderModification.CanAddOrderContents(new AssembledDefinitionNode[]
{
_toAdd
});
}
int num = 0;
foreach (AssembledDefinitionNode node in this.m_composition)
{
if (AssembledDefinitionNode.Matching(node, _toAdd))
{
num++;
}
}
Predicate<OrderContentRestriction> match = (OrderContentRestriction _specifier) => AssembledDefinitionNode.Matching(_specifier.m_content, _toAdd);
OrderContentRestriction orderContentRestriction = this.m_permittedEntries.Find(match);
return orderContentRestriction != null && num < orderContentRestriction.m_amountAllowed && !this.CompositionContainsRestrictedNode(_toAdd, orderContentRestriction);
}
Also:
// ServerPreparationContainer::CanTransferToContainer. It basically checks every component of
// the container's contents.
public virtual bool CanTransferToContainer(IIngredientContents _container)
{
if (_container.HasContents())
{
for (int i = 0; i < _container.GetContentsCount(); i++)
{
AssembledDefinitionNode contentsElement = _container.GetContentsElement(i);
if (!this.CanAddIngredient(contentsElement))
{
return false;
}
}
}
return true;
}
The Kitchen Flow Controller (ServerKitchenFlowControllerBase
and ClientKitchenFlowControllerBase
)
is responsible for keeping track of:
The order list is maintained in an inner structure called ServerOrderControllerBase
. It maintains:
RoundDataBase m_roundData
which is the implementation used to generate new ordersRoundInstanceDataBase m_roundInstanceData
, which is the current RNG state of the m_roundData
m_nextOrderID
, starting at 1, responsible for assigning an OrderID
for the next orderOrderID
is used to identify orders internally – since the active orders list shifts around.List<ServerOrderData> m_activeOrders
which is the list of currently active ordersfloat m_timerUntilOrder
, the amount of time until the next order should be automatically added
(only relevant if there are currently 2, 3, or 4 orders)m_comboIndex
, which is the index of the last delivered order.There are also a couple of other fields that are not quite relevant.
The ServerOrderData
is a serializable class that maintains:
OrderID
RecipeList.Entry
which is a reference to one element in the level’s RecipeList
float Lifetime
, the time allowed for the orderfloat Remaining
, the remaining time allowed for the order before it expiresIn the ServerOrderControllerBase::Update()
function:
<= 0
then the timeout callback is invoked;m_timerUntilOrder
is decreased by the time elapsedm_timerUntilOrder
.Now coming to ServerTeamMonitor
, which hosts the ServerOrderControllerBase
as its field; it also has
two other fields, TeamMonitor
and TeamMonitor.TeamScoreStats
. The former hosts nothing but the
RecipeFlowGUI
, which we’ll describe when talking about the client side later. The latter hosts the
scores of the team - which we discuss in the scoring section.
ServerTeamMonitor::OnFoodDelivered
function is called when a dish is delivered by the player:
public virtual bool OnFoodDelivered(AssembledDefinitionNode _definition, PlatingStepData _plateType, out OrderID o_orderID, out RecipeList.Entry o_entry, out float o_timePropRemainingPercentage, out bool o_wasCombo)
{
o_orderID = default(OrderID);
o_entry = null;
o_timePropRemainingPercentage = 0f;
o_wasCombo = false;
// This matches the recipe against the orders. It'll return the matching order with the
// least amount of time remaining. If there are no matching orders it returns false.
bool flag = this.m_ordersController.FindBestOrderForRecipe(_definition, _plateType, out o_orderID, out o_timePropRemainingPercentage);
if (flag)
{
// Calculates whether the order was a combo, as this affects scoring.
o_entry = this.m_ordersController.GetRecipe(o_orderID);
bool restart = this.m_score.TotalCombo == 0;
o_wasCombo = this.m_ordersController.IsComboOrder(o_orderID, restart);
// Removes the order from the list of orders.
this.m_ordersController.RemoveOrder(o_orderID);
return true;
}
return false;
}
Now coming back to the root struct, ServerKitchenFlowControllerBase
, which hosts a collection of
ServerTeamMonitor
s; for simplicity we only look at co-op games, i.e. one team.
OnOrderAdded
is called when the ServerOrderControllerBase
invokes the order added callback.
Its only task here is to call SendOrderAdded
to send this as a KitchenFlowMessage
to the client side.OnOrderExpired
is similar, and invokes SendOrderExpired
to send to the client side. Additionally
it updates a few fields such as resetting the combo on the team’s score struct, and resets the order’s
timer.OnFoodDelivered
is called externally. It calls the ServerTeamMonitor
’s OnFoodDelivered
andOnSuccessfulDelivery
; this computes how to affect the team’s score stats
and then calls SendDeliverySuccess
to send it to the client. The message once again includes the
full score data.OnFailedDelivery
, which resets the combo in the score struct and similarly calls
SendDeliveryFailed
to share it with the client.These OnXXX
Methods are actually overridden by the concrete implementation ServerCampaignFlowController
(or ServerCompetitiveFlowController
, which we don’t cover here). The additional logic controls minor things
like updating the timer suppressor when the first dish is delivered successfully.
The KitchenFlowMessage
can allow us to maintain on the controller side:
ServerOrderControllerBase
:m_timerUntilOrder
timer, by decrementing per frame and resetting upon adding ordersRoundInstanceDataBase
is not recoverable as is, so would need a custom implementationServerOrderData
, which comes straight from deserialization;Remaining
time is kept by decrementing per frameServerTeamMonitor
, the TeamScoreStats
struct is wholly replaced each time by
deserialization.ServerKitchenFlowControllerBase
, there aren’t any meaningful fields to warp.The client side mostly mirrors the server side but is more complicated because it additionally needs to maintain the GUI rendering of the order list.
In ClientKitchenFlowControllerBase
, we maintain:
ClientTeamMonitor
, which contains:TeamMonitor
, same as ServerTeamMonitor
ClientOrderControllerBase
TeamMonitor.TeamScoreStats
DataStore m_dataStore
; this seems to be an in-memory transient key-value store used for
notifying UI rendering logic to update UI elements.OrderDefinitionNode[] m_cachedRecipeList
, AssembledDefinitionNode[] m_cachedAssembledRecipes
,
and CookingStepData[] m_cachedCookingStepList
. They… don’t seem to actually do anything.The events from the server are handled as:
protected virtual void OnSuccessfulDelivery(TeamID _teamID, OrderID _orderID, float _timePropRemainingPercentage, int _tip, bool _wasCombo, ClientPlateStation _station)
{
GameUtils.TriggerAudio(GameOneShotAudioTag.SuccessfulDelivery, base.gameObject.layer);
ClientTeamMonitor monitorForTeam = this.GetMonitorForTeam(_teamID);
int uID = monitorForTeam.OrdersController.GetRecipe(_orderID).m_order.m_uID;
// See ClientOrderControllerBase.
monitorForTeam.OrdersController.OnFoodDelivered(true, _orderID);
// Writes into the DataStore with the key "score.team" to update UI.
this.UpdateScoreUI(_teamID);
// This is to keep track of level objectives which don't seem to be used in the game?
if (this.m_onMealDelivered != null)
{
this.m_onMealDelivered(uID, _wasCombo);
}
// Achievement stuff.
OvercookedAchievementManager overcookedAchievementManager = GameUtils.RequestManager<OvercookedAchievementManager>();
if (overcookedAchievementManager != null)
{
overcookedAchievementManager.IncStat(1, 1f, ControlPadInput.PadNum.One);
overcookedAchievementManager.AddIDStat(22, uID, ControlPadInput.PadNum.One);
overcookedAchievementManager.AddIDStat(100, uID, ControlPadInput.PadNum.One);
overcookedAchievementManager.AddIDStat(500, uID, ControlPadInput.PadNum.One);
overcookedAchievementManager.AddIDStat(801, uID, ControlPadInput.PadNum.One);
}
}
protected virtual void OnFailedDelivery(TeamID _teamID, OrderID _orderID)
{
ClientTeamMonitor monitorForTeam = this.GetMonitorForTeam(_teamID);
monitorForTeam.OrdersController.OnFoodDelivered(false, _orderID);
this.UpdateScoreUI(_teamID);
// Also for level objectives which aren't useful.
if (this.m_onFailedDelivery != null)
{
this.m_onFailedDelivery();
}
}
protected virtual void OnOrderAdded(TeamID _teamID, Serialisable _orderData)
{
ClientTeamMonitor monitorForTeam = this.GetMonitorForTeam(_teamID);
monitorForTeam.OrdersController.AddNewOrder(_orderData);
this.UpdateScoreUI(_teamID);
}
protected virtual void OnOrderExpired(TeamID _teamID, OrderID _orderID)
{
ClientTeamMonitor monitorForTeam = this.GetMonitorForTeam(_teamID);
monitorForTeam.OrdersController.OnOrderExpired(_orderID);
this.UpdateScoreUI(_teamID);
}
Note that the actual concrete class ClientCampaignFlowController
overrides these methods to call
a few extra things in ClientCampaignMode
. This includes updating the score tip display (DataStore
key “score.tip”), and timer suppression update.
Into the ClientOrderControllerBase
,
public virtual void OnFoodDelivered(bool _success, OrderID _orderID)
{
if (_success)
{
ClientOrderControllerBase.ActiveOrder activeOrder = this.m_activeOrders.Find((ClientOrderControllerBase.ActiveOrder x) => x.ID == _orderID);
if (activeOrder != null)
{
// Tells the GUI to use an animation to remove the order item. It's not immediately removed
// from the rendered list but only after the animation is done.
this.m_gui.RemoveElement(activeOrder.UIToken, new RecipeSuccessAnimation());
}
// Mirroring of the server side's active order list.
this.m_activeOrders.RemoveAll((ClientOrderControllerBase.ActiveOrder x) => x.ID == _orderID);
}
else
{
for (int i = 0; i < this.m_activeOrders.Count; i++)
{
// On failure animate all items with the failure animation.
this.m_gui.PlayAnimationOnElement(this.m_activeOrders[i].UIToken, new RecipeFailureAnimation());
}
}
}
public virtual void AddNewOrder(Serialisable _data)
{
ServerOrderData data = (ServerOrderData)_data;
RecipeList.Entry entry = new RecipeList.Entry();
entry.Copy(data.RecipeListEntry);
// Adds the item to the GUI.
RecipeFlowGUI.ElementToken token = this.m_gui.AddElement(entry.m_order, data.Lifetime, this.m_expiredDoNothingCallback);
// Adds the item to the mirrored list of active orders.
ClientOrderControllerBase.ActiveOrder item = new ClientOrderControllerBase.ActiveOrder(data.ID, entry, token);
this.m_activeOrders.Add(item);
}
public virtual void OnOrderExpired(OrderID _orderID)
{
GameUtils.TriggerAudio(GameOneShotAudioTag.RecipeTimeOut, this.m_gui.gameObject.layer);
ClientOrderControllerBase.ActiveOrder activeOrder = this.m_activeOrders.Find((ClientOrderControllerBase.ActiveOrder x) => x.ID == _orderID);
if (activeOrder != null)
{
// Order expiration is only a GUI effect because on the client side we don't mirror
// the amount of time remaining (only graphically).
this.m_gui.PlayAnimationOnElement(activeOrder.UIToken, new RecipeFailureAnimation());
this.m_gui.ResetElementTimer(activeOrder.UIToken);
}
}
Now we go into the RecipeFlowGUI
class, which is responsible for rendering the order list.
It maintains:
int m_nextIndex
, used to number newly added recipe widgets (each order is one recipe widget).List<RecipeFlowGUI.RecipeWidgetData> m_widgets
, the list of active widgets.List<RecipeFlowGUI.RecipeWidgetData> m_dyingWidgets
, the list of widgets being deleted but still going
through the deletion animation.List<RecipeFlowGUI.RecipeWidgetData> m_ordererWidgets
, which is used only as a local
variable to merge sort the m_widgets
and m_dyingWidgets
lists.bool[] m_occupiedTables
which simulates tables at a restaurant but is not actually
displayed or used anywhere in the game. We still need to maintain it though because each order does have
a table ID.The actions in RecipeFlowGUI
are:
public RecipeFlowGUI.ElementToken AddElement(OrderDefinitionNode _data, float _timeLimit, VoidGeneric<RecipeFlowGUI.ElementToken> _expirationCallback)
{
int tableNumber = this.ClaimUnoccupiedTable(); // not important
// Sets up the UI element to render the recipe.
// When the UI element is first set up, there's an initial animation transition to slide the
// recipe into place.
GameObject obj = GameUtils.InstantiateUIController(this.m_recipeWidgetPrefab.gameObject, base.transform as RectTransform);
RecipeWidgetUIController recipeWidgetUIController = obj.RequireComponent<RecipeWidgetUIController>();
recipeWidgetUIController.SetupFromOrderDefinition(_data, tableNumber);
// Sets up the data structure to maintain the UI element.
RecipeFlowGUI.RecipeWidgetData recipeWidgetData = new RecipeFlowGUI.RecipeWidgetData(recipeWidgetUIController, 0f, this.m_nextIndex, _timeLimit, _expirationCallback);
this.m_nextIndex++;
// adds it to the active list.
this.m_widgets.Add(recipeWidgetData);
return new RecipeFlowGUI.ElementToken(recipeWidgetData);
}
public void RemoveElement(RecipeFlowGUI.ElementToken _token, WidgetAnimation _deathAnim = null)
{
for (int i = this.m_widgets.Count - 1; i >= 0; i--)
{
RecipeFlowGUI.RecipeWidgetData recipeWidgetData = this.m_widgets[i];
if (new RecipeFlowGUI.ElementToken(recipeWidgetData) == _token)
{
if (_deathAnim != null)
{
// If there's an animation, add it to the list of dying widgets and
// play the animation.
this.ReleaseTable(recipeWidgetData.m_widget.GetTableNumber());
recipeWidgetData.m_widget.PlayAnimation(_deathAnim);
this.m_dyingWidgets.Add(recipeWidgetData);
}
this.m_widgets.RemoveAt(i);
}
}
}
// Called in `ClientOrderControllerBase` to decrement each order's remaining time.
public void UpdateTimers(float _dt)
{
for (int i = 0; i < this.m_widgets.Count; i++)
{
float timePropRemaining = this.m_widgets[i].m_widget.GetTimePropRemaining();
float num = timePropRemaining;
num = Mathf.Max(num - _dt / this.m_widgets[i].m_timeLimit, 0f);
this.m_widgets[i].m_widget.SetTimePropRemaining(num);
if (timePropRemaining > 0f && num <= 0f)
{
// This doesn't seem to do anything.
this.m_widgets[i].m_expirationCallback(new RecipeFlowGUI.ElementToken(this.m_widgets[i]));
}
}
}
private void Update()
{
// Remove those dying widgets that finished their deletion animations.
this.m_dyingWidgets.RemoveAll(delegate(RecipeFlowGUI.RecipeWidgetData obj)
{
if (!obj.m_widget.IsPlayingAnimation())
{
UnityEngine.Object.Destroy(obj.m_widget.gameObject);
return true;
}
return false;
});
// Layouts the remaining widgets.
this.LayoutWidgets();
}
private void LayoutWidgets()
{
// Merge sorts the active and dying widgets.
this.FindAllWidgetsOrdered(ref this.m_ordererWidgets);
float distanceFromEndOfScreen = this.m_distanceFromEndOfScreen;
float distanceBetweenOrders = this.m_distanceBetweenOrders;
float num = distanceFromEndOfScreen - distanceBetweenOrders;
for (int i = 0; i < this.m_ordererWidgets.Count; i++)
{
RecipeWidgetUIController widget = this.m_ordererWidgets[i].m_widget;
float width = widget.GetBounds().width;
// There's nothing fancy going on here. The entry animation is done within the UI prefab.
// When removing a widget there's no animation for the remaining widgets to slide over.
RectTransformExtension rectTransformExtension = widget.gameObject.RequireComponent<RectTransformExtension>();
float num2 = num + distanceBetweenOrders;
rectTransformExtension.AnchorOffset = new Vector2(0f, 0f);
rectTransformExtension.PixelOffset = new Vector2(num2, 0f);
num = num2 + width;
}
}
Finally, the RecipeWidgetUIController
:
public void SetupFromOrderDefinition(OrderDefinitionNode _data, int _tableNumber)
{
this.m_recipeTree = _data.m_orderGuiDescription;
this.m_tableNumber = _tableNumber;
// Delays the preparation of the actual UI.
base.StartCoroutine(this.RefreshSubObjectsAtEndOfFrame());
if (T17InGameFlow.Instance != null)
{
T17InGameFlow.Instance.RegisterOnPauseMenuVisibilityChanged(new BaseMenuBehaviour.BaseMenuBehaviourEvent(this.OnPauseMenuVisibilityChange));
}
}
private IEnumerator RefreshSubObjectsAtEndOfFrame()
{
yield return new WaitForEndOfFrame();
base.RefreshSubElements();
yield break;
}
// From base class UISubElementContainer:
public void RefreshSubElements()
{
// Does some stuff, mostly calling OnCreateSubobjects.
this.EnsureImagesExist();
this.m_debugActivated = true;
this.RefreshSubObjectProperties();
}
And then the OnCreateSubobjects
function adds the animator, sets up the recipe tiles, etc.
ClientOrderControllerBase
- just overwrite the entire list.RecipeFlowGUI
:m_nextIndex
- we can make it arbitrarily high, maybe just don’t reuse any IDs.m_widgets
to be the currently active orders; reinitialize all of them, force set
the timePropRemaining of each orderOnCreateSubobjects
callback need to pulled earlier. To do this, instead of invoking
SetupFromOrderDefinition
, we will do that ourselves (it’s just the m_recipeTree and
m_tableNumber), and then call RefreshSubElements
.m_dyingWidgets
to be empty.m_occupiedTables
to be consistent with whatever indexes assigned to our newly
created orders.ClientTeamMonitor
’s score struct and refresh DataStore “score.team” key.public class TeamScoreStats : Serialisable {
// ...
public int TotalBaseScore;
public int TotalTipsScore;
public int TotalMultiplier;
public int TotalCombo;
public int TotalTimeExpireDeductions;
public bool ComboMaintained = true;
public int TotalSuccessfulDeliveries;
}
ServerPlateStation
is the serving station.
ServerPlateReturnStation
s which corresponds to the stations where dirty plates will spawn.
There can be multiple because there may be multiple kinds of plates (cups and plates, e.g.).IKitchenOrderHandler m_orderHandler
handles the delivery of the order.ServerKitchenFlowControllerBase
. The OnFoodDelivered
calls its
m_plateReturnController.FoodDelivered
, which adds a PlatesPendingReturn
to its list of pending plates.PlatesPendingReturn
specifies the return station, the timer, and the PlatingStepData
(identifying the
kind of plate).PlatingStepData
is just an m_uID
plus a sprite. The actual prefab for the dirty plate is created by the
PlateReturnStation
as a spawn.OnItemAdded
, registered on the m_attachStation
(sibling component) via RegisterOnItemAdded
.Fields:
private PlayerControlsImpl_Default m_controlsImpl;
The associated PlayerControlsImpl_Default
component of the same object.private ClientInteractable m_lastInteracted;
The last object interacted with. Set in BeginInteraction and cleared in EndInteraction. Seems to only be queried by code that affects animations?private ClientInteractable m_predictedInteracted;
Part of some super complicated interaction logic:private void Update_Interact(float _deltaTime, bool isUsePressed, bool justPressed)
{
// This is the highlighted object to be interacted.
ClientInteractable highlighted = this.m_controls.CurrentInteractionObjects.m_interactable;
// Object already being interacted
bool alreadyInteracting = this.m_predictedInteracted != null;
bool hasHighlight = highlighted != null;
bool isCurrentInteractionSticky = alreadyInteracting && this.m_predictedInteracted.InteractionIsSticky();
if (isUsePressed && hasHighlight && !alreadyInteracting)
{
// If we aren't already interacting with something, and the interaction button is down (doesn't have to
// be just pressed), and we have a highlighted object to interact with, then start the interaction
// with that object.
this.m_predictedInteracted = highlighted;
ClientMessenger.ChefEventMessage(ChefEventMessage.ChefEventType.Interact, base.gameObject, highlighted);
}
else if (!hasHighlight && alreadyInteracting && isCurrentInteractionSticky)
{
// If current interaction is sticky (i.e. does not require pressing), but there is no longer a
// currently highlighted object, then stop interacting.
this.m_predictedInteracted = null;
ClientMessenger.ChefEventMessage(ChefEventMessage.ChefEventType.Interact, base.gameObject, null);
}
else if (!isUsePressed && alreadyInteracting && !isCurrentInteractionSticky)
{
// If current interaction is not sticky (requires continuous pressing) but the key is
// no longer down, then stop interacting. e.g. fire extingisher, washer jet.
this.m_predictedInteracted = null;
ClientMessenger.ChefEventMessage(ChefEventMessage.ChefEventType.Interact, base.gameObject, null);
}
else if (hasHighlight && highlighted != this.m_predictedInteracted)
{
// If the currently highlighted object is different from the currently interacted object,
// stop interacting.
this.m_predictedInteracted = null;
ClientMessenger.ChefEventMessage(ChefEventMessage.ChefEventType.Interact, base.gameObject, null);
}
if (justPressed && highlighted != null)
{
// Independently of everything else, if we had *just* pressed the use button and we have a highlighted
// object to use, trigger the interaction with that object.
ClientMessenger.ChefEventMessage(ChefEventMessage.ChefEventType.TriggerInteract, base.gameObject, highlighted);
}
}
private Transform m_Transform;
Transform associated with the chef.private ClientInteractable m_sessionInteraction;
This is set while the chef is in an active “Session Interaction”, i.e. an interaction that makes the chef
uncontrollable before it ends; e.g. operating the raft control, or being inside a cannon.private PlayerControls m_controls;
The non-synchronizing PlayerControls object.private PlayerControls.ControlSchemeData m_controlScheme;
Provides information for the control scheme, i.e. what keys are used to dash, interact, etc.private GameObject m_controlObject;
The chef.private CollisionRecorder m_collisionRecorder;
Records recent collisions, used to compute dash collision with another player.private PlayerIDProvider m_controlsPlayer;
Component on the chef that provides the ID of the player.private ICarrier m_iCarrier;
Component on the chef that deals with carrying objects. TODO: this is satisfied by both ServerPlayerAttachmentCarrier and ClientPlayerAttachmentCarrier.private IClientThrower m_iThrower;
Component on the chef that deals with throwing objects.private IClientHandleCatch m_iCatcher;
Component on the chef that deals with catching objects.private Vector3 m_lastVelocity;
Velocity on the last “chef movement” frame. Being used in Update_Movement
to compute friction:Vector3 vector3 = this.ProgressVelocityWrtFriction(this.m_lastVelocity, this.m_controls.Motion.GetVelocity(), vector2, this.GetSurfaceData());
this.m_controls.Motion.SetVelocity(vector3);
this.m_lastVelocity = vector3;
private float m_dashTimer = float.MinValue;
private float m_attemptedDistanceCounter;
Does not seem to be actually useful. Only read and written here:this.m_attemptedDistanceCounter += vector2.magnitude * _deltaTime;
if (this.m_attemptedDistanceCounter >= movement.FootstepLength)
{
this.m_attemptedDistanceCounter %= movement.FootstepLength;
}
private bool m_aimingThrow;
m_movementInputSuppressed
.private bool m_movementInputSuppressed;
If true, the chef does not move with the controls (the control’s movement axes are effectively zero); however it will still move with the dash velocity.private Vector3 m_lastMoveInputDirection;
m_controls.m_analogEnableDeadzoneThreshold
, which is 0.25).m_controls.m_analogEnableDeadzoneThreshold
m_controls.m_analogEnableAngleThreshold
(which is 15 degrees) away from the m_lastMoveInputDirection
.private float m_impactStartTime = float.MaxValue;
The description is misleading; this is the total duration of the impact. It is set in ApplyImpact and not changed after.private float m_impactTimer = float.MinValue;
This is the remaining time of the impact. It is set to the same value as m_impactStartTime
at the beginning of ApplyImpact,
and then it decrements by 1/60 of a second every “movement frame”.private Vector3 m_impactVelocity = Vector3.zero;
The initial impact velocity. This is only set in ApplyImpact and then used to calculate the chef velocity (along with movement and dash) in Update_Movement.
The calculation formula is very similar to dash.private float m_timeOffGround;
m_isFalling
controls is a trigger that starts the falling
animation.private bool m_isFalling;
See m_timeOffGround
.private VoidGeneric<ClientInteractable> m_interactTriggerCallback;
Used for animations.private VoidGeneric<GameObject> m_throwTriggerCallback;
Used for animations.private VoidGeneric<bool> m_fallingTriggerCallback;
Used for animations.private PlayerControlsHelper.ControlAxisData m_controlAxisData;
Update_Movement
and then used in Update_Rotation
and UpdateMovementSuppression
.private PlayerIDProvider m_playerIDProvider;
This should point to the same object as m_controlsPlayer
.private float m_LeftOverTime;
Remaining time to calculate “movement frame”. This is incremented by Time.deltaTime every game frame,
and then a movement frame happens as 1/60 second is subtracted from m_LeftOverTime
.private OrderedMessageReceivedCallback m_onChefEffectReceived;
A callback used to handle received ChefEffect messages, i.e. multiplayer impact and dash.private uint m_entityID;
The entity ID of the chef.private float m_lastPickupTimestamp;
ClientTime.Time()
, which is some complicated synchronized time?m_controls.m_pickupDelay
).private LayerMask m_SlopedGroundMask = 0;
Something to do with gravity.ClientInteractable m_predictedInteracted
- persisted as an entity reference. Needed to calculate next frame interaction decisions.m_sessionInteraction
- we already record movement suppression, and in general it’s not a big deal because the controller won’t be trying to
move the chef when it’s trying to interact with something, and we’ll also make sure that any sort of interactable will actually disable the control of the chef. Movement suppression is generally just not that reliable anyway because dashing is not prevented; it’s only good for aiming, really.m_collisionRecorder
- colliding with another chef is unlikely to be helpful for TAS, we’ll just avoid it in general so it’s OK if this is not warped
perfectly.Vector3 m_lastVelocity
; unlike rigidbody’s velocity, this is NOT impacted by pausing, because Update_Impl doesn’t call any of the movement update functions if the game is paused.float m_dashTimer
bool m_aimingThrow
bool m_movementInputSuppressed
Vector3 m_lastMoveInputDirection
float m_impactStartTime
float m_impactTimer
Vector3 m_impactVelocity
float m_LeftOverTime
float m_lastPickupTimestamp
: We will limit this on the controller side. Otherwise this would require modding the field itself since it doesn’t hold a pausable timestamp right now.ServerCookingHandler
manages the progress and state of cooking.
public void SetCookingProgress(float _cookingProgress)
{
CookingHandler cookingHandler = this.GetCookingHandler();
this.m_ServerData.m_cookingProgress = _cookingProgress;
CookingUIController.State cookingState = this.m_ServerData.m_cookingState;
if (this.IsBurning())
{
this.m_ServerData.m_cookingState = CookingUIController.State.Ruined;
}
else if (_cookingProgress >= cookingHandler.m_cookingtime)
{
if (this.m_ServerData.m_cookingState == CookingUIController.State.Progressing)
{
this.m_ServerData.m_cookingState = CookingUIController.State.Completed;
}
if (_cookingProgress > 1.3f * cookingHandler.m_cookingtime)
{
this.m_ServerData.m_cookingState = CookingUIController.State.OverDoing;
}
}
else
{
this.m_ServerData.m_cookingState = CookingUIController.State.Progressing;
}
if (this.m_ServerData.m_cookingState != cookingState)
{
this.m_cookingStateChangedCallback(this.m_ServerData.m_cookingState);
}
}
The cooking state is completely determined by the progress.
The m_cookingStateChangedCallback
is mostly used for cosmetics. It changes the order composition
(because it changes uncooked ingredients to cooked ones), and so can affect certain cosmetic
functions. There’s one exception to the cosmetics which is ServerCookingStation.OnItemAdded
, which
updates whether the cooking station should be cooking. Either way, calling this more than once
shouldn’t hurt.
Also, ServerCookingHandler
’s synchronization is one of the very few components that synchronize
automatically. It does so by using the EntitySynchronisationMessage
instead of EntityEventMessage
.
The message is automatically sent by ServerSynchronisationScheduler
by querying the component’s
GetServerUpdate()
.
Same with ServerCookingHandler
.
This is mostly a helper component. It does not have its own synchronized entity message.
This also uses GetServerUpdate()
and is synchronized automatically. So correctness isn’t really an
issue. We just need to make sure to call OnContentsChanged()
upon warping.
This controls two parameters: m_isCooking
and m_isTurnedOn
.
Throwing is pretty non-trivial in this game.
AttachmentThrower
is a component that must be present for anything that can throw an object. There are two
I’m aware of: chef and teleportal. The ServerAttachmentThrower::ThrowItem
is the one performing the throwing
action, but throwing is triggered via a few steps, starting elsewhere:ClientPlayerControlsImpl_Default
listens for controller events in Update_Throw()
and then
requests a throw by sending an ChefEventType.Throw
event to the server. See Chef Throwing laterServerTeleportalAttachmentReceiver
has a coroutine that initiates the throw when
receiving a teleported throwable object.ServerThrowableItem
is a component that is present on anything that can be thrown. Its two important methods
are CanHandleThrow
and HandleThrow
:CanHandleThrow
makes an object conditionally throwable. (Anything that is never throwable just doesn’t have
the ThrowableItem
component attached to it at all.) It has only one implementation, by
ServerPreparationContainer
(used by burger buns, tortillas, etc.) which is only throwable if it doesn’t
have anything inside it.HandleThrow
does a few things. See later in Throwing Details.After ClientPlayerControlsImpl_Default.Update_Throw
decides to send a throw event, the server counterpart, ServerPlayerControlsImpl_Default.ReceiveThrowEvent(GameObject _target)
processes the throwing.
public void ReceiveThrowEvent(GameObject _target)
{
if (_target != null)
{
if (this.m_iCarrier.InspectCarriedItem() == null || PlayerControlsHelper.IsHeldItemInsideStaticCollision(this.m_controls))
{
return;
}
IThrowable throwable = _target.RequireInterface<IThrowable>();
Vector3 v = this.m_controlObject.transform.forward.normalized.XZ();
if (throwable.CanHandleThrow(this.m_iThrower, v))
{
this.m_iCarrier.TakeItem();
throwable.HandleThrow(this.m_iThrower, v);
EntitySerialisationEntry entry = EntitySerialisationRegistry.GetEntry(_target);
this.SendServerEvent(new InputEventMessage(InputEventMessage.InputEventType.EndThrow)
{
entityId = entry.m_Header.m_uEntityID
});
}
else
{
this.m_iThrower.OnFailedToThrowItem(this.m_iCarrier.InspectCarriedItem());
}
}
}
First, this.m_iCarrier
is the ICarrier
component on the same chef; .InspectCarriedItem()
returns the item
currently being carried by the chef. It’s it’s null then we bail.
Then, we check if the item is inside static collision:
public static bool IsHeldItemInsideStaticCollision(PlayerControls _control)
{
if (PlayerControlsHelper.s_staticCollisionLayerMask == 0)
{
PlayerControlsHelper.s_staticCollisionLayerMask = LayerMask.GetMask(new string[]
{
"Default",
"Ground",
"Walls",
"Worktops",
"PlateStationBlock",
"CookingStationBlock"
});
}
ICarrier carrier = _control.gameObject.RequireInterface<ICarrier>();
IAttachment attachment = carrier.InspectCarriedItem().RequestInterface<IAttachment>();
Collider collider = attachment.AccessGameObject().RequestComponent<Collider>();
Bounds bounds = collider.bounds;
Quaternion rotation = collider.transform.rotation;
Vector3 position = collider.transform.position;
Vector3 halfExtents = bounds.extents * 0.5f;
int num = Physics.OverlapBoxNonAlloc(bounds.center, halfExtents, PlayerControlsHelper.s_collisions, rotation, PlayerControlsHelper.s_staticCollisionLayerMask, QueryTriggerInteraction.Ignore);
return num > 0;
}
This corresponds to the behavior that if, say, a cooking station is right in the chef’s face, the chef cannot throw anything. Similarly one cannot throw something into the wall.
After that, we check for CanHandleThrow
, which we discussed above. If this fails, we call OnFailedToThrowItem
, whose only implementation by ServerAttachmentThrower
does nothing, so we can ignore this.
Next, we TakeItem()
from the chef, which detaches the item from the chef.
Then we call ServerThrowableItem.HandleThrow
. Before we dig into it, we look at the final thing that happens after: sending a EndThrow message to the clients. This does nothing important except triggering a sound effect on the clients as well as some chef animations.
Here’s the function that performs the throwing: ServerThrowableItem.HandleThrow
:
public void HandleThrow(IThrower _thrower, Vector2 _directionXZ)
{
_thrower.ThrowItem(base.gameObject, _directionXZ);
this.m_thrower = _thrower;
this.m_previousThrower = _thrower;
if (this.m_attachment == null)
{
this.m_attachment = base.gameObject.RequestInterface<IAttachment>();
}
if (this.m_attachment != null)
{
this.m_attachment.RegisterAttachChangedCallback(this.m_OnAttachChanged);
}
if (this.m_pickupReferral != null)
{
this.m_pickupReferral.SetHandlePickupReferree(this);
}
this.m_flying = true;
this.SendStateMessage(this.m_flying, ((MonoBehaviour)_thrower).gameObject);
this.ResumeAndClearThrowStartCollisions();
this.m_ignoredCollidersCount = Physics.OverlapBoxNonAlloc(this.m_Transform.position, this.m_Collider.bounds.extents, this.m_ThrowStartColliders, this.m_Transform.rotation, ServerThrowableItem.ms_AttachmentsLayer);
for (int i = 0; i < this.m_ignoredCollidersCount; i++)
{
Physics.IgnoreCollision(this.m_Collider, this.m_ThrowStartColliders[i], true);
}
}
The first call, ThrowItem
, is implemented by ServerAttachmentThrower
:
public void ThrowItem(GameObject _object, Vector2 _directionXZ)
{
IAttachment attachment = _object.RequestInterface<IAttachment>();
if (attachment != null)
{
Vector3 velocity = this.CalculateThrowVelocity(_directionXZ);
attachment.AccessMotion().SetVelocity(velocity);
ICatchable catchable = _object.RequestInterface<ICatchable>();
if (catchable != null)
{
this.AlertPotentialCatchers(catchable, attachment.AccessGameObject().transform.position, _directionXZ);
}
}
this.m_throwCallback(_object);
}
It sets a velocity on the attachment (which basically forwards the velocity to the Rigidbody container),
and then gets the ICatchable
side of the throwable and calls AlertPotentialCatchers
. The final m_throwCallback is always a no-op (nobody registers any logic on it).
private void AlertPotentialCatchers(ICatchable _object, Vector3 _position, Vector2 _directionXZ)
{
Vector3 direction = VectorUtils.FromXZ(_directionXZ, 0f);
int num = Physics.SphereCastNonAlloc(_position, 1f, direction, ServerAttachmentThrower.ms_raycastHits, 10f, ServerAttachmentThrower.m_playersLayerMask);
for (int i = 0; i < num; i++)
{
RaycastHit raycastHit = ServerAttachmentThrower.ms_raycastHits[i];
GameObject gameObject = raycastHit.collider.gameObject;
if (!(gameObject == base.gameObject))
{
IHandleCatch handleCatch = gameObject.RequestInterface<IHandleCatch>();
if (handleCatch != null)
{
handleCatch.AlertToThrownItem(_object, this, _directionXZ);
}
}
}
}
This queries the physics system for anything that intersects a radius-1 ray in the thrown direction. For any such
objects that isn’t the thrower itself, and implements IHandleCatch
, it calls AlertToThrownItem
.
Of the five implementations of IHandleCatch
, only one has non-empty logic: ServerAttachmentCatcher.AlertToThrownItem
, i.e. another potentially catching chef:
public void AlertToThrownItem(ICatchable _thrown, IThrower _thrower, Vector2 _directionXZ)
{
if (this.m_controls != null && this.m_controls.GetDirectlyUnderPlayerControl())
{
return;
}
if (this.m_trackedThrowable != null)
{
IThrowable throwable = this.m_trackedThrowable.RequireInterface<IThrowable>();
if (throwable.IsFlying())
{
return;
}
}
if (this.m_carrier.InspectCarriedItem() == null && this.m_controls.GetCurrentlyInteracting() == null)
{
this.m_trackedThrowable = _thrown.AccessGameObject();
this.SendTrackingData(this.m_trackedThrowable);
}
}
If the potential catching chef is under a player’s control (e.g. not in a cannon, not piloting a raft, not being the non-active chef of single player), then there’s nothing to do.
If the previously tracked throwable is still flying, there’s also nothing to do.
Otherwise, if the chef is not currently carrying something, and it’s not currently doing something (e.g. chopping), start tracking this throwable and sends the same to the client side that also sets its m_trackedThrowable
. This is then used in Update_Rotation
to automatically face the thrown object. In other words, AlertPotentialCatchers
is only for the purpose of non-active player rotation in single player.
Let’s come back to HandleThrow
. The next thing that happens is setting m_thrower
and m_previousThrower
:
m_thrower
is used for three purposes:ServerThrowableItem.OnCollisionEnter
.ServerAttachmentThrower.CanHandleCatch
to prevent catching an object thrown by the
same chef.ServerHeatedStation.HandleCatch
and ServerIngredientCatcher.HandleCatch
for
achievement tracking.m_previousThrower
is used for ServerRubbishBin
for achievement tracking only.Next, it lazily fetches IAttachment
for the item itself (only implemented by ServerPhysicalAttachment
),
and then registers a callback to call OnAttachChanged
which basically forcefully ends the flight if
someone else claims attachment.
Next, it sets the pickup referree of the item to itself, whose CanHandlePickup
function returns whether
the object is flying. (I doubt this would become important without some bad network latency, such as
someone picking up an object and before the message arrives, someone else already threw the object).
Next it sets m_flying
. This field is used in a few places:
CanHandlePickup
implementation.OnCollisionEnter
does nothing if the object is not flying.ServerAttachmentCatcher
’s throwable tracking to detect if the object is still flying.ServerAttachStation.CanHandleCatch
to only catch an object if it’s NOT flying (TODO:
this is cryptic).ServerCatchableItem.AllowCatch
to return false if the object is not flying or has only
been flying for a short time.ServerLimitedQuantityItem
as an invincibility condition if the object is
flying.Next, it sends a message that the object is flying. On the client side (ClientThrowableItem
), this
triggers StartFlight
:
private void StartFlight(IClientThrower _thrower)
{
this.m_isFlying = true;
this.m_thrower = _thrower;
if (this.m_pickupReferral != null)
{
this.m_pickupReferral.SetHandlePickupReferree(this);
}
if (this.m_throwableItem.m_throwParticle != null)
{
Transform parent = NetworkUtils.FindVisualRoot(base.gameObject);
GameObject gameObject = this.m_throwableItem.m_throwParticle.InstantiateOnParent(parent, true);
this.m_pfx = gameObject.GetComponent<ParticleSystem>();
}
}
m_isFlying
here is used for the similar handle pickup referree’s CanHandlePickup
, as well as
ClientPlayerControlsImpl_Default.OnThrowableCollision
, which applies knockback if an object is thrown
onto a chef. Let’s not go into that mess here.
After that is a similar handle pickup referree, and particle effects.
Going back to server’s HandleThrow
. Next is ResumeAndClearThrowStartCollisions()
:
private void ResumeAndClearThrowStartCollisions()
{
for (int i = 0; i < this.m_ignoredCollidersCount; i++)
{
Collider collider = this.m_ThrowStartColliders[i];
if (collider != null && collider.gameObject != null)
{
Physics.IgnoreCollision(this.m_Collider, collider, false);
this.m_ThrowStartColliders[i] = null;
}
}
this.m_ignoredCollidersCount = 0;
}
this is to clear the previously ignored collisions, if any somehow remains (because the same thing
is done in EndFlight
).
Finally, we check for any initially colliding items (within the “Attachments” layer) with the thrown item, and ignore collisions for all of them. This allows us to throw an object over another object (such as if there’s a strawberry on the counter right in front of us we can still throw another strawberry over it).
ServerThrowableItem.UpdateSynchronising
does this every frame:
public override void UpdateSynchronising()
{
float deltaTime = TimeManager.GetDeltaTime(base.gameObject);
if (this.IsFlying())
{
this.m_flightTimer += deltaTime;
if (this.m_flightTimer >= 0.1f)
{
IAttachment component = base.GetComponent<IAttachment>();
if (component.AccessMotion().GetVelocity().sqrMagnitude < 0.0225f)
{
this.EndFlight();
}
if (this.m_flightTimer >= 5f)
{
this.EndFlight();
}
this.ResumeAndClearThrowStartCollisions();
}
}
else
{
this.m_throwerTimer -= deltaTime;
if (this.m_throwerTimer <= 0f)
{
this.m_previousThrower = null;
}
}
}
This essentially says that:
ResumeAndClearThrowStartCollisions()
, i.e. stop ignoring the initial collisions.m_previousThrower
field times out after a timeout. This is unimportant; we’ve
seen earlier that this field is only used for tracking rubbish bin achievement.In addition, there’s the ServerThrowableItem.OnCollisionEnter
, which is a Unity lifecycle event when
the object collides with something:
private void OnCollisionEnter(Collision _collision)
{
if (!this.IsFlying())
{
return;
}
IThrower thrower = _collision.gameObject.RequestInterface<IThrower>();
if (thrower != null && thrower == this.m_thrower)
{
return;
}
Vector3 normal = _collision.contacts[0].normal;
float num = Vector3.Angle(normal, Vector3.up);
if (num <= 45f)
{
this.EndFlight();
this.m_landedCallback(_collision.gameObject);
}
}
ServerSplattable
(which is some
logic that destroys the throwable if someone throws it in a hazard, like fire); however, ServerSplattable
is
not attached to Splattable
objects (it’s missing in MultiplayerController
), so it’s never instantiated.Flight is ended by any of the following:
ServerThrowableItem.OnCollisionEnter
.ServerThrowableItem.UpdateSynchronising
.ServerThrowableItem.OnAttachChanged
.EndFlight()
is called in any of these cases, which effectively undoes all the steps of HandleThrow()
,
and additionally the m_previousThrower
and m_throwerTimer
is set. The former is used for achievement
tracking only, and the latter is just a timeout to set the former to null.
public void EndFlight()
{
if (this.m_pickupReferral != null && this.m_pickupReferral.GetHandlePickupReferree() == this)
{
this.m_pickupReferral.SetHandlePickupReferree(null);
}
if (this.m_attachment != null)
{
this.m_attachment.UnregisterAttachChangedCallback(this.m_OnAttachChanged);
}
this.m_previousThrower = this.m_thrower;
this.m_throwerTimer = this.m_throwableItem.m_throwerTimeout;
this.m_thrower = null;
this.m_flightTimer = 0f;
this.m_flying = false;
this.SendStateMessage(this.m_flying, null);
this.ResumeAndClearThrowStartCollisions();
}
Catching has three aspects: the catchable item, the catcher, and the catching processor.
On the catchable item side, there’s a single interface ICatchable
whose only implementation is ServerCatchableItem
.
(The counterpart, ClientCatchableItem
, is an empty class.)
It is a pretty simple class that contains only one important function (comments inlined):
public bool AllowCatch(IHandleCatch _catcher, Vector2 _directionXZ)
{
if (this.m_throwable == null)
{
// This is just defensive programming in case the object does not have
// a IThrowable component.
return false;
}
GameObject gameObject = (_catcher as MonoBehaviour).gameObject;
if (_catcher != null && gameObject == null)
{
// This looks like a very weird condition but Unity Objects have an
// internal reference that may be null, and the operator == is
// overloaded to compare equal to null if the internal reference is null.
// So it's possible for the _catcher (who is an ICatchable, not a
// UnityEngine.Object) to not be null but its gameObject's operator==
// to return null. This means the object is already destroyed.
//
// Still, this is bad programming because _catcher can't be null anyway
// or it would trigger an null reference exception above.
return false;
}
if (!(gameObject.RequestComponent<ServerAttachStation>() != null) && !(gameObject.RequestComponent<ServerIngredientContainer>() != null))
{
if (!this.m_throwable.IsFlying() || (this.m_throwable.IsFlying() && this.m_throwable.GetFlightTime() < 0.1f))
{
return false;
}
}
// The item is catchable (from the item's perspective) if:
// - The catcher is a ServerAttachStation; or
// - The catcher is a ServerIngredientContainer; or
// - The item has been flying for more than 0.1f and it's still flying.
return true;
}
On the catcher side the interface is IHandleCatch
. This generally queries AllowCatch
on the
catchable item first, so the catcher can be the sole decider.
There are five implementations of IHandleCatch
:
ServerAttachmentCatcher
: chefs. Note that teleportals don’t seem to use this component.ServerAttachStation
: countertops and anything that act like countertops, like chopping boards.ServerHeatedStation
: these are the heating stations in campfire levels where chopped firewood can be thrown in. The component is also used in Surf n’ Turf barbeque stations and even coal furnaces but those don’t catch anything.ServerIngredientCatcher
: containers that can catch ingredients, e.g. pans, mixers, pots.ServerPlayerAttachmentCarrier.BlockCatching
: this is a special implementation used to block catching when an
object is being held by a chef.Let’s go through these one by one:
ServerAttachmentCatcher
:
public bool CanHandleCatch(ICatchable _object, Vector2 _directionXZ)
{
if (!_object.AllowCatch(this, _directionXZ))
{
return false;
}
GameObject gameObject = _object.AccessGameObject();
if (this.m_carrier.InspectCarriedItem() != null)
{
// If chef is already holding something, don't catch.
return false;
}
IThrower thrower = base.gameObject.RequestInterface<IThrower>();
if (thrower != null)
{
IThrowable throwable = gameObject.RequireInterface<IThrowable>();
if (throwable.GetThrower() == thrower)
{
// The chef throwing the object can't catch it (e.g. smashing
// against a wall.)
return false;
}
}
// In game data, this is 1.8. Catching will not happen if the item is
// further away from the chef than 1.8 (1.5 * grid size).
float catchDistance = this.m_catcher.m_catchDistance;
if ((base.transform.position - gameObject.transform.position).sqrMagnitude > catchDistance * catchDistance)
{
return false;
}
// Finally, catching can only happen if the chef is facing less than 120 degrees
// from the direction the item is flying (horizontal directions only).
//
// Note that catching at 120 degrees may seem weird but can happen if someone throws
// from the side but slightly behind you, towards a spot in front of you. If the
// thrown object would hit you first, it would knock you to the side instead of
// triggering a catch.
IAttachment attachment = gameObject.RequireInterface<IAttachment>();
Vector2 from = attachment.AccessMotion().GetVelocity().XZ();
Vector2 a = base.transform.forward.XZ();
float num = Vector2.Angle(from, -a);
return num <= this.m_catcher.m_catchAngleMax;
}
public void HandleCatch(ICatchable _object, Vector2 _directionXZ)
{
GameObject @object = _object.AccessGameObject();
// Cause the chef to carry the item.
this.m_carrier.CarryItem(@object);
// Stop tracking the item (for chef rotation; see the throwing section).
this.m_trackedThrowable = null;
this.SendTrackingData(this.m_trackedThrowable);
}
ServerAttachStation
:
public bool CanHandleCatch(ICatchable _object, Vector2 _directionXZ)
{
if (!_object.AllowCatch(this, _directionXZ))
{
return false;
}
if (this.HasItem())
{
// If there's something on top of the AttachStation that can also catch, forward the call to that.
IHandleCatch handleCatch = this.m_item.AccessGameObject().RequestInterface<IHandleCatch>();
if (handleCatch != null)
{
return handleCatch.CanHandleCatch(_object, _directionXZ);
}
}
IThrowable throwable = _object.AccessGameObject().RequestInterface<IThrowable>();
// Conditions for catching:
// - Object is throwable (should always be true for catchable objects).
// - The object is NOT flying.
// - The station can catch things (this is a setting that the game devs can toggle).
// - The object would normally be attachable to the station (this is a complicated query, but
// can only be true if the station is empty to begin with. It handles things like, a frier
// cannot be attached to a normal heating station).
return throwable != null && !throwable.IsFlying() && this.m_station.m_canCatch && this.CanAttachToSelf(_object.AccessGameObject(), new PlacementContext(PlacementContext.Source.Game));
}
public void HandleCatch(ICatchable _object, Vector2 _directionXZ)
{
if (this.HasItem())
{
// Forward catching to the catcher on the station if there is one.
IHandleCatch handleCatch = this.m_item.AccessGameObject().RequestInterface<IHandleCatch>();
if (handleCatch != null)
{
handleCatch.HandleCatch(_object, _directionXZ);
}
}
else
{
// Otherwise just simply attach the object.
this.AttachObject(_object.AccessGameObject(), default(PlacementContext));
}
}
ServerHeatedStation
:
public bool CanHandleCatch(ICatchable _object, Vector2 _directionXZ)
{
if (!_object.AllowCatch(this, _directionXZ))
{
return false;
}
// Conditions for catching:
// - If this is a heat transfer item (i.e. firewood); OR
// - If this is normally catchable by the associated attach station.
IHeatTransferBehaviour heatTransferBehaviour = _object.AccessGameObject().RequestInterface<IHeatTransferBehaviour>();
return (heatTransferBehaviour != null && heatTransferBehaviour.CanTransferToContainer(this)) || (this.m_attachStation != null && this.m_attachStation.CanHandleCatch(_object, _directionXZ));
}
// This one is pretty straight forward.
public void HandleCatch(ICatchable _object, Vector2 _directionXZ)
{
GameObject gameObject = _object.AccessGameObject();
IHeatTransferBehaviour heatTransferBehaviour = gameObject.RequestInterface<IHeatTransferBehaviour>();
if (heatTransferBehaviour != null)
{
IThrowable throwable = gameObject.RequireInterface<IThrowable>();
IThrower thrower = throwable.GetThrower();
if (thrower != null)
{
this.BurnAchievement((thrower as MonoBehaviour).gameObject, gameObject);
}
ICarrier carrier = new ServerHeatedStation.HolderAdapter(gameObject);
heatTransferBehaviour.TransferToContainer(carrier, this);
this.SynchroniseItemAdded();
return;
}
if (this.m_attachStation != null && this.m_attachStation.CanHandleCatch(_object, _directionXZ))
{
this.m_attachStation.HandleCatch(_object, _directionXZ);
}
}
ServerIngredientCatcher
:
public bool CanHandleCatch(ICatchable _object, Vector2 _directionXZ)
{
if (!_object.AllowCatch(this, _directionXZ))
{
return false;
}
// If m_requireAttached is configured for this object, only catch if
// the container is on an attach station. (For example, mixer can't catch
// food if it's on the floor).
if (this.m_catcher.m_requireAttached)
{
if (this.m_attachment == null)
{
return false;
}
if (!this.m_attachment.IsAttached())
{
return false;
}
}
// This is a complicated query that determines whether the container can
// accept an ingredient of this type. For example you cannot throw onion into
// a pan.
if (!this.m_allowCatchingCallback(_object.AccessGameObject()))
{
return false;
}
// A further complicated query to make sure the item can be transferred to the container.
IOrderDefinition orderDefinition = _object.AccessGameObject().RequestInterface<IOrderDefinition>();
if (orderDefinition != null && orderDefinition.GetOrderComposition().Simpilfy() != AssembledDefinitionNode.NullNode)
{
IContainerTransferBehaviour containerTransferBehaviour = _object.AccessGameObject().RequestInterface<IContainerTransferBehaviour>();
ServerIngredientContainer component = base.gameObject.GetComponent<ServerIngredientContainer>();
if (containerTransferBehaviour != null && component != null && containerTransferBehaviour.CanTransferToContainer(component))
{
return component.CanAddIngredient(orderDefinition.GetOrderComposition());
}
}
return false;
}
public void HandleCatch(ICatchable _object, Vector2 _directionXZ)
{
GameObject gameObject = _object.AccessGameObject();
IThrowable throwable = gameObject.RequireInterface<IThrowable>();
IThrower thrower = throwable.GetThrower();
if (thrower != null)
{
GameObject gameObject2 = (thrower as MonoBehaviour).gameObject;
ServerMessenger.Achievement(gameObject2, 12, 1);
}
IOrderDefinition orderDefinition = gameObject.RequireInterface<IOrderDefinition>();
if (orderDefinition.GetOrderComposition().Simpilfy() != AssembledDefinitionNode.NullNode)
{
IContainerTransferBehaviour containerTransferBehaviour = gameObject.RequireInterface<IContainerTransferBehaviour>();
ServerIngredientContainer component = base.gameObject.GetComponent<ServerIngredientContainer>();
if (containerTransferBehaviour != null && component != null && containerTransferBehaviour.CanTransferToContainer(component))
{
// Have the item decide how to transfer to the container.
containerTransferBehaviour.TransferToContainer(null, component, true);
// Send clients a message about the change of contents.
component.InformOfInternalChange();
// Destroy the thrown item.
gameObject.SetActive(false);
NetworkUtils.DestroyObject(gameObject);
}
}
}
Finally, ServerPlayerAttachmentCarrier.BlockCatching.CanHandleCatch
just returns false.
Catchable items and catchers both do not actually trigger any catching. They only decide whether something can be caught and what to do when catching. There appears to be two ways that the catching action is actually triggered:
ServerAttachmentCatchingProxy
. It is automatically added to objects with a HeatedStation
, AttachStation
, or
IngredientCatcher
component (see MultiplayerController.AsyncScanEntities
).private void AttemptToCatch(GameObject _object)
{
ICatchable catchable = _object.RequestInterface<ICatchable>();
if (catchable != null)
{
IHandleCatch controllingCatchingHandler = this.GetControllingCatchingHandler();
if (controllingCatchingHandler != null)
{
Vector2 directionXZ = (_object.transform.position - base.transform.position).SafeNormalised(base.transform.forward).XZ();
if (controllingCatchingHandler.CanHandleCatch(catchable, directionXZ))
{
controllingCatchingHandler.HandleCatch(catchable, directionXZ);
}
}
}
else
{
Vector2 param = (_object.transform.position - base.transform.position).SafeNormalised(base.transform.forward).XZ();
if (this.m_uncatchableItemCallback != null)
{
// This is only registered by one party: ServerAttachStation.HandleUncatchableItem. It's used to
// "catch" stuff like plates that happen to collide with the station.
this.m_uncatchableItemCallback(_object, param);
}
}
}
public IHandleCatch GetControllingCatchingHandler()
{
// If there's a referree then return it.
if (this.m_iHandleCatchReferree != null)
{
return this.m_iHandleCatchReferree;
}
// Otherwise return the IHandleCatch with the highest priority, if there are multiple
// of these on the same GameObject.
// I think this is only needed between ServerAttachStation and ServerHeatedStation,
// where the latter returns a int.MaxValue.
IHandleCatch handleCatch = null;
for (int i = 0; i < this.m_iHandleCatches.Length; i++)
{
IHandleCatch handleCatch2 = this.m_iHandleCatches[i];
if ((!(handleCatch2 is MonoBehaviour) || (handleCatch2 as MonoBehaviour).enabled) && (handleCatch == null || handleCatch2.GetCatchingPriority() > handleCatch.GetCatchingPriority()))
{
handleCatch = handleCatch2;
}
}
return handleCatch;
}
m_iHandleCatchReferree
is set in two cases:ServerAttachStation
sets the referree on the item to the station, if the item has a ServerAttachmentCatchingProxy
.
In other words, if we place a mixer bowl on a counter, and the mixer bowl detects a collision, we force the handling to
be done by the station and not the mixer bowl.ServerAttachmentCatchingProxy
, the referree is set to ServerPlayerAttachmentCarrier.BlockCatching
,
which prevents the carried item from catching anything.ServerPlayerControlsImpl_Default
implements the chef catching behavior:private void Update_Catch(float _deltaTime)
{
if (this.m_controls.m_bRespawning)
{
return;
}
// See below.
ICatchable catchable = this.m_controls.ScanForCatch();
if (catchable != null)
{
MonoBehaviour monoBehaviour = (MonoBehaviour)catchable;
if (monoBehaviour == null || monoBehaviour.gameObject == null)
{
return;
}
Vector2 normalized = this.m_controlObject.transform.forward.XZ().normalized;
if (this.m_iCatcher.CanHandleCatch(catchable, normalized))
{
this.m_iCatcher.HandleCatch(catchable, normalized);
EntitySerialisationEntry entry = EntitySerialisationRegistry.GetEntry(monoBehaviour.gameObject);
// This only triggers some audio and pfx on the client side. The actual attachment behavior
// is already sent over in the CarryItem call inside HandleCatch.
this.SendServerEvent(new InputEventMessage(InputEventMessage.InputEventType.Catch)
{
entityId = entry.m_Header.m_uEntityID
});
}
}
}
public ICatchable ScanForCatch()
{
// GetCollidersInArc is a complex function but when the last argument is false, it scans for all objects whose
// closest point lies in an arc in front of the chef. It returns position + 0.5 * forward * detectionRadius, so
// 0.5 in front of the chef.
// The 2f here is the distance, and 1.57f here is pi/2 so anywhere in front of the chef.
Vector3 collidersInArc = InteractWithItemHelper.GetCollidersInArc(2f, 1.5707964f, this.m_Transform, this.m_colliders, this.m_catchMask, false);
// This then gets whatever ServerCatchableItem we can find from these colliders, prioritizing the closest one to
// that chef + 0.5 point.
return InteractWithItemHelper.ScanForComponent<ServerCatchableItem>(this.m_colliders, collidersInArc, this.m_CatchCondition);
}
States we need to warp:
m_flying
(if changing, perform flight start/end actions)m_flightTimer
m_thrower
m_ThrowStartColliders
(if changing, perform physics collision changes)Things to do when warping from non-flight to flight:
Relevant classes:
ServerStack
ServerPlateStackBase
ServerCleanPlateStack
ServerDirtyPlateStack
and their client counterparts
ServerStack
does not synchronize anything using messages; it only provides some functions
to add or remove elements from the stack:
// ServerStack::AddToStack
public void AddToStack(GameObject _item)
{
if (this.m_stackItems.Contains(_item))
{
return;
}
// IAttachment's only implementation is ServerPhysicalAttachment.
// Clean plates are PhysicalAttachment's. Dirty plates aren't.
// PhysicalAttachment objects have more things to do when attaching/detaching.
IAttachment attachment = _item.RequestInterface<IAttachment>();
if (attachment != null)
{
attachment.Attach(this.m_stack);
}
else
{
_item.transform.SetParent(this.m_stack.GetAttachPoint(_item));
}
this.m_stackItems.Add(_item);
// Properly positions and aligns each item in the stack.
this.m_stack.RefreshStackTransforms(ref this.m_stackItems);
// Properly calculates the height of the colliders for the stack.
this.m_stack.RefreshStackCollider(ref this.m_boxCollider, this.m_stackItems.Count);
}
// ServerStack::RemoveFromStack - does the reverse, and recalculates transforms and collider.
public GameObject RemoveFromStack()
{
if (this.m_stackItems.Count > 0)
{
GameObject gameObject = this.m_stackItems[this.m_stackItems.Count - 1];
this.m_stackItems.Remove(gameObject);
IAttachment attachment = gameObject.RequestInterface<IAttachment>();
if (attachment != null)
{
attachment.Detach();
}
else
{
gameObject.transform.SetParent(null);
}
this.m_stack.RefreshStackTransforms(ref this.m_stackItems);
this.m_stack.RefreshStackCollider(ref this.m_boxCollider, this.m_stackItems.Count);
return gameObject;
}
return null;
}
On the client side, the things to do are very similar:
// ServerStack::AddToStack
public void AddToStack(GameObject _item)
{
if (this.m_stackItems.Contains(_item))
{
return;
}
if (_item.RequestInterface<IClientAttachment>() == null)
{
_item.transform.SetParent(this.m_stack.GetAttachPoint(_item));
}
this.m_stackItems.Add(_item);
this.m_stack.RefreshStackTransforms(ref this.m_stackItems);
this.m_stack.RefreshStackCollider(ref this.m_boxCollider, this.m_stackItems.Count);
}
// ServerStack::RemoveFromStack
public GameObject RemoveFromStack()
{
if (this.m_stackItems.Count > 0)
{
GameObject gameObject = this.m_stackItems[this.m_stackItems.Count - 1];
this.m_stackItems.Remove(gameObject);
if (gameObject.RequestInterface<IClientAttachment>() == null)
{
gameObject.transform.SetParent(null);
}
this.m_stack.RefreshStackTransforms(ref this.m_stackItems);
this.m_stack.RefreshStackCollider(ref this.m_boxCollider, this.m_stackItems.Count);
return gameObject;
}
return null;
}
Note that AddToStack
is NOT called by some server message propagated from ServerStack
.
We’ll later cover what calls ServerStack
/ClientStack
.
The functions on the client side fills in the non-synchronizing case where the object is
not a PhysicalAttachment
(which would automatically synchronize the attachment on the
client side), as well as ensuring that the transforms/collider are refreshed.
Now we turn to the other component: ServerPlateStackBase
, which is a separate component not
directly related to Stack.
In the StartSynchronising
function we add a spawnable prefab for
the element in the stack:
// ServerPlateStackBase::StartSynchronising
public override void StartSynchronising(Component synchronisedObject)
{
base.StartSynchronising(synchronisedObject);
this.m_plateStack = (PlateStackBase)synchronisedObject;
this.m_stack = base.gameObject.RequireComponent<ServerStack>();
NetworkUtils.RegisterSpawnablePrefab(base.gameObject, this.m_plateStack.m_platePrefab);
}
Then the add/remove functions (AddToStack is triggered externally):
// ServerPlateStackBase::AddToStack
public virtual void AddToStack()
{
// Adding to stack always involves spawning a prefab for the inner element.
GameObject item = NetworkUtils.ServerSpawnPrefab(base.gameObject, this.m_plateStack.m_platePrefab);
this.m_stack.AddToStack(item);
}
// ServerPlateStackBase::RemoveFromStack (this is virtual, overridden by ServerCleanPlateStack)
protected virtual GameObject RemoveFromStack()
{
GameObject result = this.m_stack.RemoveFromStack();
// This is an empty event message; the message is only used for removal from the stack.
this.SendServerEvent(this.m_data);
return result;
}
On the client side, adding to a plate is triggered by a spawning callback:
// ClientPlateStackBase::StartSynchronising
public override void StartSynchronising(Component synchronisedObject)
{
base.StartSynchronising(synchronisedObject);
this.m_plateStack = (PlateStackBase)synchronisedObject;
this.m_stack = base.gameObject.RequireComponent<ClientStack>();
// This callback is called when a prefab is spawned; the same does NOT happen on the server side.
NetworkUtils.RegisterSpawnablePrefab(base.gameObject, this.m_plateStack.m_platePrefab, new VoidGeneric<GameObject>(this.PlateSpawned));
}
// ClientPlateStackBase::ApplyServerEvent
public override void ApplyServerEvent(Serialisable serialisable)
{
this.PlateRemoved();
}
// ClientPlateStackBase::PlateSpawned (overridden later)
protected virtual void PlateSpawned(GameObject _object)
{
this.m_plateAdded(_object); // cosmetics
}
// ClientPlateStackBase::PlateRemoved (overridden later)
protected virtual void PlateRemoved()
{
this.m_plateRemoved(null); // cosmetics
}
ServerDirtyPlateStack
registers two additional spawnable prefabs: a clean plate stack and a clean
single plate, because washing with the water jet can turn the stack directly into a clean stack or a
single plate.
Placing a dirty stack on top of another dirty stack triggers HandlePlacement
:
// ServerDirtyPlateStack::HandlePlacement
public void HandlePlacement(ICarrier _carrier, Vector2 _directionXZ, PlacementContext _context)
{
GameObject obj = _carrier.InspectCarriedItem();
ServerStack serverStack = obj.RequireComponent<ServerStack>();
for (int i = 0; i < serverStack.GetSize(); i++)
{
this.AddToStack();
}
_carrier.DestroyCarriedItem();
}
this just adds N new plates to the stack and destroys the original.
AddToStack
is overridden to update washable properties. There’s also OnWashingFinished
which
I’ll skip here because it’s complicated and mostly deals with different cases of respawning the
clean plate stack or single clean plate in the same place (chef’s hands or a counter).
// ServerDirtyPlateStack::AddToStack
public override void AddToStack()
{
base.AddToStack();
if (this.m_washable != null)
{
Washable washable = base.gameObject.RequireComponent<Washable>();
this.m_washable.SetDuration((float)(washable.m_duration * this.m_stack.GetSize()));
}
}
The other place where ServerDirtyPlateStack::AddToStack
is called is in ServerPlateReturnStation
,
when a dirty plate is supposed to be returned after a plate was delivered for 7 seconds. (Note that
the same logic is also used to respawn clean plate stacks.)
// ServerPlateReturnStation::ReturnPlate
public void ReturnPlate()
{
if (this.m_stack == null)
{
this.CreateStack();
}
this.m_stack.AddToStack();
}
There isn’t a lot of useful things happening on ClientDirtyPlateStack
.
// ServerCleanPlateStack::AddToStack
public override void AddToStack()
{
bool flag = false;
if (this.m_stack.InspectTopOfStack() != null)
{
IIngredientContents ingredientContents = this.m_stack.InspectTopOfStack().RequestInterface<IIngredientContents>();
flag = (ingredientContents != null && ingredientContents.HasContents());
}
if (flag)
{
// If the top plate has stuff on it, remove it first, add a new plate, then add it back.
GameObject item = this.m_stack.RemoveFromStack();
this.AddToBaseStack();
this.m_stack.AddToStack(item);
}
else
{
// Otherwise just add one.
this.AddToBaseStack();
}
// For the plate that was just added, set the pickup and placement referral to the stack.
// (Isn't this wrong if the plate we added was the second item from the top??)
// (But then maybe it doesn't matter - when would the plate actually be queried first?)
GameObject obj = this.m_stack.InspectTopOfStack();
ServerHandlePickupReferral serverHandlePickupReferral = obj.RequestComponent<ServerHandlePickupReferral>();
if (serverHandlePickupReferral != null)
{
serverHandlePickupReferral.SetHandlePickupReferree(this);
}
ServerHandlePlacementReferral serverHandlePlacementReferral = obj.RequestComponent<ServerHandlePlacementReferral>();
if (serverHandlePlacementReferral != null)
{
serverHandlePlacementReferral.SetHandlePlacementReferree(this);
}
}
// ServerCleanPlateStack::AddToBaseStack
private void AddToBaseStack()
{
// The base class's function wich spawns the plate and adds it to the stack.
base.AddToStack();
GameObject obj = this.m_stack.InspectTopOfStack();
// This is for if the spawned plate would be destroyed, respawn it where the stack would respawn.
ServerUtensilRespawnBehaviour serverUtensilRespawnBehaviour = base.gameObject.RequestComponent<ServerUtensilRespawnBehaviour>();
ServerUtensilRespawnBehaviour serverUtensilRespawnBehaviour2 = obj.RequestComponent<ServerUtensilRespawnBehaviour>();
if (serverUtensilRespawnBehaviour != null && serverUtensilRespawnBehaviour2 != null)
{
serverUtensilRespawnBehaviour2.SetIdealRespawnLocation(serverUtensilRespawnBehaviour.GetIdealRespawnLocation());
}
}
// ServerCleanPlateStack::RemoveFromStack
protected override GameObject RemoveFromStack()
{
GameObject gameObject = base.RemoveFromStack();
// Reverses the referree settings.
ServerHandlePickupReferral serverHandlePickupReferral = gameObject.RequestComponent<ServerHandlePickupReferral>();
if (serverHandlePickupReferral != null && serverHandlePickupReferral.GetHandlePickupReferree() == this)
{
serverHandlePickupReferral.SetHandlePickupReferree(null);
}
ServerHandlePlacementReferral serverHandlePlacementReferral = gameObject.RequestComponent<ServerHandlePlacementReferral>();
if (serverHandlePlacementReferral != null && serverHandlePlacementReferral.GetHandlePlacementReferree() == this)
{
serverHandlePlacementReferral.SetHandlePlacementReferree(null);
}
return gameObject;
}
Now the client side… is a bit strange.
// ClientCleanPlateStack::PlateSpawned
protected override void PlateSpawned(GameObject _object)
{
bool flag = false;
if (this.m_stack.InspectTopOfStack() != null)
{
IIngredientContents ingredientContents = this.m_stack.InspectTopOfStack().RequestInterface<IIngredientContents>();
flag = (ingredientContents != null && ingredientContents.HasContents());
}
if (flag)
{
GameObject item = this.m_stack.RemoveFromStack();
this.m_stack.AddToStack(_object);
this.m_stack.AddToStack(item);
}
else
{
this.m_stack.AddToStack(_object);
}
this.m_delayedSetupPlates.Add(_object);
base.NotifyPlateAdded(_object);
}
// ClientCleanPlateStack::DelayedPlateSetup
private void DelayedPlateSetup(GameObject _plate)
{
if (_plate != null && _plate.gameObject != null)
{
ClientHandlePickupReferral clientHandlePickupReferral = _plate.RequestComponent<ClientHandlePickupReferral>();
if (clientHandlePickupReferral != null)
{
clientHandlePickupReferral.SetHandlePickupReferree(this);
}
ClientHandlePlacementReferral clientHandlePlacementReferral = _plate.RequestComponent<ClientHandlePlacementReferral>();
if (clientHandlePlacementReferral != null)
{
clientHandlePlacementReferral.SetHandlePlacementReferree(this);
}
}
}
// ClientCleanPlateStack::PlateRemoved
protected override void PlateRemoved()
{
GameObject gameObject = this.m_stack.RemoveFromStack();
ClientHandlePickupReferral clientHandlePickupReferral = gameObject.RequestComponent<ClientHandlePickupReferral>();
if (clientHandlePickupReferral != null && clientHandlePickupReferral.GetHandlePickupReferree() == this)
{
clientHandlePickupReferral.SetHandlePickupReferree(null);
}
ClientHandlePlacementReferral clientHandlePlacementReferral = gameObject.RequestComponent<ClientHandlePlacementReferral>();
if (clientHandlePlacementReferral != null && clientHandlePlacementReferral.GetHandlePlacementReferree() == this)
{
clientHandlePlacementReferral.SetHandlePlacementReferree(null);
}
base.NotifyPlateRemoved(gameObject);
}
// ClientCleanPlateStack::UpdateSynchronising
public override void UpdateSynchronising()
{
for (int i = this.m_delayedSetupPlates.Count - 1; i >= 0; i--)
{
this.DelayedPlateSetup(this.m_delayedSetupPlates[i]);
this.m_delayedSetupPlates.RemoveAt(i);
}
}
So… no surprises except that the setting of the referrees on the client side is delayed by 1 frame. Why??
ClientCannon
- handles most animations, e.g. the coroutine for the main flightServerCannon
- ?IClientCannonHandler
- handler that can be used to launch different kinds of objects.
Currently there’s only one implementation - ClientCannonPlayerHandler
.LaunchProjectile
is the main entry point to the coroutine. This is in response to
receiving a server event to launch the projectile.
private IEnumerator LaunchProjectile(GameObject _objectToLaunch, Transform _target)
{
IClientCannonHandler handler = this.GetHandler(_objectToLaunch);
if (handler != null)
{
handler.Launch(_objectToLaunch);
}
_objectToLaunch.transform.SetParent(null, true);
if (this.m_onLaunchedCallback != null)
{
this.m_onLaunchedCallback(_objectToLaunch);
}
IEnumerator animation = this.m_cannon.m_animation.Run(_objectToLaunch, _target);
while (animation.MoveNext())
{
yield return null;
}
if (handler != null)
{
handler.Land(_objectToLaunch);
}
this.m_cannon.EndCannonRoutine(_objectToLaunch);
if (handler != null)
{
IEnumerator exit = handler.ExitCannonRoutine(_objectToLaunch, _target.position, _target.rotation);
while (exit.MoveNext())
{
yield return null;
}
}
yield break;
}
Call handler.Launch
. This sets up the player - disabling controls, setting kinematic,
disabling collider. See ClientCannonPlayerHandler.Launch
:
public void Launch(GameObject _obj)
{
if (_obj != null)
{
this.m_inCannon = false;
this.m_controls.enabled = false;
this.m_controls.Motion.SetKinematic(true);
Collider collider = _obj.RequireComponent<Collider>();
collider.enabled = false;
}
}
Detach the transform of the object being launched from the cannon.
Call the m_onLaunchedCallback
if exists. This callback is only registered by
ClientCannonCosmeticDecisions
, so it’s cosmetics only (animations, particles,
audio).
Start the ProjectileAnimation
animation coroutine. See ProjectileAnimation::Run
;
this manually animates the projectile by positioning it according to a parabola.
Call the handler.Land
function; see ClientCannonPlayerHandler.Land
:
public void Land(GameObject _obj)
{
if (_obj != null)
{
this.m_controls.enabled = true;
if (this.m_controls.GetComponent<PlayerIDProvider>().IsLocallyControlled())
{
this.m_controls.Motion.SetKinematic(false);
}
Collider collider = _obj.RequireComponent<Collider>();
collider.enabled = true;
this.m_controls.GetComponent<ClientPlayerControlsImpl_Default>().ApplyImpact(this.m_controls.transform.forward.XZ() * 2f, 0.2f);
}
}
Call m_cannon.EndCannonRoutine
(note this is on the Cannon
, not ClientCannon
). This is registered by only ServerCannon
to call ServerCannon.EndCannonRoutine
:
public void EndCannonRoutine(GameObject _obj)
{
this.m_flying = false;
IServerCannonHandler handler = this.GetHandler(_obj);
if (handler != null)
{
handler.ExitCannonRoutine(_obj);
}
}
This handler is the IServerCannonHandler
, which also has only one implementation: ServerCannonPlayerHandler.ExitCannonRoutine
:
public void ExitCannonRoutine(GameObject _obj)
{
_obj.GetComponent<Rigidbody>().isKinematic = false;
GroundCast groundCast = _obj.RequestComponent<GroundCast>();
if (groundCast != null)
{
groundCast.ForceUpdateNow();
}
ServerWorldObjectSynchroniser serverWorldObjectSynchroniser = _obj.RequestComponent<ServerWorldObjectSynchroniser>();
if (serverWorldObjectSynchroniser != null)
{
serverWorldObjectSynchroniser.ResumeAllClients(false);
}
}
Start the coroutine from handler.ExitCannonRoutine
; see ClientCannonPlayerHandler.ExitCannonRoutine
:
public IEnumerator ExitCannonRoutine(GameObject _player, Vector3 _exitPosition, Quaternion _exitRotation)
{
this.m_inCannon = false;
this.m_controls.AllowSwitchingWhenDisabled = false;
this.m_controls = null;
this.m_playerIdProvider = null;
if (_player != null)
{
DynamicLandscapeParenting dynamicParenting = _player.RequestComponent<DynamicLandscapeParenting>();
if (dynamicParenting != null)
{
dynamicParenting.enabled = true;
}
yield return null;
}
if (_player != null)
{
ClientWorldObjectSynchroniser synchroniser = _player.RequireComponent<ClientWorldObjectSynchroniser>();
while (synchroniser != null && !synchroniser.IsReadyToResume())
{
yield return null;
}
if (synchroniser != null)
{
synchroniser.Resume();
}
}
yield break;
}
Even though this is a coroutine, it’s really only necessary to resume the ClientWorldObjectSynchroniser
. The
first yield appears unnecessary.
What is GroundCast
?
ForceUpdateNow()
. Also, the client part doesn’t call this, so maybe it’s not necessary here?What is ServerWorldObjectSynchroniser
and ClientWorldObjectSynchroniser
?
What is DynamicLandscapeParenting
?
ServerCannon
’s Load doesn’t really do anything except sending a message. We begin with ClientCannon.Load
.
public void Load(GameObject _obj)
{
this.m_loadedObject = _obj;
this.m_exitPosition = _obj.transform.position;
this.m_exitRotation = _obj.transform.rotation;
IClientCannonHandler handler = this.GetHandler(_obj);
if (handler != null)
{
handler.Load(_obj);
}
_obj.transform.position = this.m_cannon.m_attachPoint.position;
_obj.transform.rotation = this.m_cannon.m_attachPoint.rotation;
_obj.transform.SetParent(this.m_cannon.m_attachPoint, true);
if (this.m_onLoadedCallback != null)
{
this.m_onLoadedCallback(_obj);
}
}
Pretty simple here - sets all the transforms, parents, and calls ClientCannonPlayerHandler.Load
:
public void Load(GameObject _player)
{
this.m_controls = _player.RequireComponent<PlayerControls>();
this.m_controls.AllowSwitchingWhenDisabled = true;
this.m_controls.ThrowIndicator.Hide();
this.m_playerIdProvider = _player.RequireComponent<PlayerIDProvider>();
this.m_inCannon = true;
DynamicLandscapeParenting dynamicLandscapeParenting = _player.RequestComponent<DynamicLandscapeParenting>();
if (dynamicLandscapeParenting != null)
{
dynamicLandscapeParenting.enabled = false;
}
ClientWorldObjectSynchroniser clientWorldObjectSynchroniser = _player.RequestComponent<ClientWorldObjectSynchroniser>();
if (clientWorldObjectSynchroniser != null)
{
clientWorldObjectSynchroniser.Pause();
}
}
It’s mostly the inverse of the end of the launch process.
ClientCannon.Unload
:
public IEnumerator Unload(GameObject _obj, Vector3 _exitPosition, Quaternion _exitRotation)
{
if (_obj != null)
{
_obj.transform.position = _exitPosition;
_obj.transform.rotation = _exitRotation;
_obj.transform.SetParent(null, true);
}
this.m_cannon.EndCannonRoutine(_obj);
IClientCannonHandler handler = this.GetHandler(_obj);
if (handler != null)
{
IEnumerator exit = handler.ExitCannonRoutine(_obj, _exitPosition, _exitRotation);
while (exit.MoveNext())
{
yield return null;
}
}
if (this.m_onUnloadedCallback != null)
{
this.m_onUnloadedCallback(_obj);
}
yield break;
}
Basically the same as the launch process except there’s no launching.