server compiles
This commit is contained in:
parent
e9d4f04044
commit
5c82560d76
3497
Cargo.lock
generated
3497
Cargo.lock
generated
File diff suppressed because it is too large
Load diff
|
|
@ -5,4 +5,4 @@ members = [
|
||||||
"client",
|
"client",
|
||||||
"server",
|
"server",
|
||||||
"store"
|
"store"
|
||||||
]
|
]
|
||||||
|
|
|
||||||
|
|
@ -4,8 +4,9 @@
|
||||||
# https://devenv.sh/basics/
|
# https://devenv.sh/basics/
|
||||||
# env.GREET = "devenv";
|
# env.GREET = "devenv";
|
||||||
|
|
||||||
# https://devenv.sh/packages/
|
packages = [
|
||||||
packages = [ pkgs.git pkgs.git-bug ];
|
pkgs.alsaLib pkgs.udev # for bevy
|
||||||
|
];
|
||||||
|
|
||||||
# enterShell = ''
|
# enterShell = ''
|
||||||
# hello
|
# hello
|
||||||
|
|
|
||||||
|
|
@ -11,3 +11,4 @@ env_logger = "0.10.0"
|
||||||
log = "0.4.20"
|
log = "0.4.20"
|
||||||
pico-args = "0.5.0"
|
pico-args = "0.5.0"
|
||||||
renet = "0.0.13"
|
renet = "0.0.13"
|
||||||
|
bincode = "1.3.3"
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,8 @@
|
||||||
use log::{info, trace};
|
use log::{info, trace, warn};
|
||||||
|
use std::thread;
|
||||||
|
use bincode;
|
||||||
use std::net::{SocketAddr, UdpSocket, IpAddr, Ipv4Addr};
|
use std::net::{SocketAddr, UdpSocket, IpAddr, Ipv4Addr};
|
||||||
use std::time::{Duration, Instant, SystemTime};
|
use std::time::{Duration, Instant, SystemTime};
|
||||||
use store::EndGameReason;
|
|
||||||
|
|
||||||
use renet::{
|
use renet::{
|
||||||
transport::{
|
transport::{
|
||||||
|
|
@ -14,6 +15,16 @@ use renet::{
|
||||||
// This can be used to make sure players use the most recent version of the client for instance.
|
// This can be used to make sure players use the most recent version of the client for instance.
|
||||||
pub const PROTOCOL_ID: u64 = 2878;
|
pub const PROTOCOL_ID: u64 = 2878;
|
||||||
|
|
||||||
|
/// Utility function for extracting a players name from renet user data
|
||||||
|
fn name_from_user_data(user_data: &[u8; NETCODE_USER_DATA_BYTES]) -> String {
|
||||||
|
let mut buffer = [0u8; 8];
|
||||||
|
buffer.copy_from_slice(&user_data[0..8]);
|
||||||
|
let mut len = u64::from_le_bytes(buffer) as usize;
|
||||||
|
len = len.min(NETCODE_USER_DATA_BYTES - 8);
|
||||||
|
let data = user_data[8..len + 8].to_vec();
|
||||||
|
String::from_utf8(data).unwrap()
|
||||||
|
}
|
||||||
|
|
||||||
fn main() {
|
fn main() {
|
||||||
env_logger::init();
|
env_logger::init();
|
||||||
|
|
||||||
|
|
@ -33,6 +44,7 @@ fn main() {
|
||||||
|
|
||||||
trace!("❂ TricTrac server listening on {}", SERVER_ADDR);
|
trace!("❂ TricTrac server listening on {}", SERVER_ADDR);
|
||||||
|
|
||||||
|
let mut game_state = store::GameState::default();
|
||||||
let mut last_updated = Instant::now();
|
let mut last_updated = Instant::now();
|
||||||
loop {
|
loop {
|
||||||
// Update server time
|
// Update server time
|
||||||
|
|
@ -68,7 +80,7 @@ fn main() {
|
||||||
// Tell all players that a new player has joined
|
// Tell all players that a new player has joined
|
||||||
server.broadcast_message(0, bincode::serialize(&event).unwrap());
|
server.broadcast_message(0, bincode::serialize(&event).unwrap());
|
||||||
|
|
||||||
info!("Client {} connected.", id);
|
info!("Client {} connected.", client_id);
|
||||||
// In TicTacTussle the game can begin once two players has joined
|
// In TicTacTussle the game can begin once two players has joined
|
||||||
if game_state.players.len() == 2 {
|
if game_state.players.len() == 2 {
|
||||||
let event = store::GameEvent::BeginGame { goes_first: client_id };
|
let event = store::GameEvent::BeginGame { goes_first: client_id };
|
||||||
|
|
@ -86,7 +98,7 @@ fn main() {
|
||||||
|
|
||||||
// Then end the game, since tic tac toe can't go on with a single player
|
// Then end the game, since tic tac toe can't go on with a single player
|
||||||
let event = store::GameEvent::EndGame {
|
let event = store::GameEvent::EndGame {
|
||||||
reason: EndGameReason::PlayerLeft { player_id: client_id },
|
reason: store::EndGameReason::PlayerLeft { player_id: client_id },
|
||||||
};
|
};
|
||||||
game_state.consume(&event);
|
game_state.consume(&event);
|
||||||
server.broadcast_message(0, bincode::serialize(&event).unwrap());
|
server.broadcast_message(0, bincode::serialize(&event).unwrap());
|
||||||
|
|
@ -120,6 +132,6 @@ fn main() {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
server.send_packets().unwrap();
|
transport.send_packets(&mut server);
|
||||||
thread::sleep(Duration::from_millis(50));}
|
thread::sleep(Duration::from_millis(50));}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,9 +1,10 @@
|
||||||
[package]
|
[package]
|
||||||
name = "trictrac"
|
name = "store"
|
||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
edition = "2021"
|
edition = "2021"
|
||||||
|
|
||||||
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
|
rand = "0.8.5"
|
||||||
serde = { version = "1.0", features = ["derive"] }
|
serde = { version = "1.0", features = ["derive"] }
|
||||||
|
|
|
||||||
|
|
@ -80,7 +80,7 @@ impl Board {
|
||||||
/// If the field is blocked for the player, an error is returned. If the field is not blocked,
|
/// If the field is blocked for the player, an error is returned. If the field is not blocked,
|
||||||
/// but there is already one checker from the other player on the field, that checker is hit and
|
/// but there is already one checker from the other player on the field, that checker is hit and
|
||||||
/// moved to the bar.
|
/// moved to the bar.
|
||||||
pub fn set(&mut self, player: Player, field: usize, amount: i8) -> Result<(), Error> {
|
pub fn set(&mut self, player: &Player, field: usize, amount: i8) -> Result<(), Error> {
|
||||||
if field > 23 {
|
if field > 23 {
|
||||||
return Err(Error::FieldInvalid);
|
return Err(Error::FieldInvalid);
|
||||||
}
|
}
|
||||||
|
|
@ -112,7 +112,7 @@ impl Board {
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Check if a field is blocked for a player
|
/// Check if a field is blocked for a player
|
||||||
pub fn blocked(&self, player: Player, field: usize) -> Result<bool, Error> {
|
pub fn blocked(&self, player: &Player, field: usize) -> Result<bool, Error> {
|
||||||
if field > 23 {
|
if field > 23 {
|
||||||
return Err(Error::FieldInvalid);
|
return Err(Error::FieldInvalid);
|
||||||
}
|
}
|
||||||
|
|
@ -137,7 +137,7 @@ impl Board {
|
||||||
|
|
||||||
/// Set checkers for a player off the board. This method adds amount to the already existing
|
/// Set checkers for a player off the board. This method adds amount to the already existing
|
||||||
/// checkers there.
|
/// checkers there.
|
||||||
pub fn set_off(&mut self, player: Player, amount: u8) -> Result<(), Error> {
|
pub fn set_off(&mut self, player: &Player, amount: u8) -> Result<(), Error> {
|
||||||
match player.color {
|
match player.color {
|
||||||
Color::White => {
|
Color::White => {
|
||||||
let new = self.raw_board.0.off + amount;
|
let new = self.raw_board.0.off + amount;
|
||||||
|
|
@ -174,12 +174,12 @@ impl Default for PlayerBoard {
|
||||||
/// Trait to move checkers
|
/// Trait to move checkers
|
||||||
pub trait Move {
|
pub trait Move {
|
||||||
/// Move a checker
|
/// Move a checker
|
||||||
fn move_checker(&mut self, player: Player, dice: u8, from: usize) -> Result<&mut Self, Error>
|
fn move_checker(&mut self, player: &Player, dice: u8, from: usize) -> Result<&mut Self, Error>
|
||||||
where
|
where
|
||||||
Self: Sized;
|
Self: Sized;
|
||||||
|
|
||||||
/// Move permitted
|
/// Move permitted
|
||||||
fn move_permitted(&mut self, player: Player, dice: u8) -> Result<&mut Self, Error>
|
fn move_permitted(&mut self, player: &Player, dice: u8) -> Result<&mut Self, Error>
|
||||||
where
|
where
|
||||||
Self: Sized;
|
Self: Sized;
|
||||||
}
|
}
|
||||||
|
|
@ -229,7 +229,7 @@ mod tests {
|
||||||
fn set_player0() -> Result<(), Error> {
|
fn set_player0() -> Result<(), Error> {
|
||||||
let mut board = Board::new();
|
let mut board = Board::new();
|
||||||
let player = Player {name: "".into(), color: Color::White};
|
let player = Player {name: "".into(), color: Color::White};
|
||||||
board.set(player, 1, 1)?;
|
board.set(&player, 1, 1)?;
|
||||||
assert_eq!(board.get().board[1], 1);
|
assert_eq!(board.get().board[1], 1);
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
@ -238,7 +238,7 @@ mod tests {
|
||||||
fn set_player1() -> Result<(), Error> {
|
fn set_player1() -> Result<(), Error> {
|
||||||
let mut board = Board::new();
|
let mut board = Board::new();
|
||||||
let player = Player {name: "".into(), color: Color::Black};
|
let player = Player {name: "".into(), color: Color::Black};
|
||||||
board.set(player, 2, 1)?;
|
board.set(&player, 2, 1)?;
|
||||||
assert_eq!(board.get().board[21], -1);
|
assert_eq!(board.get().board[21], -1);
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
@ -274,44 +274,44 @@ mod tests {
|
||||||
#[test]
|
#[test]
|
||||||
fn blocked_player0() -> Result<(), Error> {
|
fn blocked_player0() -> Result<(), Error> {
|
||||||
let board = Board::new();
|
let board = Board::new();
|
||||||
assert!(board.blocked(Player { name:"".into(), color: Color::White }, 0)?);
|
assert!(board.blocked(&Player { name:"".into(), color: Color::White }, 0)?);
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn blocked_player1() -> Result<(), Error> {
|
fn blocked_player1() -> Result<(), Error> {
|
||||||
let board = Board::new();
|
let board = Board::new();
|
||||||
assert!(board.blocked(Player { name:"".into(), color: Color::Black }, 0)?);
|
assert!(board.blocked(&Player { name:"".into(), color: Color::Black }, 0)?);
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn blocked_player0_a() -> Result<(), Error> {
|
fn blocked_player0_a() -> Result<(), Error> {
|
||||||
let mut board = Board::new();
|
let mut board = Board::new();
|
||||||
board.set(Player { name:"".into(), color: Color::Black }, 1, 2)?;
|
board.set(&Player { name:"".into(), color: Color::Black }, 1, 2)?;
|
||||||
assert!(board.blocked(Player { name:"".into(), color: Color::White }, 22)?);
|
assert!(board.blocked(&Player { name:"".into(), color: Color::White }, 22)?);
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn blocked_player1_a() -> Result<(), Error> {
|
fn blocked_player1_a() -> Result<(), Error> {
|
||||||
let mut board = Board::new();
|
let mut board = Board::new();
|
||||||
board.set(Player { name:"".into(), color: Color::White }, 1, 2)?;
|
board.set(&Player { name:"".into(), color: Color::White }, 1, 2)?;
|
||||||
assert!(board.blocked(Player { name:"".into(), color: Color::Black }, 22)?);
|
assert!(board.blocked(&Player { name:"".into(), color: Color::Black }, 22)?);
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn blocked_invalid_field() {
|
fn blocked_invalid_field() {
|
||||||
let board = Board::new();
|
let board = Board::new();
|
||||||
assert!(board.blocked(Player { name:"".into(), color: Color::White }, 24).is_err());
|
assert!(board.blocked(&Player { name:"".into(), color: Color::White }, 24).is_err());
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn set_field_with_1_checker_player0_a() -> Result<(), Error> {
|
fn set_field_with_1_checker_player0_a() -> Result<(), Error> {
|
||||||
let mut board = Board::new();
|
let mut board = Board::new();
|
||||||
board.set(Player { name:"".into(), color: Color::White }, 1, 1)?;
|
board.set(&Player { name:"".into(), color: Color::White }, 1, 1)?;
|
||||||
board.set(Player { name:"".into(), color: Color::Black }, 22, 1)?;
|
board.set(&Player { name:"".into(), color: Color::Black }, 22, 1)?;
|
||||||
assert_eq!(board.get().board[1], -1);
|
assert_eq!(board.get().board[1], -1);
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
@ -319,8 +319,8 @@ mod tests {
|
||||||
#[test]
|
#[test]
|
||||||
fn set_field_with_1_checker_player0_b() -> Result<(), Error> {
|
fn set_field_with_1_checker_player0_b() -> Result<(), Error> {
|
||||||
let mut board = Board::new();
|
let mut board = Board::new();
|
||||||
board.set(Player { name:"".into(), color: Color::White }, 1, 1)?;
|
board.set(&Player { name:"".into(), color: Color::White }, 1, 1)?;
|
||||||
board.set(Player { name:"".into(), color: Color::Black }, 22, 1)?;
|
board.set(&Player { name:"".into(), color: Color::Black }, 22, 1)?;
|
||||||
assert_eq!(board.get().board[1], -1);
|
assert_eq!(board.get().board[1], -1);
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
@ -328,8 +328,8 @@ mod tests {
|
||||||
#[test]
|
#[test]
|
||||||
fn set_field_with_1_checker_player1_a() -> Result<(), Error> {
|
fn set_field_with_1_checker_player1_a() -> Result<(), Error> {
|
||||||
let mut board = Board::new();
|
let mut board = Board::new();
|
||||||
board.set(Player { name:"".into(), color: Color::Black }, 1, 1)?;
|
board.set(&Player { name:"".into(), color: Color::Black }, 1, 1)?;
|
||||||
board.set(Player { name:"".into(), color: Color::White }, 22, 1)?;
|
board.set(&Player { name:"".into(), color: Color::White }, 22, 1)?;
|
||||||
assert_eq!(board.get().board[22], 1);
|
assert_eq!(board.get().board[22], 1);
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
@ -337,8 +337,8 @@ mod tests {
|
||||||
#[test]
|
#[test]
|
||||||
fn set_field_with_1_checker_player1_b() -> Result<(), Error> {
|
fn set_field_with_1_checker_player1_b() -> Result<(), Error> {
|
||||||
let mut board = Board::new();
|
let mut board = Board::new();
|
||||||
board.set(Player { name:"".into(), color: Color::Black }, 1, 1)?;
|
board.set(&Player { name:"".into(), color: Color::Black }, 1, 1)?;
|
||||||
board.set(Player { name:"".into(), color: Color::White }, 22, 1)?;
|
board.set(&Player { name:"".into(), color: Color::White }, 22, 1)?;
|
||||||
assert_eq!(board.get().board[22], 1);
|
assert_eq!(board.get().board[22], 1);
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
@ -346,7 +346,7 @@ mod tests {
|
||||||
#[test]
|
#[test]
|
||||||
fn set_field_with_2_checkers_player0_a() -> Result<(), Error> {
|
fn set_field_with_2_checkers_player0_a() -> Result<(), Error> {
|
||||||
let mut board = Board::new();
|
let mut board = Board::new();
|
||||||
board.set(Player { name:"".into(), color: Color::White }, 23, 2)?;
|
board.set(&Player { name:"".into(), color: Color::White }, 23, 2)?;
|
||||||
assert_eq!(board.get().board[23], 4);
|
assert_eq!(board.get().board[23], 4);
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
@ -354,7 +354,7 @@ mod tests {
|
||||||
#[test]
|
#[test]
|
||||||
fn set_field_with_2_checkers_player0_b() -> Result<(), Error> {
|
fn set_field_with_2_checkers_player0_b() -> Result<(), Error> {
|
||||||
let mut board = Board::new();
|
let mut board = Board::new();
|
||||||
board.set(Player { name:"".into(), color: Color::White }, 23, -1)?;
|
board.set(&Player { name:"".into(), color: Color::White }, 23, -1)?;
|
||||||
assert_eq!(board.get().board[23], 1);
|
assert_eq!(board.get().board[23], 1);
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
@ -362,24 +362,24 @@ mod tests {
|
||||||
#[test]
|
#[test]
|
||||||
fn set_field_blocked() {
|
fn set_field_blocked() {
|
||||||
let mut board = Board::new();
|
let mut board = Board::new();
|
||||||
assert!(board.set(Player { name:"".into(), color: Color::White }, 0, 2).is_err());
|
assert!(board.set(&Player { name:"".into(), color: Color::White }, 0, 2).is_err());
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn set_wrong_field1() {
|
fn set_wrong_field1() {
|
||||||
let mut board = Board::new();
|
let mut board = Board::new();
|
||||||
assert!(board.set(Player { name:"".into(), color: Color::White }, 50, 2).is_err());
|
assert!(board.set(&Player { name:"".into(), color: Color::White }, 50, 2).is_err());
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn set_wrong_amount0() {
|
fn set_wrong_amount0() {
|
||||||
let mut board = Board::new();
|
let mut board = Board::new();
|
||||||
assert!(board.set(Player { name:"".into(), color: Color::White }, 23, -3).is_err());
|
assert!(board.set(&Player { name:"".into(), color: Color::White }, 23, -3).is_err());
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn set_wrong_amount1() {
|
fn set_wrong_amount1() {
|
||||||
let mut board = Board::new();
|
let mut board = Board::new();
|
||||||
assert!(board.set(Player { name:"".into(), color: Color::Black }, 23, -3).is_err());
|
assert!(board.set(&Player { name:"".into(), color: Color::Black }, 23, -3).is_err());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -35,7 +35,7 @@ impl fmt::Display for GameState {
|
||||||
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
|
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
|
||||||
let mut s = String::new();
|
let mut s = String::new();
|
||||||
s.push_str(&format!("Dices: {:?}\n", self.dices));
|
s.push_str(&format!("Dices: {:?}\n", self.dices));
|
||||||
s.push_str(&format!("Who plays: {}\n", self.who_plays().map(|player| player.name ).unwrap_or("".to_string())));
|
// s.push_str(&format!("Who plays: {}\n", self.who_plays().map(|player| &player.name ).unwrap_or("")));
|
||||||
s.push_str(&format!("Board: {:?}\n", self.board.get()));
|
s.push_str(&format!("Board: {:?}\n", self.board.get()));
|
||||||
write!(f, "{}", s)
|
write!(f, "{}", s)
|
||||||
}
|
}
|
||||||
|
|
@ -61,8 +61,8 @@ impl GameState {
|
||||||
GameState::default()
|
GameState::default()
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn who_plays(&self) -> &Option<&Player> {
|
pub fn who_plays(&self) -> Option<&Player> {
|
||||||
&self.players.get(&self.active_player_id)
|
self.players.get(&self.active_player_id)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn switch_active_player(&mut self) {
|
pub fn switch_active_player(&mut self) {
|
||||||
|
|
@ -182,8 +182,8 @@ impl GameState {
|
||||||
}
|
}
|
||||||
Move { player_id, from, to } => {
|
Move { player_id, from, to } => {
|
||||||
let player = self.players.get(player_id).unwrap();
|
let player = self.players.get(player_id).unwrap();
|
||||||
self.board.set(*player, *from, 0 as i8);
|
self.board.set(player, *from, 0 as i8);
|
||||||
self.board.set(*player, *to, 1 as i8);
|
self.board.set(player, *to, 1 as i8);
|
||||||
self.active_player_id = self
|
self.active_player_id = self
|
||||||
.players
|
.players
|
||||||
.keys()
|
.keys()
|
||||||
|
|
@ -245,7 +245,7 @@ impl Roll for GameState {
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Move for GameState {
|
impl Move for GameState {
|
||||||
fn move_checker(&mut self, player: Player, dice: u8, from: usize) -> Result<&mut Self, Error> {
|
fn move_checker(&mut self, player: &Player, dice: u8, from: usize) -> Result<&mut Self, Error> {
|
||||||
// check if move is permitted
|
// check if move is permitted
|
||||||
let _ = self.move_permitted(player, dice)?;
|
let _ = self.move_permitted(player, dice)?;
|
||||||
|
|
||||||
|
|
@ -286,7 +286,7 @@ impl Move for GameState {
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Implements checks to validate if the player is allowed to move
|
/// Implements checks to validate if the player is allowed to move
|
||||||
fn move_permitted(&mut self, player: Player, dice: u8) -> Result<&mut Self, Error> {
|
fn move_permitted(&mut self, player: &Player, dice: u8) -> Result<&mut Self, Error> {
|
||||||
let maybe_player_id = self.player_id(&player);
|
let maybe_player_id = self.player_id(&player);
|
||||||
// check if player is allowed to move
|
// check if player is allowed to move
|
||||||
if maybe_player_id != Some(&self.active_player_id) {
|
if maybe_player_id != Some(&self.active_player_id) {
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,5 @@
|
||||||
mod game;
|
mod game;
|
||||||
|
pub use game::{ GameState, GameEvent, EndGameReason };
|
||||||
|
|
||||||
mod player;
|
mod player;
|
||||||
pub use player::Player;
|
pub use player::Player;
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue