Skip to content

Commit b95e743

Browse files
committed
Add parsing for the player info in the header.
1 parent 2518ac4 commit b95e743

File tree

1 file changed

+167
-6
lines changed

1 file changed

+167
-6
lines changed

broodrep/src/lib.rs

Lines changed: 167 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -62,9 +62,9 @@ impl<R: Read + Seek> Replay<R> {
6262
// to repeat that step here)
6363
reader.read_u32::<LE>()?;
6464
if format == ReplayFormat::Modern121 {
65-
// This is the length of the "legacy" sections that don't have tags (so the `seRS`
66-
// magic is effectively its own tagged section). In older formats, the `reRS` is just
67-
// magic and no length is provided
65+
// This is the offset of the first section after the "legacy" sections, I guess as a
66+
// way to be able to skip them easily? In older formats, this offset is not present
67+
// (even though the Modern non-1.21 version does have other sections)
6868
reader.read_u32::<LE>()?;
6969
}
7070

@@ -170,11 +170,11 @@ impl<R: Read + Seek> Replay<R> {
170170
let engine = cursor.read_u8()?.into();
171171
let frames = cursor.read_u32::<LE>()?;
172172

173-
cursor.seek(SeekFrom::Current(3))?; // unknown/padding?
173+
cursor.seek(SeekFrom::Current(3))?; // replay_campaign_mission + 0x48 (lobby init command)
174174

175175
let start_time = cursor.read_u32::<LE>()?;
176176

177-
cursor.seek(SeekFrom::Current(12))?; // player bytes?
177+
cursor.seek(SeekFrom::Current(12))?; // player bytes
178178

179179
// TODO(tec27): Handle non-UTF-8 string formats
180180
let mut title = vec![0u8; 29];
@@ -214,6 +214,37 @@ impl<R: Read + Seek> Replay<R> {
214214
.to_string_lossy()
215215
.into_owned();
216216

217+
cursor.seek(SeekFrom::Current(38))?; // unknown
218+
219+
let players = (0..12)
220+
.map(|_i| {
221+
let slot_id = cursor.read_u16::<LE>()?;
222+
cursor.seek(SeekFrom::Current(2))?; // unknown
223+
let network_id = cursor.read_u8()?;
224+
cursor.seek(SeekFrom::Current(3))?; // unknown
225+
let player_type: PlayerType = cursor.read_u8()?.try_into()?;
226+
let race: Race = cursor.read_u8()?.try_into()?;
227+
let team = cursor.read_u8()?;
228+
let mut name = vec![0u8; 26];
229+
cursor.read_exact(&mut name[..25])?;
230+
let name = CStr::from_bytes_until_nul(&name)
231+
// This should never happen (we left an extra byte to ensure the null) but just
232+
// in case
233+
.map_err(|_e| BroodrepError::MalformedHeader("invalid player name"))?
234+
.to_string_lossy()
235+
.into_owned();
236+
237+
Ok::<Player, BroodrepError>(Player {
238+
slot_id,
239+
network_id,
240+
player_type,
241+
race,
242+
team,
243+
name,
244+
})
245+
})
246+
.collect::<Result<Vec<_>, _>>()?;
247+
217248
Ok(ReplayHeader {
218249
engine,
219250
frames,
@@ -227,6 +258,7 @@ impl<R: Read + Seek> Replay<R> {
227258
game_sub_type,
228259
host_name,
229260
map_name,
261+
slots: players,
230262
})
231263
}
232264
}
@@ -361,7 +393,105 @@ pub struct ReplayHeader {
361393
pub game_sub_type: u16,
362394
pub host_name: String,
363395
pub map_name: String,
364-
// TODO(tec27): Player data
396+
/// All of the slots in the game, including empty slots.
397+
pub slots: Vec<Player>,
398+
}
399+
400+
impl ReplayHeader {
401+
/// Returns an iterator over all of the filled slots in the game (not including observers).
402+
pub fn players(&self) -> impl Iterator<Item = &Player> {
403+
self.slots
404+
.iter()
405+
.filter(|p| !p.is_empty() && !p.is_observer())
406+
}
407+
408+
/// Returns an iterator over all of the filled observer slots in the game.
409+
pub fn observers(&self) -> impl Iterator<Item = &Player> {
410+
self.slots
411+
.iter()
412+
.filter(|p| !p.is_empty() && p.is_observer())
413+
}
414+
}
415+
416+
#[derive(Clone, Debug, Eq, PartialEq)]
417+
pub struct Player {
418+
/// ID of the map slot the player was placed in (post-randomization, if applicable).
419+
pub slot_id: u16,
420+
/// Network ID of the player. Computer players will be 255. Observers will be 128-131.
421+
pub network_id: u8,
422+
pub player_type: PlayerType,
423+
pub race: Race,
424+
pub team: u8,
425+
pub name: String,
426+
// TODO(tec27): implement colors
427+
}
428+
429+
impl Player {
430+
/// Returns true if this [Player] represents an empty slot.
431+
pub fn is_empty(&self) -> bool {
432+
self.name.is_empty()
433+
}
434+
435+
/// Returns true if this [Player] is an observer.
436+
pub fn is_observer(&self) -> bool {
437+
(128..=131).contains(&self.network_id)
438+
}
439+
}
440+
441+
#[derive(Copy, Clone, Debug, PartialEq, Eq, Hash)]
442+
pub enum PlayerType {
443+
Inactive = 0,
444+
Computer = 1,
445+
Human = 2,
446+
RescuePassive = 3,
447+
Unused = 4,
448+
ComputerControlled = 5,
449+
Open = 6,
450+
Neutral = 7,
451+
Closed = 8,
452+
}
453+
454+
impl TryFrom<u8> for PlayerType {
455+
type Error = BroodrepError;
456+
457+
fn try_from(value: u8) -> Result<Self, Self::Error> {
458+
match value {
459+
0 => Ok(PlayerType::Inactive),
460+
1 => Ok(PlayerType::Computer),
461+
2 => Ok(PlayerType::Human),
462+
3 => Ok(PlayerType::RescuePassive),
463+
4 => Ok(PlayerType::Unused),
464+
5 => Ok(PlayerType::ComputerControlled),
465+
6 => Ok(PlayerType::Open),
466+
7 => Ok(PlayerType::Neutral),
467+
8 => Ok(PlayerType::Closed),
468+
_ => Err(BroodrepError::MalformedHeader("invalid player type")),
469+
}
470+
}
471+
}
472+
473+
#[derive(Copy, Clone, Debug, PartialEq, Eq, Hash)]
474+
pub enum Race {
475+
Zerg = 0,
476+
Terran = 1,
477+
Protoss = 2,
478+
// NOTE(tec27): Generally this shouldn't be present for occupied slots in a replay (as it will
479+
// have been resolved by the replay write time), but for empty slots it may be
480+
Random = 6,
481+
}
482+
483+
impl TryFrom<u8> for Race {
484+
type Error = BroodrepError;
485+
486+
fn try_from(value: u8) -> Result<Self, Self::Error> {
487+
match value {
488+
0 => Ok(Race::Zerg),
489+
1 => Ok(Race::Terran),
490+
2 => Ok(Race::Protoss),
491+
6 => Ok(Race::Random),
492+
_ => Err(BroodrepError::MalformedHeader("invalid assigned race")),
493+
}
494+
}
365495
}
366496

367497
#[cfg(test)]
@@ -452,5 +582,36 @@ mod tests {
452582
replay.header.map_name,
453583
"\u{0007}제3세계(Third World) \u{0005}"
454584
);
585+
586+
assert_eq!(replay.header.slots.len(), 12);
587+
assert_eq!(
588+
replay.header.slots[0],
589+
Player {
590+
slot_id: 0,
591+
network_id: 0,
592+
player_type: PlayerType::Human,
593+
name: "u".into(),
594+
race: Race::Terran,
595+
team: 1,
596+
}
597+
);
598+
assert!(!replay.header.slots[0].is_observer());
599+
assert_eq!(
600+
replay.header.slots[1],
601+
Player {
602+
slot_id: 1,
603+
network_id: 255,
604+
player_type: PlayerType::Computer,
605+
name: "Sargas Tribe".into(),
606+
race: Race::Protoss,
607+
team: 1,
608+
}
609+
);
610+
assert!(replay.header.slots[2].is_empty());
611+
612+
let occupied = replay.header.players().collect::<Vec<_>>();
613+
assert_eq!(occupied.len(), 2);
614+
let observers = replay.header.observers().collect::<Vec<_>>();
615+
assert_eq!(observers.len(), 0);
455616
}
456617
}

0 commit comments

Comments
 (0)