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
extern crate loe;

use std::fmt::Display;
use std::io::Cursor;

use radix_fmt::radix_36;
use loe::{process, Config, TransformMode};

pub mod colour;
pub mod dialogue;
pub mod ending;
pub mod exit;
pub mod game;
pub mod image;
pub mod item;
pub mod mock;
pub mod palette;
pub mod position;
pub mod room;
pub mod sprite;
pub mod text;
pub mod tile;
pub mod variable;

pub mod test_omnibus;

pub use colour::Colour;
pub use dialogue::Dialogue;
pub use ending::Ending;
pub use exit::*;
pub use game::*;
pub use image::Image;
pub use item::Item;
pub use palette::Palette;
pub use position::Position;
pub use room::Room;
pub use sprite::Sprite;
pub use text::*;
pub use tile::Tile;
pub use variable::Variable;

#[derive(Clone, Debug, Eq, PartialEq)]
pub struct Instance {
    position: Position,
    id: String, // item / ending id
}

/// a Room can have many Exits in different positions,
/// optionally with a transition and dialogue
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct ExitInstance {
    position: Position,
    exit: Exit,
    transition: Option<Transition>,
    dialogue_id: Option<String>,
}

pub trait AnimationFrames {
    fn to_string(&self) -> String;
}

impl AnimationFrames for Vec<Image> {
    fn to_string(&self) -> String {
        let mut string = String::new();
        let last_frame = self.len() - 1;

        for (i, frame) in self.into_iter().enumerate() {
            string.push_str(&frame.to_string());

            if i < last_frame {
                string.push_str(&"\n>\n".to_string());
            }
        }

        string
    }
}

/// this doesn't work inside ToBase36 for some reason
fn to_base36(int: u64) -> String {
    format!("{}", radix_36(int))
}

pub trait ToBase36 {
    fn to_base36(&self) -> String;
}

impl ToBase36 for u64 {
    fn to_base36(&self) -> String {
        to_base36(*self)
    }
}

/// e.g. `\nNAME DLG_0`
fn optional_data_line<T: Display>(label: &str, item: Option<T>) -> String {
    if item.is_some() {
        format!("\n{} {}", label, item.unwrap())
    } else {
        "".to_string()
    }
}

fn transform_line_endings(input: String, mode: TransformMode) -> String {
    let mut input = Cursor::new(input);
    let mut output = Cursor::new(Vec::new());

    process(&mut input, &mut output, Config::default().transform(mode)).unwrap();
    String::from_utf8(output.into_inner()).unwrap()
}

fn segments_from_string(string: String) -> Vec<String> {
    // this is pretty weird but a dialogue can just have an empty line followed by a name
    // however, on entering two empty lines, dialogue will be wrapped in triple quotation marks
    // so, handle this here
    let string = string.replace("\n\nNAME", "\n\"\"\"\n\"\"\"\nNAME");

    let mut output:Vec<String> = Vec::new();
    // are we inside `"""\n...\n"""`? if so, ignore empty lines
    let mut inside_escaped_block = false;
    let mut current_segment : Vec<String> = Vec::new();

    for line in string.lines() {
        if line == "\"\"\"" {
            inside_escaped_block = ! inside_escaped_block;
        }

        if line == "" && !inside_escaped_block {
            output.push(current_segment.join("\n"));
            current_segment = Vec::new();
        } else {
            current_segment.push(line.to_string());
        }
    }

    output.push(current_segment.join("\n"));

    output
}

/// tries to use an existing ID - if it is already in use, generate a new one
/// then return the ID (either original or new)
/// todo refactor (unnecessary clones etc.)
fn try_id(ids: &Vec<String>, id: &String) -> String {
    let id = id.clone();
    let ids = ids.clone();
    if is_id_available(&ids, &id) {
        id
    } else {
        new_unique_id(ids)
    }
}

fn is_id_available(ids: &Vec<String>, id: &String) -> bool {
    ! ids.contains(id)
}

/// e.g. pass all tile IDs into this to get a new non-conflicting tile ID
fn new_unique_id(ids: Vec<String>) -> String {
    let mut new_id: u64 = 0;

    while ids.contains(&new_id.to_base36()) {
        new_id += 1;
    }

    return to_base36(new_id);
}

pub trait Quote {
    fn quote(&self) -> String;
}

impl Quote for String {
    fn quote(&self) -> String {
        format!("\"\"\"\n{}\n\"\"\"", self).to_string()
    }
}

pub trait Unquote {
    fn unquote(&self) -> String;
}

impl Unquote for String {
    fn unquote(&self) -> String {
        self.trim_matches('\"').trim_matches('\n').to_string()
    }
}

#[cfg(test)]
mod test {
    use crate::{ToBase36, optional_data_line, mock, segments_from_string, Quote, Unquote, new_unique_id, try_id};

    #[test]
    fn to_base36() {
        assert_eq!((37 as u64).to_base36(), "11");
    }

    #[test]
    fn test_optional_data_line() {
        let output = optional_data_line("NAME", mock::item().name);
        assert_eq!(output, "\nNAME door".to_string());
    }

    #[test]
    fn string_to_segments() {
        let output = segments_from_string(
            include_str!("./test-resources/segments").to_string()
        );

        let expected = vec![
            "\"\"\"\nthe first segment is a long bit of text\n\n\nit contains empty lines\n\n\"\"\"".to_string(),
            "this is a new segment\nthis is still the second segment\nblah\nblah".to_string(),
            "DLG SEGMENT_3\n\"\"\"\nthis is a short \"long\" bit of text\n\"\"\"".to_string(),
            "this is the last segment".to_string(),
        ];

        assert_eq!(output, expected);
    }

    #[test]
    fn quote() {
        let output = "this is a string.\nIt has 2 lines".to_string().quote();
        let expected = "\"\"\"\nthis is a string.\nIt has 2 lines\n\"\"\"".to_string();
        assert_eq!(output, expected);
    }

    #[test]
    fn unquote() {
        let output = "\"\"\"\nwho the fuck is scraeming \"LOG OFF\" at my house.\nshow yourself, coward.\ni will never log off\n\"\"\"".to_string().unquote();
        let expected = "who the fuck is scraeming \"LOG OFF\" at my house.\nshow yourself, coward.\ni will never log off".to_string();
        assert_eq!(output, expected);
    }

    #[test]
    fn test_try_id() {
        // does a conflict generate a new ID?
        assert_eq!(
            try_id(&vec!["0".to_string(), "1".to_string()], &"1".to_string()),
            "2".to_string()
        );
        // with no conflict, does the ID remain the same?
        assert_eq!(
            try_id(&vec!["0".to_string(), "1".to_string()], &"3".to_string()),
            "3".to_string()
        );
    }

    #[test]
    fn test_new_unique_id() {
        // start
        assert_eq!(new_unique_id(vec!["1".to_string(), "z".to_string()]), "0".to_string());
        // middle
        assert_eq!(new_unique_id(vec!["0".to_string(), "2".to_string()]), "1".to_string());
        // end
        assert_eq!(new_unique_id(vec!["0".to_string(), "1".to_string()]), "2".to_string());
        // check sorting
        assert_eq!(new_unique_id(vec!["1".to_string(), "0".to_string()]), "2".to_string());
        // check deduplication
        assert_eq!(
            new_unique_id(vec!["0".to_string(), "0".to_string(), "1".to_string()]),
            "2".to_string()
        );
    }
}