1
0
mirror of https://github.com/tobspr/shapez.io.git synced 2026-03-02 03:39:21 +00:00

Refactor item processor to use charges and thus be more correct, even at low tick rates

This commit is contained in:
tobspr
2020-08-30 15:31:53 +02:00
parent 091401e52b
commit 9b8745535b
20 changed files with 2105 additions and 1975 deletions

View File

@@ -104,8 +104,12 @@ export default {
// Renders information about wire networks
// renderWireNetworkInfos: true,
// -----------------------------------------------------------------------------------
// Disables ejector animations and processing
// disableEjectorProcessing: true,
// Disables ejector animations and processing
// disableEjectorProcessing: true,
// -----------------------------------------------------------------------------------
// Allows manual ticking
// manualTickOnly: true,
// -----------------------------------------------------------------------------------
/* dev:end */
};

File diff suppressed because it is too large Load Diff

View File

@@ -1,144 +1,150 @@
import { enumDirection, enumInvertedDirections, Vector } from "../../core/vector";
import { types } from "../../savegame/serialization";
import { BaseItem } from "../base_item";
import { Component } from "../component";
/** @typedef {{
* pos: Vector,
* directions: enumDirection[],
* filter?: ItemType
* }} ItemAcceptorSlot */
/**
* Contains information about a slot plus its location
* @typedef {{
* slot: ItemAcceptorSlot,
* index: number,
* acceptedDirection: enumDirection
* }} ItemAcceptorLocatedSlot */
/** @typedef {{
* pos: Vector,
* directions: enumDirection[],
* filter?: ItemType
* }} ItemAcceptorSlotConfig */
export class ItemAcceptorComponent extends Component {
static getId() {
return "ItemAcceptor";
}
duplicateWithoutContents() {
const slotsCopy = [];
for (let i = 0; i < this.slots.length; ++i) {
const slot = this.slots[i];
slotsCopy.push({
pos: slot.pos.copy(),
directions: slot.directions.slice(),
filter: slot.filter,
});
}
return new ItemAcceptorComponent({
slots: slotsCopy,
});
}
/**
*
* @param {object} param0
* @param {Array<ItemAcceptorSlotConfig>} param0.slots The slots from which we accept items
*/
constructor({ slots = [] }) {
super();
/**
* Fixes belt animations
* @type {Array<{ item: BaseItem, slotIndex: number, animProgress: number, direction: enumDirection }>}
*/
this.itemConsumptionAnimations = [];
this.setSlots(slots);
}
/**
*
* @param {Array<ItemAcceptorSlotConfig>} slots
*/
setSlots(slots) {
/** @type {Array<ItemAcceptorSlot>} */
this.slots = [];
for (let i = 0; i < slots.length; ++i) {
const slot = slots[i];
this.slots.push({
pos: slot.pos,
directions: slot.directions,
// Which type of item to accept (shape | color | all) @see ItemType
filter: slot.filter,
});
}
}
/**
* Returns if this acceptor can accept a new item at slot N
* @param {number} slotIndex
* @param {BaseItem=} item
*/
canAcceptItem(slotIndex, item) {
const slot = this.slots[slotIndex];
return !slot.filter || slot.filter === item.getItemType();
}
/**
* Called when an item has been accepted so that
* @param {number} slotIndex
* @param {enumDirection} direction
* @param {BaseItem} item
*/
onItemAccepted(slotIndex, direction, item) {
this.itemConsumptionAnimations.push({
item,
slotIndex,
direction,
animProgress: 0.0,
});
}
/**
* Tries to find a slot which accepts the current item
* @param {Vector} targetLocalTile
* @param {enumDirection} fromLocalDirection
* @returns {ItemAcceptorLocatedSlot|null}
*/
findMatchingSlot(targetLocalTile, fromLocalDirection) {
// We need to invert our direction since the acceptor specifies *from* which direction
// it accepts items, but the ejector specifies *into* which direction it ejects items.
// E.g.: Ejector ejects into "right" direction but acceptor accepts from "left" direction.
const desiredDirection = enumInvertedDirections[fromLocalDirection];
// Go over all slots and try to find a target slot
for (let slotIndex = 0; slotIndex < this.slots.length; ++slotIndex) {
const slot = this.slots[slotIndex];
// Make sure the acceptor slot is on the right position
if (!slot.pos.equals(targetLocalTile)) {
continue;
}
// Check if the acceptor slot accepts items from our direction
for (let i = 0; i < slot.directions.length; ++i) {
// const localDirection = targetStaticComp.localDirectionToWorld(slot.directions[l]);
if (desiredDirection === slot.directions[i]) {
return {
slot,
index: slotIndex,
acceptedDirection: desiredDirection,
};
}
}
}
return null;
}
}
import { enumDirection, enumInvertedDirections, Vector } from "../../core/vector";
import { types } from "../../savegame/serialization";
import { BaseItem } from "../base_item";
import { Component } from "../component";
/** @typedef {{
* pos: Vector,
* directions: enumDirection[],
* filter?: ItemType
* }} ItemAcceptorSlot */
/**
* Contains information about a slot plus its location
* @typedef {{
* slot: ItemAcceptorSlot,
* index: number,
* acceptedDirection: enumDirection
* }} ItemAcceptorLocatedSlot */
/** @typedef {{
* pos: Vector,
* directions: enumDirection[],
* filter?: ItemType
* }} ItemAcceptorSlotConfig */
export class ItemAcceptorComponent extends Component {
static getId() {
return "ItemAcceptor";
}
duplicateWithoutContents() {
const slotsCopy = [];
for (let i = 0; i < this.slots.length; ++i) {
const slot = this.slots[i];
slotsCopy.push({
pos: slot.pos.copy(),
directions: slot.directions.slice(),
filter: slot.filter,
});
}
return new ItemAcceptorComponent({
slots: slotsCopy,
});
}
/**
*
* @param {object} param0
* @param {Array<ItemAcceptorSlotConfig>} param0.slots The slots from which we accept items
*/
constructor({ slots = [] }) {
super();
/**
* Fixes belt animations
* @type {Array<{
* item: BaseItem,
* slotIndex: number,
* animProgress: number,
* direction: enumDirection
* }>}
*/
this.itemConsumptionAnimations = [];
this.setSlots(slots);
}
/**
*
* @param {Array<ItemAcceptorSlotConfig>} slots
*/
setSlots(slots) {
/** @type {Array<ItemAcceptorSlot>} */
this.slots = [];
for (let i = 0; i < slots.length; ++i) {
const slot = slots[i];
this.slots.push({
pos: slot.pos,
directions: slot.directions,
// Which type of item to accept (shape | color | all) @see ItemType
filter: slot.filter,
});
}
}
/**
* Returns if this acceptor can accept a new item at slot N
* @param {number} slotIndex
* @param {BaseItem=} item
*/
canAcceptItem(slotIndex, item) {
const slot = this.slots[slotIndex];
return !slot.filter || slot.filter === item.getItemType();
}
/**
* Called when an item has been accepted so that
* @param {number} slotIndex
* @param {enumDirection} direction
* @param {BaseItem} item
* @param {number} remainingProgress World space remaining progress, can be set to set the start position of the item
*/
onItemAccepted(slotIndex, direction, item, remainingProgress = 0.0) {
this.itemConsumptionAnimations.push({
item,
slotIndex,
direction,
animProgress: Math.min(1, remainingProgress * 2),
});
}
/**
* Tries to find a slot which accepts the current item
* @param {Vector} targetLocalTile
* @param {enumDirection} fromLocalDirection
* @returns {ItemAcceptorLocatedSlot|null}
*/
findMatchingSlot(targetLocalTile, fromLocalDirection) {
// We need to invert our direction since the acceptor specifies *from* which direction
// it accepts items, but the ejector specifies *into* which direction it ejects items.
// E.g.: Ejector ejects into "right" direction but acceptor accepts from "left" direction.
const desiredDirection = enumInvertedDirections[fromLocalDirection];
// Go over all slots and try to find a target slot
for (let slotIndex = 0; slotIndex < this.slots.length; ++slotIndex) {
const slot = this.slots[slotIndex];
// Make sure the acceptor slot is on the right position
if (!slot.pos.equals(targetLocalTile)) {
continue;
}
// Check if the acceptor slot accepts items from our direction
for (let i = 0; i < slot.directions.length; ++i) {
// const localDirection = targetStaticComp.localDirectionToWorld(slot.directions[l]);
if (desiredDirection === slot.directions[i]) {
return {
slot,
index: slotIndex,
acceptedDirection: desiredDirection,
};
}
}
}
return null;
}
}

View File

@@ -29,6 +29,17 @@ export const enumItemProcessorRequirements = {
filter: "filter",
};
/** @typedef {{
* item: BaseItem,
* requiredSlot?: number,
* preferredSlot?: number
* }} EjectorItemToEject */
/** @typedef {{
* remainingTime: number,
* items: Array<EjectorItemToEject>,
* }} EjectorCharge */
export class ItemProcessorComponent extends Component {
static getId() {
return "ItemProcessor";
@@ -37,20 +48,6 @@ export class ItemProcessorComponent extends Component {
static getSchema() {
return {
nextOutputSlot: types.uint,
inputSlots: types.array(
types.structured({
item: typeItemSingleton,
sourceSlot: types.uint,
})
),
itemsToEject: types.array(
types.structured({
item: typeItemSingleton,
requiredSlot: types.nullable(types.uint),
preferredSlot: types.nullable(types.uint),
})
),
secondsUntilEject: types.float,
};
}
@@ -101,21 +98,15 @@ export class ItemProcessorComponent extends Component {
* What we are currently processing, empty if we don't produce anything rn
* requiredSlot: Item *must* be ejected on this slot
* preferredSlot: Item *can* be ejected on this slot, but others are fine too if the one is not usable
* @type {Array<{item: BaseItem, requiredSlot?: number, preferredSlot?: number}>}
* @type {Array<EjectorCharge>}
*/
this.itemsToEject = [];
/**
* How long it takes until we are done with the current items
* @type {number}
*/
this.secondsUntilEject = 0;
this.ongoingCharges = [];
/**
* How much processing time we have left from the last tick
* @type {number}
*/
this.bonusFromLastTick = 0;
this.bonusTime = 0;
}
/**

View File

@@ -125,6 +125,24 @@ export class GameCore {
// @ts-ignore
window.globalRoot = root;
}
// @todo Find better place
if (G_IS_DEV && globalConfig.debug.manualTickOnly) {
this.root.gameState.inputReciever.keydown.add(key => {
if (key.keyCode === 84) {
// 'T'
// Extract current real time
this.root.time.updateRealtimeNow();
// Perform logic ticks
this.root.time.performTicks(this.root.dynamicTickrate.deltaMs, this.boundInternalTick);
// Update analytics
root.productionAnalytics.update();
}
});
}
}
/**
@@ -244,11 +262,13 @@ export class GameCore {
// Camera is always updated, no matter what
root.camera.update(deltaMs);
// Perform logic ticks
this.root.time.performTicks(deltaMs, this.boundInternalTick);
if (!(G_IS_DEV && globalConfig.debug.manualTickOnly)) {
// Perform logic ticks
this.root.time.performTicks(deltaMs, this.boundInternalTick);
// Update analytics
root.productionAnalytics.update();
// Update analytics
root.productionAnalytics.update();
}
// Update automatic save after everything finished
root.automaticSave.update();

View File

@@ -1,124 +1,125 @@
import { GameRoot } from "./root";
import { createLogger } from "../core/logging";
import { globalConfig } from "../core/config";
const logger = createLogger("dynamic_tickrate");
const fpsAccumulationTime = 1000;
export class DynamicTickrate {
/**
*
* @param {GameRoot} root
*/
constructor(root) {
this.root = root;
this.currentTickStart = null;
this.capturedTicks = [];
this.averageTickDuration = 0;
this.accumulatedFps = 0;
this.accumulatedFpsLastUpdate = 0;
this.averageFps = 60;
this.setTickRate(60);
if (G_IS_DEV && globalConfig.debug.renderForTrailer) {
this.setTickRate(300);
}
}
onFrameRendered() {
++this.accumulatedFps;
const now = performance.now();
const timeDuration = now - this.accumulatedFpsLastUpdate;
if (timeDuration > fpsAccumulationTime) {
const avgFps = (this.accumulatedFps / fpsAccumulationTime) * 1000;
this.averageFps = avgFps;
this.accumulatedFps = 0;
this.accumulatedFpsLastUpdate = now;
}
}
/**
* Sets the tick rate to N updates per second
* @param {number} rate
*/
setTickRate(rate) {
logger.log("Applying tick-rate of", rate);
this.currentTickRate = rate;
this.deltaMs = 1000.0 / this.currentTickRate;
this.deltaSeconds = 1.0 / this.currentTickRate;
}
/**
* Increases the tick rate marginally
*/
increaseTickRate() {
if (G_IS_DEV && globalConfig.debug.renderForTrailer) {
return;
}
const desiredFps = this.root.app.settings.getDesiredFps();
this.setTickRate(Math.round(Math.min(desiredFps, this.currentTickRate * 1.2)));
}
/**
* Decreases the tick rate marginally
*/
decreaseTickRate() {
if (G_IS_DEV && globalConfig.debug.renderForTrailer) {
return;
}
const desiredFps = this.root.app.settings.getDesiredFps();
this.setTickRate(Math.round(Math.max(desiredFps / 2, this.currentTickRate * 0.8)));
}
/**
* Call whenever a tick began
*/
beginTick() {
assert(this.currentTickStart === null, "BeginTick called twice");
this.currentTickStart = performance.now();
if (this.capturedTicks.length > this.currentTickRate * 2) {
// Take only a portion of the ticks
this.capturedTicks.sort();
this.capturedTicks.splice(0, 10);
this.capturedTicks.splice(this.capturedTicks.length - 11, 10);
let average = 0;
for (let i = 0; i < this.capturedTicks.length; ++i) {
average += this.capturedTicks[i];
}
average /= this.capturedTicks.length;
this.averageTickDuration = average;
const desiredFps = this.root.app.settings.getDesiredFps();
if (this.averageFps > desiredFps * 0.9) {
// if (average < maxTickDuration) {
this.increaseTickRate();
} else if (this.averageFps < desiredFps * 0.7) {
this.decreaseTickRate();
}
this.capturedTicks = [];
}
}
/**
* Call whenever a tick ended
*/
endTick() {
assert(this.currentTickStart !== null, "EndTick called without BeginTick");
const duration = performance.now() - this.currentTickStart;
this.capturedTicks.push(duration);
this.currentTickStart = null;
}
}
import { GameRoot } from "./root";
import { createLogger } from "../core/logging";
import { globalConfig } from "../core/config";
const logger = createLogger("dynamic_tickrate");
const fpsAccumulationTime = 1000;
export class DynamicTickrate {
/**
*
* @param {GameRoot} root
*/
constructor(root) {
this.root = root;
this.currentTickStart = null;
this.capturedTicks = [];
this.averageTickDuration = 0;
this.accumulatedFps = 0;
this.accumulatedFpsLastUpdate = 0;
this.averageFps = 60;
this.setTickRate(this.root.app.settings.getDesiredFps());
if (G_IS_DEV && globalConfig.debug.renderForTrailer) {
this.setTickRate(300);
}
}
onFrameRendered() {
++this.accumulatedFps;
const now = performance.now();
const timeDuration = now - this.accumulatedFpsLastUpdate;
if (timeDuration > fpsAccumulationTime) {
const avgFps = (this.accumulatedFps / fpsAccumulationTime) * 1000;
this.averageFps = avgFps;
this.accumulatedFps = 0;
this.accumulatedFpsLastUpdate = now;
}
}
/**
* Sets the tick rate to N updates per second
* @param {number} rate
*/
setTickRate(rate) {
logger.log("Applying tick-rate of", rate);
this.currentTickRate = rate;
this.deltaMs = 1000.0 / this.currentTickRate;
this.deltaSeconds = 1.0 / this.currentTickRate;
}
/**
* Increases the tick rate marginally
*/
increaseTickRate() {
if (G_IS_DEV && globalConfig.debug.renderForTrailer) {
return;
}
const desiredFps = this.root.app.settings.getDesiredFps();
this.setTickRate(Math.round(Math.min(desiredFps, this.currentTickRate * 1.2)));
}
/**
* Decreases the tick rate marginally
*/
decreaseTickRate() {
if (G_IS_DEV && globalConfig.debug.renderForTrailer) {
return;
}
const desiredFps = this.root.app.settings.getDesiredFps();
this.setTickRate(Math.round(Math.max(desiredFps / 2, this.currentTickRate * 0.8)));
}
/**
* Call whenever a tick began
*/
beginTick() {
assert(this.currentTickStart === null, "BeginTick called twice");
this.currentTickStart = performance.now();
if (this.capturedTicks.length > this.currentTickRate * 2) {
// Take only a portion of the ticks
this.capturedTicks.sort();
this.capturedTicks.splice(0, 10);
this.capturedTicks.splice(this.capturedTicks.length - 11, 10);
let average = 0;
for (let i = 0; i < this.capturedTicks.length; ++i) {
average += this.capturedTicks[i];
}
average /= this.capturedTicks.length;
this.averageTickDuration = average;
const desiredFps = this.root.app.settings.getDesiredFps();
// Disabled for now: Dynamicall adjusting tick rate
// if (this.averageFps > desiredFps * 0.9) {
// // if (average < maxTickDuration) {
// this.increaseTickRate();
// } else if (this.averageFps < desiredFps * 0.7) {
// this.decreaseTickRate();
// }
this.capturedTicks = [];
}
}
/**
* Call whenever a tick ended
*/
endTick() {
assert(this.currentTickStart !== null, "EndTick called without BeginTick");
const duration = performance.now() - this.currentTickStart;
this.capturedTicks.push(duration);
this.currentTickStart = null;
}
}

View File

@@ -110,6 +110,10 @@ export class GameSystemManager {
// Order is important!
// IMPORTANT: Item acceptor must be before the belt, because it may not tick after the belt
// has put in the item into the acceptor animation, otherwise its off
add("itemAcceptor", ItemAcceptorSystem);
add("belt", BeltSystem);
add("undergroundBelt", UndergroundBeltSystem);
@@ -134,11 +138,6 @@ export class GameSystemManager {
add("constantSignal", ConstantSignalSystem);
// IMPORTANT: Must be after belt system since belt system can change the
// orientation of an entity after it is placed -> the item acceptor cache
// then would be invalid
add("itemAcceptor", ItemAcceptorSystem);
// WIRES section
add("lever", LeverSystem);

View File

@@ -1,82 +1,80 @@
import { globalConfig } from "../../core/config";
import { DrawParameters } from "../../core/draw_parameters";
import { fastArrayDelete } from "../../core/utils";
import { enumDirectionToVector } from "../../core/vector";
import { ItemAcceptorComponent } from "../components/item_acceptor";
import { GameSystemWithFilter } from "../game_system_with_filter";
import { MapChunkView } from "../map_chunk_view";
export class ItemAcceptorSystem extends GameSystemWithFilter {
constructor(root) {
super(root, [ItemAcceptorComponent]);
}
update() {
const progress = this.root.dynamicTickrate.deltaSeconds * 2; // * 2 because its only a half tile
for (let i = 0; i < this.allEntities.length; ++i) {
const entity = this.allEntities[i];
const aceptorComp = entity.components.ItemAcceptor;
const animations = aceptorComp.itemConsumptionAnimations;
// Process item consumption animations to avoid items popping from the belts
for (let animIndex = 0; animIndex < animations.length; ++animIndex) {
const anim = animations[animIndex];
anim.animProgress +=
progress * this.root.hubGoals.getBeltBaseSpeed() * globalConfig.itemSpacingOnBelts;
if (anim.animProgress > 1) {
// Original
// animations.splice(animIndex, 1);
// Faster variant
fastArrayDelete(animations, animIndex);
animIndex -= 1;
}
}
}
}
/**
* @param {DrawParameters} parameters
* @param {MapChunkView} chunk
*/
drawChunk(parameters, chunk) {
const contents = chunk.containedEntitiesByLayer.regular;
for (let i = 0; i < contents.length; ++i) {
const entity = contents[i];
const acceptorComp = entity.components.ItemAcceptor;
if (!acceptorComp) {
continue;
}
const staticComp = entity.components.StaticMapEntity;
for (let animIndex = 0; animIndex < acceptorComp.itemConsumptionAnimations.length; ++animIndex) {
const { item, slotIndex, animProgress, direction } = acceptorComp.itemConsumptionAnimations[
animIndex
];
const slotData = acceptorComp.slots[slotIndex];
const realSlotPos = staticComp.localTileToWorld(slotData.pos);
if (!chunk.tileSpaceRectangle.containsPoint(realSlotPos.x, realSlotPos.y)) {
// Not within this chunk
continue;
}
const fadeOutDirection = enumDirectionToVector[staticComp.localDirectionToWorld(direction)];
const finalTile = realSlotPos.subScalars(
fadeOutDirection.x * (animProgress / 2 - 0.5),
fadeOutDirection.y * (animProgress / 2 - 0.5)
);
item.drawItemCenteredClipped(
(finalTile.x + 0.5) * globalConfig.tileSize,
(finalTile.y + 0.5) * globalConfig.tileSize,
parameters,
globalConfig.defaultItemDiameter
);
}
}
}
}
import { globalConfig } from "../../core/config";
import { DrawParameters } from "../../core/draw_parameters";
import { fastArrayDelete } from "../../core/utils";
import { enumDirectionToVector } from "../../core/vector";
import { ItemAcceptorComponent } from "../components/item_acceptor";
import { GameSystemWithFilter } from "../game_system_with_filter";
import { MapChunkView } from "../map_chunk_view";
export class ItemAcceptorSystem extends GameSystemWithFilter {
constructor(root) {
super(root, [ItemAcceptorComponent]);
}
update() {
const progress =
this.root.dynamicTickrate.deltaSeconds *
2 *
this.root.hubGoals.getBeltBaseSpeed() *
globalConfig.itemSpacingOnBelts; // * 2 because its only a half tile
for (let i = 0; i < this.allEntities.length; ++i) {
const entity = this.allEntities[i];
const aceptorComp = entity.components.ItemAcceptor;
const animations = aceptorComp.itemConsumptionAnimations;
// Process item consumption animations to avoid items popping from the belts
for (let animIndex = 0; animIndex < animations.length; ++animIndex) {
const anim = animations[animIndex];
anim.animProgress += progress;
if (anim.animProgress > 1) {
fastArrayDelete(animations, animIndex);
animIndex -= 1;
}
}
}
}
/**
* @param {DrawParameters} parameters
* @param {MapChunkView} chunk
*/
drawChunk(parameters, chunk) {
const contents = chunk.containedEntitiesByLayer.regular;
for (let i = 0; i < contents.length; ++i) {
const entity = contents[i];
const acceptorComp = entity.components.ItemAcceptor;
if (!acceptorComp) {
continue;
}
const staticComp = entity.components.StaticMapEntity;
for (let animIndex = 0; animIndex < acceptorComp.itemConsumptionAnimations.length; ++animIndex) {
const { item, slotIndex, animProgress, direction } = acceptorComp.itemConsumptionAnimations[
animIndex
];
const slotData = acceptorComp.slots[slotIndex];
const realSlotPos = staticComp.localTileToWorld(slotData.pos);
if (!chunk.tileSpaceRectangle.containsPoint(realSlotPos.x, realSlotPos.y)) {
// Not within this chunk
continue;
}
const fadeOutDirection = enumDirectionToVector[staticComp.localDirectionToWorld(direction)];
const finalTile = realSlotPos.subScalars(
fadeOutDirection.x * (animProgress / 2 - 0.5),
fadeOutDirection.y * (animProgress / 2 - 0.5)
);
item.drawItemCenteredClipped(
(finalTile.x + 0.5) * globalConfig.tileSize,
(finalTile.y + 0.5) * globalConfig.tileSize,
parameters,
globalConfig.defaultItemDiameter
);
}
}
}
}

View File

@@ -1,4 +1,3 @@
import { globalConfig } from "../../core/config";
import { BaseItem } from "../base_item";
import { enumColorMixingResults, enumColors } from "../colors";
import {
@@ -12,6 +11,11 @@ import { BOOL_TRUE_SINGLETON, isTruthyItem } from "../items/boolean_item";
import { ColorItem, COLOR_ITEM_SINGLETONS } from "../items/color_item";
import { ShapeItem } from "../items/shape_item";
/**
* We need to allow queuing charges, otherwise the throughput will stall
*/
const MAX_QUEUED_CHARGES = 2;
export class ItemProcessorSystem extends GameSystemWithFilter {
constructor(root) {
super(root, [ItemProcessorComponent]);
@@ -24,60 +28,64 @@ export class ItemProcessorSystem extends GameSystemWithFilter {
const processorComp = entity.components.ItemProcessor;
const ejectorComp = entity.components.ItemEjector;
// First of all, process the current recipe
const newSecondsUntilEject =
processorComp.secondsUntilEject - this.root.dynamicTickrate.deltaSeconds;
const currentCharge = processorComp.ongoingCharges[0];
processorComp.secondsUntilEject = Math.max(0, newSecondsUntilEject);
if (currentCharge) {
// Process next charge
if (currentCharge.remainingTime > 0.0) {
currentCharge.remainingTime -= this.root.dynamicTickrate.deltaSeconds;
if (currentCharge.remainingTime < 0.0) {
// Add bonus time, this is the time we spent too much
processorComp.bonusTime += -currentCharge.remainingTime;
}
}
if (newSecondsUntilEject < 0) {
processorComp.bonusFromLastTick -= newSecondsUntilEject;
}
// Check if it finished
if (currentCharge.remainingTime <= 0.0) {
const itemsToEject = currentCharge.items;
if (G_IS_DEV && globalConfig.debug.instantProcessors) {
processorComp.secondsUntilEject = 0;
}
// Go over all items and try to eject them
for (let j = 0; j < itemsToEject.length; ++j) {
const { item, requiredSlot, preferredSlot } = itemsToEject[j];
// Check if we have any finished items we can eject
if (
processorComp.secondsUntilEject === 0 && // it was processed in time
processorComp.itemsToEject.length > 0 // we have some items left to eject
) {
for (let itemIndex = 0; itemIndex < processorComp.itemsToEject.length; ++itemIndex) {
const { item, requiredSlot, preferredSlot } = processorComp.itemsToEject[itemIndex];
let slot = null;
if (requiredSlot !== null && requiredSlot !== undefined) {
// We have a slot override, check if that is free
if (ejectorComp.canEjectOnSlot(requiredSlot)) {
slot = requiredSlot;
}
} else if (preferredSlot !== null && preferredSlot !== undefined) {
// We have a slot preference, try using it but otherwise use a free slot
if (ejectorComp.canEjectOnSlot(preferredSlot)) {
slot = preferredSlot;
let slot = null;
if (requiredSlot !== null && requiredSlot !== undefined) {
// We have a slot override, check if that is free
if (ejectorComp.canEjectOnSlot(requiredSlot)) {
slot = requiredSlot;
}
} else if (preferredSlot !== null && preferredSlot !== undefined) {
// We have a slot preference, try using it but otherwise use a free slot
if (ejectorComp.canEjectOnSlot(preferredSlot)) {
slot = preferredSlot;
} else {
slot = ejectorComp.getFirstFreeSlot();
}
} else {
// We can eject on any slot
slot = ejectorComp.getFirstFreeSlot();
}
} else {
// We can eject on any slot
slot = ejectorComp.getFirstFreeSlot();
if (slot !== null) {
// Alright, we can actually eject
if (!ejectorComp.tryEject(slot, item)) {
assert(false, "Failed to eject");
} else {
itemsToEject.splice(j, 1);
j -= 1;
}
}
}
if (slot !== null) {
// Alright, we can actually eject
if (!ejectorComp.tryEject(slot, item)) {
assert(false, "Failed to eject");
} else {
processorComp.itemsToEject.splice(itemIndex, 1);
itemIndex -= 1;
}
// If the charge was entirely emptied to the outputs, start the next charge
if (itemsToEject.length === 0) {
processorComp.ongoingCharges.shift();
}
}
}
// Check if we have an empty queue and can start a new charge
if (processorComp.itemsToEject.length === 0) {
if (processorComp.ongoingCharges.length < MAX_QUEUED_CHARGES) {
if (this.canProcess(entity)) {
this.startNewCharge(entity);
}
@@ -236,12 +244,6 @@ export class ItemProcessorSystem extends GameSystemWithFilter {
itemsBySlot[items[i].sourceSlot] = items[i];
}
const baseSpeed = this.root.hubGoals.getProcessorBaseSpeed(processorComp.type);
// Substract one tick because we already process it this frame
processorComp.secondsUntilEject = Math.max(0, 1 / baseSpeed - processorComp.bonusFromLastTick);
processorComp.bonusFromLastTick = 0;
/** @type {Array<{item: BaseItem, requiredSlot?: number, preferredSlot?: number}>} */
const outItems = [];
@@ -544,6 +546,35 @@ export class ItemProcessorSystem extends GameSystemWithFilter {
}
}
processorComp.itemsToEject = outItems;
// Queue Charge
const baseSpeed = this.root.hubGoals.getProcessorBaseSpeed(processorComp.type);
const originalTime = 1 / baseSpeed;
const bonusTimeToApply = Math.min(originalTime, processorComp.bonusTime);
const timeToProcess = originalTime - bonusTimeToApply;
// Substract one tick because we already process it this frame
// if (processorComp.bonusTime > originalTime) {
// if (processorComp.type === enumItemProcessorTypes.reader) {
// console.log(
// "Bonus time",
// round4Digits(processorComp.bonusTime),
// "Original time",
// round4Digits(originalTime),
// "Overcomit by",
// round4Digits(processorComp.bonusTime - originalTime),
// "->",
// round4Digits(timeToProcess),
// "reduced by",
// round4Digits(bonusTimeToApply)
// );
// }
// }
processorComp.bonusTime -= bonusTimeToApply;
processorComp.ongoingCharges.push({
items: outItems,
remainingTime: timeToProcess,
});
}
}

View File

@@ -1,82 +1,82 @@
import { globalConfig } from "../../core/config";
import { DrawParameters } from "../../core/draw_parameters";
import { GameSystem } from "../game_system";
import { MapChunkView } from "../map_chunk_view";
export class StaticMapEntitySystem extends GameSystem {
constructor(root) {
super(root);
/** @type {Set<number>} */
this.drawnUids = new Set();
this.root.signals.gameFrameStarted.add(this.clearUidList, this);
}
/**
* Clears the uid list when a new frame started
*/
clearUidList() {
this.drawnUids.clear();
}
/**
* Draws the static entities
* @param {DrawParameters} parameters
* @param {MapChunkView} chunk
*/
drawChunk(parameters, chunk) {
if (G_IS_DEV && globalConfig.debug.doNotRenderStatics) {
return;
}
const contents = chunk.containedEntitiesByLayer.regular;
for (let i = 0; i < contents.length; ++i) {
const entity = contents[i];
const staticComp = entity.components.StaticMapEntity;
const sprite = staticComp.getSprite();
if (sprite) {
// Avoid drawing an entity twice which has been drawn for
// another chunk already
if (this.drawnUids.has(entity.uid)) {
continue;
}
this.drawnUids.add(entity.uid);
staticComp.drawSpriteOnBoundsClipped(parameters, sprite, 2);
}
}
}
/**
* Draws the static wire entities
* @param {DrawParameters} parameters
* @param {MapChunkView} chunk
*/
drawWiresChunk(parameters, chunk) {
if (G_IS_DEV && globalConfig.debug.doNotRenderStatics) {
return;
}
const drawnUids = new Set();
const contents = chunk.wireContents;
for (let y = 0; y < globalConfig.mapChunkSize; ++y) {
for (let x = 0; x < globalConfig.mapChunkSize; ++x) {
const entity = contents[x][y];
if (entity) {
if (drawnUids.has(entity.uid)) {
continue;
}
drawnUids.add(entity.uid);
const staticComp = entity.components.StaticMapEntity;
const sprite = staticComp.getSprite();
if (sprite) {
staticComp.drawSpriteOnBoundsClipped(parameters, sprite, 2);
}
}
}
}
}
}
import { globalConfig } from "../../core/config";
import { DrawParameters } from "../../core/draw_parameters";
import { GameSystem } from "../game_system";
import { MapChunkView } from "../map_chunk_view";
export class StaticMapEntitySystem extends GameSystem {
constructor(root) {
super(root);
/** @type {Set<number>} */
this.drawnUids = new Set();
this.root.signals.gameFrameStarted.add(this.clearUidList, this);
}
/**
* Clears the uid list when a new frame started
*/
clearUidList() {
this.drawnUids.clear();
}
/**
* Draws the static entities
* @param {DrawParameters} parameters
* @param {MapChunkView} chunk
*/
drawChunk(parameters, chunk) {
if (G_IS_DEV && globalConfig.debug.doNotRenderStatics) {
return;
}
const contents = chunk.containedEntitiesByLayer.regular;
for (let i = 0; i < contents.length; ++i) {
const entity = contents[i];
const staticComp = entity.components.StaticMapEntity;
const sprite = staticComp.getSprite();
if (sprite) {
// Avoid drawing an entity twice which has been drawn for
// another chunk already
if (this.drawnUids.has(entity.uid)) {
continue;
}
this.drawnUids.add(entity.uid);
staticComp.drawSpriteOnBoundsClipped(parameters, sprite, 2);
}
}
}
/**
* Draws the static wire entities
* @param {DrawParameters} parameters
* @param {MapChunkView} chunk
*/
drawWiresChunk(parameters, chunk) {
if (G_IS_DEV && globalConfig.debug.doNotRenderStatics) {
return;
}
const drawnUids = new Set();
const contents = chunk.wireContents;
for (let y = 0; y < globalConfig.mapChunkSize; ++y) {
for (let x = 0; x < globalConfig.mapChunkSize; ++x) {
const entity = contents[x][y];
if (entity) {
if (drawnUids.has(entity.uid)) {
continue;
}
drawnUids.add(entity.uid);
const staticComp = entity.components.StaticMapEntity;
const sprite = staticComp.getSprite();
if (sprite) {
staticComp.drawSpriteOnBoundsClipped(parameters, sprite, 2);
}
}
}
}
}
}

View File

@@ -123,10 +123,9 @@ export const autosaveIntervals = [
},
];
const refreshRateOptions = ["60", "75", "100", "120", "144", "165", "250", "500"];
const refreshRateOptions = ["30", "60", "120", "180", "240"];
if (G_IS_DEV) {
refreshRateOptions.unshift("30");
refreshRateOptions.unshift("10");
refreshRateOptions.unshift("5");
refreshRateOptions.push("1000");
@@ -511,7 +510,7 @@ export class ApplicationSettings extends ReadWriteProxy {
}
getCurrentVersion() {
return 23;
return 24;
}
/** @param {{settings: SettingsStorage, version: number}} data */
@@ -614,6 +613,11 @@ export class ApplicationSettings extends ReadWriteProxy {
data.version = 23;
}
if (data.version < 24) {
data.settings.refreshRate = "60";
data.version = 24;
}
return ExplainedResult.good();
}
}