1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
//! This is the graphical client for playing the game.
//!
//! For the actual game logic, see also the [server](../server/index.html) and
//! [common] crates.

#![windows_subsystem = "windows"]

#[cfg(not(feature = "sdl"))]
fn main() {
    eprintln!("Enable the 'sdl' feature to run the sdl_client");
}

#[cfg(feature = "sdl")]
mod renderer;
#[cfg(feature = "sdl")]
mod sprites;

/// The main function that runs the game's graphical client.
#[cfg(feature = "sdl")]
fn fallible_main() -> anyhow::Result<()> {
    use anyhow::Context;
    use client::GameplaySocket;
    use common::{
        ChunkUpdate, ClientAction, ServerAction, TileIndex, World, CHUNKS_X, CHUNKS_Y,
        CHUNK_HEIGHT, CHUNK_WIDTH,
    };
    use fontdue::Font;
    use fontdue_sdl2::FontTexture;
    use log::LevelFilter;
    use renderer::RenderingContext;
    use sdl2::event::Event;
    use sdl2::keyboard::Keycode;
    use sdl2::pixels::Color;
    use sdl2::render::BlendMode;
    use sprites::*;
    use std::time::{Duration, Instant};

    static LOGGER: client::Logger = client::Logger;

    let _ = log::set_logger(&LOGGER).map(|()| {
        log::set_max_level(if cfg!(debug_assertions) {
            LevelFilter::Debug
        } else {
            LevelFilter::Info
        })
    });

    let sdl_context = sdl2::init()
        .map_err(SdlError)
        .context("Failed to initialize SDL 2, exiting.")?;
    let video_context = sdl_context.video().map_err(SdlError).context(
        "Failed to initialize video system. The game cannot be played in a non-graphical environment.",
    )?;
    let window = video_context
        .window("Networked Game", 640, 480)
        .resizable()
        .build()
        .context("Cannot create a window for the game, exiting.")?;
    let canvas = &mut window
        .into_canvas()
        .build()
        .context("Cannot create a canvas to draw the game on, exiting.")?;
    let texture_creator = canvas.texture_creator();

    let font_texture = &mut FontTexture::new(&texture_creator).unwrap();
    let wizards_manse_font = &include_bytes!("../graphics/Wizard's Manse.otf")[..];
    let unifont_font = &include_bytes!("../graphics/unifont-15.0.01.otf")[..];
    let font_err = |font_name| format!("Could not load font '{font_name}'? This is a bug.");
    let title_font = Font::from_bytes(wizards_manse_font, Default::default())
        .map_err(FontLoadingError::from)
        .context(font_err("Wizard's Manse"))?;
    let body_font = Font::from_bytes(unifont_font, Default::default())
        .map_err(FontLoadingError::from)
        .context(font_err("Unifont"))?;
    let fonts = &[title_font, body_font];

    let mut tileset = load_tileset(&texture_creator)?;

    let mut world = World::empty();
    let mut latest_verified_world = world.clone();
    let mut animated_time = 0.0;
    let mut player_point = None;
    let mut dt = 0.016;

    // (Message text, lifetime left).
    let mut notifications: Vec<(String, f32)> = Vec::new();
    let mut show_debug = false;

    let mut gameplay_socket = GameplaySocket::new()?;
    let mut pc_handle = None;
    let mut connected = false;
    gameplay_socket.send(ClientAction::Hello);

    let (mut move_up, mut move_down, mut move_left, mut move_right) = (false, false, false, false);
    let mut event_pump = sdl_context
        .event_pump()
        .map_err(SdlError)
        .context("Cannot create an event pump to detect user inputs, exiting.")?;
    'gameloop: loop {
        let start_time = Instant::now();
        for event in event_pump.poll_iter() {
            match event {
                Event::Quit { .. } => break 'gameloop,
                Event::KeyDown {
                    keycode: Some(keycode),
                    ..
                } => match keycode {
                    Keycode::W | Keycode::K | Keycode::Up => move_up = true,
                    Keycode::S | Keycode::J | Keycode::Down => move_down = true,
                    Keycode::D | Keycode::L | Keycode::Right => move_right = true,
                    Keycode::A | Keycode::H | Keycode::Left => move_left = true,
                    _ => {}
                },
                Event::KeyUp {
                    keycode: Some(keycode),
                    ..
                } => match keycode {
                    Keycode::W | Keycode::K | Keycode::Up => move_up = false,
                    Keycode::S | Keycode::J | Keycode::Down => move_down = false,
                    Keycode::D | Keycode::L | Keycode::Right => move_right = false,
                    Keycode::A | Keycode::H | Keycode::Left => move_left = false,
                    Keycode::F3 => show_debug = !show_debug,
                    _ => {}
                },
                _ => {}
            }
        }

        let mut action_to_send = None;
        match (move_up, move_down, move_left, move_right) {
            (true, false, false, false) => action_to_send = Some(ClientAction::MoveUp),
            (false, true, false, false) => action_to_send = Some(ClientAction::MoveDown),
            (false, false, true, false) => action_to_send = Some(ClientAction::MoveLeft),
            (false, false, false, true) => action_to_send = Some(ClientAction::MoveRight),
            _ => {}
        }

        if let Some(action) = action_to_send.take() {
            gameplay_socket.send(action);
        }

        gameplay_socket.transport();
        while let Some(server_action) = gameplay_socket.recv() {
            match server_action {
                ServerAction::Effects {
                    time,
                    chunk_updates,
                } => {
                    if world.time > time {
                        // The world has been eagerly simulated past the Effect, rewind
                        world = latest_verified_world;
                    }
                    while world.time < time {
                        world.update();
                    }
                    for (i, update) in chunk_updates {
                        match update {
                            ChunkUpdate::Effects(effects) => {
                                if !world.apply_effects(i, effects) {
                                    log::warn!(
                                        "Got effects for chunk at index {i}, which is not loaded!"
                                    );
                                }
                            }
                            ChunkUpdate::ResetTo(chunk) => {
                                world.chunks[i] = Some(chunk);
                            }
                        }
                    }
                    world.update();
                    latest_verified_world = world.clone();
                    animated_time = world.time as f32;
                }
                ServerAction::Welcome(handle) => {
                    notifications.push(("Connected to the server!".to_string(), 2.0));
                    pc_handle = Some(handle);
                    connected = true;
                }
                ServerAction::UnloadChunks(chunk_indices) => {
                    for i in chunk_indices {
                        world.chunks[i] = None;
                    }
                }
            }
        }
        while animated_time as u64 > world.time {
            world.update();
        }

        if let Some((i, pc)) = pc_handle.and_then(|h| world.get_player_character(h)) {
            let cx = i % CHUNKS_X;
            let cy = i / CHUNKS_X;
            let px = (cx * CHUNK_WIDTH) as f32 + pc.position.0 as f32;
            let py = (cy * CHUNK_HEIGHT) as f32 + pc.position.1 as f32;
            let (prev_x, prev_y) = player_point.unwrap_or((px, py));
            let dx = px - prev_x;
            let dy = py - prev_y;
            let dl_squared = dx * dx + dy * dy;
            if dl_squared > 0.0 {
                let dl = dl_squared.sqrt();
                let step = dl.min(2.0 * dt * dl.min(4.0));
                let step_x = dx / dl * step;
                let step_y = dy / dl * step;
                player_point = Some((prev_x + step_x, prev_y + step_y));
            } else {
                player_point = Some((px, py));
            }
        }

        if connected && gameplay_socket.disconnected() {
            connected = false;
            pc_handle = None;
            player_point = None;
            notifications.push((
                "Disconnected. The servers may be under maintenance: try restarting the game in a moment.".to_string(),
                std::f32::INFINITY,
            ));
        }

        canvas.set_draw_color(sdl2::pixels::Color::RGB(0x33, 0x33, 0x33));
        canvas.clear();
        let mut ctx = RenderingContext {
            canvas,
            font_texture,
            fonts,
        };
        if let Some(cam) = player_point {
            let brightness_at = |x: f32, y: f32| {
                let dx = cam.0 as f32 - x;
                let dy = cam.1 as f32 - y;
                let dist = ((dx * dx) + (dy * dy)).sqrt();
                let brightness = (1.0 - dist / 8.0).min(1.0).max(0.0).sqrt();
                (brightness * 255.0) as u8
            };

            ctx.canvas.set_blend_mode(BlendMode::Add);
            for chunk_y in 0..CHUNKS_Y {
                for chunk_x in 0..CHUNKS_X {
                    let x = (chunk_x * CHUNK_WIDTH) as f32;
                    let y = (chunk_y * CHUNK_HEIGHT) as f32;
                    let (w, h) = (CHUNK_WIDTH as f32, CHUNK_HEIGHT as f32);
                    if let Some(chunk) = &world.chunks[(chunk_x + chunk_y * CHUNKS_X) as usize] {
                        for tile_y in 0..CHUNK_HEIGHT {
                            for tile_x in 0..CHUNK_WIDTH {
                                let x = x + tile_x as f32;
                                let y = y + tile_y as f32;
                                let i = (tile_x + tile_y * CHUNK_WIDTH) as usize;
                                let brightness = if show_debug {
                                    0xFF
                                } else {
                                    brightness_at(x, y)
                                };
                                tileset.set_alpha_mod(brightness);
                                if let Some(TileIndex(tile_index)) = chunk.walls[i] {
                                    ctx.draw_tile(cam, x, y, &tileset, tile_index);
                                } else if let Some(TileIndex(tile_index)) = chunk.floors[i] {
                                    ctx.draw_tile(cam, x, y, &tileset, tile_index);
                                }
                                tileset.set_alpha_mod(0xFF);
                            }
                        }
                        if show_debug {
                            ctx.draw_debug_region(cam, x, y, w, h, Color::RGB(0, 0x66, 0));
                        }
                    }
                }
            }

            for chunk_y in 0..CHUNKS_Y {
                for chunk_x in 0..CHUNKS_X {
                    if let Some(chunk) = &world.chunks[(chunk_x + chunk_y * CHUNKS_X) as usize] {
                        for (handle, pc) in &chunk.pcs {
                            let mut px = pc.position.0 as f32;
                            let mut py = pc.position.1 as f32;
                            if let Some(current_move) = &pc.current_move {
                                let t_curr = animated_time - current_move.start_frame as f32 - 1.0;
                                let t = t_curr / current_move.duration as f32;
                                let hop_y_offset = -(1.0 - (2.0 * t - 1.0).powf(2.0)) * 0.15;
                                px = current_move.from.0 as f32 * (1.0 - t)
                                    + current_move.to.0 as f32 * t;
                                py = current_move.from.1 as f32 * (1.0 - t)
                                    + current_move.to.1 as f32 * t
                                    + hop_y_offset;
                            }
                            let px = px + (chunk_x * CHUNK_WIDTH) as f32;
                            let py = py + (chunk_y * CHUNK_HEIGHT) as f32;

                            let brightness = if show_debug {
                                0xFF
                            } else {
                                brightness_at(px, py)
                            };
                            tileset.set_alpha_mod(brightness);
                            if Some(*handle) == pc_handle {
                                tileset.set_color_mod(0x33, 0xDD, 0xFF);
                            } else {
                                tileset.set_color_mod(0x33, 0xDD, 0xBB);
                            }
                            ctx.draw_tile(cam, px, py, &tileset, TILE_INDEX_PLAYER);
                            tileset.set_color_mod(0xFF, 0xFF, 0xFF);
                            tileset.set_alpha_mod(0xFF);
                        }
                    }
                }
            }

            ctx.canvas.set_blend_mode(BlendMode::None);
        }

        let mut notif_counter = 0;
        let mut notif_nth = || {
            notif_counter += 1;
            notif_counter - 1
        };
        if show_debug {
            let max = gameplay_socket.debug_max_message_length as f32 / 1000.0;
            ctx.draw_notification(
                &format!("Max message size so far: {max:.3} KB"),
                notif_nth(),
            );
        }
        for (text, lifetime) in &mut notifications {
            *lifetime -= dt;
            ctx.draw_notification(text, notif_nth());
        }
        notifications.retain(|(_, lifetime)| *lifetime > 0.0);

        canvas.present();

        let frame_time_so_far = Instant::now() - start_time;
        let target_frame_time = canvas
            .window()
            .display_mode()
            .map(|dm| Duration::from_micros(1_000_000 / dm.refresh_rate.max(1) as u64))
            .unwrap_or_else(|_| Duration::from_micros(16_667));
        if let Some(sleep_time) = target_frame_time.checked_sub(frame_time_so_far) {
            std::thread::sleep(sleep_time);
        }

        dt = (Instant::now() - start_time).as_secs_f32();
        animated_time += dt * common::SECOND as f32;
    }
    Ok(())
}

/// Simple wrapper around [fallible_main], shows a message box containing the
/// error if [fallible_main] returns an error.
#[cfg(feature = "sdl")]
fn main() -> anyhow::Result<()> {
    use sdl2::messagebox::{show_simple_message_box, MessageBoxFlag};
    if let Err(err) = fallible_main() {
        let message = format!("{:?}", err);
        let _ = show_simple_message_box(MessageBoxFlag::ERROR, "Fatal Error", &message, None);
        Err(err)
    } else {
        Ok(())
    }
}

/// Wrapper for [sdl2] errors which implements Error and Display.
#[derive(Debug)]
pub struct SdlError(String);
impl From<String> for SdlError {
    fn from(s: String) -> SdlError {
        SdlError(s)
    }
}
impl std::fmt::Display for SdlError {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        write!(f, "Simple DirectMedia Layer error: {}", self.0)
    }
}
impl std::error::Error for SdlError {
    fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
        None
    }
}

/// Wrapper for [fontdue] errors which implements Error and Display.
#[derive(Debug)]
pub struct FontLoadingError(&'static str);
impl From<&'static str> for FontLoadingError {
    fn from(s: &'static str) -> FontLoadingError {
        FontLoadingError(s)
    }
}
impl std::fmt::Display for FontLoadingError {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        write!(f, "Error loading font: {}", self.0)
    }
}
impl std::error::Error for FontLoadingError {
    fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
        None
    }
}