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
#![warn(missing_docs)]

//! The `dwarf-term` crate allows you to draw a screen that ends up looking like
//! dwarf fortress.
//!
//! At the moment there's only a single graphics set supported, but perhaps in
//! future versions that will become an element that you can change at runtime.

extern crate retro_pixel;
pub use retro_pixel::*;

#[macro_use]
extern crate glium;
use glium::{
  backend::glutin::DisplayCreationError, glutin::{
    dpi::{LogicalPosition, LogicalSize}, ContextBuilder, EventsLoop, WindowBuilder,
  },
  program::ProgramCreationError,
  texture::{
    texture2d::Texture2d, unsigned_texture2d::UnsignedTexture2d, ClientFormat, MipmapsOption, RawImage2d, TextureCreationError,
    UncompressedFloatFormat,
  },
  uniforms::{MagnifySamplerFilter, MinifySamplerFilter, Sampler}, vertex::BufferCreationError, Display, DrawError, Program, Surface,
  SwapBuffersError, VertexBuffer,
};

pub use glium::glutin::*;

// std
use std::borrow::Cow;

// crate
mod demo_font;
use demo_font::*;

#[derive(Copy, Clone)]
struct Vertex {
  position: [f32; 2],
}
implement_vertex!(Vertex, position);

static VERTICES: [Vertex; 6] = [
  Vertex { position: [-1.0, -1.0] },
  Vertex { position: [-1.0, 1.0] },
  Vertex { position: [1.0, 1.0] },
  Vertex { position: [1.0, 1.0] },
  Vertex { position: [1.0, -1.0] },
  Vertex { position: [-1.0, -1.0] },
];

/// Some sort of OpenGL error happened.
///
/// If it did, this is probably my fault.
#[derive(Debug)]
#[allow(missing_docs)]
pub enum DwarfTermGliumError {
  DisplayCreationError(DisplayCreationError),
  ProgramCreationError(ProgramCreationError),
  TextureCreationError(TextureCreationError),
  BufferCreationError(BufferCreationError),
  DrawError(DrawError),
  SwapBuffersError(SwapBuffersError),
}

/// A terminal where each cell has a foreground, background, and sprite id.
pub struct DwarfTerm {
  fgs: VecImage<u32>,
  bgs: VecImage<u32>,
  ids: VecImage<u8>,
  clear_color: (f32, f32, f32, f32),
  events_loop: EventsLoop,
  display: Display,
  program: Program,
  sprite_texture: Texture2d,
}
impl ::std::fmt::Debug for DwarfTerm {
  fn fmt(&self, f: &mut ::std::fmt::Formatter) -> Result<(), ::std::fmt::Error> {
    write!(f, "DwarfTerm [ width {}, height {} ]", self.ids.width(), self.ids.height())
  }
}

impl DwarfTerm {
  /// Makes a new terminal of the given size and with the given title.
  pub fn new<T>(grid_width: u32, grid_height: u32, title: T) -> Result<Self, DwarfTermGliumError>
  where
    T: Into<String>,
  {
    // Ready the Display we will use
    let window_pixel_width = DEMO_FONT_TILE_SIZE.0 * grid_width;
    let window_pixel_height = DEMO_FONT_TILE_SIZE.1 * grid_height;
    let window_logical_size: LogicalSize = (window_pixel_width, window_pixel_height).into();
    let events_loop = EventsLoop::new();
    let window_builder = WindowBuilder::new()
      .with_dimensions(window_logical_size)
      .with_min_dimensions(window_logical_size)
      .with_max_dimensions(window_logical_size)
      .with_title(title);
    let context_builder = ContextBuilder::new().with_vsync(true);
    let display = Display::new(window_builder, context_builder, &events_loop).map_err(|e| DwarfTermGliumError::DisplayCreationError(e))?;
    display.gl_window().set_position(LogicalPosition::new(500.0, 200.0));

    // Ready the Program we will use
    let program = Program::from_source(&display, VERT_SRC, FRAG_SRC, None).map_err(|e| DwarfTermGliumError::ProgramCreationError(e))?;

    // Ready the sprite sheet
    let raw_image = RawImage2d::from_raw_rgba_reversed(&DEMO_FONT, DEMO_FONT_SHEET_SIZE);
    let sprite_texture = Texture2d::with_format(
      &display,
      raw_image,
      UncompressedFloatFormat::U8U8U8U8,
      MipmapsOption::AutoGeneratedMipmaps,
    ).map_err(|e| DwarfTermGliumError::TextureCreationError(e))?;

    Ok(Self {
      fgs: VecImage::new(grid_width as usize, grid_height as usize),
      bgs: VecImage::new(grid_width as usize, grid_height as usize),
      ids: VecImage::new(grid_width as usize, grid_height as usize),
      events_loop,
      display,
      program,
      clear_color: (1.0, 1.0, 1.0, 1.0),
      sprite_texture,
    })
  }

  /// Sets the foreground color of every cell to be the given value.
  pub fn set_all_foregrounds(&mut self, rgba: u32) {
    self.fgs.set_all(rgba);
  }

  /// Sets the foreground color of every cell to be the given value.
  pub fn set_all_backgrounds(&mut self, rgba: u32) {
    self.bgs.set_all(rgba);
  }

  /// Sets the tile id of every cell to be the given value.
  pub fn set_all_ids(&mut self, tile_id: u8) {
    self.ids.set_all(tile_id);
  }

  /// Sets the "clear" color that's used.
  ///
  /// This just passes the call along to OpenGL. Inputs should each be in the
  /// 0.0 to 1.0 (inclusive) range. Out of bounds inputs are automatically
  /// clamped by OpenGL.
  pub fn set_clear_color(&mut self, r: f32, g: f32, b: f32, a: f32) {
    self.clear_color = (r, g, b, a);
  }

  /// gets a mutable reference to the foreground at the position specified.
  ///
  /// Fails if the position would be out of bounds.
  pub fn get_foreground_mut(&mut self, position: (usize, usize)) -> Option<&mut u32> {
    self.fgs.get_mut(position)
  }

  /// gets a mutable reference to the background at the position specified.
  ///
  /// Fails if the position would be out of bounds.
  pub fn get_background_mut(&mut self, position: (usize, usize)) -> Option<&mut u32> {
    self.bgs.get_mut(position)
  }

  /// gets a mutable reference to the tile id at the position specified.
  ///
  /// Fails if the position would be out of bounds.
  pub fn get_id_mut(&mut self, position: (usize, usize)) -> Option<&mut u8> {
    self.ids.get_mut(position)
  }

  /// Lets you poll for events on the window.
  ///
  /// The DwarfTerm will automatically check for `WindowEvent::Resized` events
  /// and then keep itself up to date. However, it will _also_ pass along such
  /// events to your callback so that you can make any necessary changes on your
  /// end as well.
  pub fn poll_events<F>(&mut self, mut callback: F)
  where
    F: FnMut(Event),
  {
    self.events_loop.poll_events(|event| {
      callback(event);
    });
  }

  /// Gives back all three layers at once in image slice form.
  ///
  /// (fgs, bgs, ids).
  pub fn layer_slices(&self) -> (ImageSlice<u32>, ImageSlice<u32>, ImageSlice<u8>) {
    let zero = (0, 0);
    let fg_extent = (self.fgs.width(), self.fgs.height());
    let bg_extent = (self.bgs.width(), self.bgs.height());
    let id_extent = (self.ids.width(), self.ids.height());
    (
      self.fgs.slice(zero..fg_extent),
      self.bgs.slice(zero..bg_extent),
      self.ids.slice(zero..id_extent),
    )
  }

  /// Gives back all three layers at once in image mutable slice form.
  ///
  /// (fgs, bgs, ids).
  pub fn layer_slices_mut(&mut self) -> (ImageMutSlice<u32>, ImageMutSlice<u32>, ImageMutSlice<u8>) {
    let zero = (0, 0);
    let fg_extent = (self.fgs.width(), self.fgs.height());
    let bg_extent = (self.bgs.width(), self.bgs.height());
    let id_extent = (self.ids.width(), self.ids.height());
    (
      self.fgs.slice_mut(zero..fg_extent),
      self.bgs.slice_mut(zero..bg_extent),
      self.ids.slice_mut(zero..id_extent),
    )
  }

  /// Clears the old screen, draws the new data, and then swaps the buffers.
  ///
  /// This should hopefully never error! But it might. If it does, there's a
  /// strong chance it's my fault, not yours.
  pub fn clear_draw_swap(&mut self) -> Result<(), DwarfTermGliumError> {
    // double check things
    debug_assert_eq!(self.ids.as_ref().len(), self.ids.width() * self.ids.height());
    debug_assert_eq!(self.fgs.as_ref().len(), self.fgs.width() * self.fgs.height());
    debug_assert_eq!(self.bgs.as_ref().len(), self.bgs.width() * self.bgs.height());

    let the_resolution = {
      let logical_size = self.display.gl_window().window().get_inner_size().unwrap();
      [logical_size.width, logical_size.height]
    };
    let the_grid_count_f32 = [self.ids.width() as f32, self.ids.height() as f32];
    let the_tile_id_texture = UnsignedTexture2d::new(
      &self.display,
      RawImage2d {
        data: Cow::Borrowed(self.ids.as_ref()),
        width: self.ids.width() as u32,
        height: self.ids.height() as u32,
        format: ClientFormat::U8,
      },
    ).map_err(|e| DwarfTermGliumError::TextureCreationError(e))?;
    let the_tile_foreground_texture = Texture2d::new(
      &self.display,
      RawImage2d {
        data: Cow::Borrowed(self.fgs.as_ref()),
        width: self.fgs.width() as u32,
        height: self.fgs.height() as u32,
        format: ClientFormat::U8U8U8U8,
      },
    ).map_err(|e| DwarfTermGliumError::TextureCreationError(e))?;
    let the_tile_background_texture = Texture2d::new(
      &self.display,
      RawImage2d {
        data: Cow::Borrowed(self.bgs.as_ref()),
        width: self.bgs.width() as u32,
        height: self.bgs.height() as u32,
        format: ClientFormat::U8U8U8U8,
      },
    ).map_err(|e| DwarfTermGliumError::TextureCreationError(e))?;
    let vertex_buffer = VertexBuffer::new(&self.display, &VERTICES).map_err(|e| DwarfTermGliumError::BufferCreationError(e))?;
    let indices = glium::index::NoIndices(glium::index::PrimitiveType::TrianglesList);
    let uniforms = uniform!{
      screen_resolution: the_resolution,
      tile_size: DEMO_FONT_TILE_SIZE_VEC2,
      grid_count: the_grid_count_f32,
      tile_sheet_texture: Sampler::new(&self.sprite_texture)
        .minify_filter(MinifySamplerFilter::Nearest)
        .magnify_filter(MagnifySamplerFilter::Nearest),
      tile_id_texture: Sampler::new(&the_tile_id_texture)
        .minify_filter(MinifySamplerFilter::Nearest)
        .magnify_filter(MagnifySamplerFilter::Nearest),
      tile_foreground_texture: Sampler::new(&the_tile_foreground_texture)
        .minify_filter(MinifySamplerFilter::Nearest)
        .magnify_filter(MagnifySamplerFilter::Nearest),
      tile_background_texture: Sampler::new(&the_tile_background_texture)
        .minify_filter(MinifySamplerFilter::Nearest)
        .magnify_filter(MagnifySamplerFilter::Nearest),
    };

    let mut target = self.display.draw();
    target.clear_color(self.clear_color.0, self.clear_color.1, self.clear_color.2, self.clear_color.3);
    // Note(Lokathor): We CANNOT return early using `?` once there's a Surface
    // in play, or the Drop impl of the drawing target will make the whole
    // program panic since we didn't explicitly call `target.finish()`. I'm
    // pretty sure it's stupid like that because `?` was added after Glium
    // stopped major work, but there you go.
    match target.draw(&vertex_buffer, &indices, &self.program, &uniforms, &Default::default()) {
      Ok(()) => target.finish().map_err(|e| DwarfTermGliumError::SwapBuffersError(e)),
      Err(draw_error) => {
        target.finish().ok();
        Err(DwarfTermGliumError::DrawError(draw_error))
      }
    }
  }
}

//
// SHADER STUFF
//

static VERT_SRC: &'static str = include_str!("dwarf.vert");

static FRAG_SRC: &'static str = include_str!("dwarf.frag");