本文简单记录下阅读icpc resolver源码的经历
因为在resolver的说明文档里所说的支持CDP的格式,其链接已经404了,在2.1版本试出来的格式又不支持,在网上搜寻他人使用Resolver的经验无果后迫不得已尝试阅读resolver的源码,以期待找出其支持的CDP格式。

Resolver的文档可以得知,Resolver支持两种形式的滚榜数据:以CDS为数据源的通过网络方式获取的,和以CDP为数据源的通过本地方式获取的。由于颁奖典礼的地方经常没有网络,而滚榜时也想加入队伍照片的展示,因此采用CDP格式是比较好的。

CDP格式

先说结果吧,Resolver2.4版本支持的CDP格式如下:

.
├── config // 非必需
│   ├── contest.yaml // 从domjudge Import/export页面导出即可
│   ├── groups.tsv // 从domjudge Import/export页面导出即可
│   ├── problemset.yaml
│   └── teams.tsv // 从domjudge Import/export页面导出即可
├── contest
│   ├── banner.png // resolver无用,但在cds放置于此就可显示banner
│   └── logo.png // resolver主页面的图片&无照片队伍的默认照片
├── events.xml // 滚榜数据
├── groups // Categories照片,但在resolver似乎没起到作用
│   └── 3 // Categories的id
│   └── logo.png
├── organizations // Affiliations照片,只要某Affiliations的队伍有logo,其他同Affiliations的队伍就都是该logo
│   ├── 3000 // 该Affiliations所对应的任一队伍的id
│   │   └── logo.png
│   ├── 3001
│   │   └── logo.png
│   ├── 3012
│   │   └── logo.png
│   ├── 3017
│   │   ├── country_flag.png // 照源码里是这样放置的,但在resolver似乎没起到作用
│   │   └── logo.png
│   └── 3187
│   └── logo.png
└── teams // 队伍照片
├── 3000 // 队伍的id
│   └── photo.png // 照片名字固定是photo
├── 3001
│   └── photo.png
├── 3009
│   └── photo.png
└── 3010
   └── photo.png

其中problemset.yaml格式如下:

problems:
- letter: A
short-name: A
color: yellow
rgb: '#ffff00'

- letter: B
short-name: B
color: red
rgb: '#ff0000'

- letter: C
short-name: C
color: green
rgb: '#00ff00'

该文件非必需。需有该文件,请务必确保其short-name为题号,因为resolver的一血奖’first-to-solve-A’中的A与该short-name对应,而不是与letter对应。


Resolver 2.1版本的CDP格式如下:

.
├── config // 非必需
│   ├── contest.yaml
│   ├── groups.tsv
│   ├── problemset.yaml
│   └── teams.tsv
├── events.xml // 滚榜数据,其与2.4版少了个<penalty>属性,2.4无此属性直接报ERROR(而2.1没事
└── images
   ├── logo // Affiliations的logo,数字为属于该Affiliations的任意队伍的id
   │   └── 3001.png
   └── team // 队伍照片
   ├── 3001.jpg // 数字为队伍的id
   ├── 3002.jpg
   ├── 3003.jpg
   ├── 3004.jpg
   └── 3005.jpg

其中滚榜数据events.xml的生成脚本可以用这个:https://github.com/Lanly109/icpc-resolver-from-domjudge

源码阅读

帮助

帮助文案就在比较显而易见的地方:Resolver/src/org/icpc/tools/resolver/Resolver.java

protected static void showHelp() {
System.out.println("Usage: resolver.bat/sh contestURL user password [options]");
System.out.println(" or: resolver.bat/sh contestPath [options]");
System.out.println();
System.out.println(" General options:");
System.out.println(" --info");
System.out.println(" Show additional info to presenter client");
System.out.println(" --speed speedFactor");
System.out.println(" Resolution delay multiplier. e.g. 0.5 will be twice");
System.out.println(" as fast, 2 will be twice as slow");
System.out.println(" --singleStep startRow");
System.out.println(" Require a click for each step starting at a specific");
System.out.println(" row, or for entire contest if no row specified");
System.out.println(" --rowDisplayOffset numRows");
System.out.println(" Move the display up the screen by some number of");
System.out.println(" rows (default 4)");
System.out.println(" --display #");
System.out.println(" Use the specified display");
System.out.println(" 1 = primary display, 2 = secondary display, etc.");
System.out.println(" --multi-display p@wxh");
System.out.println(" Stretch the presentation across multiple clients. Use \"2@3x2\"");
System.out.println(" to indicate this client is position 2 (top middle) in a 3x2 grid");
System.out.println(" --display_name template");
System.out.println(" Change the way teams are displayed using a template. Parameters:");
System.out.println(" {team.display_name), {team.name), {org.formal_name}, and {org.name}");
System.out.println(" --groups");
System.out.println(" Resolve only the groups in the given regex pattern for ids");
System.out.println(" If multiple groups are given, each is resolved separately");
System.out.println(" --pause #");
System.out.println(" Start at the given pause #. Useful for testing/preview");
System.out.println(" --judgeQueue");
System.out.println(" Start the resolution using a judge queue. Must have at least one list award");
System.out.println(" --test");
System.out.println(" Test on an unfinished contest. Ignores (removes) all unjudged runs");
System.out.println(" --light");
System.out.println(" Use light mode");
System.out.println(" --help");
System.out.println(" Shows this message");
System.out.println(" --version");
System.out.println(" Displays version information");
System.out.println();
System.out.println(" Client options:");
System.out.println(" --presenter");
System.out.println(" connect to a CDS and control it");
System.out.println(" --client");
System.out.println(" connect to a CDS in slave (view-only) mode");
System.out.println(" --side");
System.out.println(" same as --client, but displays logos suitable for");
System.out.println(" a lower resolution/side display");
System.out.println(" --team");
System.out.println(" same as --client, but displays minimal content, e.g.");
System.out.println(" to display on all team machines");

System.out.println();
System.out.println(" Keyboard shortcuts:");
System.out.println(" Ctrl-Q - Quit");
System.out.println(" r - Rewind");
System.out.println(" 0 - Restart (jump to beginning)");
System.out.println(" 2 - Fast forward (jump one step without delays)");
System.out.println(" 1 - Fast rewind (jump one step back without delays)");
System.out.println(" +/up - Speed up (reduce resolution delay)");
System.out.println(" -/down - Slow down (increase resolution delay)");
System.out.println(" j - Reset resolution speed");
System.out.println(" p - Pause/unpause scrolling");
System.out.println(" i - Toggle additional info");
}

上面提到了个选项judgeQueue,但我运行时加上这个参数似乎没什么效果,我以为是像在2018 ICPC World Final滚榜前展示的那个各队提交的通过情况judge queue一览。可能是我姿势不对。

xml文件读取

其代码位于ContestModel/src/org/icpc/tools/contest/model/feed/XMLFeedParser.java

上述events.xml可以参照该源码所给定的格式编写。

从第179行protected void createContestObject(Contest contest, String name, List<Property> list) 处可以看到它对各属性进行解析。

xml整体结构如下:

<contest>

<info>
</info>

<problem>
</problem>

<region>
</region>

<team>
</team>

<judgement>
</judgement>

<run>
</run>

<award>
</award>

<finalized>
</finalized>

</contest>

contest INFO

info的结构如下

<info>
<contest-id>4</contest-id>
<title>Contest Title</title>
<short-title>Contest Short Name</short-title>
<length>4:00:00.000</length>
<scoreboard-freeze-length>1:00:00.000</scoreboard-freeze-length>
<starttime>1650686700.0</starttime>
<penalty>20</penalty>
</info>

对应源码如下

if (INFO.equals(name)) {
Info info = new Info();
for (Property p : list) {
if ("contest-id".equals(p.name))
add(info, ID, p.value);
else if ("title".equals(p.name))
add(info, "formal_name", p.value);
else if ("short-title".equals(p.name))
add(info, "name", p.value);
else if ("starttime".equals(p.name)) {
try {
double d = Double.parseDouble(p.value);
add(info, "start_time", Timestamp.format((long) (d * 1000.0)));
} catch (Exception e) {
// ignore
}
} else if ("length".equals(p.name))
add(info, "duration", p.value);
else if ("scoreboard-freeze-length".equals(p.name))
add(info, "scoreboard_freeze_duration", p.value);
else if ("penalty".equals(p.name))
add(info, "penalty_time", p.value);
}

if (info.getId() == null)
add(info, ID, "id-" + Math.random());

if (info.getName() == null)
add(info, "name", info.getActualFormalName());

contest.add(info);
}

Problem

problem结构如下

<problem>
<id>1</id>
<label>A</label>
<name>Problem_Name</name>
</problem>

对应源码如下

 else if (PROBLEM.equals(name)) {
Problem problem = new Problem();
for (Property p : list) {
if (ID.equals(p.name)) {
try {
add(problem, "ordinal", (Integer.parseInt(p.value) - 1) + "");
} catch (Exception e) {
add(problem, "ordinal", p.value);
}
} else if (LABEL.equals(p.name))
add(problem, LABEL, p.value);
else if (LETTER.equals(p.name))
add(problem, LABEL, p.value);
else if (NAME.equals(p.name))
add(problem, NAME, p.value);
else if ("color".equals(p.name))
add(problem, "color", p.value);
else if ("rgb".equals(p.name))
add(problem, "rgb", p.value);
else if ("test_data_count".equals(p.name))
add(problem, "test_data_count", p.value);
}

// find a problem with matching ordinal
IProblem[] probs = contest.getProblems();
for (IProblem p : probs) {
if (p.getOrdinal() == problem.getOrdinal()) {
add(problem, ID, p.getId());
if (problem.getLabel() == null)
add(problem, LABEL, p.getLabel());
}
}
if (problem.getId() == null) {
// assume ordinal is an index
try {
// if no id, assume ordinal A = 0, B = 1, etc.
int i = problem.getOrdinal();
if (i >= 0 && i < LETTERS.length()) {
add(problem, ID, LETTERS.charAt(i) + "");
if (problem.getLabel() == null)
add(problem, LABEL, LETTERS.charAt(i) + "");
}
} catch (Exception e) {
// ignore
}
}

// last attempt: if no label, use the id
if (problem.getLabel() == null)
add(problem, LABEL, problem.getId());

contest.add(problem);
}

它会和problemset.yaml读取到的problem进行对应,对应方式是就是出现序号id

regision

regisiongroupcategory,其结构如下

<region>
<external-id>3</external-id>
<name>Participants</name>
</region>

对应源码如下

} else if (REGION.equals(name)) {
Group group = new Group();
for (Property p : list) {
if ("external-id".equals(p.name)) {
add(group, ICPC_ID, p.value);
add(group, ID, p.value);
} else if (NAME.equals(p.name))
add(group, NAME, p.value);
}

contest.add(group);
}

team

team的结构如下

<team>
<id>3000</id>
<external-id>3000</external-id>
<name>请问你今天要来点拿铁吗(Is_the_order_a_Honor)</name>
<university>兔之镇大学</university>
<university-short-name>兔之镇大学</university-short-name>
<region>Girls</region>
</team>

对应源码如下

 else if (TEAM.equals(name)) {
String instId = null;
Organization org = new Organization();
for (Property p : list) {
if (ID.equals(p.name)) {
add(org, ID, p.value);
instId = p.value;
} else if ("university".equals(p.name)) {
add(org, "formal_name", p.value);
} else if ("university-short-name".equals(p.name)) {
add(org, "name", p.value);
} else if ("nationality".equals(p.name)) {
add(org, "country", p.value);
}
}

if (org.getName() == null)
add(org, "name", org.getActualFormalName());

boolean exists = false;
for (IOrganization org2 : contest.getOrganizations()) {
if (org2.getActualFormalName().equals(org.getActualFormalName())) {
exists = true;
instId = org2.getId();
}
}

if (org.getName() == null || org.getName().isEmpty())
add(org, "name", org.getActualFormalName());
if (!exists)
contest.add(org);

Team team = new Team();
for (Property p : list) {
if (ID.equals(p.name)) {
add(team, ID, p.value);
} else if ("external-id".equals(p.name))
add(team, ICPC_ID, p.value);
else if (NAME.equals(p.name))
add(team, NAME, p.value);
else if ("region".equals(p.name)) {
IGroup[] groups = contest.getGroups();
for (IGroup g : groups) {
if (g.getName().equals(p.value))
add(team, "group_id", g.getId());
}
}
}

add(team, "organization_id", instId);

// if we already have a team name, let it stand
ITeam existing = contest.getTeamById(team.getId());
if (existing != null && existing.getName() != null)
add(team, NAME, existing.getName());

contest.add(team);
}

上述xml结构里没有nationality字段,但源码会对该字段解析,因此xml也可以加上这个信息只是似乎没用

judgement

judgement就是表示评测结果,其结构如下

<judgement>
<acronym>CE</acronym>
</judgement>

对应源码如下

 else if (JUDGEMENT.equals(name)) {
JudgementType type = new JudgementType();
for (Property p : list) {
if ("acronym".equals(p.name)) {
add(type, ID, p.value);
} else if (NAME.equals(p.name)) {
add(type, NAME, p.value);
} else if ("penalty".equals(p.name)) {
add(type, "penalty", p.value);
inferJudgementTypes = false;
} else if ("solved".equals(p.name)) {
add(type, "solved", p.value);
inferJudgementTypes = false;
}
}
contest.add(type);
}

penalty一般默认都是20分钟,solved字段没试过用来干嘛的,可能是表示另一种同样表示过题的状态?

run

一份run就是一次选手代码提交的信息,其xml结构如下

<run>
<id>1906</id>
<problem>1</problem>
<team>3021</team>
<judged>true</judged>
<result>WA</result>
<solved>false</solved>
<penalty>true</penalty>
<time>286.296</time>
</run>

对应源码如下

else if (RUN.equals(name)) {
Submission s = new Submission();
for (Property p : list) {
if (ID.equals(p.name)) {
add(s, ID, p.value);
} else if ("language".equals(p.name)) {
ILanguage[] langs = contest.getLanguages();
for (ILanguage l : langs) {
if (l.getName().equals(p.value))
add(s, "language_id", l.getId());
}
} else if ("problem".equals(p.name)) {
String pId = p.value;
try {
pId = (Integer.parseInt(pId) - 1) + "";
} catch (Exception e) {
//
}
IProblem[] probs = contest.getProblems();
for (IProblem pp : probs) {
if (pId.equals(pp.getOrdinal() + "")) {
pId = pp.getId();
break;
}
}
add(s, "problem_id", pId);
} else if ("team".equals(p.name)) {
add(s, "team_id", p.value);
} else if (TIME.equals(p.name)) {
add(s, CONTEST_TIME, RelativeTime.format(RelativeTime.parseOld(p.value)));
} else if (TIMESTAMP.equals(p.name)) {
add(s, TIME, Timestamp.format(Timestamp.parseOld(p.value)));
}
}

// don't change submission time
ISubmission oldS = contest.getSubmissionById(s.getId());
if (oldS != null)
add(s, TIME, Timestamp.format(oldS.getTime()));

checkContestState(contest, s.getContestTime());
contest.add(s);

for (Property pp : list) {
if ("judged".equals(pp.name) && "true".equalsIgnoreCase(pp.value)) {
Judgement sj = new Judgement();
boolean solved = false;
boolean penalty = false;
IJudgementType type = null;
for (Property p : list) {
if (ID.equals(p.name)) {
add(sj, ID, p.value);
add(sj, "submission_id", p.value);
} else if (TIME.equals(p.name)) {
add(sj, START_CONTEST_TIME, RelativeTime.format(RelativeTime.parseOld(p.value)));
add(sj, END_CONTEST_TIME, RelativeTime.format(RelativeTime.parseOld(p.value)));
} else if (TIMESTAMP.equals(p.name)) {
add(sj, START_TIME, Timestamp.format(Timestamp.parseOld(p.value)));
add(sj, END_TIME, Timestamp.format(Timestamp.parseOld(p.value)));
} else if ("result".equals(p.name)) {
add(sj, "judgement_type_id", p.value);
type = contest.getJudgementTypeById(p.value);
} else if ("solved".equals(p.name)) {
solved = "true".equalsIgnoreCase(p.value);
} else if ("penalty".equals(p.name)) {
penalty = "true".equalsIgnoreCase(p.value);
}
}
if (inferJudgementTypes && type != null && (solved || penalty)) {
boolean update = false;
JudgementType typeMatch = (JudgementType) ((JudgementType) type).clone();
if (solved && !typeMatch.isSolved()) {
add(typeMatch, "solved", "true");
update = true;
}
if (penalty && !typeMatch.isPenalty()) {
add(typeMatch, "penalty", "true");
update = true;
}
if (update)
contest.add(typeMatch);
}
checkContestState(contest, sj.getStartContestTime());
checkContestState(contest, sj.getEndContestTime());
contest.add(sj);
}
}
}

这部分没多大细看,应该逻辑也不复杂。

award

award表示颁发的获奖信息,其xml结构如下

<award>
<id>gold-medal</id>
<citation>Gold Medalist</citation>
<show>true</show>
<teamId>3167</teamId>
<teamId>3204</teamId>
<teamId>3036</teamId>
<teamId>3009</teamId>
</award>

注意同类型的获奖的所有队伍都必须写在同一个award中,否则会覆盖。比如前一个awardgold-medal,后一个award也是gold-medal,那么最终颁给gold-medal的队伍是后一个award对应的队伍。

对应源码如下:

else if (AWARD.equals(name)) {
Award award = new Award();
List<String> teamIds = new ArrayList<>();
for (Property p : list) {
if ("teamId".equals(p.name))
teamIds.add(p.value);
else if (ID.equals(p.name))
add(award, ID, p.value);
else if ("citation".equals(p.name))
add(award, p.name, p.value);
else if ("show".equals(p.name))
add(award, "show", p.value);
}
award.add("team_ids", "[\"" + String.join("\",\"", teamIds) + "\"]");

contest.add(award);
}

idresolver定义好的一些获奖类型,citation是在滚榜程序显示的文字。

finalized

finalized由于国内存在打星队伍等情况,一般就没用到了,其xml结构如下

<finalized>
<last-gold>0</last-gold>
<last-silver>0</last-silver>
<last-bronze>0</last-bronze>
<timestamp>0</timestamp>
</finalized>

对应源码如下

else if (FINALIZED.equals(name)) {
int gold = 4;
int silver = 4;
int bronze = 4;
for (Property p : list) {
if ("last-gold".equals(p.name))
gold = Integer.parseInt(p.value);
else if ("last-silver".equals(p.name))
silver = Integer.parseInt(p.value);
else if ("last-bronze".equals(p.name))
bronze = Integer.parseInt(p.value);
}

// convert to actual numbers, not last
bronze -= silver;
silver -= gold;

State state = (State) contest.getState();
Long startTime = contest.getStartTime();
if (state != null && !state.isFinal() && startTime != null) { // end the contest
state = (State) state.clone();
Long time = startTime + contest.getDuration();
state.setEnded(time);
state.setThawed(time);
state.setFinalized(time);
contest.add(state);

// add awards
AwardUtil.createMedalAwards(contest, gold, silver, bronze);

// end of updates
state = (State) state.clone();
state.setEndOfUpdates(time);
contest.add(state);
}
// contest.add(f);
}

其他字段

除了上述字段之外,从源码中可以看到其实还有其他字段

testcase

其源码如下

else if (TESTCASE.equals(name)) {
Run run = new Run();
String i = null;
String n = null;
String runId = null;
for (Property p : list) {
if ("i".equals(p.name)) {
i = p.value;
add(run, "ordinal", p.value);
// add(run, "i", p.value);
} else if ("n".equals(p.name)) {
n = p.value;
// add(run, "n", p.value);
} else if ("run-id".equals(p.name)) {
runId = p.value;
add(run, "judgement_id", p.value);
} else if ("judgement".equals(p.name)) {
add(run, "judgement_type_id", p.value);
} else if (TIME.equals(p.name)) {
add(run, CONTEST_TIME, RelativeTime.format(RelativeTime.parseOld(p.value)));
} else if (TIMESTAMP.equals(p.name)) {
add(run, TIME, Timestamp.format(Timestamp.parseOld(p.value)));
}
}
checkContestState(contest, run.getContestTime());
add(run, ID, runId + "-" + i);

ISubmission s = contest.getSubmissionById(runId);
if (s != null) {
IProblem p = contest.getProblemById(s.getProblemId());
int nn = Integer.parseInt(n);
if (p != null && p.getTestDataCount() < nn) {
Problem pp = (Problem) ((Problem) p).clone();
add(pp, "test_data_count", n);
contest.add(pp);
}
}

// make sure judgement exists before runs that refer to it
IJudgement j = contest.getJudgementById(runId);
if (j == null) {
Judgement sj = new Judgement();
add(sj, "id", runId);
add(sj, "submission_id", runId);

ISubmission ss = contest.getSubmissionById(runId);
if (ss != null) {
add(sj, START_CONTEST_TIME, RelativeTime.format(ss.getContestTime()));
add(sj, START_TIME, Timestamp.format(ss.getTime()));
}
contest.add(sj);
}

contest.add(run);
}

clar

其源码如下

else if (CLAR.equals(name)) {
Clarification clar = new Clarification();

String id = null;
String question = null;
String answer = null;
String team = null;
boolean toAll = false;
for (Property p : list) {
if (ID.equals(p.name)) {
id = p.value;
} else if ("team".equals(p.name)) {
team = p.value;
} else if ("problem".equals(p.name)) {
String pId = p.value;
try {
pId = (Integer.parseInt(p.value) - 1) + "";
} catch (Exception e) {
// ignore
}

IProblem[] probs = contest.getProblems();
for (IProblem pr : probs)
if ((pr.getOrdinal() + "").equals(pId))
add(clar, "problem_id", pr.getId());
if (clar.getProblemId() == null)
add(clar, "problem_id", pId);
} else if ("question".equals(p.name)) {
question = p.value;
} else if ("answer".equals(p.name)) {
answer = p.value;
} else if ("to-all".equals(p.name)) {
toAll = Boolean.valueOf(p.value);
} else if (TIME.equals(p.name)) {
add(clar, CONTEST_TIME, RelativeTime.format(RelativeTime.parseOld(p.value)));
} else if (TIMESTAMP.equals(p.name)) {
add(clar, TIME, Timestamp.format(Timestamp.parseOld(p.value)));
}
}
if (answer != null && !answer.trim().isEmpty()) {
add(clar, ID, id + "-reply");
add(clar, "reply_to_id", id);
add(clar, "text", answer);
if (!toAll)
add(clar, "to_team_id", team);
} else {
add(clar, ID, id);
add(clar, "text", question);
add(clar, "from_team_id", team);
}
contest.add(clar);
}

获奖类型

获奖类型的定义在ContestModel/src/org/icpc/tools/contest/model/IAward.java

共有以下十一种获奖类型

AwardType WINNER = new AwardType("Winner", "winner");
AwardType RANK = new AwardType("Rank", "rank-.*");
AwardType MEDAL = new AwardType("Medal", ".*-medal");
AwardType FIRST_TO_SOLVE = new AwardType("First to Solve", "first-to-solve-.*");
AwardType GROUP = new AwardType("Group Winner", "group-winner-.*");
AwardType ORGANIZATION = new AwardType("Organization Winner", "organization-winner-.*");
AwardType GROUP_HIGHLIGHT = new AwardType("Group Highlight", "group-highlight-.*");
AwardType SOLVED = new AwardType("Solved", "solved-.*");
AwardType TOP = new AwardType("Top", "top-.*");
AwardType HONORS = new AwardType("Honors", "honors-.*");
// AwardType HONORABLE_MENTION = new AwardType("Honorable Mention", "honorable-mention");
AwardType OTHER = new AwardType("Other", ".*");

AwardType[] KNOWN_TYPES = new AwardType[] { WINNER, RANK, MEDAL, FIRST_TO_SOLVE, GROUP, ORGANIZATION,
GROUP_HIGHLIGHT, SOLVED, TOP, HONORS, OTHER };

AwardType第二个参数就是正则匹配规则。打铁奖还被注释掉了

不同类型的获奖规则呈现在resolver上的话就是排序顺序和文字颜色和是否粗体的不同。

yaml文件

yaml读取的源代码在ContestModel/src/org/icpc/tools/contest/model/internal/YamlParser.java

contest.yaml

该文件可以直接从domjudgeimport/export页面导出。

其源码如下

Info info = new Info();
info.add("id", "1");
for (Object ob : map.keySet()) {
if (ob instanceof String) {
String key = (String) ob;
Object val = map.get(key);
String value = null;
if (val != null)
value = val.toString();

try {
if ("name".equals(key) && oldFormat)
info.add("formal_name", value);
else if ("short-name".equals(key))
info.add("name", value);
else if ("length".equals(key) || "duration".equals(key)) {
int length = RelativeTime.parse(value);
if (length >= 0)
info.add("duration", RelativeTime.format(length));
} else if ("scoreboard-freeze".equals(key)) {
int length = RelativeTime.parse(value);
int d = info.getDuration();
if (length >= 0 && d > 0)
info.add("scoreboard_freeze_duration", RelativeTime.format(d / 1000 - length));
} else if ("scoreboard-freeze-length".equals(key)) {
int length = RelativeTime.parse(value);
if (length >= 0)
info.add("scoreboard_freeze_duration", RelativeTime.format(length));
} else if ("penalty-time".equals(key)) {
info.add("penalty_time", value);
} else if ("start-time".equals(key)) {
info.add("start_time", value);
} else if ("banner".equals(key)) {
info.setBanner(parseFileReferenceList((List<?>) val));
} else if ("logo".equals(key)) {
info.setLogo(parseFileReferenceList((List<?>) val));
} else
info.add(key, value);
} catch (Exception ex) {
Trace.trace(Trace.ERROR, "Could not parse " + key + ": " + value);
}
}
}

从源码可以看出来其实从domjudge导出的contest.yaml有很多字段是没必要的。

problemset.yaml

problemset就要自己编写了。其格式上面提到。

for (Object o : list) {
if (o instanceof Map<?, ?>) {
Map<?, ?> map = (Map<?, ?>) o;

Problem problem = new Problem();
problem.add("ordinal", "" + i);
i++;

for (Object ob : map.keySet()) {
if (ob instanceof String) {
String key = (String) ob;
Object val = map.get(key);
String value = null;
if (val != null)
value = val.toString();

if ("letter".equals(key))
problem.add("label", value);
else if ("short-name".equals(key)) {
if (problem.getId() == null)
problem.add("id", value);
if (problem.getName() == null)
problem.add("name", value);
} else
problem.add(key, value);
}
}

if (problem.getId() != null && (problem.getTestDataCount() <= 0 || problem.getTimeLimit() <= 0)) {
File problemFolder = new File(f.getParentFile(), problem.getId());
if (problemFolder.exists()) {
addProblemTestDataCount(problemFolder, problem);
addProblemTimeLimit(problemFolder, problem);
try {
importProblem(problemFolder, problem);
} catch (Exception e) {
// ignore for now
}
}
}
problems.add(problem);
}
}

从源码可以看出,它是把short-name作为problemid的。而在分析一血奖时,它会根据first-to-solve-A中的A和该id进行匹配,若未匹配到,则会报告无法解析一血奖。

分析(呈现)获奖的代码在PresContest/src/org/icpc/tools/presentation/contest/internal/presentations/resolver/TeamAwardPresentation.java

402行处,

for (IAward a : currentCache.awards) {
if (a.getAwardType() == IAward.FIRST_TO_SOLVE) {
String pId = a.getId().substring(patternLen);
IProblem p = getContest().getProblemById(pId);
if (p == null) {
Trace.trace(Trace.WARNING, "Could not consolidate FTS award: " + a.getId());
continue;
}
fts.add(p.getLabel());
if (a.getDisplayMode() != null)
mode = a.getDisplayMode();
} else if (a.getId().contains("solution")) {
hasSolutionAward = true;
list.add(a);
} else {
if (a.getAwardType() == IAward.MEDAL)
hasMedal = true;
list.add(a);
}
}

pid未找到则会报告Could not consolidate FTS award

资源路径

资源路径相关的代码位于ContestModel/src/org/icpc/tools/contest/model/feed/DiskContestSource.java

823行的函数protected FilePattern getLocalPattern(IContestObject.ContestType type, String id, String property)

protected FilePattern getLocalPattern(IContestObject.ContestType type, String id, String property) {
if (type == ContestType.CONTEST) {
if (LOGO.equals(property))
return new FilePattern(null, id, property, LOGO_EXTENSIONS);
if (BANNER.equals(property))
return new FilePattern(null, id, property, LOGO_EXTENSIONS);
} else if (type == ContestType.TEAM) {
if (PHOTO.equals(property))
return new FilePattern(type, id, property, PHOTO_EXTENSIONS);
if (VIDEO.equals(property))
return new FilePattern(type, id, property, "m2ts");
if (BACKUP.equals(property))
return new FilePattern(type, id, property, "zip");
if (KEY_LOG.equals(property))
return new FilePattern(type, id, property, "txt");
if (TOOL_DATA.equals(property))
return new FilePattern(type, id, property, "txt");
} else if (type == ContestType.PERSON) {
if (PHOTO.equals(property))
return new FilePattern(type, id, property, PHOTO_EXTENSIONS);
} else if (type == ContestType.ORGANIZATION) {
if (LOGO.equals(property))
return new FilePattern(type, id, property, LOGO_EXTENSIONS);
if (COUNTRY_FLAG.equals(property))
return new FilePattern(type, id, property, LOGO_EXTENSIONS);
} else if (type == ContestType.SUBMISSION) {
if (FILES.equals(property))
return new FilePattern(type, id, property, "zip");
if (REACTION.equals(property))
return new FilePattern(type, id, property, "m2ts");
} else if (type == ContestType.GROUP) {
if (LOGO.equals(property))
return new FilePattern(type, id, property, LOGO_EXTENSIONS);
}
return null;
}

其中contestType定义在ContestModel/src/org/icpc/tools/contest/model/IContestObject.java

enum ContestType {
CONTEST, LANGUAGE, GROUP, ORGANIZATION, TEAM, STATE, RUN, SUBMISSION, JUDGEMENT, CLARIFICATION, AWARD, JUDGEMENT_TYPE, TEST_DATA, PROBLEM, PAUSE, TEAM_MEMBER, MAP_INFO, START_STATUS, COMMENTARY
}

String[] ContestTypeNames = new String[] { "contests", "languages", "groups", "organizations", "teams", "state",
"runs", "submissions", "judgements", "clarifications", "awards", "judgement-types", "testdata", "problems",
"pause", "team-members", "map-info", "start-status", "commentary" };

而大写变量就定义在DiskContestSource.java的开头处

private static final String LOGO = "logo";
private static final String PHOTO = "photo";
private static final String VIDEO = "video";
private static final String BANNER = "banner";
private static final String BACKUP = "backup";
private static final String KEY_LOG = "key_log";
private static final String TOOL_DATA = "tool_data";
private static final String FILES = "files";
private static final String REACTION = "reaction";
private static final String COUNTRY_FLAG = "country_flag";

private static final String[] LOGO_EXTENSIONS = new String[] { "png", "svg", "jpg", "jpeg" };
private static final String[] PHOTO_EXTENSIONS = new String[] { "jpg", "jpeg", "png", "svg" };

以队伍照片为例,为看清它从哪里读取队伍照片,我们找到这个入口,其代码为

else if (type == ContestType.TEAM) {
if (PHOTO.equals(property))
return new FilePattern(type, id, property, PHOTO_EXTENSIONS);

FilePattern的定义就在该文件的78行。

static class FilePattern {
// the folder containing the file
protected String folder;

// the file name, without extension
protected String name; // e.g. "logo" or "files"

// the file extensions
protected String[] extensions; // e.g. "jpg" or "zip"

// the partial url to access the file
protected String url;

public FilePattern(IContestObject.ContestType type, String id, String property, String fileExtension) {
this(type, id, property, new String[] { fileExtension });
}

public FilePattern(IContestObject.ContestType type, String id, String property, String[] fileExtensions) {
if (type == null) {
this.folder = "contest";
this.url = property;
} else {
String typeName = IContestObject.getTypeName(type);
this.folder = typeName + File.separator + id;
this.url = typeName + "/" + id + "/" + property;
}

this.name = property;
this.extensions = fileExtensions;
}

@Override
public String toString() {
return folder + " " + name + " " + String.join(",", extensions) + " " + url;
}
}

从构造函数可以看出,其定义的路径为typeName/id/property

再看传入的参数,typeContestType.TEAM,即teamsid为队伍的icpc idpropertyPHOTOphoto,文件后缀名为PHOTO_EXTENSIONSjpg, jpeg, png, svg都可以。

因此队伍的icpc id3000的队伍照片应放置于teams/3000/photo.png,相对于CDP根目录,即events.xml所在的目录。

以同样的方式可以得出以下资源的放置路径:

  • 队伍录像为teams/3000/video.m2ts
  • 队伍备份为teams/3000/backup.zip
  • 队伍KEY_LOG为teams/3000/key_log.txt
  • 队伍TOOL_DATA为teams/3000/tool_data.txt
  • 个人照片为persons/id/photo.png(暂不清楚该id是什么)
  • 组织(ORGANIZATION、Affiliations)logo为organizations/3000/logo.png
  • 组织(ORGANIZATION、Affiliations)country_flag为organizations/3000/country_flag.png
  • 提交文件为submissions/id/files.zip(暂不清楚该id是什么)
  • 提交的reaction为submissions/id/reaction.m2ts(暂不清楚该id是什么)
  • 组别(group、Categories)的logo为groups/id/logo.png(暂不清楚该id是什么)
  • 比赛的logo为contest/logo.png
  • 比赛的banner为contest/banner.png

读取的yaml

该文件再往下看,第974行的函数protected void loadConfigFiles() 可以看到其读取的yaml文件有

  • contest.yaml
  • accounts.yaml
  • problemset.yaml
  • groups.tsv
  • institutions.tsv
  • teams.tsv
  • members.tsv

resolver.tsv

如果查看resolver的运行日志log的话,会发现一开始resolver似乎在尝试寻找叫resolver.tsv的文件

2022.04.30 20:10.03 I Connection failed to resolver.tsv, trying again
java.net.MalformedURLException: no protocol: nullresolver.tsv
at java.base/java.net.URL.<init>(URL.java:674)
at java.base/java.net.URL.<init>(URL.java:569)
at java.base/java.net.URL.<init>(URL.java:516)
at org.icpc.tools.contest.model.feed.RESTContestSource.createConnection(RESTContestSource.java:223)
at org.icpc.tools.contest.model.feed.RESTContestSource.downloadIfNecessaryImpl(RESTContestSource.java:316)
at org.icpc.tools.contest.model.feed.RESTContestSource.downloadIfNecessary(RESTContestSource.java:291)
at org.icpc.tools.contest.model.feed.RESTContestSource.getFile(RESTContestSource.java:244)
at org.icpc.tools.resolver.Resolver.loadSteps(Resolver.java:450)
at org.icpc.tools.resolver.Resolver.main(Resolver.java:199)
2022.04.30 20:10.03 I Connection failed to resolver.tsv again, trying again after 500ms
java.net.MalformedURLException: no protocol: nullresolver.tsv
at java.base/java.net.URL.<init>(URL.java:674)
at java.base/java.net.URL.<init>(URL.java:569)
at java.base/java.net.URL.<init>(URL.java:516)
at org.icpc.tools.contest.model.feed.RESTContestSource.createConnection(RESTContestSource.java:223)
at org.icpc.tools.contest.model.feed.RESTContestSource.downloadIfNecessaryImpl(RESTContestSource.java:316)
at org.icpc.tools.contest.model.feed.RESTContestSource.downloadIfNecessary(RESTContestSource.java:291)
at org.icpc.tools.contest.model.feed.RESTContestSource.getFile(RESTContestSource.java:244)
at org.icpc.tools.resolver.Resolver.loadSteps(Resolver.java:450)
at org.icpc.tools.resolver.Resolver.main(Resolver.java:199)

在源码中搜索该resolver.tsv的话,在Resolver/src/org/icpc/tools/resolver/Resolver.java中的loadSteps函数可以看到其文件输入会被读入到一个叫predeterminedSteps的变量。

String s = br.readLine();
while (s != null) {
String[] st = s.split("\\t");
if (st != null && st.length > 0)
predeterminedSteps.add(new PredeterminedStep(st[0], st[1]));

s = br.readLine();
}

从这里我们可以看到其格式就是一行用\t分隔开的两个数,从PredeterminedStep的构造函数public PredeterminedStep(String teamId, String problemLabel)我们就可以看出来这两个数分别表示teamidproblemLabel

继续搜索该变量名predeterminedSteps,会在ContestModel/src/org/icpc/tools/contest/model/resolver/ResolverLogic.javagetNextResolve函数看到它起到的作用

private SubmissionInfo getNextResolve() {
ITeam[] teams = contest.getOrderedTeams();
int numProblems = contest.getNumProblems();
for (int i = teams.length - 1; i >= 0; i--) {
ITeam team = teams[i];

// check for predetermined steps first
for (PredeterminedStep ps : predeterminedSteps) {
if (ps.teamId.equals(team.getId())) {
int pInd = contest.getProblemIndexByLabel(ps.problemLabel);
if (pInd >= 0) {
IResult r1 = contest.getResult(team, pInd);
if (r1.getStatus() == Status.SUBMITTED) {
return new SubmissionInfo(team, pInd);
}
}
}
}

// otherwise, default to pick left
for (int j = 0; j < numProblems; j++) {
IResult r1 = contest.getResult(team, j);
if (r1.getStatus() == Status.SUBMITTED) {
return new SubmissionInfo(team, j);
}
}
}
return null;
}

从该代码的逻辑可以得知,在滚榜、揭晓各队伍提交结果的时候,一般默认是队伍从左到右的题目的提交结果,而我们通过该resolver.tsv就可以自定义揭晓的顺序,而不是默认的从左到右。

结尾

感谢你能看到最后。相信阅读完这些你会对icpc tools的源码有了进一步的了解。无论是文档是否未跟上,还是文档所引述的标准莫名404了,只要源码是开源的,终有办法摸清其内在的逻辑。无论之后resolver的代码如何变化,本文对阅读其源码,找到自己想要的信息有比较好的指引。