Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Livesplit - Provide a TimeCalc URL for Season/Trilogy speedruns #42

Merged
merged 5 commits into from
Dec 12, 2022
Merged
Show file tree
Hide file tree
Changes from 4 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
12 changes: 4 additions & 8 deletions components/discordRp.ts
Original file line number Diff line number Diff line change
Expand Up @@ -312,11 +312,7 @@ export function scenePathToRpAsset(
// haven
case "assembly:/_pro/scenes/missions/opulent/mission_stingray/scene_stingray_basic.entity":
case "assembly:/_pro/scenes/missions/opulent/mission_stingray/scene_stingray_arcticthyme.entity":
return [
"havenlastresort",
"The Last Resort",
"Haven - The Maldives",
]
return ["havenlastresort", "The Last Resort", "Haven"]
LennardF1989 marked this conversation as resolved.
Show resolved Hide resolved

// miami
case "assembly:/_pro/scenes/missions/miami/scene_et_sambuca.entity":
Expand All @@ -328,11 +324,11 @@ export function scenePathToRpAsset(

// hawkes bay
case "assembly:/_pro/scenes/missions/sheep/scene_adonis.entity":
return ["elusiveadonis", "The Politician", "New Zealand"]
return ["elusiveadonis", "The Politician", "Hawke's Bay"]
case "assembly:/_pro/scenes/missions/sheep/scene_sheep.entity":
return ["hawkenightcall", "Nightcall", "New Zealand"]
return ["hawkenightcall", "Nightcall", "Hawke's Bay"]
case "assembly:/_pro/scenes/missions/sheep/scene_opuntia.entity":
return ["opuntia", "Opuntia", "New Zealand"]
return ["opuntia", "Opuntia", "Hawke's Bay"]

// sgail
case "assembly:/_pro/scenes/missions/theark/scene_magpie.entity":
Expand Down
157 changes: 152 additions & 5 deletions components/livesplit/liveSplitManager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,8 @@ import { getAllCampaigns } from "../menus/campaigns"
import { Campaign, GameVersion, IHit, Seconds, StoryData } from "../types/types"
import { getFlag } from "../flags"
import { controller } from "../controller"
import { scenePathToRpAsset } from "../discordRp"
import { LiveSplitTimeCalcEntry } from "../types/livesplit"

export class LiveSplitManager {
private readonly _liveSplitClient: LiveSplitClient
Expand All @@ -36,6 +38,7 @@ export class LiveSplitManager {
private _currentMissionTotalTime: number
private _campaignTotalTime: number
private _completedMissions: string[]
private _timeCalcEntries: LiveSplitTimeCalcEntry[]
private _raceMode: boolean | undefined // gets late-initialized, use _isRaceMode to access

constructor() {
Expand All @@ -45,6 +48,7 @@ export class LiveSplitManager {
this._currentMission = undefined
this._inValidCampaignRun = false
this._completedMissions = []
this._timeCalcEntries = []
this._currentMissionTotalTime = 0
this._campaignTotalTime = 0
this._raceMode = undefined
Expand Down Expand Up @@ -103,6 +107,7 @@ export class LiveSplitManager {
// un-split. total time is not reset on mission complete, so it should still be valid.
// do pop the completed mission though as we're entering a new attempt
this._completedMissions.pop()
this._unsplitLastTimeCalcEntry()
if (!this._isRaceMode) {
logLiveSplitError(
await this._liveSplitClient.unsplit(),
Expand Down Expand Up @@ -160,10 +165,9 @@ export class LiveSplitManager {
}

if (this._inValidCampaignRun) {
const location = this._getMissionLocationName(this._currentMission)
log(LogLevel.DEBUG, `Current location: ${location}`)
this._addMissionTime(attemptTime)
LiveSplitManager._logAttempt(attemptTime)
const computedTime = this._addMissionTime(attemptTime)
this._addTimeCalcEntry(this._currentMission, computedTime, false)
LiveSplitManager._logAttempt(computedTime)
await this._pushGameTime()
}
}
Expand All @@ -177,6 +181,7 @@ export class LiveSplitManager {

if (this._inValidCampaignRun) {
this._addMissionTime(attemptTime)
this._addTimeCalcEntry(this._currentMission, attemptTime, true)
log(
LogLevel.INFO,
`Total mission time with resets: ${this._currentMissionTotalTime}`,
Expand Down Expand Up @@ -218,6 +223,11 @@ export class LiveSplitManager {
await this._liveSplitClient.pause(),
"pause",
)

log(
LogLevel.INFO,
`TimeCalc link(s):\n${this._generateTimeCalcLinks()}`,
)
}
}
// purposely do not reset this._currentMissionTotalTime yet in case mission complete
Expand Down Expand Up @@ -317,6 +327,7 @@ export class LiveSplitManager {
`Detected campaign missions: ${this._currentCampaign}`,
)
this._completedMissions = []
this._timeCalcEntries = []
this._inValidCampaignRun = true
this._currentMissionTotalTime = 0
this._campaignTotalTime = 0
Expand Down Expand Up @@ -368,19 +379,23 @@ export class LiveSplitManager {
}

private _addMissionTime(time: Seconds) {
let computedTime = time
// always add at least minimum
if (time <= this._resetMinimum) {
computedTime = this._resetMinimum
this._currentMissionTotalTime += this._resetMinimum
this._campaignTotalTime += this._resetMinimum
} else if (time > 0 && time <= 1) {
// if in game time is between 0 and 1, add full second
computedTime = 1
this._currentMissionTotalTime += 1
this._campaignTotalTime += 1
} else {
// important to always floor before adding time
this._currentMissionTotalTime += Math.floor(time)
this._campaignTotalTime += Math.floor(time)
}
return computedTime
}
alex73630 marked this conversation as resolved.
Show resolved Hide resolved

private async _pushGameTime() {
Expand All @@ -392,7 +407,139 @@ export class LiveSplitManager {
}

private _getMissionLocationName(contractId: string) {
return controller.resolveContract(contractId)?.Metadata.Location
const contract = controller.resolveContract(contractId)
const [, , location] = scenePathToRpAsset(
contract.Metadata.ScenePath,
contract.Data.Bricks,
)
return location
}

private _addTimeCalcEntry(
contractId: string,
time: Seconds,
isCompleted: boolean,
) {
const location = this._getMissionLocationName(contractId)
const entry: LiveSplitTimeCalcEntry = {
contractId,
location,
time,
isCompleted,
}
log(LogLevel.DEBUG, `New TimeCalc entry: ${JSON.stringify(entry)}`)
this._timeCalcEntries.push(entry)
}

private _unsplitLastTimeCalcEntry() {
const entry = this._timeCalcEntries.pop()
entry.isCompleted = false
log(
LogLevel.DEBUG,
`Unsplitted TimeCalc entry: ${JSON.stringify(entry)}`,
)
this._timeCalcEntries.push(entry)
}

private _generateTimeCalcLinks() {
const baseUrl =
"https://solderq35.github.io/fg-time-calc/?mode=0&fs3=1&ft2=1&f3t1=1&f4t0=1&d=:&o1=1&fps="

const links: string[] = []

const searchParams = new URLSearchParams()

const completedEntries = this._timeCalcEntries.filter(
(e) => e.isCompleted,
)
const resetEntries = this._timeCalcEntries.filter((e) => !e.isCompleted)

let timecalcLine = 0

completedEntries.forEach((entry) => {
timecalcLine += 1
searchParams.set(
`t${timecalcLine}`,
`${this._formatSecondsToTime(entry.time)}`,
)
searchParams.set(`c${timecalcLine}`, entry.location)
})

const totalEntries = completedEntries.length + resetEntries.length
if (totalEntries + 1 > 40) {
// We need to make multiple TimeCalc links
const completedEntriesLink = new URL(baseUrl)

// Calculate combined reset time
timecalcLine += 2
searchParams.set(
`t${timecalcLine}`,
`${this._formatSecondsToTime(
resetEntries.reduce((total, entry) => {
return (total += Math.floor(entry.time))
}, 0),
)}`,
)
searchParams.set(`c${timecalcLine}`, "Combined reset time")

// Append new search
completedEntriesLink.search +=
"&" +
searchParams
.toString()
.replaceAll("+", "%20")
.replaceAll("%3A", ":")
links.push(completedEntriesLink.toString())

timecalcLine = 0

if (resetEntries.length > 40) {
// TODO: We'll need more than 1 link for resets...
}
} else {
timecalcLine += 1
}

let resetLocation = ""
let resetCount = 0

resetEntries.forEach((entry) => {
timecalcLine += 1
if (resetLocation === entry.location) {
resetCount += 1
} else {
resetLocation = entry.location
resetCount = 1
}
searchParams.set(
`t${timecalcLine}`,
`${this._formatSecondsToTime(entry.time)}`,
)
searchParams.set(
`c${timecalcLine}`,
`${entry.location} reset ${resetCount}`,
)
})
const resetEntriesLink = new URL(baseUrl)
// Append new search
resetEntriesLink.search +=
"&" +
searchParams
.toString()
.replaceAll("+", "%20")
.replaceAll("%3A", ":")
links.push(resetEntriesLink.toString())

return links.join("\n")
}

private _formatSecondsToTime(time: Seconds) {
const roundedTime = Math.floor(time)
if (time < 60) return roundedTime
const minutes = Math.floor(roundedTime / 60)
const seconds = roundedTime - minutes * 60

return `${minutes}:${seconds}`
}
}

Expand Down
12 changes: 12 additions & 0 deletions components/types/livesplit.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
/**
* LiveSplit-related types
*/

import { Seconds } from "./types"

export interface LiveSplitTimeCalcEntry {
contractId: string
time: Seconds
location: string
isCompleted: boolean
}