@@ -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