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 all 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
171 changes: 155 additions & 16 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 @@ -176,7 +180,8 @@ export class LiveSplitManager {
}

if (this._inValidCampaignRun) {
this._addMissionTime(attemptTime)
const computedTime = this._addMissionTime(attemptTime)
this._addTimeCalcEntry(this._currentMission, computedTime, 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 @@ -367,20 +378,21 @@ export class LiveSplitManager {
)
}

private _addMissionTime(time: Seconds) {
// always add at least minimum
private _addMissionTime(time: Seconds): Seconds {
let computedTime = Math.floor(time)

// always add at least minimum, which is usually 0 except on cutscenes where
// you can gain an advantage by restarting in cs (bangkok, sgail specific starts)
if (time <= this._resetMinimum) {
this._currentMissionTotalTime += this._resetMinimum
this._campaignTotalTime += this._resetMinimum
computedTime = this._resetMinimum
} else if (time > 0 && time <= 1) {
// if in game time is between 0 and 1, add full second
this._currentMissionTotalTime += 1
this._campaignTotalTime += 1
} else {
// important to always floor before adding time
this._currentMissionTotalTime += Math.floor(time)
this._campaignTotalTime += Math.floor(time)
computedTime = 1
}

this._currentMissionTotalTime += computedTime
this._campaignTotalTime += computedTime
return computedTime
}

private async _pushGameTime() {
Expand All @@ -392,7 +404,134 @@ 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,
}
this._timeCalcEntries.push(entry)
}

private _unsplitLastTimeCalcEntry() {
const entry = this._timeCalcEntries.pop()
entry.isCompleted = false
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 flooredTime = Math.floor(time)
if (flooredTime < 60) return flooredTime
const minutes = Math.floor(flooredTime / 60)
const seconds = Math.floor(flooredTime % 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
}