Skip to content

Commit

Permalink
Basic dmoj support
Browse files Browse the repository at this point in the history
  • Loading branch information
kunyavskiy committed May 24, 2024
1 parent f061865 commit f76c839
Show file tree
Hide file tree
Showing 6 changed files with 242 additions and 0 deletions.
6 changes: 6 additions & 0 deletions config/_examples/dmoj-icpc/settings.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
{
"type": "dmoj",
"url": "https://dmoj.ca/",
"contestId": "waterloo2023f",
"apiKey": "$creds.dmoj"
}
37 changes: 37 additions & 0 deletions schemas/settings.schema.json
Original file line number Diff line number Diff line change
Expand Up @@ -337,6 +337,37 @@
],
"title": "codedrills"
},
"dmoj": {
"type": "object",
"properties": {
"type": {
"const": "dmoj",
"default": "dmoj"
},
"contestId": {
"type": "string"
},
"apiKey": {
"type": "string"
},
"emulation": {
"$ref": "#/$defs/org.icpclive.cds.settings.EmulationSettings?<kotlin.Double,InstantH>"
},
"network": {
"$ref": "#/$defs/org.icpclive.cds.settings.NetworkSettings?<kotlin.Boolean>"
},
"previousDays": {
"$ref": "#/$defs/kotlin.collections.ArrayList<org.icpclive.cds.settings.PreviousDaySettings>"
}
},
"additionalProperties": false,
"required": [
"type",
"contestId",
"apiKey"
],
"title": "dmoj"
},
"kotlin.collections.LinkedHashMap<kotlin.String,kotlin.Double>": {
"type": "object",
"patternProperties": {
Expand Down Expand Up @@ -658,6 +689,9 @@
{
"$ref": "#/$defs/codedrills"
},
{
"$ref": "#/$defs/dmoj"
},
{
"$ref": "#/$defs/ejudge"
},
Expand Down Expand Up @@ -783,6 +817,9 @@
{
"$ref": "#/$defs/codedrills"
},
{
"$ref": "#/$defs/dmoj"
},
{
"$ref": "#/$defs/ejudge"
},
Expand Down
1 change: 1 addition & 0 deletions settings.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,7 @@ val cdsPlugins = listOf(
"cms",
"codedrills",
"codeforces",
"dmoj",
"ejudge",
"eolymp",
"krsu",
Expand Down
1 change: 1 addition & 0 deletions src/cds/full/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ dependencies {
api(projects.cds.plugins.cms)
api(projects.cds.plugins.codedrills)
api(projects.cds.plugins.codeforces)
api(projects.cds.plugins.dmoj)
api(projects.cds.plugins.ejudge)
api(projects.cds.plugins.eolymp)
api(projects.cds.plugins.krsu)
Expand Down
8 changes: 8 additions & 0 deletions src/cds/plugins/dmoj/build.gradle.kts
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
plugins {
id("live.cds-plugin-conventions")
}

dependencies {
implementation(projects.cds.ktor)
implementation(projects.cds.utils)
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,189 @@
package org.icpclive.cds.plugins.dmoj

import kotlinx.datetime.Instant
import kotlinx.serialization.*
import org.icpclive.cds.*
import org.icpclive.cds.api.*
import org.icpclive.cds.ktor.*
import org.icpclive.cds.settings.*
import org.icpclive.ksp.cds.Builder
import kotlin.time.Duration.Companion.seconds

@Builder("dmoj")
public sealed interface DmojSettings : CDSSettings {
public val url: String
public val contestId: String
public val apiKey: Credential

override fun toDataSource(): ContestDataSource = DmojDataSource(this)
}

@Serializable
private class Error(val code: Int, val message: String)

@Serializable
private class Wrapper<T>(
val data: T? = null,
val error: Error? = null
) {
fun unwrap(): T {
if (error != null) throw IllegalStateException("Dmoj returned error: $error")
return data!!
}
}

@Serializable
private class ContestResponse(
val `object`: Contest
)

@Serializable
private class Contest(
val name: String,
val start_time: Instant,
val end_time: Instant,
val time_limit: Double?,
val format: Format,
val problems: List<Problem>,
val rankings: List<User>
)

@Serializable
private class Problem(
val points: Int,
val label: String,
val name: String,
val code: String,
)

@Serializable
private class User(
val user: String,
val start_time: Instant?,
val is_disqualified: Boolean?
)

@Serializable
private class Format(val name: String)

@Serializable
private class SubmissionsResult(
val has_more: Boolean,
val objects: List<Submission>,
)

@Serializable
private class Submission(
val id: Int,
val problem: String,
val user: String,
val date: Instant,
val points: Double?,
val result: String
)

internal class DmojDataSource(val settings: DmojSettings) : FullReloadContestDataSource(5.seconds) {

private val auth = ClientAuth.bearer(settings.apiKey.value)
private val contestInfoLoader = DataLoader.json<Wrapper<ContestResponse>>(
settings.network,
auth,
UrlOrLocalPath.Url(settings.url).subDir("api/v2/contest").subDir(settings.contestId)
).map { it.unwrap().`object` }

override suspend fun loadOnce(): ContestParseResult {
val contest = contestInfoLoader.load()

val contestLength = contest.time_limit?.seconds ?: (contest.end_time - contest.start_time)

val resultType = when (contest.format.name) {
"icpc" -> ContestResultType.ICPC
"ioi" -> ContestResultType.IOI
else -> error("Unknown contest format: ${contest.format.name}")
}
val startTimeMap = mutableMapOf<TeamId, Instant>()
val info = ContestInfo(
name = contest.name,
status = ContestStatus.byCurrentTime(contest.start_time, contestLength),
resultType = resultType,
startTime = contest.start_time,
contestLength = contestLength,
freezeTime = contestLength,
penaltyRoundingMode = when (resultType) {
ContestResultType.ICPC -> PenaltyRoundingMode.SUM_IN_SECONDS
ContestResultType.IOI -> PenaltyRoundingMode.ZERO
},
groupList = emptyList(),
teamList = contest.rankings.map {
TeamInfo(
id = it.user.toTeamId(),
displayName = it.user,
fullName = it.user,
groups = emptyList(),
hashTag = null,
medias = emptyMap(),
isHidden = it.is_disqualified == true,
isOutOfContest = false,
organizationId = null
).also { team ->
startTimeMap[team.id] = it.start_time ?: contest.start_time
}
},
organizationList = emptyList(),
problemList = contest.problems.mapIndexed { index, it ->
ProblemInfo(
id = it.code.toProblemId(),
displayName = it.label,
fullName = it.name,
ordinal = index,
minScore = if (resultType == ContestResultType.IOI) 0.0 else null,
maxScore = if (resultType == ContestResultType.IOI) it.points.toDouble() else null,
scoreMergeMode = if (resultType == ContestResultType.IOI) ScoreMergeMode.LAST_OK else null
)
}
)
val submissions = buildList {
for (problem in contest.problems) {
var page = 0
val loader = DataLoader.json<Wrapper<SubmissionsResult>>(
settings.network,
auth
) {
UrlOrLocalPath.Url(settings.url).subDir("api/v2/submissions?problem=${problem.code}&page=$page")
}.map { it.unwrap() }
while (true) {
++page
val data = loader.load()
for (submission in data.objects) {
val userStartTime = startTimeMap[submission.user.toTeamId()] ?: continue
val time = submission.date - userStartTime
if (time > contestLength) continue
val verdict = Verdict.lookup(
shortName = submission.result,
isAddingPenalty = submission.result != "CE" && submission.result != "AC",
isAccepted = submission.result == "AC"
)
val result = when (resultType) {
ContestResultType.ICPC -> {
verdict.toICPCRunResult()
}
ContestResultType.IOI -> RunResult.IOI(
listOf(submission.points ?: 0.0),
wrongVerdict = verdict.takeIf { submission.points == null }
)
}
add(RunInfo(
id = submission.id.toRunId(),
result = result,
problemId = submission.problem.toProblemId(),
teamId = submission.user.toTeamId(),
time = time
))
}
if (!data.has_more) break
}
}
}
return ContestParseResult(info, submissions, emptyList())
}
}

0 comments on commit f76c839

Please sign in to comment.