- add some proper Factorio settings to control EventDataCache

- bump Typescript version
- make some classes a bit more typescript-ey
- comment out some unused code
This commit is contained in:
Adam 2022-03-09 22:28:29 +01:00
parent e630ce3702
commit 8eac84da50
15 changed files with 331 additions and 219 deletions

View file

@ -0,0 +1,20 @@
[mod-setting-name]
kafkatorio_event-cache_default-expiration-duration-seconds_PLAYER=Kafkatorio: Player update rate (seconds)
kafkatorio_event-cache_default-expiration-duration-seconds_MAP_CHUNK=Kafkatorio: Map update rate (seconds)
kafkatorio_event-cache_default-expiration-duration-seconds_ENTITY=Kafkatorio: Entity update rate (seconds)
[mod-setting-description]
kafkatorio_event-cache_default-expiration-duration-seconds_PLAYER=How frequently Player updates are sent to the Kafkatorio server, in seconds. Lower means updates are sent more frequently, which may cause network congestion or reduce performance. The default (0.25 seconds) is probably fine!
kafkatorio_event-cache_default-expiration-duration-seconds_MAP_CHUNK=How frequently Map updates are sent to the Kafkatorio server, in seconds. Lower means updates are sent more frequently, which may cause network congestion or reduce performance. The default (30 seconds) is probably fine!
kafkatorio_event-cache_default-expiration-duration-seconds_ENTITY=How frequently Entity updates are sent to the Kafkatorio server, in seconds. Lower means updates are sent more frequently, which may cause network congestion or reduce performance. The default (5 seconds) is probably fine!
[string-mod-setting]
#<setting-name>-<dropdown-item-name>=<translated dropdown item>
[string-mod-setting-description]
#<setting-name>-<dropdown-item-name>=<tooltip of dropdown item>

View file

@ -1,6 +1,7 @@
import {emitConfigurationUpdate} from "./main/config-update/handleConfigurationUpdate";
import {emitPrototypes} from "./main/config-update/prototypes";
import {initGlobal} from "./main/global-init";
import KafkatorioSettings from "./main/settings/KafkatorioSettings";
require("./main/events/registerHandlers")
require("./main/events/onMapChunkEventListeners")
@ -12,6 +13,7 @@ script.on_configuration_changed((data: ConfigurationChangedData) => {
// runs whenever the version of the base game, or a mod, changes
initGlobal(true)
KafkatorioSettings.loadSettings()
emitConfigurationUpdate(data)
emitPrototypes()
})
@ -20,4 +22,5 @@ script.on_configuration_changed((data: ConfigurationChangedData) => {
script.on_init(() => {
// runs only once (when the mod is first added to the save)
initGlobal(true)
KafkatorioSettings.loadSettings()
})

View file

@ -6,6 +6,12 @@ interface FactorioPrototype {
prototypeObjectName: PrototypeObjectName;
}
interface FactorioPrototypes extends KafkatorioPacket {
modVersion: string;
packetType: PacketType;
prototypes: FactorioPrototype[];
}
interface MapTilePrototype extends FactorioPrototype {
canBeMined: boolean;
collisionMasks: string[];
@ -15,9 +21,3 @@ interface MapTilePrototype extends FactorioPrototype {
order: string;
prototypeObjectName: PrototypeObjectName;
}
interface FactorioPrototypes extends KafkatorioPacket {
modVersion: string;
packetType: PacketType;
prototypes: FactorioPrototype[];
}

View file

@ -7,6 +7,14 @@ interface KafkatorioPacket {
packetType: PacketType;
}
interface FactorioEvent extends KafkatorioPacket {
data: FactorioObjectData;
eventType: string;
modVersion: string;
packetType: PacketType;
tick: uint;
}
interface MapEntityPosition {
x: double;
y: double;
@ -46,14 +54,6 @@ interface MapTile {
y: int;
}
interface FactorioEvent extends KafkatorioPacket {
data: FactorioObjectData;
eventType: string;
modVersion: string;
packetType: PacketType;
tick: uint;
}
interface EntityIdentifiers {
name: string;
type: string;

View file

@ -1,30 +1,25 @@
export namespace EventDataCache {
import KafkatorioSettings from "../settings/KafkatorioSettings";
export namespace EventUpdates {
declare const global: {
cache: LuaTable<string, CacheEntry<FactorioEventUpdateType>>,
defaultCacheDurationTicks: Record<FactorioEventUpdateType | "default", uint>,
}
export function init(force?: boolean) {
export class Manager {
const isAnythingUndefined = global.cache == undefined ||
global.defaultCacheDurationTicks == undefined
init(force?: boolean) {
const isAnythingUndefined = global.cache == undefined
log(`Initialising EventDataCache (force=${force}, isAnythingUndefined=${isAnythingUndefined})...`)
if (force == true || isAnythingUndefined) {
log("Initialising global.cache")
global.cache = new LuaTable<string, CacheEntry<FactorioEventUpdateType>>()
log("Initialising global.DEFAULT_CACHE_DURATION_TICKS")
global.defaultCacheDurationTicks = {
"PLAYER": 15, // 0.25 seconds
"MAP_CHUNK": 60 * 30, // 30 seconds
"ENTITY": 60 * 2, // 2 seconds
"default": 60 * 5, // 5 seconds
}
}
log(`Finished initialising EventDataCache`)
}
@ -34,11 +29,11 @@ export namespace EventDataCache {
* Update the cache data without restarting the 'time-to-emit' countdown, so that the data won't
* be emitted more than once per {@link CacheEntry.expirationDurationTicks}.
*/
export function throttle<TYPE extends FactorioEventUpdateType>(
public throttle<TYPE extends FactorioEventUpdateType>(
key: CacheKey<TYPE>,
mutate: CacheDataMutator<TYPE>,
) {
update(key, mutate, false)
this.update(key, mutate, false)
}
@ -46,20 +41,20 @@ export namespace EventDataCache {
* Update the cache data and reset the 'time-to-emit' countdown, so the data won't be emitted
* until there's an inactivity gap of {@link CacheEntry.expirationDurationTicks}.
*/
export function debounce<TYPE extends FactorioEventUpdateType>(
public debounce<TYPE extends FactorioEventUpdateType>(
key: CacheKey<TYPE>,
mutate: CacheDataMutator<TYPE>,
) {
update(key, mutate, true)
this.update(key, mutate, true)
}
function update<TYPE extends FactorioEventUpdateType>(
private update<TYPE extends FactorioEventUpdateType>(
key: CacheKey<TYPE>,
mutate: CacheDataMutator<TYPE>,
resetLastUpdated: boolean,
) {
const entry = getCacheEntry(key) ?? createCacheEntry(key, mutate)
const entry = this.getCacheEntry(key) ?? this.createCacheEntry(key, mutate)
mutate(entry.data)
if (resetLastUpdated) {
entry.lastUpdatedTick = game.tick
@ -71,24 +66,24 @@ export namespace EventDataCache {
* Usage example: If an urgent update comes in, then the expiration can be reduced so that
* it's transmitted sooner.
*/
export function setExpiration(
public setExpiration(
key: CacheKey<FactorioEventUpdateType>,
expirationDurationTicks: uint,
) {
const entry = getCacheEntry(key)
const entry = this.getCacheEntry(key)
if (entry != undefined) {
entry.expirationDurationTicks = expirationDurationTicks
}
}
export function extractExpired(): Array<FactorioEventUpdate> {
public extractExpired(): Array<FactorioEventUpdate> {
let countExpired = 0
let countTotal = 0
const expiredData: Array<FactorioEventUpdate> = []
for (let [key, entry] of global.cache) {
countTotal++
if (isExpired(entry)) {
if (this.isExpired(entry)) {
countExpired++
expiredData.push(entry.data)
global.cache.delete(key)
@ -101,7 +96,7 @@ export namespace EventDataCache {
}
function getCacheEntry<TYPE extends FactorioEventUpdateType>(
private getCacheEntry<TYPE extends FactorioEventUpdateType>(
key: CacheKey<TYPE>
): CacheEntry<TYPE> | undefined {
const hash = game.encode_string(game.table_to_json(key))!!
@ -109,7 +104,7 @@ export namespace EventDataCache {
return undefined
}
const value: CacheEntry<any> = global.cache.get(hash)
if (isEntryInstanceOf(value, key.updateType)) {
if (this.isEntryInstanceOf(value, key.updateType)) {
return value
} else {
// Type mismatch. This shouldn't happen...
@ -118,7 +113,7 @@ export namespace EventDataCache {
}
function createCacheEntry<TYPE extends FactorioEventUpdateType>(
private createCacheEntry<TYPE extends FactorioEventUpdateType>(
key: CacheKey<TYPE>,
mutate: CacheDataMutator<TYPE>,
): CacheEntry<TYPE> {
@ -131,6 +126,40 @@ export namespace EventDataCache {
}
isExpired<TYPE extends FactorioEventUpdateType>(entry: CacheEntry<TYPE>): boolean {
const expiryDuration = entry.expirationDurationTicks
??
KafkatorioSettings.getEventCacheExpirationTicks(entry.data.updateType)
return game.tick - entry.lastUpdatedTick > expiryDuration
}
isEntryInstanceOf<TYPE extends FactorioEventUpdateType>(
entry: CacheEntry<any> | undefined,
updateType: TYPE,
): entry is CacheEntry<TYPE> {
return this.isDataInstanceOf<TYPE>(entry?.data, updateType)
}
isDataInstanceOf<TYPE extends FactorioEventUpdateType>(
data: CacheData<any> | undefined,
updateType: TYPE,
): data is CacheData<TYPE> {
return data != undefined && data.updateType == updateType
}
// function isKeyInstanceOf<TYPE extends FactorioEventUpdateType>(
// key: CacheKey<any> | undefined,
// updateType: TYPE,
// ): key is CacheKey<TYPE> {
// return key != undefined && key.updateType == updateType
// }
}
/** Map a {@link FactorioEventUpdateType} to a {@link FactorioEventUpdate} DTO */
type ConvertToUpdate<TYPE extends FactorioEventUpdateType> =
TYPE extends "PLAYER" ? PlayerUpdate :
@ -184,34 +213,7 @@ export namespace EventDataCache {
}
function isExpired<TYPE extends FactorioEventUpdateType>(entry: CacheEntry<TYPE>): boolean {
const expiryDuration =
entry.expirationDurationTicks ?? global.defaultCacheDurationTicks[entry.data.updateType]
return game.tick - entry.lastUpdatedTick > expiryDuration
}
function isEntryInstanceOf<TYPE extends FactorioEventUpdateType>(
entry: CacheEntry<any> | undefined,
updateType: TYPE,
): entry is CacheEntry<TYPE> {
return isDataInstanceOf<TYPE>(entry?.data, updateType)
}
function isDataInstanceOf<TYPE extends FactorioEventUpdateType>(
data: CacheData<any> | undefined,
updateType: TYPE,
): data is CacheData<TYPE> {
return data != undefined && data.updateType == updateType
}
// function isKeyInstanceOf<TYPE extends FactorioEventUpdateType>(
// key: CacheKey<any> | undefined,
// updateType: TYPE,
// ): key is CacheKey<TYPE> {
// return key != undefined && key.updateType == updateType
// }
}
const EventUpdatesManager = new EventUpdates.Manager()
export default EventUpdatesManager

View file

@ -1,9 +1,9 @@
import {EventDataCache} from "./EventDataCache";
import {emitPacket} from "../emitKafkatorioPacket";
import EventCacheService from "./EventDataCache";
/** Every 0.1 seconds */
script.on_nth_tick(6, (nthTick: NthTickEventData) => {
const cachedEvents: Array<FactorioEventUpdate> = EventDataCache.extractExpired()
const cachedEvents: Array<FactorioEventUpdate> = EventCacheService.extractExpired()
if (cachedEvents.length > 0) {
log(`nth tick ${nthTick.tick} has ${cachedEvents.length} events`)

View file

@ -78,31 +78,31 @@ export function handleChunkUpdate(
emitEvent(mapChunk, tick, eventType)
}
export function handleTilesUpdate(
tick: uint,
eventType: string,
surfaceIndex: uint,
newTileType: LuaTilePrototype,
updatedTiles: OldTileAndPosition[],
) {
if (!newTileType.collision_mask["ground-tile"] && !newTileType.collision_mask["water-tile"]) {
return
} else {
let convertedTiles: MapTile[] = updatedTiles.map((it) => {
return {
x: it.position.x,
y: it.position.y,
proto: newTileType.name
}
})
let mapTiles: MapTiles = createMapTiles(surfaceIndex, convertedTiles)
emitEvent(mapTiles, tick, eventType)
}
}
// export function handleTilesUpdate(
// tick: uint,
// eventType: string,
// surfaceIndex: uint,
// newTileType: LuaTilePrototype,
// updatedTiles: OldTileAndPosition[],
// ) {
//
// if (!newTileType.collision_mask["ground-tile"] && !newTileType.collision_mask["water-tile"]) {
// return
// } else {
//
// let convertedTiles: MapTile[] = updatedTiles.map((it) => {
// return {
// x: it.position.x,
// y: it.position.y,
// proto: newTileType.name
// }
// })
//
// let mapTiles: MapTiles = createMapTiles(surfaceIndex, convertedTiles)
//
// emitEvent(mapTiles, tick, eventType)
// }
// }
function createMapTiles(
surfaceIndex: uint,

View file

@ -1,8 +1,8 @@
import {EventDataCache} from "../cache/EventDataCache";
import {isEventType} from "./eventTypeCheck";
import {Converters} from "./converters";
import CacheKey = EventDataCache.CacheKey;
import CacheData = EventDataCache.CacheData;
import EventUpdatesManager, {EventUpdates} from "../cache/EventDataCache";
import CacheKey = EventUpdates.CacheKey;
import CacheData = EventUpdates.CacheData;
type MapChunkUpdater = (data: CacheData<"MAP_CHUNK">) => void
@ -37,7 +37,7 @@ function mapTilesUpdateDebounce(
updateType: "MAP_CHUNK",
}
EventDataCache.debounce<"MAP_CHUNK">(
EventUpdatesManager.debounce<"MAP_CHUNK">(
key,
data => {
data.tileDictionary ??= <MapTileDictionary>{
@ -242,14 +242,14 @@ script.on_event(
updateType: "MAP_CHUNK",
}
EventDataCache.debounce<"MAP_CHUNK">(
EventUpdatesManager.debounce<"MAP_CHUNK">(
key,
data => {
data.isDeleted = true
}
)
// deletion is important - emit ASAP
EventDataCache.setExpiration(key, 0)
EventUpdatesManager.setExpiration(key, 0)
}
}
)

View file

@ -1,6 +1,7 @@
import {EventDataCache} from "../cache/EventDataCache";
import {Converters} from "./converters";
import CacheData = EventDataCache.CacheData;
import EventUpdatesManager, {EventUpdates} from "../cache/EventDataCache";
import CacheData = EventUpdates.CacheData;
type PlayerUpdater = (player: LuaPlayer, data: CacheData<"PLAYER">) => void
@ -9,9 +10,9 @@ function playerUpdateThrottle(
eventName: string,
mutate: PlayerUpdater,
) {
EventDataCache.throttle<"PLAYER">(
EventUpdatesManager.throttle<"PLAYER">(
{index: playerIndex, updateType: "PLAYER"},
(data => {
data => {
const player = game.players[playerIndex]
if (player != undefined) {
mutate(player, data)
@ -21,19 +22,19 @@ function playerUpdateThrottle(
data.eventCounts = {}
}
data.eventCounts[eventName] = ((data.eventCounts ?? {}) [eventName] ?? 0) + 1
})
}
)
}
function playerUpdateDebounce(playerIndex: uint, mutate: PlayerUpdater) {
EventDataCache.debounce<"PLAYER">(
{index: playerIndex, updateType: "PLAYER"},
(data => {
const player = game.players[playerIndex]
mutate(player, data)
})
)
}
// function playerUpdateDebounce(playerIndex: uint, mutate: PlayerUpdater) {
// EventUpdatesManager.debounce<"PLAYER">(
// {index: playerIndex, updateType: "PLAYER"},
// data => {
// const player = game.players[playerIndex]
// mutate(player, data)
// }
// )
// }
function playerOnlineInfo(player: LuaPlayer, data: CacheData<"PLAYER">) {
data.lastOnline = player.last_online

View file

@ -7,6 +7,7 @@ import {
import {Queue} from "../queue/queue";
import {isEventType} from "./eventTypeCheck";
import {Converters} from "./converters";
import KafkatorioSettings from "../settings/KafkatorioSettings";
script.on_event(
@ -46,22 +47,22 @@ script.on_event(
// }
}
if (e.tick % 30 == 0) {
let events: EventData[] = Queue.dequeueValues(1)
if (events.length > 0) {
log(`[${e.tick}] dequed ${events.length} events, current size: ${Queue.size()}`)
for (const event of events) {
if (isEventType(event, defines.events.on_chunk_generated)) {
let eName = Converters.eventNameString(event.name)
log(`[${e.tick}] dequed event ${eName}`)
handleChunkUpdate(e.tick, eName, event.surface.index, event.position, event.area)
}
}
}
}
// if (e.tick % 30 == 0) {
// let events: EventData[] = Queue.dequeueValues(1)
//
// if (events.length > 0) {
// log(`[${e.tick}] dequed ${events.length} events, current size: ${Queue.size()}`)
//
// for (const event of events) {
// if (isEventType(event, defines.events.on_chunk_generated)) {
// let eName = Converters.eventNameString(event.name)
//
// log(`[${e.tick}] dequed event ${eName}`)
// handleChunkUpdate(e.tick, eName, event.surface.index, event.position, event.area)
// }
// }
// }
// }
}
)
@ -80,3 +81,11 @@ script.on_event(
// defines.events.on_robot_built_tile, ], (builtTilesEvent) => { handleTilesUpdate(
// builtTilesEvent.tick, mapEventIdToName.get(builtTilesEvent.name), builtTilesEvent.surface_index,
// builtTilesEvent.tile, builtTilesEvent.tiles, ) } )
script.on_event(
defines.events.on_runtime_mod_setting_changed,
(e: OnRuntimeModSettingChangedEvent) => {
print(`Settings changed ${e.setting_type}, ${e.player_index}, ${e.setting}, ${e.mod_name}`)
KafkatorioSettings.loadSettings()
}
)

View file

@ -1,5 +1,5 @@
import {Queue} from "./queue/queue";
import {EventDataCache} from "./cache/EventDataCache";
import EventUpdatesManager from "./cache/EventDataCache";
export function initGlobal(force: boolean = false) {
@ -9,7 +9,7 @@ export function initGlobal(force: boolean = false) {
global.FACTORIO_VERSION = script.active_mods["base"]
Queue.init(true)
EventDataCache.init(true)
EventUpdatesManager.init(true)
log("Finished initialising Kafkatorio Global variables")
}

View file

@ -0,0 +1,70 @@
import {Data, DoubleSettingDefinition} from "typed-factorio/settings/types"
export class KafkatorioSettingsConfig {
private static eventCacheExpirationDefaultSeconds: Record<FactorioEventUpdateType, uint> = {
"PLAYER": 0.25,
"MAP_CHUNK": 30,
"ENTITY": 5,
}
private eventCacheExpirationTicks: Record<FactorioEventUpdateType, double> = {
"PLAYER": KafkatorioSettingsConfig.eventCacheExpirationDefaultSeconds["PLAYER"],
"MAP_CHUNK": KafkatorioSettingsConfig.eventCacheExpirationDefaultSeconds["MAP_CHUNK"],
"ENTITY": KafkatorioSettingsConfig.eventCacheExpirationDefaultSeconds["ENTITY"],
}
public getEventCacheExpirationTicks(type: FactorioEventUpdateType): uint {
return this.eventCacheExpirationTicks[type]
}
loadSettings(): void {
const types: FactorioEventUpdateType[] = ["PLAYER", "MAP_CHUNK", "ENTITY"]
for (const type of types) {
const seconds: double = settings.global[KafkatorioSettingsConfig.cacheDurationSettingName(type)].value as double
this.eventCacheExpirationTicks[type] = seconds * 60 // convert ticks to seconds
}
}
initialiseSettings(data: Data): void {
data.extend(
[
this.createDefaultCacheDuration("PLAYER"),
this.createDefaultCacheDuration("MAP_CHUNK"),
this.createDefaultCacheDuration("ENTITY"),
]
)
}
private createDefaultCacheDuration(
type: FactorioEventUpdateType,
defaultExpirationSeconds: uint = KafkatorioSettingsConfig.eventCacheExpirationDefaultSeconds[type],
): DoubleSettingDefinition {
return <DoubleSettingDefinition>{
type: "double-setting",
name: KafkatorioSettingsConfig.cacheDurationSettingName(type),
setting_type: "runtime-global",
default_value: defaultExpirationSeconds,
// min: 0.25 seconds, max: 2 minutes
minimum_value: 0.25,
maximum_value: 120,
}
}
private static cacheDurationSettingName(type: FactorioEventUpdateType): string {
return `kafkatorio_event-cache_default-expiration-duration-seconds_${type}`
}
}
const KafkatorioSettings = new KafkatorioSettingsConfig()
export default KafkatorioSettings

View file

@ -80,9 +80,9 @@
"integrity": "sha512-/iLa916Cw33vC6wgmCb4cPLjmvUA2zVviTx/vy80iEvpOHjoNkuHkGSsmvss1aItwUKUlHaLi3k0fK5OPiw1iA=="
},
"typescript": {
"version": "4.5.5",
"resolved": "https://registry.npmjs.org/typescript/-/typescript-4.5.5.tgz",
"integrity": "sha512-TCTIul70LyWe6IJWT8QSYeA54WQe8EjQFU4wY52Fasj5UKx88LNYKCgBEHcOMOrFF1rKGbD8v/xcNWVUq9SymA=="
"version": "4.6.2",
"resolved": "https://registry.npmjs.org/typescript/-/typescript-4.6.2.tgz",
"integrity": "sha512-HM/hFigTBHZhLXshn9sN37H085+hQGeJHJ/X7LpBWLID/fbc2acUMfU+lGD98X81sKP+pFa9f0DZmCwB9GnbAg=="
},
"typescript-to-lua": {
"version": "1.3.4",

View file

@ -15,7 +15,7 @@
"lua-types": "^2.11.0",
"typescript-to-lua": "1.3.4",
"typed-factorio": "0.19.1",
"typescript": "4.5.5"
"typescript": "4.6.2"
},
"// @types/node": "^12.0.8",
"license": "UNLICENSED",

View file

@ -0,0 +1,7 @@
import {Data, Mods} from "typed-factorio/settings/types"
import KafkatorioSettings from "./main/settings/KafkatorioSettings";
declare const data: Data
declare const mods: Mods
KafkatorioSettings.initialiseSettings(data)