mirror of
https://github.com/SpaceManiac/SpacemanDMM.git
synced 2025-12-23 05:36:47 +00:00
Compare commits
197 commits
suite-1.7.
...
master
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
213284aa51 | ||
|
|
0db4298875 | ||
|
|
1259ccd3a0 | ||
|
|
70176ca27d | ||
|
|
660457818e | ||
|
|
ab4e94def1 | ||
|
|
b6125848e2 | ||
|
|
b8bb72a543 | ||
|
|
6087530e0d | ||
|
|
848352e671 | ||
|
|
567e57b817 | ||
|
|
0290db5c75 | ||
|
|
1b930c0a80 | ||
|
|
38f570f04e | ||
|
|
873b75c9d5 | ||
|
|
63b34b6585 | ||
|
|
98e69a4b4a | ||
|
|
814f96a538 | ||
|
|
72ed2bce62 | ||
|
|
5fa2cbb4db | ||
|
|
8187b2e065 | ||
|
|
b40d2176cd | ||
|
|
208c5bfac8 | ||
|
|
3162794032 | ||
|
|
7a1a990934 | ||
|
|
5c3ea44a99 | ||
|
|
442845ff48 | ||
|
|
936a5ee5a0 | ||
|
|
e2c596a99b | ||
|
|
5a96f6bc22 | ||
|
|
8ef9761b8e | ||
|
|
ed90d86e8f | ||
|
|
56a699be34 | ||
|
|
7f2641466b | ||
|
|
68a0d9d5f8 | ||
|
|
c1101fb3ad | ||
|
|
a58420e0b7 | ||
|
|
d6bcdd50d1 | ||
|
|
fcc814dfbd | ||
|
|
e447107f6c | ||
|
|
4b77cd487d | ||
|
|
039e8208c2 | ||
|
|
18a5d84ba5 | ||
|
|
9fd698e2d6 | ||
|
|
fa715c9194 | ||
|
|
76725c5681 | ||
|
|
c5e4b0b1e3 | ||
|
|
db59a08003 | ||
|
|
b3eb12c128 | ||
|
|
844ca6395a | ||
|
|
c7448ea917 | ||
|
|
4c8b80ec36 | ||
|
|
a026dc6c09 | ||
|
|
e22ff4757f | ||
|
|
d19602a6f0 | ||
|
|
6ede21716e | ||
|
|
7c3593538b | ||
|
|
63f43a1afc | ||
|
|
88a9fd048c | ||
|
|
86172de965 | ||
|
|
a7649bdce1 | ||
|
|
cb9444dc9e | ||
|
|
8c06700a4a | ||
|
|
4ca3cdf670 | ||
|
|
6cae59f0ef | ||
|
|
d423f00f78 | ||
|
|
568f4f417f | ||
|
|
a24a15a0fe | ||
|
|
20dabdca3a | ||
|
|
8df23f7793 | ||
|
|
0a003dfd46 | ||
|
|
9d988a1101 | ||
|
|
47643caae1 | ||
|
|
58258e6013 | ||
|
|
afc817e668 | ||
|
|
e72dd60746 | ||
|
|
5de66004a7 | ||
|
|
49a0f89624 | ||
|
|
b93a13f6a5 | ||
|
|
7c188f1f34 | ||
|
|
367bcdd75f | ||
|
|
ed98cd63d9 | ||
|
|
9d99263bec | ||
|
|
239ff91947 | ||
|
|
b62b10ce40 | ||
|
|
8d264957b8 | ||
|
|
ffa13126f9 | ||
|
|
5919d61c27 | ||
|
|
181067efd1 | ||
|
|
b80cd15fa2 | ||
|
|
c2ed4ed6e1 | ||
|
|
2d00d0e2d7 | ||
|
|
3206ddb261 | ||
|
|
d75085266b | ||
|
|
17373dc1d9 | ||
|
|
aec2fc9173 | ||
|
|
ca9e5a2c8c | ||
|
|
ef9485246d | ||
|
|
d704d63485 | ||
|
|
fbf6838291 | ||
|
|
e47fb7c91c | ||
|
|
5f5236f379 | ||
|
|
a97f9b47bb | ||
|
|
f030e316b9 | ||
|
|
e1b51dfee9 | ||
|
|
b4a6857928 | ||
|
|
8bbd2e1b19 | ||
|
|
a4afc52612 | ||
|
|
1a03fe0d27 | ||
|
|
551c3c921f | ||
|
|
30055a5b9f | ||
|
|
c6d85c798f | ||
|
|
6c5a751516 | ||
|
|
1f331f5db1 | ||
|
|
2dac6eba15 | ||
|
|
4883ef0e15 | ||
|
|
707c7b4fb8 | ||
|
|
671f484b40 | ||
|
|
e5fa81d78e | ||
|
|
59cfdbf826 | ||
|
|
74cc3b870b | ||
|
|
02b7d9c10d | ||
|
|
219269b9c4 | ||
|
|
af80187a7f | ||
|
|
5311ff5f02 | ||
|
|
356eeacab0 | ||
|
|
cef9528642 | ||
|
|
5b4ba580ab | ||
|
|
6d0e149cc3 | ||
|
|
53f9447fa0 | ||
|
|
0213b66f10 | ||
|
|
f6b4c8f6fe | ||
|
|
ec65eb1cf9 | ||
|
|
7dc22d522b | ||
|
|
587361db60 | ||
|
|
311e43472a | ||
|
|
6207188963 | ||
|
|
e5291ce927 | ||
|
|
2b3d30b9ac | ||
|
|
cdbb02897c | ||
|
|
970e9d0cfd | ||
|
|
61165ec220 | ||
|
|
e5dbc57757 | ||
|
|
a50249e6f8 | ||
|
|
39ef2d9aac | ||
|
|
d3dbed0fb2 | ||
|
|
47ef0c58ab | ||
|
|
90992c7e6e | ||
|
|
6df1be0be8 | ||
|
|
6fc78ac43b | ||
|
|
9467bd3ab3 | ||
|
|
fb57ec3c25 | ||
|
|
269ae0ebb2 | ||
|
|
5eb5358fd9 | ||
|
|
3797c66c2f | ||
|
|
1f9b4416b3 | ||
|
|
fa608c5c9f | ||
|
|
44a9a2ab3b | ||
|
|
34a656baf6 | ||
|
|
3eddd3204b | ||
|
|
32bf1b3b98 | ||
|
|
41297597fb | ||
|
|
240d8e02c4 | ||
|
|
e5c7b9fcb5 | ||
|
|
037f28d766 | ||
|
|
3be92a95ae | ||
|
|
7a23489a3b | ||
|
|
20dec28f21 | ||
|
|
56cd51d04c | ||
|
|
d37df4c7c2 | ||
|
|
00df33a093 | ||
|
|
b02afe7786 | ||
|
|
75c989bf14 | ||
|
|
7a2f3b00da | ||
|
|
c40fa46644 | ||
|
|
9d278dcd8e | ||
|
|
10d9575f9e | ||
|
|
5c07083706 | ||
|
|
8641274b25 | ||
|
|
4420bb5d60 | ||
|
|
4a7d19401e | ||
|
|
f80d6373e7 | ||
|
|
2be4bc87e3 | ||
|
|
6f26e074de | ||
|
|
860d6c48a6 | ||
|
|
e979b2e69a | ||
|
|
a2eaf42f9e | ||
|
|
59e161b02f | ||
|
|
4302562211 | ||
|
|
8d57ddf399 | ||
|
|
a0430d44e3 | ||
|
|
7e5335e78d | ||
|
|
963c2c0792 | ||
|
|
0a121f6ae5 | ||
|
|
4c24fcb883 | ||
|
|
4756f31219 | ||
|
|
0bc598a459 |
130 changed files with 13869 additions and 6903 deletions
1
.gitignore
vendored
1
.gitignore
vendored
|
|
@ -1,2 +1,3 @@
|
|||
target/
|
||||
.vscode/
|
||||
test_codebase/
|
||||
|
|
|
|||
16
.travis.yml
16
.travis.yml
|
|
@ -1,16 +0,0 @@
|
|||
language: rust
|
||||
rust:
|
||||
- stable
|
||||
branches:
|
||||
only:
|
||||
- master
|
||||
cache: cargo
|
||||
before_cache:
|
||||
- cargo-cache
|
||||
install:
|
||||
- rustc -Vv
|
||||
- cargo -V
|
||||
- cargo install cargo-cache --no-default-features --features ci-autoclean cargo-cache
|
||||
script:
|
||||
- cargo build --verbose --all
|
||||
- cargo test --verbose --all
|
||||
|
|
@ -50,6 +50,7 @@ Raised by DreamChecker:
|
|||
* `control_condition_static` - Raised on a control condition such as `if`/`while` having a static condition such as `1` or `"string"`
|
||||
* `if_condition_determinate` - Raised on if condition being always true or always false
|
||||
* `loop_condition_determinate` - Raised on loop condition such as in `for` being always true or always false
|
||||
* `improper_index` - Raised on accessing a non list with []
|
||||
|
||||
Raised by Lexer:
|
||||
|
||||
|
|
|
|||
2175
Cargo.lock
generated
2175
Cargo.lock
generated
File diff suppressed because it is too large
Load diff
|
|
@ -1,4 +1,5 @@
|
|||
[workspace]
|
||||
resolver = "2"
|
||||
members = [
|
||||
"crates/builtins-proc-macro",
|
||||
"crates/dap-types",
|
||||
|
|
@ -12,8 +13,14 @@ members = [
|
|||
#"crates/spaceman-dmm",
|
||||
]
|
||||
|
||||
[workspace.package]
|
||||
version = "1.11.0"
|
||||
authors = ["Tad Hardesty <tad@platymuus.com>"]
|
||||
edition = "2021"
|
||||
|
||||
[profile.dev]
|
||||
opt-level = 2
|
||||
|
||||
[profile.release]
|
||||
lto = true
|
||||
codegen-units = 1
|
||||
|
|
|
|||
17
README.md
17
README.md
|
|
@ -15,7 +15,7 @@ integration build; see [/tg/station's CI suite][ci] for an example.
|
|||
Support is currently provided in /tg/station13's coderbus (ping `SpaceManiac`)
|
||||
and on the [issue tracker]. Pull requests are welcome.
|
||||
|
||||
[DreamMaker]: https://secure.byond.com/
|
||||
[DreamMaker]: https://www.byond.com/
|
||||
[language server]: https://langserver.org/
|
||||
[releases]: https://github.com/SpaceManiac/SpacemanDMM/releases
|
||||
[ci]: https://github.com/tgstation/tgstation/blob/master/.github/workflows/ci_suite.yml#L45
|
||||
|
|
@ -93,21 +93,6 @@ the individual packages.
|
|||
[rust]: https://www.rust-lang.org/en-US/install.html
|
||||
[source readme]: ./crates/README.md
|
||||
|
||||
### Docker
|
||||
|
||||
A `dockerfile` is provided for the map generator binary. To build the docker
|
||||
image, enter the SpacemanDMM directory and run:
|
||||
|
||||
```shell
|
||||
docker build -t spacemandmm .
|
||||
```
|
||||
|
||||
To use the image, switch to the codebase you want to generate maps for and invoke the container:
|
||||
|
||||
```shell
|
||||
docker run -v "$PWD":/usr/src/codebase --rm -it spacemandmm -e /usr/src/codebase/tgstation.dme minimap /usr/src/codebase/_maps/map_files/BoxStation/BoxStation.dmm
|
||||
```
|
||||
|
||||
## License
|
||||
|
||||
SpacemanDMM is free software: you can redistribute it and/or modify
|
||||
|
|
|
|||
|
|
@ -2,12 +2,12 @@
|
|||
name = "builtins-proc-macro"
|
||||
version = "0.0.0"
|
||||
authors = ["Tad Hardesty <tad@platymuus.com>"]
|
||||
edition = "2018"
|
||||
edition = "2021"
|
||||
|
||||
[lib]
|
||||
proc-macro = true
|
||||
|
||||
[dependencies]
|
||||
syn = "1.0"
|
||||
syn = { version = "1.0", features = ["full"] }
|
||||
quote = "1.0"
|
||||
proc-macro2 = "1.0.24"
|
||||
proc-macro2 = "1.0.89"
|
||||
|
|
|
|||
|
|
@ -1,16 +1,17 @@
|
|||
use proc_macro::TokenStream;
|
||||
use proc_macro2::TokenStream as TokenStream2;
|
||||
use quote::{quote, quote_spanned};
|
||||
use syn::*;
|
||||
use syn::ext::IdentExt;
|
||||
use syn::parse::{Parse, ParseStream};
|
||||
use syn::punctuated::Punctuated;
|
||||
use syn::spanned::Spanned;
|
||||
use proc_macro2::TokenStream as TokenStream2;
|
||||
use syn::*;
|
||||
|
||||
#[derive(Clone, Default)]
|
||||
struct Header {
|
||||
attrs: Vec<Attribute>,
|
||||
path: Vec<Ident>,
|
||||
operator_overload_target: Option<String>,
|
||||
}
|
||||
|
||||
impl Header {
|
||||
|
|
@ -24,7 +25,110 @@ impl Header {
|
|||
input.parse::<Token![/]>()?;
|
||||
self.path.push(Ident::parse_any(input)?);
|
||||
}
|
||||
if let Some(final_ident) = self.path.last() {
|
||||
// If we find an operator{some token}() pattern we allow the some token part
|
||||
if final_ident == "operator" {
|
||||
self.parse_operator(input)?;
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn parse_operator(&mut self, input: ParseStream) -> Result<()> {
|
||||
let text_token: Option<&str> = if input.parse::<Token![%]>().is_ok() {
|
||||
if input.parse::<Token![%]>().is_ok() {
|
||||
Some("%%")
|
||||
} else if input.parse::<Token![%=]>().is_ok() {
|
||||
Some("%%=")
|
||||
} else {
|
||||
Some("%")
|
||||
}
|
||||
} else if input.parse::<Token![&]>().is_ok() {
|
||||
Some("&")
|
||||
} else if input.parse::<Token![&=]>().is_ok() {
|
||||
Some("&=")
|
||||
} else if input.parse::<Token![*]>().is_ok() {
|
||||
if input.parse::<Token![*]>().is_ok() {
|
||||
Some("**")
|
||||
} else {
|
||||
Some("*")
|
||||
}
|
||||
} else if input.parse::<Token![*=]>().is_ok() {
|
||||
Some("*=")
|
||||
} else if input.parse::<Token![/]>().is_ok() {
|
||||
Some("/")
|
||||
} else if input.parse::<Token![/=]>().is_ok() {
|
||||
Some("/=")
|
||||
} else if input.parse::<Token![+]>().is_ok() {
|
||||
if input.parse::<Token![+]>().is_ok() {
|
||||
Some("++")
|
||||
} else {
|
||||
Some("+")
|
||||
}
|
||||
} else if input.parse::<Token![+=]>().is_ok() {
|
||||
Some("+=")
|
||||
} else if input.parse::<Token![-]>().is_ok() {
|
||||
if input.parse::<Token![-]>().is_ok() {
|
||||
Some("--")
|
||||
} else {
|
||||
Some("-")
|
||||
}
|
||||
} else if input.parse::<Token![-=]>().is_ok() {
|
||||
Some("-=")
|
||||
} else if input.parse::<Token![<]>().is_ok() {
|
||||
Some("<")
|
||||
} else if input.parse::<Token![<<]>().is_ok() {
|
||||
Some("<<")
|
||||
} else if input.parse::<Token![<<=]>().is_ok() {
|
||||
Some("<<=")
|
||||
} else if input.parse::<Token![<=]>().is_ok() {
|
||||
Some("<=")
|
||||
} else if input.parse::<Token![>=]>().is_ok() {
|
||||
Some(">=")
|
||||
} else if input.parse::<Token![>>]>().is_ok() {
|
||||
Some(">>")
|
||||
} else if input.parse::<Token![>>=]>().is_ok() {
|
||||
Some(">>=")
|
||||
} else if input.parse::<Token![^]>().is_ok() {
|
||||
Some("^")
|
||||
} else if input.parse::<Token![^=]>().is_ok() {
|
||||
Some("^=")
|
||||
} else if input.parse::<Token![|]>().is_ok() {
|
||||
Some("|")
|
||||
} else if input.parse::<Token![|=]>().is_ok() {
|
||||
Some("|=")
|
||||
} else if input.parse::<Token![~]>().is_ok() {
|
||||
if input.parse::<Token![=]>().is_ok() {
|
||||
Some("~=")
|
||||
} else {
|
||||
Some("~")
|
||||
}
|
||||
} else if input.parse::<Token![~]>().is_ok() {
|
||||
Some("~")
|
||||
} else if input.peek(Token![:]) && input.peek2(Token![=]) {
|
||||
input.parse::<Token![:]>()?;
|
||||
input.parse::<Token![=]>()?;
|
||||
Some(":=")
|
||||
} else if self.brackets_next(input).is_ok() {
|
||||
if input.parse::<Token![=]>().is_ok() {
|
||||
Some("[]=")
|
||||
} else {
|
||||
Some("[]")
|
||||
}
|
||||
} else {
|
||||
// Todo: Implement operator""() support. Unsure how to expect an empty string
|
||||
None
|
||||
};
|
||||
if let Some(text) = text_token {
|
||||
self.operator_overload_target = Some(text.to_string());
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn brackets_next(&mut self, input: ParseStream) -> Result<()> {
|
||||
// Sorry
|
||||
let _bracket_dummy;
|
||||
bracketed!(_bracket_dummy in input);
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
|
@ -48,15 +152,13 @@ impl Parse for ProcArgument {
|
|||
input.parse::<Token![=]>()?;
|
||||
input.parse::<Expr>()?;
|
||||
}
|
||||
Ok(ProcArgument {
|
||||
name,
|
||||
})
|
||||
Ok(ProcArgument { name })
|
||||
}
|
||||
}
|
||||
|
||||
enum EntryBody {
|
||||
None,
|
||||
Variable(Option<Expr>),
|
||||
Variable(Option<Box<Expr>>),
|
||||
Proc(Punctuated<ProcArgument, Token![,]>),
|
||||
}
|
||||
|
||||
|
|
@ -64,11 +166,13 @@ impl EntryBody {
|
|||
fn parse_with_path(path: &[Ident], input: ParseStream) -> Result<Self> {
|
||||
if input.peek(Token![=]) {
|
||||
input.parse::<Token![=]>()?;
|
||||
Ok(EntryBody::Variable(Some(input.parse::<Expr>()?)))
|
||||
Ok(EntryBody::Variable(Some(Box::new(input.parse::<Expr>()?))))
|
||||
} else if input.peek(syn::token::Paren) {
|
||||
let content;
|
||||
parenthesized!(content in input);
|
||||
Ok(EntryBody::Proc(content.parse_terminated(ProcArgument::parse)?))
|
||||
Ok(EntryBody::Proc(
|
||||
content.parse_terminated(ProcArgument::parse)?,
|
||||
))
|
||||
} else if path.iter().any(|i| i == "var") {
|
||||
Ok(EntryBody::Variable(None))
|
||||
} else {
|
||||
|
|
@ -87,18 +191,19 @@ impl Parse for BuiltinEntry {
|
|||
let header: Header = input.parse()?;
|
||||
let body = EntryBody::parse_with_path(&header.path, input)?;
|
||||
|
||||
input.parse::<Token![;]>()?.span;
|
||||
Ok(BuiltinEntry {
|
||||
header,
|
||||
body,
|
||||
})
|
||||
input.parse::<Token![;]>()?;
|
||||
Ok(BuiltinEntry { header, body })
|
||||
}
|
||||
}
|
||||
|
||||
struct BuiltinsTable(Vec<BuiltinEntry>);
|
||||
|
||||
impl BuiltinsTable {
|
||||
fn parse_with_header_into(vec: &mut Vec<BuiltinEntry>, header: &Header, input: ParseStream) -> Result<()> {
|
||||
fn parse_with_header_into(
|
||||
vec: &mut Vec<BuiltinEntry>,
|
||||
header: &Header,
|
||||
input: ParseStream,
|
||||
) -> Result<()> {
|
||||
while !input.is_empty() {
|
||||
let mut new_header = header.clone();
|
||||
new_header.parse_mut(input)?;
|
||||
|
|
@ -143,7 +248,19 @@ pub fn builtins_table(input: TokenStream) -> TokenStream {
|
|||
let mut output = Vec::new();
|
||||
for entry in builtins {
|
||||
let span = entry.header.path.first().unwrap().span();
|
||||
let lit_strs: Vec<_> = entry.header.path.into_iter().map(|x| LitStr::new(&x.to_string(), x.span())).collect();
|
||||
let mut lit_strs: Vec<_> = entry
|
||||
.header
|
||||
.path
|
||||
.into_iter()
|
||||
.map(|x| LitStr::new(&x.to_string(), x.span()))
|
||||
.collect();
|
||||
if let Some(operator) = entry.header.operator_overload_target {
|
||||
let last_entry = lit_strs.pop().unwrap();
|
||||
lit_strs.push(LitStr::new(
|
||||
(last_entry.value() + operator.as_str()).as_str(),
|
||||
last_entry.span(),
|
||||
));
|
||||
}
|
||||
let path = quote! {
|
||||
&[ #(#lit_strs),* ]
|
||||
};
|
||||
|
|
@ -159,7 +276,7 @@ pub fn builtins_table(input: TokenStream) -> TokenStream {
|
|||
if ident == "doc" {
|
||||
markdown_span = Some(attr_span);
|
||||
markdown.push_str(&syn::parse2::<DocComment>(attr.tokens).unwrap().0.value());
|
||||
markdown.push_str("\n");
|
||||
markdown.push('\n');
|
||||
} else {
|
||||
attr_calls.extend(quote_spanned! { attr_span => .docs.#path });
|
||||
attr_calls.extend(attr.tokens);
|
||||
|
|
@ -188,14 +305,17 @@ pub fn builtins_table(input: TokenStream) -> TokenStream {
|
|||
}
|
||||
},
|
||||
EntryBody::Proc(args) => {
|
||||
let args: Vec<_> = args.into_iter().map(|x| LitStr::new(&x.name.to_string(), x.name.span())).collect();
|
||||
let args: Vec<_> = args
|
||||
.into_iter()
|
||||
.map(|x| LitStr::new(&x.name.to_string(), x.name.span()))
|
||||
.collect();
|
||||
quote_spanned! { span =>
|
||||
tree.add_builtin_proc(#path, &[ #(#args),* ]) #attr_calls;
|
||||
}
|
||||
}
|
||||
},
|
||||
};
|
||||
output.push(line);
|
||||
}
|
||||
|
||||
output.into_iter().flat_map(|x| TokenStream::from(x)).collect()
|
||||
output.into_iter().flat_map(TokenStream::from).collect()
|
||||
}
|
||||
|
|
|
|||
|
|
@ -2,10 +2,10 @@
|
|||
name = "dap-types"
|
||||
version = "0.0.0"
|
||||
authors = ["Tad Hardesty <tad@platymuus.com>"]
|
||||
edition = "2018"
|
||||
edition = "2021"
|
||||
|
||||
[dependencies]
|
||||
serde = "1.0.27"
|
||||
serde_json = "1.0.10"
|
||||
serde_derive = "1.0.27"
|
||||
ahash = "0.7.6"
|
||||
serde = "1.0.213"
|
||||
serde_json = "1.0.132"
|
||||
serde_derive = "1.0.213"
|
||||
foldhash = "0.2.0"
|
||||
|
|
|
|||
|
|
@ -4,10 +4,9 @@
|
|||
#![deny(unsafe_code)]
|
||||
#![allow(non_snake_case)]
|
||||
|
||||
use std::collections::HashMap;
|
||||
use foldhash::HashMap;
|
||||
use serde_derive::{Deserialize, Serialize};
|
||||
use serde_json::Value;
|
||||
use serde_derive::{Serialize, Deserialize};
|
||||
use ahash::RandomState;
|
||||
|
||||
pub trait Request {
|
||||
type Params;
|
||||
|
|
@ -176,8 +175,8 @@ pub struct ThreadEvent {
|
|||
pub reason: String,
|
||||
|
||||
/**
|
||||
* The identifier of the thread.
|
||||
*/
|
||||
* The identifier of the thread.
|
||||
*/
|
||||
pub threadId: i64,
|
||||
}
|
||||
|
||||
|
|
@ -578,7 +577,7 @@ pub struct EvaluateResponse {
|
|||
/**
|
||||
* The optional type of the evaluate result.
|
||||
*/
|
||||
#[serde(rename="type")]
|
||||
#[serde(rename = "type")]
|
||||
pub type_: Option<String>,
|
||||
|
||||
/**
|
||||
|
|
@ -611,13 +610,19 @@ pub struct EvaluateResponse {
|
|||
|
||||
impl From<String> for EvaluateResponse {
|
||||
fn from(result: String) -> EvaluateResponse {
|
||||
EvaluateResponse { result, .. Default::default() }
|
||||
EvaluateResponse {
|
||||
result,
|
||||
..Default::default()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<&str> for EvaluateResponse {
|
||||
fn from(result: &str) -> EvaluateResponse {
|
||||
EvaluateResponse { result: result.to_owned(), .. Default::default() }
|
||||
EvaluateResponse {
|
||||
result: result.to_owned(),
|
||||
..Default::default()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -929,7 +934,10 @@ pub struct SourceResponse {
|
|||
|
||||
impl From<String> for SourceResponse {
|
||||
fn from(content: String) -> SourceResponse {
|
||||
SourceResponse { content, mimeType: None }
|
||||
SourceResponse {
|
||||
content,
|
||||
mimeType: None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -1078,9 +1086,9 @@ pub struct VariablesArguments {
|
|||
|
||||
#[derive(Serialize, Deserialize, Debug)]
|
||||
pub enum VariablesFilter {
|
||||
#[serde(rename="indexed")]
|
||||
#[serde(rename = "indexed")]
|
||||
Indexed,
|
||||
#[serde(rename="named")]
|
||||
#[serde(rename = "named")]
|
||||
Named,
|
||||
}
|
||||
|
||||
|
|
@ -1359,16 +1367,16 @@ pub struct DisassembledInstruction {
|
|||
#[derive(Serialize, Deserialize, Debug)]
|
||||
pub enum ExceptionBreakMode {
|
||||
/// never breaks
|
||||
#[serde(rename="never")]
|
||||
#[serde(rename = "never")]
|
||||
Never,
|
||||
/// always breaks
|
||||
#[serde(rename="always")]
|
||||
#[serde(rename = "always")]
|
||||
Always,
|
||||
/// breaks when exception unhandled
|
||||
#[serde(rename="unhandled")]
|
||||
#[serde(rename = "unhandled")]
|
||||
Unhandled,
|
||||
/// breaks if the exception is not handled by user code
|
||||
#[serde(rename="userUnhandled")]
|
||||
#[serde(rename = "userUnhandled")]
|
||||
UserUnhandled,
|
||||
}
|
||||
|
||||
|
|
@ -1530,7 +1538,7 @@ pub struct Message {
|
|||
/**
|
||||
* An object used as a dictionary for looking up the variables in the format string.
|
||||
*/
|
||||
pub variables: Option<HashMap<String, String, RandomState>>,
|
||||
pub variables: Option<HashMap<String, String>>,
|
||||
|
||||
/**
|
||||
* If true send to telemetry.
|
||||
|
|
@ -1661,7 +1669,6 @@ pub struct Source {
|
|||
* Optional data that a debug adapter might want to loop through the client. The client should leave the data intact and persist it across sessions. The client should not interpret the data.
|
||||
*/
|
||||
pub adapterData: Option<Value>,
|
||||
|
||||
/*/**
|
||||
* The checksums associated with this file.
|
||||
*/
|
||||
|
|
@ -1670,11 +1677,11 @@ pub struct Source {
|
|||
|
||||
#[derive(Serialize, Deserialize, Debug)]
|
||||
pub enum SourcePresentationHint {
|
||||
#[serde(rename="normal")]
|
||||
#[serde(rename = "normal")]
|
||||
Normal,
|
||||
#[serde(rename="emphasize")]
|
||||
#[serde(rename = "emphasize")]
|
||||
Emphasize,
|
||||
#[serde(rename="deemphasize")]
|
||||
#[serde(rename = "deemphasize")]
|
||||
Deemphasize,
|
||||
}
|
||||
|
||||
|
|
@ -1754,7 +1761,6 @@ pub struct StackFrame {
|
|||
* The module associated with this frame, if any.
|
||||
*/
|
||||
moduleId?: number | string;*/
|
||||
|
||||
/**
|
||||
* An optional hint for how to present this frame in the UI. A value of 'label' can be used to indicate that the frame is an artificial frame that is used as a visual label or separator. A value of 'subtle' can be used to change the appearance of a frame in a 'subtle' way.
|
||||
*/
|
||||
|
|
@ -1763,11 +1769,11 @@ pub struct StackFrame {
|
|||
|
||||
#[derive(Serialize, Deserialize, Debug)]
|
||||
pub enum StackFramePresentationHint {
|
||||
#[serde(rename="normal")]
|
||||
#[serde(rename = "normal")]
|
||||
Normal,
|
||||
#[serde(rename="label")]
|
||||
#[serde(rename = "label")]
|
||||
Label,
|
||||
#[serde(rename="subtle")]
|
||||
#[serde(rename = "subtle")]
|
||||
Subtle,
|
||||
}
|
||||
|
||||
|
|
@ -1871,7 +1877,7 @@ pub struct Variable {
|
|||
/**
|
||||
* The type of the variable's value. Typically shown in the UI when hovering over the value.
|
||||
*/
|
||||
#[serde(rename="type")]
|
||||
#[serde(rename = "type")]
|
||||
pub type_: Option<String>,
|
||||
|
||||
/**
|
||||
|
|
|
|||
|
|
@ -1,27 +1,31 @@
|
|||
[package]
|
||||
name = "dm-langserver"
|
||||
version = "1.5.1"
|
||||
authors = ["Tad Hardesty <tad@platymuus.com>"]
|
||||
edition = "2018"
|
||||
version.workspace = true
|
||||
authors.workspace = true
|
||||
edition.workspace = true
|
||||
|
||||
[dependencies]
|
||||
url = "2.1.0"
|
||||
serde = "1.0.27"
|
||||
serde_json = "1.0.10"
|
||||
serde_derive = "1.0.27"
|
||||
bincode = "1.3.1"
|
||||
jsonrpc-core = "14.0.3"
|
||||
lsp-types = "0.80.0"
|
||||
url = "2.5.2"
|
||||
serde = "1.0.213"
|
||||
serde_json = "1.0.132"
|
||||
serde_derive = "1.0.213"
|
||||
bincode = "1.3.3"
|
||||
jsonrpc-core = "18.0.0"
|
||||
lsp-types = "0.93.2"
|
||||
dap-types = { path = "../dap-types" }
|
||||
dreammaker = { path = "../dreammaker" }
|
||||
dreamchecker = { path = "../dreamchecker" }
|
||||
interval-tree = { path = "../interval-tree" }
|
||||
libc = "0.2.65"
|
||||
guard = "0.5.0"
|
||||
regex = "1.3"
|
||||
lazy_static = "1.4"
|
||||
ahash = "0.7.6"
|
||||
libc = "0.2.161"
|
||||
regex = "1.11.1"
|
||||
lazy_static = "1.5"
|
||||
foldhash = "0.2.0"
|
||||
|
||||
[build-dependencies]
|
||||
chrono = "0.4.0"
|
||||
git2 = { version = "0.13", default-features = false }
|
||||
chrono = "0.4.38"
|
||||
git2 = { version = "0.20.2", default-features = false }
|
||||
sha256 = { version = "1.5.0", default-features = false }
|
||||
ureq = "2.10.1"
|
||||
|
||||
[lints.rust]
|
||||
unexpected_cfgs = { level = "warn", check-cfg = ['cfg(auxtools_bundle)', 'cfg(extools_bundle)'] }
|
||||
|
|
|
|||
|
|
@ -12,7 +12,7 @@ client support.
|
|||
compatible.
|
||||
|
||||
[language server]: https://langserver.org/
|
||||
[BYOND]: https://secure.byond.com/
|
||||
[BYOND]: https://www.byond.com/
|
||||
|
||||
## Code completion
|
||||
|
||||
|
|
|
|||
|
|
@ -1,33 +1,41 @@
|
|||
extern crate chrono;
|
||||
extern crate git2;
|
||||
|
||||
use std::io::Write;
|
||||
use std::fs::File;
|
||||
use std::env;
|
||||
use std::path::PathBuf;
|
||||
use std::fs::File;
|
||||
use std::io::Write;
|
||||
use std::path::{Path, PathBuf};
|
||||
|
||||
fn main() {
|
||||
// build info
|
||||
let out_dir = PathBuf::from(env::var_os("OUT_DIR").unwrap());
|
||||
let mut f = File::create(&out_dir.join("build-info.txt")).unwrap();
|
||||
let mut f = File::create(out_dir.join("build-info.txt")).unwrap();
|
||||
|
||||
match read_commit() {
|
||||
Ok(commit) => writeln!(f, "commit: {}", commit).unwrap(),
|
||||
Err(err) => println!("cargo:warning=Failed to fetch commit info: {}", err)
|
||||
Ok(commit) => writeln!(f, "commit: {commit}").unwrap(),
|
||||
Err(err) => println!("cargo:warning=Failed to fetch commit info: {err}"),
|
||||
}
|
||||
writeln!(f, "build date: {}", chrono::Utc::today()).unwrap();
|
||||
writeln!(f, "build date: {}", chrono::Utc::now().date_naive()).unwrap();
|
||||
|
||||
// extools bundling
|
||||
println!("cargo:rerun-if-env-changed=EXTOOLS_BUNDLE_DLL");
|
||||
if env::var_os("EXTOOLS_BUNDLE_DLL").is_some() {
|
||||
println!("cargo:rustc-cfg=extools_bundle");
|
||||
}
|
||||
println!("cargo:rustc-cfg=extools_bundle");
|
||||
download_dll(
|
||||
&out_dir,
|
||||
"extools.dll",
|
||||
"v0.0.7", // EXTOOLS_TAG
|
||||
"https://github.com/tgstation/tgstation/raw/34f0cc6394a064b87cbd1d6cb225f1d3df444ba7/byond-extools.dll", // EXTOOLS_DLL_URL
|
||||
"073dd08790a13580bae71758e9217917700dd85ce8d35cb030cef0cf5920fca8", // EXTOOLS_DLL_SHA256
|
||||
);
|
||||
|
||||
// auxtools bundling
|
||||
println!("cargo:rerun-if-env-changed=AUXTOOLS_BUNDLE_DLL");
|
||||
if env::var_os("AUXTOOLS_BUNDLE_DLL").is_some() {
|
||||
println!("cargo:rustc-cfg=auxtools_bundle");
|
||||
}
|
||||
println!("cargo:rustc-cfg=auxtools_bundle");
|
||||
download_dll(
|
||||
&out_dir,
|
||||
"debug_server.dll",
|
||||
"v2.3.5", // DEBUG_SERVER_TAG
|
||||
"https://github.com/willox/auxtools/releases/download/v2.3.5/debug_server.dll", // DEBUG_SERVER_DLL_URL
|
||||
"dfcaa1086608047559103b55396f99504320f2b0ec1695baa3dc34dbd41695b2", // DEBUG_SERVER_DLL_SHA256
|
||||
);
|
||||
}
|
||||
|
||||
fn read_commit() -> Result<String, git2::Error> {
|
||||
|
|
@ -35,28 +43,64 @@ fn read_commit() -> Result<String, git2::Error> {
|
|||
let head = repo.head()?.peel_to_commit()?.id();
|
||||
|
||||
let mut all_tags = Vec::new();
|
||||
repo.tag_foreach(|oid, _| { all_tags.push(oid); true })?;
|
||||
repo.tag_foreach(|oid, _| {
|
||||
all_tags.push(oid);
|
||||
true
|
||||
})?;
|
||||
|
||||
let mut best = None;
|
||||
for tag_id in all_tags {
|
||||
let tag_commit = repo.find_tag(tag_id)?.as_object().peel_to_commit()?.id();
|
||||
let (ahead, behind) = repo.graph_ahead_behind(head, tag_commit)?;
|
||||
if behind == 0 {
|
||||
match best {
|
||||
None => best = Some(ahead),
|
||||
Some(prev) if ahead < prev => best = Some(ahead),
|
||||
_ => {}
|
||||
if let Ok(possible_tag) = repo.find_tag(tag_id) {
|
||||
let tag_commit = possible_tag.as_object().peel_to_commit()?.id();
|
||||
let (ahead, behind) = repo.graph_ahead_behind(head, tag_commit)?;
|
||||
if behind == 0 {
|
||||
match best {
|
||||
None => best = Some(ahead),
|
||||
Some(prev) if ahead < prev => best = Some(ahead),
|
||||
_ => {},
|
||||
}
|
||||
}
|
||||
if ahead == 0 {
|
||||
break;
|
||||
}
|
||||
}
|
||||
if ahead == 0 {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
match best {
|
||||
None | Some(0) => {}
|
||||
Some(ahead) => println!("cargo:rustc-env=CARGO_PKG_VERSION={}+{}", std::env::var("CARGO_PKG_VERSION").unwrap(), ahead),
|
||||
None | Some(0) => {},
|
||||
Some(ahead) => println!(
|
||||
"cargo:rustc-env=CARGO_PKG_VERSION={}+{}",
|
||||
std::env::var("CARGO_PKG_VERSION").unwrap(),
|
||||
ahead
|
||||
),
|
||||
}
|
||||
|
||||
Ok(head.to_string())
|
||||
}
|
||||
|
||||
fn download_dll(out_dir: &Path, fname: &str, tag: &str, url: &str, sha256: &str) {
|
||||
let full_path = out_dir.join(fname);
|
||||
println!(
|
||||
"cargo:rustc-env=BUNDLE_PATH_{}={}",
|
||||
fname,
|
||||
full_path.display()
|
||||
);
|
||||
println!("cargo:rustc-env=BUNDLE_VERSION_{fname}={tag}");
|
||||
|
||||
if let Ok(digest) = sha256::try_digest(&full_path) {
|
||||
if digest == sha256 {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
std::io::copy(
|
||||
&mut ureq::get(url)
|
||||
.call()
|
||||
.expect("Error downloading DLL to bundle")
|
||||
.into_reader(),
|
||||
&mut std::fs::File::create(&full_path).unwrap(),
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
assert_eq!(sha256, sha256::try_digest(&full_path).unwrap());
|
||||
}
|
||||
|
|
|
|||
|
|
@ -34,9 +34,9 @@ impl<T: Send + 'static> Background<T> {
|
|||
match rx.try_recv() {
|
||||
Ok(v) => {
|
||||
self.value = Some(v);
|
||||
}
|
||||
},
|
||||
Err(TryRecvError::Empty) => self.rx = Some(rx),
|
||||
Err(TryRecvError::Disconnected) => {}
|
||||
Err(TryRecvError::Disconnected) => {},
|
||||
}
|
||||
}
|
||||
self
|
||||
|
|
|
|||
|
|
@ -7,7 +7,7 @@
|
|||
use regex::Regex;
|
||||
|
||||
/// Extract ranges and colors from an input string.
|
||||
pub fn extract_colors<'a>(input: &'a str) -> impl Iterator<Item=(usize, usize, [u8; 4])> + 'a {
|
||||
pub fn extract_colors(input: &str) -> impl Iterator<Item = (usize, usize, [u8; 4])> + '_ {
|
||||
COLOR_REGEX.captures_iter(input).flat_map(|capture| {
|
||||
parse_capture(&capture).map(|rgba| {
|
||||
let totality = capture.get(0).unwrap();
|
||||
|
|
@ -26,7 +26,7 @@ pub enum ColorFormat {
|
|||
},
|
||||
Rgb {
|
||||
alpha: bool,
|
||||
}
|
||||
},
|
||||
}
|
||||
|
||||
impl ColorFormat {
|
||||
|
|
@ -53,26 +53,45 @@ impl ColorFormat {
|
|||
|
||||
pub fn format(self, [r, g, b, a]: [u8; 4]) -> String {
|
||||
match self {
|
||||
ColorFormat::Hex { single_quoted, short, alpha } => {
|
||||
ColorFormat::Hex {
|
||||
single_quoted,
|
||||
short,
|
||||
alpha,
|
||||
} => {
|
||||
let q = if single_quoted { '\'' } else { '"' };
|
||||
let short = short && r % 0x11 == 0 && g % 0x11 == 0 && b % 0x11 == 0 && a % 0x11 == 0;
|
||||
let short =
|
||||
short && r % 0x11 == 0 && g % 0x11 == 0 && b % 0x11 == 0 && a % 0x11 == 0;
|
||||
let alpha = alpha || a != 255;
|
||||
match (short, alpha) {
|
||||
(false, false) => format!("{}#{:02x}{:02x}{:02x}{}", q, r, g, b, q),
|
||||
(false, true) => format!("{}#{:02x}{:02x}{:02x}{:02x}{}", q, r, g, b, a, q),
|
||||
(true, false) => format!("{}#{:x}{:x}{:x}{}", q, r / 0x11, g / 0x11, b / 0x11, q),
|
||||
(true, true) => format!("{}#{:x}{:x}{:x}{:x}{}", q, r / 0x11, g / 0x11, b / 0x11, a / 0x11, q),
|
||||
(false, false) => format!("{q}#{r:02x}{g:02x}{b:02x}{q}"),
|
||||
(false, true) => format!("{q}#{r:02x}{g:02x}{b:02x}{a:02x}{q}"),
|
||||
(true, false) => {
|
||||
format!("{}#{:x}{:x}{:x}{}", q, r / 0x11, g / 0x11, b / 0x11, q)
|
||||
},
|
||||
(true, true) => format!(
|
||||
"{}#{:x}{:x}{:x}{:x}{}",
|
||||
q,
|
||||
r / 0x11,
|
||||
g / 0x11,
|
||||
b / 0x11,
|
||||
a / 0x11,
|
||||
q
|
||||
),
|
||||
}
|
||||
},
|
||||
ColorFormat::Rgb { alpha } if alpha || a != 255 => format!("rgb({}, {}, {}, {})", r, g, b, a),
|
||||
ColorFormat::Rgb { alpha: _ } => format!("rgb({}, {}, {})", r, g, b),
|
||||
ColorFormat::Rgb { alpha } if alpha || a != 255 => format!("rgb({r}, {g}, {b}, {a})"),
|
||||
ColorFormat::Rgb { alpha: _ } => format!("rgb({r}, {g}, {b})"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for ColorFormat {
|
||||
fn default() -> ColorFormat {
|
||||
ColorFormat::Hex { single_quoted: false, short: false, alpha: false }
|
||||
ColorFormat::Hex {
|
||||
single_quoted: false,
|
||||
short: false,
|
||||
alpha: false,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -83,11 +102,19 @@ lazy_static! {
|
|||
|
||||
fn parse_capture(capture: ®ex::Captures) -> Option<[u8; 4]> {
|
||||
// Tied closely to the regex above.
|
||||
match (capture.get(1), capture.get(2), capture.get(3), capture.get(4), capture.get(5), capture.get(6)) {
|
||||
(Some(cap), _, _, _, _, _) |
|
||||
(_, Some(cap), _, _, _, _) => parse_hex(cap.as_str()),
|
||||
(_, _, Some(r), Some(g), Some(b), a) => parse_rgba(r.as_str(), g.as_str(), b.as_str(), a.map(|a| a.as_str())),
|
||||
_ => None
|
||||
match (
|
||||
capture.get(1),
|
||||
capture.get(2),
|
||||
capture.get(3),
|
||||
capture.get(4),
|
||||
capture.get(5),
|
||||
capture.get(6),
|
||||
) {
|
||||
(Some(cap), _, _, _, _, _) | (_, Some(cap), _, _, _, _) => parse_hex(cap.as_str()),
|
||||
(_, _, Some(r), Some(g), Some(b), a) => {
|
||||
parse_rgba(r.as_str(), g.as_str(), b.as_str(), a.map(|a| a.as_str()))
|
||||
},
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -97,28 +124,27 @@ fn parse_hex(hex: &str) -> Option<[u8; 4]> {
|
|||
sum = 16 * sum + ch.to_digit(16).unwrap_or(0);
|
||||
}
|
||||
|
||||
if hex.len() == 8 { // #rrggbbaa
|
||||
if hex.len() == 8 {
|
||||
// #rrggbbaa
|
||||
Some([
|
||||
(sum >> 24) as u8,
|
||||
(sum >> 16) as u8,
|
||||
(sum >> 8) as u8,
|
||||
sum as u8,
|
||||
])
|
||||
} else if hex.len() == 6 { // #rrggbb
|
||||
Some([
|
||||
(sum >> 16) as u8,
|
||||
(sum >> 8) as u8,
|
||||
sum as u8,
|
||||
255,
|
||||
])
|
||||
} else if hex.len() == 4 { // #rgba
|
||||
} else if hex.len() == 6 {
|
||||
// #rrggbb
|
||||
Some([(sum >> 16) as u8, (sum >> 8) as u8, sum as u8, 255])
|
||||
} else if hex.len() == 4 {
|
||||
// #rgba
|
||||
Some([
|
||||
(0x11 * ((sum >> 12) & 0xf)) as u8,
|
||||
(0x11 * ((sum >> 8) & 0xf)) as u8,
|
||||
(0x11 * ((sum >> 4) & 0xf)) as u8,
|
||||
(0x11 * (sum & 0xf)) as u8,
|
||||
])
|
||||
} else if hex.len() == 3 { // #rgb
|
||||
} else if hex.len() == 3 {
|
||||
// #rgb
|
||||
Some([
|
||||
(0x11 * ((sum >> 8) & 0xf)) as u8,
|
||||
(0x11 * ((sum >> 4) & 0xf)) as u8,
|
||||
|
|
|
|||
|
|
@ -1,24 +1,25 @@
|
|||
//! Supporting functions for completion and go-to-definition.
|
||||
|
||||
use std::collections::HashSet;
|
||||
use foldhash::{HashSet, HashSetExt};
|
||||
|
||||
use lsp_types::*;
|
||||
|
||||
use dm::ast::PathOp;
|
||||
use dm::annotation::Annotation;
|
||||
use dm::objtree::{TypeRef, TypeVar, TypeProc, ProcValue};
|
||||
use dm::ast::PathOp;
|
||||
use dm::objtree::{ProcValue, TypeProc, TypeRef, TypeVar};
|
||||
|
||||
use crate::{Engine, Span, is_constructor_name};
|
||||
use crate::symbol_search::contains;
|
||||
use crate::{is_constructor_name, Engine, Span};
|
||||
|
||||
use ahash::RandomState;
|
||||
|
||||
#[rustfmt::skip]
|
||||
static PROC_KEYWORDS: &[&str] = &[
|
||||
// Implicit variables
|
||||
"args",
|
||||
"global",
|
||||
"src",
|
||||
"usr",
|
||||
"caller",
|
||||
"callee",
|
||||
|
||||
// Term
|
||||
"null",
|
||||
|
|
@ -58,7 +59,7 @@ fn item_var(ty: TypeRef, name: &str, var: &TypeVar) -> CompletionItem {
|
|||
if ty.is_root() {
|
||||
detail = constant.to_string();
|
||||
} else {
|
||||
detail = format!("{} - {}", constant, detail);
|
||||
detail = format!("{constant} - {detail}");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -66,10 +67,10 @@ fn item_var(ty: TypeRef, name: &str, var: &TypeVar) -> CompletionItem {
|
|||
|
||||
CompletionItem {
|
||||
label: name.to_owned(),
|
||||
kind: Some(CompletionItemKind::Field),
|
||||
kind: Some(CompletionItemKind::FIELD),
|
||||
detail: Some(detail),
|
||||
documentation: item_documentation(&var.value.docs),
|
||||
.. Default::default()
|
||||
..Default::default()
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -77,11 +78,11 @@ fn item_proc(ty: TypeRef, name: &str, proc: &TypeProc) -> CompletionItem {
|
|||
CompletionItem {
|
||||
label: name.to_owned(),
|
||||
kind: Some(if ty.is_root() {
|
||||
CompletionItemKind::Function
|
||||
CompletionItemKind::FUNCTION
|
||||
} else if is_constructor_name(name) {
|
||||
CompletionItemKind::Constructor
|
||||
CompletionItemKind::CONSTRUCTOR
|
||||
} else {
|
||||
CompletionItemKind::Method
|
||||
CompletionItemKind::METHOD
|
||||
}),
|
||||
detail: Some(format!("on {}", ty.pretty_path())),
|
||||
documentation: item_documentation(&proc.main_value().docs),
|
||||
|
|
@ -102,7 +103,7 @@ fn item_documentation(docs: &dm::docs::DocCollection) -> Option<Documentation> {
|
|||
|
||||
fn items_ty<'a>(
|
||||
results: &mut Vec<CompletionItem>,
|
||||
skip: &mut HashSet<(&str, &'a String), RandomState>,
|
||||
skip: &mut HashSet<(&str, &'a String)>,
|
||||
ty: TypeRef<'a>,
|
||||
query: &str,
|
||||
) {
|
||||
|
|
@ -124,14 +125,19 @@ fn items_ty<'a>(
|
|||
if contains(name, query) {
|
||||
results.push(CompletionItem {
|
||||
insert_text: Some(name.to_owned()),
|
||||
.. item_proc(ty, name, proc)
|
||||
..item_proc(ty, name, proc)
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn combine_tree_path<'a, I>(iter: &I, mut absolute: bool, mut parts: &'a [String]) -> impl Iterator<Item=&'a str>
|
||||
where I: Iterator<Item=(Span, &'a Annotation)> + Clone
|
||||
pub fn combine_tree_path<'a, I>(
|
||||
iter: &I,
|
||||
mut absolute: bool,
|
||||
mut parts: &'a [String],
|
||||
) -> impl Iterator<Item = &'a str>
|
||||
where
|
||||
I: Iterator<Item = (Span, &'a Annotation)> + Clone,
|
||||
{
|
||||
// cut off the part of the path we haven't selected
|
||||
if_annotation! { Annotation::InSequence(idx) in iter; {
|
||||
|
|
@ -165,10 +171,14 @@ pub fn combine_tree_path<'a, I>(iter: &I, mut absolute: bool, mut parts: &'a [St
|
|||
prefix_parts.iter().chain(parts).map(|x| &**x)
|
||||
}
|
||||
|
||||
impl<'a> Engine<'a> {
|
||||
pub fn follow_type_path<'b, I>(&'b self, iter: &I, mut parts: &'b [(PathOp, String)]) -> Option<TypePathResult<'b>>
|
||||
impl Engine {
|
||||
pub fn follow_type_path<'b, I>(
|
||||
&'b self,
|
||||
iter: &I,
|
||||
mut parts: &'b [(PathOp, String)],
|
||||
) -> Option<TypePathResult<'b>>
|
||||
where
|
||||
I: Iterator<Item = (Span, &'a Annotation)> + Clone,
|
||||
I: Iterator<Item = (Span, &'b Annotation)> + Clone,
|
||||
{
|
||||
// cut off the part of the path we haven't selected
|
||||
if_annotation! { Annotation::InSequence(idx) in iter; {
|
||||
|
|
@ -177,7 +187,7 @@ impl<'a> Engine<'a> {
|
|||
// if we're on the right side of a 'list/', start the lookup there
|
||||
match parts.split_first() {
|
||||
Some(((PathOp::Slash, kwd), rest)) if kwd == "list" && !rest.is_empty() => parts = rest,
|
||||
_ => {}
|
||||
_ => {},
|
||||
}
|
||||
|
||||
// use the first path op to select the starting type of the lookup
|
||||
|
|
@ -189,7 +199,7 @@ impl<'a> Engine<'a> {
|
|||
});
|
||||
}
|
||||
let mut ty = match parts[0].0 {
|
||||
PathOp::Colon => return None, // never finds anything, apparently?
|
||||
PathOp::Colon => return None, // never finds anything, apparently?
|
||||
PathOp::Slash => self.objtree.root(),
|
||||
PathOp::Dot => match self.find_type_context(iter) {
|
||||
(Some(base), _) => base,
|
||||
|
|
@ -200,7 +210,7 @@ impl<'a> Engine<'a> {
|
|||
// follow the path ops until we hit 'proc' or 'verb'
|
||||
let mut iter = parts.iter();
|
||||
let mut decl = None;
|
||||
while let Some(&(op, ref name)) = iter.next() {
|
||||
for &(op, ref name) in iter.by_ref() {
|
||||
if name == "proc" {
|
||||
decl = Some("proc");
|
||||
break;
|
||||
|
|
@ -228,14 +238,20 @@ impl<'a> Engine<'a> {
|
|||
Some(TypePathResult { ty, decl, proc })
|
||||
}
|
||||
|
||||
pub fn tree_completions(&self, results: &mut Vec<CompletionItem>, exact: bool, ty: TypeRef, query: &str) {
|
||||
pub fn tree_completions(
|
||||
&self,
|
||||
results: &mut Vec<CompletionItem>,
|
||||
exact: bool,
|
||||
ty: TypeRef,
|
||||
query: &str,
|
||||
) {
|
||||
// path keywords
|
||||
for &name in ["proc", "var", "verb"].iter() {
|
||||
if contains(name, query) {
|
||||
results.push(CompletionItem {
|
||||
label: name.to_owned(),
|
||||
kind: Some(CompletionItemKind::Keyword),
|
||||
.. Default::default()
|
||||
kind: Some(CompletionItemKind::KEYWORD),
|
||||
..Default::default()
|
||||
})
|
||||
}
|
||||
}
|
||||
|
|
@ -246,16 +262,16 @@ impl<'a> Engine<'a> {
|
|||
if contains(child.name(), query) {
|
||||
results.push(CompletionItem {
|
||||
label: child.name().to_owned(),
|
||||
kind: Some(CompletionItemKind::Class),
|
||||
kind: Some(CompletionItemKind::CLASS),
|
||||
documentation: item_documentation(&child.docs),
|
||||
.. Default::default()
|
||||
..Default::default()
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let mut next = Some(ty).filter(|ty| !ty.is_root());
|
||||
let mut skip = HashSet::with_hasher(RandomState::default());
|
||||
let mut skip = HashSet::new();
|
||||
while let Some(ty) = next {
|
||||
// override a parent's var
|
||||
for (name, var) in ty.get().vars.iter() {
|
||||
|
|
@ -264,8 +280,8 @@ impl<'a> Engine<'a> {
|
|||
}
|
||||
if contains(name, query) {
|
||||
results.push(CompletionItem {
|
||||
insert_text: Some(format!("{} = ", name)),
|
||||
.. item_var(ty, name, var)
|
||||
insert_text: Some(format!("{name} = ")),
|
||||
..item_var(ty, name, var)
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
@ -278,11 +294,11 @@ impl<'a> Engine<'a> {
|
|||
if contains(name, query) {
|
||||
use std::fmt::Write;
|
||||
|
||||
let mut completion = format!("{}(", name);
|
||||
let mut completion = format!("{name}(");
|
||||
let mut sep = "";
|
||||
for param in proc.main_value().parameters.iter() {
|
||||
for each in param.var_type.type_path.iter() {
|
||||
let _ = write!(completion, "{}{}", sep, each);
|
||||
let _ = write!(completion, "{sep}{each}");
|
||||
sep = "/";
|
||||
}
|
||||
let _ = write!(completion, "{}{}", sep, param.name);
|
||||
|
|
@ -292,7 +308,7 @@ impl<'a> Engine<'a> {
|
|||
|
||||
results.push(CompletionItem {
|
||||
insert_text: Some(completion),
|
||||
.. item_proc(ty, name, proc)
|
||||
..item_proc(ty, name, proc)
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
@ -323,8 +339,8 @@ impl<'a> Engine<'a> {
|
|||
if contains(name, query) {
|
||||
results.push(CompletionItem {
|
||||
label: name.to_owned(),
|
||||
kind: Some(CompletionItemKind::Keyword),
|
||||
.. Default::default()
|
||||
kind: Some(CompletionItemKind::KEYWORD),
|
||||
..Default::default()
|
||||
})
|
||||
}
|
||||
}
|
||||
|
|
@ -334,9 +350,9 @@ impl<'a> Engine<'a> {
|
|||
if contains(child.name(), query) {
|
||||
results.push(CompletionItem {
|
||||
label: child.name().to_owned(),
|
||||
kind: Some(CompletionItemKind::Class),
|
||||
kind: Some(CompletionItemKind::CLASS),
|
||||
documentation: item_documentation(&child.docs),
|
||||
.. Default::default()
|
||||
..Default::default()
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
@ -349,7 +365,7 @@ impl<'a> Engine<'a> {
|
|||
proc: None,
|
||||
}) => {
|
||||
let mut next = Some(ty);
|
||||
let mut skip = HashSet::with_hasher(RandomState::default());
|
||||
let mut skip = HashSet::new();
|
||||
while let Some(ty) = next {
|
||||
// reference a declared proc
|
||||
for (name, proc) in ty.get().procs.iter() {
|
||||
|
|
@ -371,12 +387,16 @@ impl<'a> Engine<'a> {
|
|||
next = ty.parent_type_without_root();
|
||||
}
|
||||
},
|
||||
_ => {}
|
||||
_ => {},
|
||||
}
|
||||
}
|
||||
|
||||
pub fn unscoped_completions<'b, I>(&'b self, results: &mut Vec<CompletionItem>, iter: &I, query: &str)
|
||||
where
|
||||
pub fn unscoped_completions<'b, I>(
|
||||
&'b self,
|
||||
results: &mut Vec<CompletionItem>,
|
||||
iter: &I,
|
||||
query: &str,
|
||||
) where
|
||||
I: Iterator<Item = (Span, &'b Annotation)> + Clone,
|
||||
{
|
||||
let (ty, proc_name) = self.find_type_context(iter);
|
||||
|
|
@ -387,8 +407,8 @@ impl<'a> Engine<'a> {
|
|||
if contains(name, query) {
|
||||
results.push(CompletionItem {
|
||||
label: name.to_owned(),
|
||||
kind: Some(CompletionItemKind::Keyword),
|
||||
.. Default::default()
|
||||
kind: Some(CompletionItemKind::KEYWORD),
|
||||
..Default::default()
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
@ -400,16 +420,16 @@ impl<'a> Engine<'a> {
|
|||
if contains(name, query) {
|
||||
results.push(CompletionItem {
|
||||
label: name.clone(),
|
||||
kind: Some(CompletionItemKind::Variable),
|
||||
kind: Some(CompletionItemKind::VARIABLE),
|
||||
detail: Some("(local)".to_owned()),
|
||||
.. Default::default()
|
||||
..Default::default()
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// proc parameters
|
||||
let ty = ty.unwrap_or(self.objtree.root());
|
||||
let ty = ty.unwrap_or_else(|| self.objtree.root());
|
||||
if let Some((proc_name, idx)) = proc_name {
|
||||
if let Some(proc) = ty.get().procs.get(proc_name) {
|
||||
if let Some(value) = proc.value.get(idx) {
|
||||
|
|
@ -417,9 +437,9 @@ impl<'a> Engine<'a> {
|
|||
if contains(¶m.name, query) {
|
||||
results.push(CompletionItem {
|
||||
label: param.name.clone(),
|
||||
kind: Some(CompletionItemKind::Variable),
|
||||
kind: Some(CompletionItemKind::VARIABLE),
|
||||
detail: Some("(parameter)".to_owned()),
|
||||
.. Default::default()
|
||||
..Default::default()
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
@ -430,14 +450,14 @@ impl<'a> Engine<'a> {
|
|||
// macros
|
||||
if let Some(ref defines) = self.defines {
|
||||
// TODO: verify that the macro is in scope at the location
|
||||
for (_, &(ref name, ref define)) in defines.iter() {
|
||||
for (_, (name, define)) in defines.iter() {
|
||||
if contains(name, query) {
|
||||
results.push(CompletionItem {
|
||||
label: name.to_owned(),
|
||||
kind: Some(CompletionItemKind::Constant),
|
||||
kind: Some(CompletionItemKind::CONSTANT),
|
||||
detail: Some(define.display_with_name(name).to_string()),
|
||||
documentation: item_documentation(define.docs()),
|
||||
.. Default::default()
|
||||
..Default::default()
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
@ -445,7 +465,7 @@ impl<'a> Engine<'a> {
|
|||
|
||||
// fields
|
||||
let mut next = Some(ty);
|
||||
let mut skip = HashSet::with_hasher(RandomState::default());
|
||||
let mut skip = HashSet::new();
|
||||
while let Some(ty) = next {
|
||||
items_ty(results, &mut skip, ty, query);
|
||||
next = ty.parent_type();
|
||||
|
|
@ -462,7 +482,7 @@ impl<'a> Engine<'a> {
|
|||
I: Iterator<Item = (Span, &'b Annotation)> + Clone,
|
||||
{
|
||||
let mut next = self.find_scoped_type(iter, priors);
|
||||
let mut skip = HashSet::with_hasher(RandomState::default());
|
||||
let mut skip = HashSet::new();
|
||||
while let Some(ty) = next {
|
||||
items_ty(results, &mut skip, ty, query);
|
||||
next = ty.parent_type_without_root();
|
||||
|
|
|
|||
|
|
@ -1,5 +1,4 @@
|
|||
use super::auxtools_types::*;
|
||||
use std::{net::TcpListener, sync::mpsc};
|
||||
use std::thread;
|
||||
use std::{
|
||||
io::{Read, Write},
|
||||
|
|
@ -9,6 +8,7 @@ use std::{
|
|||
sync::{Arc, RwLock},
|
||||
thread::JoinHandle,
|
||||
};
|
||||
use std::{net::TcpListener, sync::mpsc};
|
||||
|
||||
use super::SequenceNumber;
|
||||
|
||||
|
|
@ -23,6 +23,7 @@ enum StreamState {
|
|||
}
|
||||
|
||||
pub struct Auxtools {
|
||||
#[allow(dead_code)]
|
||||
seq: Arc<SequenceNumber>,
|
||||
responses: mpsc::Receiver<Response>,
|
||||
_thread: JoinHandle<()>,
|
||||
|
|
@ -36,6 +37,12 @@ pub struct AuxtoolsThread {
|
|||
last_error: Arc<RwLock<String>>,
|
||||
}
|
||||
|
||||
pub struct AuxtoolsScopes {
|
||||
pub arguments: Option<VariablesRef>,
|
||||
pub locals: Option<VariablesRef>,
|
||||
pub globals: Option<VariablesRef>,
|
||||
}
|
||||
|
||||
impl Auxtools {
|
||||
pub fn connect(seq: Arc<SequenceNumber>, port: Option<u16>) -> std::io::Result<Self> {
|
||||
let addr: SocketAddr = (Ipv4Addr::LOCALHOST, port.unwrap_or(DEFAULT_PORT)).into();
|
||||
|
|
@ -51,8 +58,9 @@ impl Auxtools {
|
|||
AuxtoolsThread {
|
||||
seq,
|
||||
responses: responses_sender,
|
||||
last_error: last_error,
|
||||
}.run(stream);
|
||||
last_error,
|
||||
}
|
||||
.run(stream);
|
||||
})
|
||||
};
|
||||
|
||||
|
|
@ -79,26 +87,35 @@ impl Auxtools {
|
|||
AuxtoolsThread {
|
||||
seq,
|
||||
responses: responses_sender,
|
||||
last_error: last_error,
|
||||
}.spawn_listener(listener, connection_sender)
|
||||
last_error,
|
||||
}
|
||||
.spawn_listener(listener, connection_sender)
|
||||
};
|
||||
|
||||
Ok((port, Auxtools {
|
||||
seq,
|
||||
responses: responses_receiver,
|
||||
_thread: thread,
|
||||
stream: StreamState::Waiting(connection_receiver),
|
||||
last_error,
|
||||
}))
|
||||
Ok((
|
||||
port,
|
||||
Auxtools {
|
||||
seq,
|
||||
responses: responses_receiver,
|
||||
_thread: thread,
|
||||
stream: StreamState::Waiting(connection_receiver),
|
||||
last_error,
|
||||
},
|
||||
))
|
||||
}
|
||||
|
||||
fn read_response_or_disconnect(&mut self) -> Result<Response, Box<dyn std::error::Error>> {
|
||||
match self.responses.recv_timeout(std::time::Duration::from_secs(5)) {
|
||||
match self
|
||||
.responses
|
||||
.recv_timeout(std::time::Duration::from_secs(5))
|
||||
{
|
||||
Ok(response) => Ok(response),
|
||||
Err(_) => {
|
||||
self.disconnect();
|
||||
Err(Box::new(super::GenericError("timed out waiting for response")))
|
||||
}
|
||||
Err(Box::new(super::GenericError(
|
||||
"timed out waiting for response",
|
||||
)))
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -116,12 +133,12 @@ impl Auxtools {
|
|||
stream.write_all(&data[..])?;
|
||||
stream.flush()?;
|
||||
Ok(())
|
||||
}
|
||||
},
|
||||
|
||||
_ => {
|
||||
// Success if not connected (kinda dumb)
|
||||
Ok(())
|
||||
}
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -150,7 +167,7 @@ impl Auxtools {
|
|||
self.send_or_disconnect(Request::Configured)?;
|
||||
|
||||
match self.read_response_or_disconnect()? {
|
||||
Response::Ack { .. } => Ok(()),
|
||||
Response::Ack => Ok(()),
|
||||
response => Err(Box::new(UnexpectedResponse::new("Ack", response))),
|
||||
}
|
||||
}
|
||||
|
|
@ -164,8 +181,13 @@ impl Auxtools {
|
|||
}
|
||||
}
|
||||
|
||||
pub fn eval(&mut self, frame_id: Option<u32>, command: &str, context: Option<String>) -> Result<EvalResponse, Box<dyn std::error::Error>> {
|
||||
self.send_or_disconnect(Request::Eval{
|
||||
pub fn eval(
|
||||
&mut self,
|
||||
frame_id: Option<u32>,
|
||||
command: &str,
|
||||
context: Option<String>,
|
||||
) -> Result<EvalResponse, Box<dyn std::error::Error>> {
|
||||
self.send_or_disconnect(Request::Eval {
|
||||
frame_id,
|
||||
command: command.to_owned(),
|
||||
context,
|
||||
|
|
@ -177,16 +199,29 @@ impl Auxtools {
|
|||
}
|
||||
}
|
||||
|
||||
pub fn get_current_proc(&mut self, frame_id: u32) -> Result<Option<(String, u32)>, Box<dyn std::error::Error>> {
|
||||
#[allow(dead_code)]
|
||||
pub fn get_current_proc(
|
||||
&mut self,
|
||||
frame_id: u32,
|
||||
) -> Result<Option<(String, u32)>, Box<dyn std::error::Error>> {
|
||||
self.send_or_disconnect(Request::CurrentInstruction { frame_id })?;
|
||||
|
||||
match self.read_response_or_disconnect()? {
|
||||
Response::CurrentInstruction(ins) => Ok(ins.map(|x| (x.proc.path, x.proc.override_id))),
|
||||
response => Err(Box::new(UnexpectedResponse::new("CurrentInstruction", response))),
|
||||
response => Err(Box::new(UnexpectedResponse::new(
|
||||
"CurrentInstruction",
|
||||
response,
|
||||
))),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn get_line_number(&mut self, path: &str, override_id: u32, offset: u32) -> Result<Option<u32>, Box<dyn std::error::Error>> {
|
||||
#[allow(dead_code)]
|
||||
pub fn get_line_number(
|
||||
&mut self,
|
||||
path: &str,
|
||||
override_id: u32,
|
||||
offset: u32,
|
||||
) -> Result<Option<u32>, Box<dyn std::error::Error>> {
|
||||
self.send_or_disconnect(Request::LineNumber {
|
||||
proc: ProcRef {
|
||||
path: path.to_owned(),
|
||||
|
|
@ -201,7 +236,12 @@ impl Auxtools {
|
|||
}
|
||||
}
|
||||
|
||||
pub fn get_offset(&mut self, path: &str, override_id: u32, line: u32) -> Result<Option<u32>, Box<dyn std::error::Error>> {
|
||||
pub fn get_offset(
|
||||
&mut self,
|
||||
path: &str,
|
||||
override_id: u32,
|
||||
line: u32,
|
||||
) -> Result<Option<u32>, Box<dyn std::error::Error>> {
|
||||
self.send_or_disconnect(Request::Offset {
|
||||
proc: ProcRef {
|
||||
path: path.to_owned(),
|
||||
|
|
@ -216,10 +256,14 @@ impl Auxtools {
|
|||
}
|
||||
}
|
||||
|
||||
pub fn set_breakpoint(&mut self, instruction: InstructionRef, condition: Option<String>) -> Result<BreakpointSetResult, Box<dyn std::error::Error>> {
|
||||
pub fn set_breakpoint(
|
||||
&mut self,
|
||||
instruction: InstructionRef,
|
||||
condition: Option<String>,
|
||||
) -> Result<BreakpointSetResult, Box<dyn std::error::Error>> {
|
||||
self.send_or_disconnect(Request::BreakpointSet {
|
||||
instruction,
|
||||
condition
|
||||
condition,
|
||||
})?;
|
||||
|
||||
match self.read_response_or_disconnect()? {
|
||||
|
|
@ -228,14 +272,20 @@ impl Auxtools {
|
|||
}
|
||||
}
|
||||
|
||||
pub fn unset_breakpoint(&mut self, instruction: &InstructionRef) -> Result<(), Box<dyn std::error::Error>> {
|
||||
pub fn unset_breakpoint(
|
||||
&mut self,
|
||||
instruction: &InstructionRef,
|
||||
) -> Result<(), Box<dyn std::error::Error>> {
|
||||
self.send_or_disconnect(Request::BreakpointUnset {
|
||||
instruction: instruction.clone(),
|
||||
})?;
|
||||
|
||||
match self.read_response_or_disconnect()? {
|
||||
Response::BreakpointUnset { .. } => Ok(()),
|
||||
response => Err(Box::new(UnexpectedResponse::new("BreakpointUnset", response))),
|
||||
response => Err(Box::new(UnexpectedResponse::new(
|
||||
"BreakpointUnset",
|
||||
response,
|
||||
))),
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -245,7 +295,7 @@ impl Auxtools {
|
|||
})?;
|
||||
|
||||
match self.read_response_or_disconnect()? {
|
||||
Response::Ack { .. } => Ok(()),
|
||||
Response::Ack => Ok(()),
|
||||
response => Err(Box::new(UnexpectedResponse::new("Ack", response))),
|
||||
}
|
||||
}
|
||||
|
|
@ -256,7 +306,7 @@ impl Auxtools {
|
|||
})?;
|
||||
|
||||
match self.read_response_or_disconnect()? {
|
||||
Response::Ack { .. } => Ok(()),
|
||||
Response::Ack => Ok(()),
|
||||
response => Err(Box::new(UnexpectedResponse::new("Ack", response))),
|
||||
}
|
||||
}
|
||||
|
|
@ -267,7 +317,7 @@ impl Auxtools {
|
|||
})?;
|
||||
|
||||
match self.read_response_or_disconnect()? {
|
||||
Response::Ack { .. } => Ok(()),
|
||||
Response::Ack => Ok(()),
|
||||
response => Err(Box::new(UnexpectedResponse::new("Ack", response))),
|
||||
}
|
||||
}
|
||||
|
|
@ -278,7 +328,7 @@ impl Auxtools {
|
|||
})?;
|
||||
|
||||
match self.read_response_or_disconnect()? {
|
||||
Response::Ack { .. } => Ok(()),
|
||||
Response::Ack => Ok(()),
|
||||
response => Err(Box::new(UnexpectedResponse::new("Ack", response))),
|
||||
}
|
||||
}
|
||||
|
|
@ -287,7 +337,7 @@ impl Auxtools {
|
|||
self.send_or_disconnect(Request::Pause)?;
|
||||
|
||||
match self.read_response_or_disconnect()? {
|
||||
Response::Ack { .. } => Ok(()),
|
||||
Response::Ack => Ok(()),
|
||||
response => Err(Box::new(UnexpectedResponse::new("Ack", response))),
|
||||
}
|
||||
}
|
||||
|
|
@ -323,25 +373,31 @@ impl Auxtools {
|
|||
}
|
||||
|
||||
// TODO: return all the scopes
|
||||
pub fn get_scopes(&mut self, frame_id: u32) -> Result<(Option<VariablesRef>, Option<VariablesRef>, Option<VariablesRef>), Box<dyn std::error::Error>> {
|
||||
self.send_or_disconnect(Request::Scopes {
|
||||
frame_id
|
||||
})?;
|
||||
pub fn get_scopes(
|
||||
&mut self,
|
||||
frame_id: u32,
|
||||
) -> Result<AuxtoolsScopes, Box<dyn std::error::Error>> {
|
||||
self.send_or_disconnect(Request::Scopes { frame_id })?;
|
||||
|
||||
match self.read_response_or_disconnect()? {
|
||||
Response::Scopes {
|
||||
arguments,
|
||||
locals,
|
||||
globals,
|
||||
} => Ok((arguments, locals, globals)),
|
||||
} => Ok(AuxtoolsScopes {
|
||||
arguments,
|
||||
locals,
|
||||
globals,
|
||||
}),
|
||||
response => Err(Box::new(UnexpectedResponse::new("Scopes", response))),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn get_variables(&mut self, vars: VariablesRef) -> Result<Vec<Variable>, Box<dyn std::error::Error>> {
|
||||
self.send_or_disconnect(Request::Variables {
|
||||
vars
|
||||
})?;
|
||||
pub fn get_variables(
|
||||
&mut self,
|
||||
vars: VariablesRef,
|
||||
) -> Result<Vec<Variable>, Box<dyn std::error::Error>> {
|
||||
self.send_or_disconnect(Request::Variables { vars })?;
|
||||
|
||||
match self.read_response_or_disconnect()? {
|
||||
Response::Variables { vars } => Ok(vars),
|
||||
|
|
@ -353,7 +409,10 @@ impl Auxtools {
|
|||
self.last_error.read().unwrap().clone()
|
||||
}
|
||||
|
||||
pub fn set_catch_runtimes(&mut self, should_catch: bool) -> Result<(), Box<dyn std::error::Error>> {
|
||||
pub fn set_catch_runtimes(
|
||||
&mut self,
|
||||
should_catch: bool,
|
||||
) -> Result<(), Box<dyn std::error::Error>> {
|
||||
self.send_or_disconnect(Request::CatchRuntimes { should_catch })
|
||||
}
|
||||
}
|
||||
|
|
@ -367,19 +426,19 @@ impl AuxtoolsThread {
|
|||
thread::spawn(move || match listener.accept() {
|
||||
Ok((stream, _)) => {
|
||||
match connection_sender.send(stream.try_clone().unwrap()) {
|
||||
Ok(_) => {}
|
||||
Ok(_) => {},
|
||||
Err(e) => {
|
||||
eprintln!("Debug client thread failed to pass cloned TcpStream: {}", e);
|
||||
eprintln!("Debug client thread failed to pass cloned TcpStream: {e}");
|
||||
return;
|
||||
}
|
||||
},
|
||||
}
|
||||
|
||||
self.run(stream);
|
||||
}
|
||||
},
|
||||
|
||||
Err(e) => {
|
||||
eprintln!("Debug client failed to accept connection: {}", e);
|
||||
}
|
||||
eprintln!("Debug client failed to accept connection: {e}");
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
|
|
@ -392,7 +451,7 @@ impl AuxtoolsThread {
|
|||
|
||||
Response::Notification { message: _message } => {
|
||||
debug_output!(in self.seq, "[auxtools] {}", _message);
|
||||
}
|
||||
},
|
||||
|
||||
Response::BreakpointHit { reason } => {
|
||||
let mut description = None;
|
||||
|
|
@ -402,10 +461,10 @@ impl AuxtoolsThread {
|
|||
BreakpointReason::Pause => dap_types::StoppedEvent::REASON_PAUSE,
|
||||
BreakpointReason::Breakpoint => dap_types::StoppedEvent::REASON_BREAKPOINT,
|
||||
BreakpointReason::Runtime(error) => {
|
||||
*(self.last_error.write().unwrap()) = error.clone();
|
||||
self.last_error.write().unwrap().clone_from(&error);
|
||||
description = Some(error);
|
||||
dap_types::StoppedEvent::REASON_EXCEPTION
|
||||
}
|
||||
},
|
||||
};
|
||||
|
||||
self.seq.issue_event(dap_types::StoppedEvent {
|
||||
|
|
@ -415,11 +474,11 @@ impl AuxtoolsThread {
|
|||
allThreadsStopped: Some(true),
|
||||
..Default::default()
|
||||
});
|
||||
}
|
||||
},
|
||||
|
||||
x => {
|
||||
self.responses.send(x)?;
|
||||
}
|
||||
},
|
||||
}
|
||||
|
||||
Ok(false)
|
||||
|
|
@ -438,9 +497,9 @@ impl AuxtoolsThread {
|
|||
Ok(_) => u32::from_le_bytes(len_bytes),
|
||||
|
||||
Err(e) => {
|
||||
eprintln!("Debug server thread read error: {}", e);
|
||||
eprintln!("Debug server thread read error: {e}");
|
||||
break;
|
||||
}
|
||||
},
|
||||
};
|
||||
|
||||
buf.resize(len as usize, 0);
|
||||
|
|
@ -448,9 +507,9 @@ impl AuxtoolsThread {
|
|||
Ok(_) => (),
|
||||
|
||||
Err(e) => {
|
||||
eprintln!("Debug server thread read error: {}", e);
|
||||
eprintln!("Debug server thread read error: {e}");
|
||||
break;
|
||||
}
|
||||
},
|
||||
};
|
||||
|
||||
match self.handle_response(&buf[..]) {
|
||||
|
|
@ -459,12 +518,12 @@ impl AuxtoolsThread {
|
|||
eprintln!("Debug server disconnected");
|
||||
break;
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
Err(e) => {
|
||||
eprintln!("Debug server thread failed to handle request: {}", e);
|
||||
eprintln!("Debug server thread failed to handle request: {e}");
|
||||
break;
|
||||
}
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -477,7 +536,9 @@ pub struct UnexpectedResponse(String);
|
|||
|
||||
impl UnexpectedResponse {
|
||||
fn new(expected: &'static str, received: Response) -> Self {
|
||||
Self(format!("received unexpected response: expected {}, got {:?}", expected, received))
|
||||
Self(format!(
|
||||
"received unexpected response: expected {expected}, got {received:?}"
|
||||
))
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -3,7 +3,7 @@ use std::fs::File;
|
|||
use std::io::{Result, Write};
|
||||
use std::path::{Path, PathBuf};
|
||||
|
||||
const BYTES: &[u8] = include_bytes!(env!("AUXTOOLS_BUNDLE_DLL"));
|
||||
const BYTES: &[u8] = include_bytes!(env!("BUNDLE_PATH_debug_server.dll"));
|
||||
|
||||
fn write(path: &Path) -> Result<()> {
|
||||
File::create(path)?.write_all(BYTES)
|
||||
|
|
@ -13,7 +13,7 @@ pub fn extract() -> Result<PathBuf> {
|
|||
let exe = std::env::current_exe()?;
|
||||
let directory = exe.parent().unwrap();
|
||||
for i in 0..9 {
|
||||
let dll = directory.join(format!("auxtools_debug_server{}.dll", i));
|
||||
let dll = directory.join(format!("auxtools_debug_server{i}.dll"));
|
||||
if let Ok(()) = write(&dll) {
|
||||
return Ok(dll);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
use dap_types::*;
|
||||
use super::*;
|
||||
use dap_types::*;
|
||||
|
||||
const EXTOOLS_HELP: &str = "
|
||||
#dis, #disassemble: show disassembly for current stack frame";
|
||||
|
|
@ -19,37 +19,38 @@ impl Debugger {
|
|||
return Ok(EvaluateResponse::from(EXTOOLS_HELP.trim()));
|
||||
}
|
||||
|
||||
guard!(let Some(frame_id) = params.frameId else {
|
||||
return Err(Box::new(GenericError("Must select a stack frame to evaluate in")));
|
||||
});
|
||||
let Some(frame_id) = params.frameId else {
|
||||
return Err(Box::new(GenericError(
|
||||
"Must select a stack frame to evaluate in",
|
||||
)));
|
||||
};
|
||||
|
||||
let (thread, frame_no) = extools.get_thread_by_frame_id(frame_id)?;
|
||||
|
||||
if input.starts_with('#') {
|
||||
if input == "#dis" || input == "#disassemble" {
|
||||
guard!(let Some(frame) = thread.call_stack.get(frame_no) else {
|
||||
let Some(frame) = thread.call_stack.get(frame_no) else {
|
||||
return Err(Box::new(GenericError("Stack frame out of range")));
|
||||
});
|
||||
};
|
||||
|
||||
let bytecode = extools.bytecode(&frame.proc, frame.override_id);
|
||||
return Ok(EvaluateResponse::from(Self::format_disassembly(bytecode)));
|
||||
} else {
|
||||
return Err(Box::new(GenericError("Unknown #command")));
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
DebugClient::Auxtools(auxtools) => {
|
||||
let response = auxtools.eval(
|
||||
params.frameId.map(|x| x as u32),
|
||||
input,
|
||||
params.context,
|
||||
)?;
|
||||
let response =
|
||||
auxtools.eval(params.frameId.map(|x| x as u32), input, params.context)?;
|
||||
|
||||
return Ok(EvaluateResponse {
|
||||
result: response.value,
|
||||
variablesReference: response.variables.map(|x| x.0 as i64).unwrap_or(0),
|
||||
..Default::default()
|
||||
});
|
||||
}
|
||||
},
|
||||
}
|
||||
|
||||
Err(Box::new(GenericError("Not yet implemented")))
|
||||
|
|
|
|||
|
|
@ -1,16 +1,14 @@
|
|||
//! Client for the Extools debugger protocol.
|
||||
|
||||
use std::time::Duration;
|
||||
use std::sync::{mpsc, Arc, Mutex};
|
||||
use std::net::{SocketAddr, Ipv4Addr, TcpStream, TcpListener};
|
||||
use std::collections::HashMap;
|
||||
use std::io::{Read, Write};
|
||||
use foldhash::{HashMap, HashMapExt};
|
||||
use std::error::Error;
|
||||
use std::io::{Read, Write};
|
||||
use std::net::{Ipv4Addr, SocketAddr, TcpListener, TcpStream};
|
||||
use std::sync::{mpsc, Arc, Mutex};
|
||||
use std::time::Duration;
|
||||
|
||||
use ahash::RandomState;
|
||||
|
||||
use super::SequenceNumber;
|
||||
use super::extools_types::*;
|
||||
use super::SequenceNumber;
|
||||
|
||||
pub const DEFAULT_PORT: u16 = 2448;
|
||||
|
||||
|
|
@ -34,6 +32,7 @@ enum ExtoolsHolderInner {
|
|||
/// Used to avoid a layer of Option.
|
||||
None,
|
||||
Listening {
|
||||
#[allow(dead_code)]
|
||||
port: u16,
|
||||
conn_rx: mpsc::Receiver<Extools>,
|
||||
},
|
||||
|
|
@ -41,7 +40,7 @@ enum ExtoolsHolderInner {
|
|||
cancel_tx: mpsc::Sender<()>,
|
||||
conn_rx: mpsc::Receiver<Extools>,
|
||||
},
|
||||
Active(Extools),
|
||||
Active(Box<Extools>),
|
||||
}
|
||||
|
||||
impl Default for ExtoolsHolder {
|
||||
|
|
@ -59,7 +58,7 @@ impl ExtoolsHolder {
|
|||
let (conn_tx, conn_rx) = mpsc::channel();
|
||||
|
||||
std::thread::Builder::new()
|
||||
.name(format!("extools listening on port {}", port))
|
||||
.name(format!("extools listening on port {port}"))
|
||||
.spawn(move || {
|
||||
let stream = match listener.accept() {
|
||||
Ok((stream, _)) => stream,
|
||||
|
|
@ -72,10 +71,10 @@ impl ExtoolsHolder {
|
|||
}
|
||||
})?;
|
||||
|
||||
Ok((port, ExtoolsHolder(ExtoolsHolderInner::Listening {
|
||||
Ok((
|
||||
port,
|
||||
conn_rx,
|
||||
})))
|
||||
ExtoolsHolder(ExtoolsHolderInner::Listening { port, conn_rx }),
|
||||
))
|
||||
}
|
||||
|
||||
pub fn attach(seq: Arc<SequenceNumber>, port: u16) -> std::io::Result<ExtoolsHolder> {
|
||||
|
|
@ -86,10 +85,12 @@ impl ExtoolsHolder {
|
|||
let (cancel_tx, cancel_rx) = mpsc::channel();
|
||||
|
||||
std::thread::Builder::new()
|
||||
.name(format!("extools attaching on port {}", port))
|
||||
.name(format!("extools attaching on port {port}"))
|
||||
.spawn(move || {
|
||||
while let Err(mpsc::TryRecvError::Empty) = cancel_rx.try_recv() {
|
||||
if let Ok(stream) = TcpStream::connect_timeout(&addr, std::time::Duration::from_secs(5)) {
|
||||
if let Ok(stream) =
|
||||
TcpStream::connect_timeout(&addr, std::time::Duration::from_secs(5))
|
||||
{
|
||||
let (conn, mut thread) = Extools::from_stream(seq, stream);
|
||||
if conn_tx.send(conn).is_ok() {
|
||||
thread.read_loop();
|
||||
|
|
@ -106,30 +107,35 @@ impl ExtoolsHolder {
|
|||
}
|
||||
|
||||
pub fn get(&mut self) -> Result<&mut Extools, Box<dyn Error>> {
|
||||
self.as_ref().ok_or_else(|| Box::new(super::GenericError("No extools connection")) as Box<dyn Error>)
|
||||
self.as_ref()
|
||||
.ok_or_else(|| Box::new(super::GenericError("No extools connection")) as Box<dyn Error>)
|
||||
}
|
||||
|
||||
pub fn as_ref(&mut self) -> Option<&mut Extools> {
|
||||
match &mut self.0 {
|
||||
ExtoolsHolderInner::Listening { conn_rx, .. } |
|
||||
ExtoolsHolderInner::Attaching { conn_rx, .. } => {
|
||||
ExtoolsHolderInner::Listening { conn_rx, .. }
|
||||
| ExtoolsHolderInner::Attaching { conn_rx, .. } => {
|
||||
if let Ok(conn) = conn_rx.try_recv() {
|
||||
self.0 = ExtoolsHolderInner::Active(conn);
|
||||
self.0 = ExtoolsHolderInner::Active(Box::new(conn));
|
||||
}
|
||||
}
|
||||
_ => {}
|
||||
},
|
||||
_ => {},
|
||||
}
|
||||
match &mut self.0 {
|
||||
ExtoolsHolderInner::Active(conn) => Some(conn),
|
||||
_ => None
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn disconnect(&mut self) {
|
||||
// This part of code is not complete, we don't want to use matches!
|
||||
#[allow(clippy::single_match)]
|
||||
match std::mem::replace(&mut self.0, ExtoolsHolderInner::None) {
|
||||
ExtoolsHolderInner::Attaching { cancel_tx, .. } => { let _ = cancel_tx.send(()); },
|
||||
ExtoolsHolderInner::Attaching { cancel_tx, .. } => {
|
||||
let _ = cancel_tx.send(());
|
||||
},
|
||||
// TODO: ExtoolsHolderInner::Listening
|
||||
_ => {}
|
||||
_ => {},
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -139,7 +145,7 @@ pub struct Extools {
|
|||
seq: Arc<SequenceNumber>,
|
||||
sender: ExtoolsSender,
|
||||
threads: Arc<Mutex<HashMap<i64, ThreadInfo>>>,
|
||||
bytecode: HashMap<(String, usize), Vec<DisassembledInstruction>, RandomState>,
|
||||
bytecode: HashMap<(String, usize), Vec<DisassembledInstruction>>,
|
||||
get_type_rx: mpsc::Receiver<GetTypeResponse>,
|
||||
bytecode_rx: mpsc::Receiver<DisassembledProc>,
|
||||
get_field_rx: mpsc::Receiver<GetAllFieldsResponse>,
|
||||
|
|
@ -171,7 +177,7 @@ impl Extools {
|
|||
seq,
|
||||
sender,
|
||||
threads: Arc::new(Mutex::new(HashMap::new())),
|
||||
bytecode: HashMap::with_hasher(RandomState::default()),
|
||||
bytecode: HashMap::new(),
|
||||
bytecode_rx,
|
||||
get_type_rx,
|
||||
get_field_rx,
|
||||
|
|
@ -197,27 +203,43 @@ impl Extools {
|
|||
(extools, thread)
|
||||
}
|
||||
|
||||
pub fn get_all_threads(&self) -> std::sync::MutexGuard<HashMap<i64, ThreadInfo>> {
|
||||
pub fn get_all_threads(&self) -> std::sync::MutexGuard<'_, HashMap<i64, ThreadInfo>> {
|
||||
self.threads.lock().unwrap()
|
||||
}
|
||||
|
||||
pub fn get_thread(&self, thread_id: i64) -> Result<ThreadInfo, Box<dyn Error>> {
|
||||
self.threads.lock().unwrap().get(&thread_id).cloned()
|
||||
.ok_or_else(|| Box::new(super::GenericError("Getting call stack failed")) as Box<dyn Error>)
|
||||
self.threads
|
||||
.lock()
|
||||
.unwrap()
|
||||
.get(&thread_id)
|
||||
.cloned()
|
||||
.ok_or_else(|| {
|
||||
Box::new(super::GenericError("Getting call stack failed")) as Box<dyn Error>
|
||||
})
|
||||
}
|
||||
|
||||
pub fn get_thread_by_frame_id(&self, frame_id: i64) -> Result<(ThreadInfo, usize), Box<dyn Error>> {
|
||||
pub fn get_thread_by_frame_id(
|
||||
&self,
|
||||
frame_id: i64,
|
||||
) -> Result<(ThreadInfo, usize), Box<dyn Error>> {
|
||||
let frame_id = frame_id as usize;
|
||||
let threads = self.threads.lock().unwrap();
|
||||
let thread_id = (frame_id % threads.len()) as i64;
|
||||
let frame_no = frame_id / threads.len();
|
||||
let thread = threads.get(&thread_id).cloned()
|
||||
.ok_or_else(|| Box::new(super::GenericError("Getting call stack failed")) as Box<dyn Error>)?;
|
||||
let thread = threads.get(&thread_id).cloned().ok_or_else(|| {
|
||||
Box::new(super::GenericError("Getting call stack failed")) as Box<dyn Error>
|
||||
})?;
|
||||
Ok((thread, frame_no))
|
||||
}
|
||||
|
||||
pub fn bytecode(&mut self, proc_ref: &str, override_id: usize) -> &[DisassembledInstruction] {
|
||||
let Extools { bytecode, sender, seq: _seq, bytecode_rx, .. } = self;
|
||||
let Extools {
|
||||
bytecode,
|
||||
sender,
|
||||
seq: _seq,
|
||||
bytecode_rx,
|
||||
..
|
||||
} = self;
|
||||
bytecode.entry((proc_ref.to_owned(), override_id)).or_insert_with(|| {
|
||||
debug_output!(in _seq, "[extools] Fetching bytecode for {}#{}", proc_ref, override_id);
|
||||
sender.send(ProcDisassemblyRequest(ProcId {
|
||||
|
|
@ -228,7 +250,12 @@ impl Extools {
|
|||
})
|
||||
}
|
||||
|
||||
pub fn offset_to_line(&mut self, proc_ref: &str, override_id: usize, offset: i64) -> Option<i64> {
|
||||
pub fn offset_to_line(
|
||||
&mut self,
|
||||
proc_ref: &str,
|
||||
override_id: usize,
|
||||
offset: i64,
|
||||
) -> Option<i64> {
|
||||
let bc = self.bytecode(proc_ref, override_id);
|
||||
let mut comment = "";
|
||||
for instr in bc.iter() {
|
||||
|
|
@ -261,11 +288,19 @@ impl Extools {
|
|||
}
|
||||
|
||||
pub fn set_breakpoint(&self, proc: &str, override_id: usize, offset: i64) {
|
||||
self.sender.send(BreakpointSet(ProcOffset { proc: proc.to_owned(), override_id, offset }));
|
||||
self.sender.send(BreakpointSet(ProcOffset {
|
||||
proc: proc.to_owned(),
|
||||
override_id,
|
||||
offset,
|
||||
}));
|
||||
}
|
||||
|
||||
pub fn unset_breakpoint(&self, proc: &str, override_id: usize, offset: i64) {
|
||||
self.sender.send(BreakpointUnset(ProcOffset { proc: proc.to_owned(), override_id, offset }));
|
||||
self.sender.send(BreakpointUnset(ProcOffset {
|
||||
proc: proc.to_owned(),
|
||||
override_id,
|
||||
offset,
|
||||
}));
|
||||
}
|
||||
|
||||
pub fn continue_execution(&self) {
|
||||
|
|
@ -292,13 +327,17 @@ impl Extools {
|
|||
self.sender.send(Pause);
|
||||
}
|
||||
|
||||
#[allow(dead_code)]
|
||||
pub fn get_reference_type(&self, reference: Ref) -> Result<String, Box<dyn Error>> {
|
||||
// TODO: error handling
|
||||
self.sender.send(GetType(reference));
|
||||
Ok(self.get_type_rx.recv_timeout(RECV_TIMEOUT)?.0)
|
||||
}
|
||||
|
||||
pub fn get_all_fields(&self, reference: Ref) -> Result<HashMap<String, ValueText, RandomState>, Box<dyn Error>> {
|
||||
pub fn get_all_fields(
|
||||
&self,
|
||||
reference: Ref,
|
||||
) -> Result<HashMap<String, ValueText>, Box<dyn Error>> {
|
||||
self.sender.send(GetAllFields(reference));
|
||||
Ok(self.get_field_rx.recv_timeout(RECV_TIMEOUT)?.0)
|
||||
}
|
||||
|
|
@ -337,9 +376,8 @@ impl Drop for Extools {
|
|||
}
|
||||
|
||||
fn parse_lineno(comment: &str) -> Option<i64> {
|
||||
let prefix = "Line number: ";
|
||||
if comment.starts_with(prefix) {
|
||||
comment[prefix.len()..].parse::<i64>().ok()
|
||||
if let Some(rest) = comment.strip_prefix("Line number: ") {
|
||||
rest.parse::<i64>().ok()
|
||||
} else {
|
||||
None
|
||||
}
|
||||
|
|
@ -372,8 +410,8 @@ impl ExtoolsThread {
|
|||
terminator = Some(buffer.len() + pos);
|
||||
}
|
||||
buffer.extend_from_slice(slice);
|
||||
}
|
||||
Err(e) => panic!("extools read error: {:?}", e),
|
||||
},
|
||||
Err(e) => panic!("extools read error: {e:?}"),
|
||||
}
|
||||
|
||||
// chop off as many full messages from the buffer as we can
|
||||
|
|
@ -421,96 +459,140 @@ impl ExtoolsThread {
|
|||
reason: "sleep".to_owned(),
|
||||
threadId: Some(k),
|
||||
preserveFocusHint: Some(true),
|
||||
.. Default::default()
|
||||
..Default::default()
|
||||
});
|
||||
}
|
||||
}
|
||||
self.seq.issue_event(dap_types::StoppedEvent {
|
||||
threadId: Some(0),
|
||||
.. base
|
||||
..base
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
handle_extools! {
|
||||
on Raw(&mut self, Raw(message)) {
|
||||
type R = Result<(), Box<dyn Error>>;
|
||||
|
||||
macro_rules! handle_response_table {
|
||||
($($what:ident;)*) => {
|
||||
fn handle_response_table(type_: &str) -> Option<fn(&mut Self, serde_json::Value) -> Result<(), Box<dyn Error>>> {
|
||||
match type_ {
|
||||
$(<$what as Response>::TYPE => {
|
||||
Some(|this, content| {
|
||||
let deserialized: $what = serde_json::from_value(content)?;
|
||||
this.$what(deserialized)
|
||||
})
|
||||
},)*
|
||||
_ => None
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[allow(non_snake_case)]
|
||||
impl ExtoolsThread {
|
||||
handle_response_table! {
|
||||
Raw;
|
||||
BreakpointSet;
|
||||
BreakpointUnset;
|
||||
BreakpointHit;
|
||||
Runtime;
|
||||
CallStack;
|
||||
DisassembledProc;
|
||||
GetTypeResponse;
|
||||
GetAllFieldsResponse;
|
||||
ListContents;
|
||||
GetSource;
|
||||
BreakOnRuntime;
|
||||
}
|
||||
|
||||
fn Raw(&mut self, Raw(message): Raw) -> R {
|
||||
output!(in self.seq, "[extools] Message: {}", message);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
on BreakpointSet(&mut self, BreakpointSet(_bp)) {
|
||||
fn BreakpointSet(&mut self, BreakpointSet(_bp): BreakpointSet) -> R {
|
||||
debug_output!(in self.seq, "[extools] {}#{}@{} validated", _bp.proc, _bp.override_id, _bp.offset);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
on BreakpointUnset(&mut self, _) {
|
||||
fn BreakpointUnset(&mut self, _: BreakpointUnset) -> R {
|
||||
// silent
|
||||
Ok(())
|
||||
}
|
||||
|
||||
on BreakpointHit(&mut self, hit) {
|
||||
fn BreakpointHit(&mut self, hit: BreakpointHit) -> R {
|
||||
match hit.reason {
|
||||
BreakpointHitReason::Step => {
|
||||
self.stopped(dap_types::StoppedEvent {
|
||||
reason: dap_types::StoppedEvent::REASON_STEP.to_owned(),
|
||||
.. Default::default()
|
||||
..Default::default()
|
||||
});
|
||||
}
|
||||
BreakpointHitReason::Pause => {
|
||||
self.stopped(dap_types::StoppedEvent {
|
||||
reason: dap_types::StoppedEvent::REASON_PAUSE.to_owned(),
|
||||
description: Some("Paused by request".to_owned()),
|
||||
.. Default::default()
|
||||
})
|
||||
}
|
||||
},
|
||||
BreakpointHitReason::Pause => self.stopped(dap_types::StoppedEvent {
|
||||
reason: dap_types::StoppedEvent::REASON_PAUSE.to_owned(),
|
||||
description: Some("Paused by request".to_owned()),
|
||||
..Default::default()
|
||||
}),
|
||||
_ => {
|
||||
debug_output!(in self.seq, "[extools] {}#{}@{} hit", hit.proc, hit.override_id, hit.offset);
|
||||
self.stopped(dap_types::StoppedEvent {
|
||||
reason: dap_types::StoppedEvent::REASON_BREAKPOINT.to_owned(),
|
||||
.. Default::default()
|
||||
..Default::default()
|
||||
});
|
||||
}
|
||||
},
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
on Runtime(&mut self, runtime) {
|
||||
fn Runtime(&mut self, runtime: Runtime) -> R {
|
||||
output!(in self.seq, "[extools] Runtime in {}: {}", runtime.proc, runtime.message);
|
||||
self.stopped(dap_types::StoppedEvent {
|
||||
reason: dap_types::StoppedEvent::REASON_EXCEPTION.to_owned(),
|
||||
text: Some(runtime.message.clone()),
|
||||
.. Default::default()
|
||||
..Default::default()
|
||||
});
|
||||
self.queue(&self.runtime_tx, runtime);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
on CallStack(&mut self, stack) {
|
||||
fn CallStack(&mut self, stack: CallStack) -> R {
|
||||
let mut map = self.threads.lock().unwrap();
|
||||
map.clear();
|
||||
map.entry(0).or_default().call_stack = stack.current;
|
||||
for (i, list) in stack.suspended.into_iter().enumerate() {
|
||||
map.entry((i + 1) as i64).or_default().call_stack = list;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
on DisassembledProc(&mut self, disasm) {
|
||||
fn DisassembledProc(&mut self, disasm: DisassembledProc) -> R {
|
||||
self.queue(&self.bytecode_tx, disasm);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
on GetTypeResponse(&mut self, response) {
|
||||
fn GetTypeResponse(&mut self, response: GetTypeResponse) -> R {
|
||||
self.queue(&self.get_type_tx, response);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
on GetAllFieldsResponse(&mut self, response) {
|
||||
fn GetAllFieldsResponse(&mut self, response: GetAllFieldsResponse) -> R {
|
||||
self.queue(&self.get_field_tx, response);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
on ListContents(&mut self, response) {
|
||||
fn ListContents(&mut self, response: ListContents) -> R {
|
||||
self.queue(&self.get_list_contents_tx, response);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
on GetSource(&mut self, response) {
|
||||
fn GetSource(&mut self, response: GetSource) -> R {
|
||||
self.queue(&self.get_source_tx, response);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
on BreakOnRuntime(&mut self, _) {
|
||||
fn BreakOnRuntime(&mut self, _: BreakOnRuntime) -> R {
|
||||
// Either it worked or it didn't, nothing we can do about it now.
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -523,7 +605,10 @@ impl Clone for ExtoolsSender {
|
|||
fn clone(&self) -> ExtoolsSender {
|
||||
ExtoolsSender {
|
||||
seq: self.seq.clone(),
|
||||
stream: self.stream.try_clone().expect("TcpStream::try_clone failed in ExtoolsSender::clone")
|
||||
stream: self
|
||||
.stream
|
||||
.try_clone()
|
||||
.expect("TcpStream::try_clone failed in ExtoolsSender::clone"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -534,9 +619,12 @@ impl ExtoolsSender {
|
|||
let mut buffer = serde_json::to_vec(&ProtocolMessage {
|
||||
type_: M::TYPE.to_owned(),
|
||||
content: Some(content),
|
||||
}).expect("extools encode error");
|
||||
})
|
||||
.expect("extools encode error");
|
||||
buffer.push(0);
|
||||
// TODO: needs more synchronization
|
||||
(&self.stream).write_all(&buffer[..]).expect("extools write error");
|
||||
(&self.stream)
|
||||
.write_all(&buffer[..])
|
||||
.expect("extools write error");
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,9 +1,9 @@
|
|||
#![cfg(extools_bundle)]
|
||||
use std::fs::File;
|
||||
use std::io::{Result, Write};
|
||||
use std::path::{Path, PathBuf};
|
||||
use std::fs::File;
|
||||
|
||||
const BYTES: &[u8] = include_bytes!(env!("EXTOOLS_BUNDLE_DLL"));
|
||||
const BYTES: &[u8] = include_bytes!(env!("BUNDLE_PATH_extools.dll"));
|
||||
|
||||
fn write(path: &Path) -> Result<()> {
|
||||
File::create(path)?.write_all(BYTES)
|
||||
|
|
@ -13,7 +13,7 @@ pub fn extract() -> Result<PathBuf> {
|
|||
let exe = std::env::current_exe()?;
|
||||
let directory = exe.parent().unwrap();
|
||||
for i in 0..9 {
|
||||
let dll = directory.join(format!("extools{}.dll", i));
|
||||
let dll = directory.join(format!("extools{i}.dll"));
|
||||
if let Ok(()) = write(&dll) {
|
||||
return Ok(dll);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,15 +1,13 @@
|
|||
//! Serde types for the Extools debugger protocol.
|
||||
//!
|
||||
//! * https://github.com/MCHSL/extools/blob/master/byond-extools/src/debug_server/protocol.h
|
||||
///
|
||||
/// > All communication happens over a TCP socket using a JSON-based protocol.
|
||||
/// > A null byte signifies the end of a message.
|
||||
|
||||
use std::collections::HashMap;
|
||||
//!
|
||||
//! > All communication happens over a TCP socket using a JSON-based protocol.
|
||||
//! > A null byte signifies the end of a message.
|
||||
#![allow(dead_code)]
|
||||
use foldhash::HashMap;
|
||||
use serde_json::Value as Json;
|
||||
|
||||
use ahash::RandomState;
|
||||
|
||||
// ----------------------------------------------------------------------------
|
||||
// Extools data structures
|
||||
|
||||
|
|
@ -134,11 +132,14 @@ impl ValueText {
|
|||
let ref_ = Ref(raw);
|
||||
let is_list = raw >> 24 == 0x0F;
|
||||
|
||||
(ValueText {
|
||||
literal: Literal::Ref(ref_),
|
||||
has_vars: !is_list,
|
||||
is_list,
|
||||
}, ref_)
|
||||
(
|
||||
ValueText {
|
||||
literal: Literal::Ref(ref_),
|
||||
has_vars: !is_list,
|
||||
is_list,
|
||||
},
|
||||
ref_,
|
||||
)
|
||||
}
|
||||
|
||||
pub fn to_variables_reference(&self) -> i64 {
|
||||
|
|
@ -154,7 +155,7 @@ impl std::fmt::Display for Ref {
|
|||
match *self {
|
||||
Ref::NULL => fmt.write_str("null"),
|
||||
Ref::WORLD => fmt.write_str("world"),
|
||||
Ref(v) => write!(fmt, "[0x{:08x}]", v),
|
||||
Ref(v) => write!(fmt, "[0x{v:08x}]"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -162,17 +163,15 @@ impl std::fmt::Display for Ref {
|
|||
impl std::fmt::Display for Literal {
|
||||
fn fmt(&self, fmt: &mut std::fmt::Formatter) -> std::fmt::Result {
|
||||
match self {
|
||||
Literal::Ref(v) => write!(fmt, "{}", v),
|
||||
Literal::Number(n) => write!(fmt, "{}", n),
|
||||
Literal::String(s) => write!(fmt, "{:?}", s),
|
||||
Literal::Typepath(t) => write!(fmt, "{}", t),
|
||||
Literal::Resource(f) => write!(fmt, "'{}'", f),
|
||||
Literal::Proc(p) => {
|
||||
match p.rfind('/') {
|
||||
Some(idx) => write!(fmt, "{}/proc/{}", &p[..idx], &p[idx + 1..]),
|
||||
None => write!(fmt, "{}", p),
|
||||
}
|
||||
}
|
||||
Literal::Ref(v) => write!(fmt, "{v}"),
|
||||
Literal::Number(n) => write!(fmt, "{n}"),
|
||||
Literal::String(s) => write!(fmt, "{s:?}"),
|
||||
Literal::Typepath(t) => write!(fmt, "{t}"),
|
||||
Literal::Resource(f) => write!(fmt, "'{f}'"),
|
||||
Literal::Proc(p) => match p.rfind('/') {
|
||||
Some(idx) => write!(fmt, "{}/proc/{}", &p[..idx], &p[idx + 1..]),
|
||||
None => write!(fmt, "{p}"),
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -330,7 +329,7 @@ impl Request for GetAllFields {
|
|||
}
|
||||
|
||||
#[derive(Deserialize, Debug)]
|
||||
pub struct GetAllFieldsResponse(pub HashMap<String, ValueText, RandomState>);
|
||||
pub struct GetAllFieldsResponse(pub HashMap<String, ValueText>);
|
||||
|
||||
impl Response for GetAllFieldsResponse {
|
||||
const TYPE: &'static str = "get all fields";
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@
|
|||
#![allow(unsafe_code)]
|
||||
|
||||
use super::SequenceNumber;
|
||||
use foldhash::HashMap;
|
||||
use std::process::{Command, Stdio};
|
||||
use std::sync::{Arc, Mutex};
|
||||
|
||||
|
|
@ -30,24 +31,26 @@ pub struct Launched {
|
|||
pub enum EngineParams {
|
||||
Extools {
|
||||
port: u16,
|
||||
dll: Option<std::path::PathBuf>
|
||||
dll: Option<std::path::PathBuf>,
|
||||
},
|
||||
Auxtools {
|
||||
port: u16,
|
||||
dll: Option<std::path::PathBuf>
|
||||
}
|
||||
dll: Option<std::path::PathBuf>,
|
||||
},
|
||||
}
|
||||
|
||||
impl Launched {
|
||||
pub fn new(
|
||||
seq: Arc<SequenceNumber>,
|
||||
dreamseeker_exe: &str,
|
||||
env: Option<&HashMap<String, String>>,
|
||||
dmb: &str,
|
||||
params: Option<EngineParams>,
|
||||
) -> std::io::Result<Launched> {
|
||||
let mut command = Command::new(dreamseeker_exe);
|
||||
|
||||
#[cfg(unix)] {
|
||||
#[cfg(unix)]
|
||||
{
|
||||
if let Some(parent) = std::path::Path::new(dreamseeker_exe).parent() {
|
||||
command.env("LD_LIBRARY_PATH", parent);
|
||||
}
|
||||
|
|
@ -60,6 +63,10 @@ impl Launched {
|
|||
.stdout(Stdio::piped())
|
||||
.stderr(Stdio::piped());
|
||||
|
||||
if let Some(env) = env {
|
||||
command.envs(env);
|
||||
}
|
||||
|
||||
match params {
|
||||
Some(EngineParams::Extools { port, dll }) => {
|
||||
command.env("EXTOOLS_MODE", "LAUNCHED");
|
||||
|
|
@ -67,7 +74,7 @@ impl Launched {
|
|||
if let Some(dll) = dll {
|
||||
command.env("EXTOOLS_DLL", dll);
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
Some(EngineParams::Auxtools { port, dll }) => {
|
||||
command.env("AUXTOOLS_DEBUG_MODE", "LAUNCHED");
|
||||
|
|
@ -75,7 +82,7 @@ impl Launched {
|
|||
if let Some(dll) = dll {
|
||||
command.env("AUXTOOLS_DEBUG_DLL", dll);
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
None => (),
|
||||
}
|
||||
|
|
@ -134,12 +141,12 @@ impl Launched {
|
|||
true => Ok(()),
|
||||
false => Err(std::io::Error::last_os_error()),
|
||||
}
|
||||
}
|
||||
},
|
||||
State::Exited => Ok(()),
|
||||
_other => {
|
||||
debug_output!(in self.seq, "[launched] kill no-op in state {:?}", _other);
|
||||
Ok(())
|
||||
}
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -149,18 +156,24 @@ impl Launched {
|
|||
State::Active => {
|
||||
output!(in self.seq, "[launched] Detaching from child process...");
|
||||
*state = State::Detached;
|
||||
}
|
||||
},
|
||||
_other => {
|
||||
debug_output!(in self.seq, "[launched] detach no-op in state {:?}", _other);
|
||||
}
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn pipe_output<R: std::io::Read + Send + 'static>(seq: Arc<SequenceNumber>, keyword: &'static str, stream: Option<R>) -> std::io::Result<()> {
|
||||
guard!(let Some(stream2) = stream else { return Ok(()); });
|
||||
fn pipe_output<R: std::io::Read + Send + 'static>(
|
||||
seq: Arc<SequenceNumber>,
|
||||
keyword: &'static str,
|
||||
stream: Option<R>,
|
||||
) -> std::io::Result<()> {
|
||||
let Some(stream2) = stream else {
|
||||
return Ok(());
|
||||
};
|
||||
std::thread::Builder::new()
|
||||
.name(format!("launched debuggee {} relay", keyword))
|
||||
.name(format!("launched debuggee {keyword} relay"))
|
||||
.spawn(move || {
|
||||
use std::io::BufRead;
|
||||
|
||||
|
|
@ -176,15 +189,15 @@ fn pipe_output<R: std::io::Read + Send + 'static>(seq: Arc<SequenceNumber>, keyw
|
|||
category: Some(keyword.to_owned()),
|
||||
..Default::default()
|
||||
});
|
||||
}
|
||||
},
|
||||
Err(e) => {
|
||||
seq.issue_event(dap_types::OutputEvent {
|
||||
output: format!("[launched {}] {}", keyword, e),
|
||||
output: format!("[launched {keyword}] {e}"),
|
||||
category: Some("console".to_owned()),
|
||||
..Default::default()
|
||||
});
|
||||
break;
|
||||
}
|
||||
},
|
||||
}
|
||||
}
|
||||
})?;
|
||||
|
|
|
|||
File diff suppressed because it is too large
Load diff
|
|
@ -2,30 +2,32 @@
|
|||
//! language server protocol.
|
||||
#![allow(dead_code)]
|
||||
|
||||
use std::io::{self, Read, BufRead};
|
||||
use foldhash::HashMap;
|
||||
use std::borrow::Cow;
|
||||
use std::collections::HashMap;
|
||||
use std::io::{self, BufRead, Read};
|
||||
use std::rc::Rc;
|
||||
use url::Url;
|
||||
|
||||
use jsonrpc;
|
||||
use lsp_types::{TextDocumentItem, TextDocumentIdentifier,
|
||||
VersionedTextDocumentIdentifier, TextDocumentContentChangeEvent};
|
||||
use lsp_types::{
|
||||
TextDocumentContentChangeEvent, TextDocumentIdentifier, TextDocumentItem,
|
||||
VersionedTextDocumentIdentifier,
|
||||
};
|
||||
|
||||
use super::{invalid_request, url_to_path};
|
||||
|
||||
use ahash::RandomState;
|
||||
|
||||
/// A store for the contents of currently-open documents, with appropriate
|
||||
/// fallback for documents which are not currently open.
|
||||
#[derive(Default)]
|
||||
pub struct DocumentStore {
|
||||
map: HashMap<Url, Document, RandomState>,
|
||||
map: HashMap<Url, Document>,
|
||||
}
|
||||
|
||||
impl DocumentStore {
|
||||
pub fn open(&mut self, doc: TextDocumentItem) -> Result<(), jsonrpc::Error> {
|
||||
match self.map.insert(doc.uri.clone(), Document::new(doc.version, doc.text)) {
|
||||
match self
|
||||
.map
|
||||
.insert(doc.uri.clone(), Document::new(doc.version, doc.text))
|
||||
{
|
||||
None => Ok(()),
|
||||
Some(_) => Err(invalid_request(format!("opened twice: {}", doc.uri))),
|
||||
}
|
||||
|
|
@ -34,7 +36,10 @@ impl DocumentStore {
|
|||
pub fn close(&mut self, id: TextDocumentIdentifier) -> Result<Url, jsonrpc::Error> {
|
||||
match self.map.remove(&id.uri) {
|
||||
Some(_) => Ok(id.uri),
|
||||
None => Err(invalid_request(format!("cannot close non-opened: {}", id.uri))),
|
||||
None => Err(invalid_request(format!(
|
||||
"cannot close non-opened: {}",
|
||||
id.uri
|
||||
))),
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -43,24 +48,26 @@ impl DocumentStore {
|
|||
doc_id: VersionedTextDocumentIdentifier,
|
||||
changes: Vec<TextDocumentContentChangeEvent>,
|
||||
) -> Result<Url, jsonrpc::Error> {
|
||||
// "If a versioned text document identifier is sent from the server to
|
||||
// the client and the file is not open in the editor (the server has
|
||||
// not received an open notification before) the server can send `null`
|
||||
// to indicate that the version is known and the content on disk is the
|
||||
// truth (as speced with document content ownership)."
|
||||
let new_version = match doc_id.version {
|
||||
Some(version) => version,
|
||||
None => return Err(invalid_request("document version is missing")),
|
||||
};
|
||||
let new_version = doc_id.version;
|
||||
|
||||
let document = match self.map.get_mut(&doc_id.uri) {
|
||||
Some(doc) => doc,
|
||||
None => return Err(invalid_request(format!("cannot change non-opened: {}", doc_id.uri))),
|
||||
None => {
|
||||
return Err(invalid_request(format!(
|
||||
"cannot change non-opened: {}",
|
||||
doc_id.uri
|
||||
)))
|
||||
},
|
||||
};
|
||||
|
||||
if new_version < document.version {
|
||||
eprintln!("new_version: {} < document_version: {}", new_version, document.version);
|
||||
return Err(invalid_request("document version numbers shouldn't go backwards"));
|
||||
eprintln!(
|
||||
"new_version: {} < document_version: {}",
|
||||
new_version, document.version
|
||||
);
|
||||
return Err(invalid_request(
|
||||
"document version numbers shouldn't go backwards",
|
||||
));
|
||||
}
|
||||
document.version = new_version;
|
||||
|
||||
|
|
@ -86,8 +93,10 @@ impl DocumentStore {
|
|||
return Ok(Cow::Owned(text));
|
||||
}
|
||||
|
||||
Err(io::Error::new(io::ErrorKind::NotFound,
|
||||
format!("URL not opened and schema is not 'file': {}", url)))
|
||||
Err(io::Error::new(
|
||||
io::ErrorKind::NotFound,
|
||||
format!("URL not opened and schema is not 'file': {url}"),
|
||||
))
|
||||
}
|
||||
|
||||
pub fn read(&self, url: &Url) -> io::Result<Box<dyn io::Read>> {
|
||||
|
|
@ -100,19 +109,21 @@ impl DocumentStore {
|
|||
return Ok(Box::new(file) as Box<dyn io::Read>);
|
||||
}
|
||||
|
||||
Err(io::Error::new(io::ErrorKind::NotFound,
|
||||
format!("URL not opened and schema is not 'file': {}", url)))
|
||||
Err(io::Error::new(
|
||||
io::ErrorKind::NotFound,
|
||||
format!("URL not opened and schema is not 'file': {url}"),
|
||||
))
|
||||
}
|
||||
}
|
||||
|
||||
/// The internal representation of document contents received from the client.
|
||||
struct Document {
|
||||
version: i64,
|
||||
version: i32,
|
||||
text: Rc<String>,
|
||||
}
|
||||
|
||||
impl Document {
|
||||
fn new(version: i64, text: String) -> Document {
|
||||
fn new(version: i32, text: String) -> Document {
|
||||
Document {
|
||||
version,
|
||||
text: Rc::new(text),
|
||||
|
|
@ -128,7 +139,7 @@ impl Document {
|
|||
// considered to be the full content of the document."
|
||||
self.text = Rc::new(change.text);
|
||||
return Ok(());
|
||||
}
|
||||
},
|
||||
};
|
||||
|
||||
let start_pos = total_offset(&self.text, range.start.line, range.start.character)?;
|
||||
|
|
@ -140,7 +151,7 @@ impl Document {
|
|||
|
||||
/// Find the offset into the given text at which the given zero-indexed line
|
||||
/// number begins.
|
||||
fn line_offset(text: &str, line_number: u64) -> Result<usize, jsonrpc::Error> {
|
||||
fn line_offset(text: &str, line_number: u32) -> Result<usize, jsonrpc::Error> {
|
||||
// Hopefully this logic isn't too far off.
|
||||
let mut start_pos = 0;
|
||||
for _ in 0..line_number {
|
||||
|
|
@ -152,16 +163,16 @@ fn line_offset(text: &str, line_number: u64) -> Result<usize, jsonrpc::Error> {
|
|||
Ok(start_pos)
|
||||
}
|
||||
|
||||
fn total_offset(text: &str, line: u64, mut character: u64) -> Result<usize, jsonrpc::Error> {
|
||||
fn total_offset(text: &str, line: u32, mut character: u32) -> Result<usize, jsonrpc::Error> {
|
||||
let start = line_offset(text, line)?;
|
||||
|
||||
// column is measured in UTF-16 code units, which is really inconvenient.
|
||||
let mut chars = text[start..].chars();
|
||||
while character > 0 {
|
||||
if let Some(ch) = chars.next() {
|
||||
character = character.saturating_sub(ch.len_utf16() as u64);
|
||||
character = character.saturating_sub(ch.len_utf16() as u32);
|
||||
} else {
|
||||
break
|
||||
break;
|
||||
}
|
||||
}
|
||||
Ok(text.len() - chars.as_str().len())
|
||||
|
|
@ -181,17 +192,17 @@ pub fn offset_to_position(text: &str, offset: usize) -> lsp_types::Position {
|
|||
|
||||
let mut character = 0;
|
||||
for ch in text[line_start..offset].chars() {
|
||||
character += ch.len_utf16() as u64;
|
||||
character += ch.len_utf16() as u32;
|
||||
}
|
||||
|
||||
lsp_types::Position { line, character }
|
||||
}
|
||||
|
||||
pub fn get_range(text: &str, range: lsp_types::Range) -> Result<&str, jsonrpc::Error> {
|
||||
Ok(&text[
|
||||
total_offset(text, range.start.line, range.start.character)?
|
||||
..total_offset(text, range.end.line, range.end.character)?
|
||||
])
|
||||
Ok(
|
||||
&text[total_offset(text, range.start.line, range.start.character)?
|
||||
..total_offset(text, range.end.line, range.end.character)?],
|
||||
)
|
||||
}
|
||||
|
||||
pub fn find_word(text: &str, offset: usize) -> &str {
|
||||
|
|
@ -202,7 +213,7 @@ pub fn find_word(text: &str, offset: usize) -> &str {
|
|||
while !text.is_char_boundary(start_next) {
|
||||
start_next -= 1;
|
||||
}
|
||||
if !text[start_next..start].chars().next().map_or(false, is_ident) {
|
||||
if !text[start_next..start].chars().next().is_some_and(is_ident) {
|
||||
break;
|
||||
}
|
||||
start = start_next;
|
||||
|
|
@ -215,7 +226,7 @@ pub fn find_word(text: &str, offset: usize) -> &str {
|
|||
while !text.is_char_boundary(end_next) {
|
||||
end_next += 1;
|
||||
}
|
||||
if !text[end..end_next].chars().next().map_or(false, is_ident) {
|
||||
if !text[end..end_next].chars().next().is_some_and(is_ident) {
|
||||
break;
|
||||
}
|
||||
end = end_next;
|
||||
|
|
@ -229,7 +240,7 @@ pub fn find_word(text: &str, offset: usize) -> &str {
|
|||
}
|
||||
|
||||
fn is_ident(ch: char) -> bool {
|
||||
(ch >= '0' && ch <= '9') || (ch >= 'a' && ch <= 'z') || (ch >= 'A' && ch <= 'Z') || ch == '_'
|
||||
ch.is_ascii_digit() || ch.is_ascii_lowercase() || ch.is_ascii_uppercase() || ch == '_'
|
||||
}
|
||||
|
||||
/// An adaptation of `std::io::Cursor` which works on an `Rc<String>`, which
|
||||
|
|
|
|||
|
|
@ -1,8 +1,10 @@
|
|||
//! Extensions to the language server protocol.
|
||||
|
||||
use lsp_types::SymbolKind;
|
||||
use foldhash::HashMap;
|
||||
|
||||
use lsp_types::notification::*;
|
||||
use lsp_types::request::*;
|
||||
use lsp_types::SymbolKind;
|
||||
|
||||
pub enum WindowStatus {}
|
||||
impl Notification for WindowStatus {
|
||||
|
|
@ -63,6 +65,7 @@ impl Request for StartDebugger {
|
|||
#[derive(Debug, Serialize, Deserialize)]
|
||||
pub struct StartDebuggerParams {
|
||||
pub dreamseeker_exe: String,
|
||||
pub env: Option<HashMap<String, String>>,
|
||||
}
|
||||
#[derive(Debug, Serialize, Deserialize)]
|
||||
pub struct StartDebuggerResult {
|
||||
|
|
|
|||
|
|
@ -1,15 +1,13 @@
|
|||
//! The symbol table used for "Find References" support.
|
||||
|
||||
use std::collections::HashMap;
|
||||
use foldhash::{HashMap, HashMapExt};
|
||||
|
||||
use dm::Location;
|
||||
use dm::objtree::*;
|
||||
use dm::ast::*;
|
||||
|
||||
use ahash::RandomState;
|
||||
use dm::objtree::*;
|
||||
use dm::Location;
|
||||
|
||||
pub struct ReferencesTable {
|
||||
uses: HashMap<SymbolId, References, RandomState>,
|
||||
uses: HashMap<SymbolId, References>,
|
||||
symbols: SymbolIdSource,
|
||||
}
|
||||
|
||||
|
|
@ -22,16 +20,19 @@ struct References {
|
|||
impl ReferencesTable {
|
||||
pub fn new(objtree: &ObjectTree) -> Self {
|
||||
let mut tab = ReferencesTable {
|
||||
uses: HashMap::with_hasher(RandomState::default()),
|
||||
uses: HashMap::new(),
|
||||
symbols: SymbolIdSource::new(SymbolIdCategory::LocalVars),
|
||||
};
|
||||
|
||||
// Insert the "definition" locations for the types and such
|
||||
objtree.root().recurse(&mut |ty| {
|
||||
tab.uses.insert(ty.id, References {
|
||||
references: vec![],
|
||||
implementations: vec![ty.location],
|
||||
});
|
||||
tab.uses.insert(
|
||||
ty.id,
|
||||
References {
|
||||
references: vec![],
|
||||
implementations: vec![ty.location],
|
||||
},
|
||||
);
|
||||
for (name, var) in ty.vars.iter() {
|
||||
if let Some(decl) = ty.get_var_declaration(name) {
|
||||
tab.impl_symbol(decl.id, var.value.location);
|
||||
|
|
@ -49,7 +50,9 @@ impl ReferencesTable {
|
|||
if let Some(ref expr) = var.value.expression {
|
||||
let mut walk = WalkProc::from_ty(&mut tab, objtree, ty);
|
||||
let type_hint = match ty.get_var_declaration(name) {
|
||||
Some(decl) => walk.static_type(decl.location, &decl.var_type.type_path).basic_type(),
|
||||
Some(decl) => walk
|
||||
.static_type(decl.location, &decl.var_type.type_path)
|
||||
.basic_type(),
|
||||
None => None,
|
||||
};
|
||||
walk.visit_expression(var.value.location, expr, type_hint);
|
||||
|
|
@ -88,19 +91,30 @@ impl ReferencesTable {
|
|||
|
||||
fn new_symbol(&mut self, location: Location) -> SymbolId {
|
||||
let id = self.symbols.allocate();
|
||||
self.uses.insert(id, References {
|
||||
references: vec![location],
|
||||
implementations: vec![],
|
||||
});
|
||||
self.uses.insert(
|
||||
id,
|
||||
References {
|
||||
references: vec![location],
|
||||
implementations: vec![],
|
||||
},
|
||||
);
|
||||
id
|
||||
}
|
||||
|
||||
fn use_symbol(&mut self, symbol: SymbolId, location: Location) {
|
||||
self.uses.entry(symbol).or_default().references.push(location);
|
||||
self.uses
|
||||
.entry(symbol)
|
||||
.or_default()
|
||||
.references
|
||||
.push(location);
|
||||
}
|
||||
|
||||
fn impl_symbol(&mut self, symbol: SymbolId, location: Location) {
|
||||
self.uses.entry(symbol).or_default().implementations.push(location);
|
||||
self.uses
|
||||
.entry(symbol)
|
||||
.or_default()
|
||||
.implementations
|
||||
.push(location);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -134,35 +148,50 @@ struct WalkProc<'o> {
|
|||
objtree: &'o ObjectTree,
|
||||
ty: TypeRef<'o>,
|
||||
proc: Option<ProcRef<'o>>,
|
||||
local_vars: HashMap<String, Local<'o>, RandomState>,
|
||||
local_vars: HashMap<String, Local<'o>>,
|
||||
}
|
||||
|
||||
impl<'o> WalkProc<'o> {
|
||||
fn from_proc(tab: &'o mut ReferencesTable, objtree: &'o ObjectTree, proc: ProcRef<'o>) -> Self {
|
||||
let mut local_vars = HashMap::with_hasher(RandomState::default());
|
||||
local_vars.insert("global".to_owned(), Local {
|
||||
ty: StaticType::Type(objtree.root()),
|
||||
symbol: objtree.root().id,
|
||||
});
|
||||
local_vars.insert(".".to_owned(), Local {
|
||||
ty: StaticType::None,
|
||||
symbol: tab.new_symbol(proc.location),
|
||||
});
|
||||
local_vars.insert("args".to_owned(), Local {
|
||||
ty: StaticType::Type(objtree.expect("/list")),
|
||||
symbol: tab.new_symbol(proc.location),
|
||||
});
|
||||
local_vars.insert("usr".to_owned(), Local {
|
||||
ty: StaticType::Type(objtree.expect("/mob")),
|
||||
symbol: tab.new_symbol(proc.location),
|
||||
});
|
||||
let mut local_vars = HashMap::new();
|
||||
local_vars.insert(
|
||||
"global".to_owned(),
|
||||
Local {
|
||||
ty: StaticType::Type(objtree.root()),
|
||||
symbol: objtree.root().id,
|
||||
},
|
||||
);
|
||||
local_vars.insert(
|
||||
".".to_owned(),
|
||||
Local {
|
||||
ty: StaticType::None,
|
||||
symbol: tab.new_symbol(proc.location),
|
||||
},
|
||||
);
|
||||
local_vars.insert(
|
||||
"args".to_owned(),
|
||||
Local {
|
||||
ty: StaticType::Type(objtree.expect("/list")),
|
||||
symbol: tab.new_symbol(proc.location),
|
||||
},
|
||||
);
|
||||
local_vars.insert(
|
||||
"usr".to_owned(),
|
||||
Local {
|
||||
ty: StaticType::Type(objtree.expect("/mob")),
|
||||
symbol: tab.new_symbol(proc.location),
|
||||
},
|
||||
);
|
||||
|
||||
let ty = proc.ty();
|
||||
if !ty.is_root() {
|
||||
local_vars.insert("src".to_owned(), Local {
|
||||
ty: StaticType::Type(ty),
|
||||
symbol: tab.new_symbol(proc.location),
|
||||
});
|
||||
local_vars.insert(
|
||||
"src".to_owned(),
|
||||
Local {
|
||||
ty: StaticType::Type(ty),
|
||||
symbol: tab.new_symbol(proc.location),
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
WalkProc {
|
||||
|
|
@ -170,23 +199,26 @@ impl<'o> WalkProc<'o> {
|
|||
objtree,
|
||||
ty: proc.ty(),
|
||||
proc: Some(proc),
|
||||
local_vars
|
||||
local_vars,
|
||||
}
|
||||
}
|
||||
|
||||
fn from_ty(tab: &'o mut ReferencesTable, objtree: &'o ObjectTree, ty: TypeRef<'o>) -> Self {
|
||||
let mut local_vars = HashMap::with_hasher(RandomState::default());
|
||||
local_vars.insert("global".to_owned(), Local {
|
||||
ty: StaticType::Type(objtree.root()),
|
||||
symbol: objtree.root().id,
|
||||
});
|
||||
let mut local_vars = HashMap::new();
|
||||
local_vars.insert(
|
||||
"global".to_owned(),
|
||||
Local {
|
||||
ty: StaticType::Type(objtree.root()),
|
||||
symbol: objtree.root().id,
|
||||
},
|
||||
);
|
||||
|
||||
WalkProc {
|
||||
tab,
|
||||
objtree,
|
||||
ty,
|
||||
proc: None,
|
||||
local_vars
|
||||
local_vars,
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -197,10 +229,13 @@ impl<'o> WalkProc<'o> {
|
|||
if let Some(expr) = ¶m.default {
|
||||
self.visit_expression(param.location, expr, None);
|
||||
}
|
||||
self.local_vars.insert(param.name.to_owned(), Local {
|
||||
ty,
|
||||
symbol: self.tab.new_symbol(param.location)
|
||||
});
|
||||
self.local_vars.insert(
|
||||
param.name.to_owned(),
|
||||
Local {
|
||||
ty,
|
||||
symbol: self.tab.new_symbol(param.location),
|
||||
},
|
||||
);
|
||||
}
|
||||
self.visit_block(block);
|
||||
}
|
||||
|
|
@ -213,7 +248,9 @@ impl<'o> WalkProc<'o> {
|
|||
|
||||
fn visit_statement(&mut self, location: Location, statement: &'o Statement) {
|
||||
match statement {
|
||||
Statement::Expr(expr) => { self.visit_expression(location, expr, None); },
|
||||
Statement::Expr(expr) => {
|
||||
self.visit_expression(location, expr, None);
|
||||
},
|
||||
Statement::Return(expr) => {
|
||||
let dot = self.local_vars.get(".").unwrap().symbol;
|
||||
self.tab.use_symbol(dot, location);
|
||||
|
|
@ -221,7 +258,9 @@ impl<'o> WalkProc<'o> {
|
|||
self.visit_expression(location, expr, None);
|
||||
}
|
||||
},
|
||||
Statement::Throw(expr) => { self.visit_expression(location, expr, None); },
|
||||
Statement::Throw(expr) => {
|
||||
self.visit_expression(location, expr, None);
|
||||
},
|
||||
Statement::While { condition, block } => {
|
||||
self.visit_expression(location, condition, None);
|
||||
self.visit_block(block);
|
||||
|
|
@ -241,8 +280,13 @@ impl<'o> WalkProc<'o> {
|
|||
},
|
||||
Statement::ForInfinite { block } => {
|
||||
self.visit_block(block);
|
||||
}
|
||||
Statement::ForLoop { init, test, inc, block } => {
|
||||
},
|
||||
Statement::ForLoop {
|
||||
init,
|
||||
test,
|
||||
inc,
|
||||
block,
|
||||
} => {
|
||||
if let Some(init) = init {
|
||||
self.visit_statement(location, init);
|
||||
}
|
||||
|
|
@ -255,7 +299,13 @@ impl<'o> WalkProc<'o> {
|
|||
self.visit_block(block);
|
||||
},
|
||||
Statement::ForList(for_list) => {
|
||||
let ForListStatement { var_type, name, in_list, block, .. } = &**for_list;
|
||||
let ForListStatement {
|
||||
var_type,
|
||||
name,
|
||||
in_list,
|
||||
block,
|
||||
..
|
||||
} = &**for_list;
|
||||
if let Some(in_list) = in_list {
|
||||
self.visit_expression(location, in_list, None);
|
||||
}
|
||||
|
|
@ -265,7 +315,14 @@ impl<'o> WalkProc<'o> {
|
|||
self.visit_block(block);
|
||||
},
|
||||
Statement::ForRange(for_range) => {
|
||||
let ForRangeStatement { var_type, name, start, end, step, block } = &**for_range;
|
||||
let ForRangeStatement {
|
||||
var_type,
|
||||
name,
|
||||
start,
|
||||
end,
|
||||
step,
|
||||
block,
|
||||
} = &**for_range;
|
||||
self.visit_expression(location, end, None);
|
||||
if let Some(step) = step {
|
||||
self.visit_expression(location, step, None);
|
||||
|
|
@ -288,16 +345,22 @@ impl<'o> WalkProc<'o> {
|
|||
}
|
||||
self.visit_block(block);
|
||||
},
|
||||
Statement::Switch { input, cases, default } => {
|
||||
Statement::Switch {
|
||||
input,
|
||||
cases,
|
||||
default,
|
||||
} => {
|
||||
self.visit_expression(location, input, None);
|
||||
for (case, ref block) in cases.iter() {
|
||||
for case_part in case.elem.iter() {
|
||||
match case_part {
|
||||
dm::ast::Case::Exact(expr) => { self.visit_expression(case.location, expr, None); },
|
||||
dm::ast::Case::Exact(expr) => {
|
||||
self.visit_expression(case.location, expr, None);
|
||||
},
|
||||
dm::ast::Case::Range(start, end) => {
|
||||
self.visit_expression(case.location, start, None);
|
||||
self.visit_expression(case.location, end, None);
|
||||
}
|
||||
},
|
||||
}
|
||||
}
|
||||
self.visit_block(block);
|
||||
|
|
@ -306,16 +369,20 @@ impl<'o> WalkProc<'o> {
|
|||
self.visit_block(default);
|
||||
}
|
||||
},
|
||||
Statement::TryCatch { try_block, catch_params, catch_block } => {
|
||||
Statement::TryCatch {
|
||||
try_block,
|
||||
catch_params,
|
||||
catch_block,
|
||||
} => {
|
||||
self.visit_block(try_block);
|
||||
for caught in catch_params.iter() {
|
||||
let (var_name, mut type_path) = match caught.split_last() {
|
||||
Some(x) => x,
|
||||
None => continue
|
||||
None => continue,
|
||||
};
|
||||
match type_path.split_first() {
|
||||
Some((first, rest)) if first == "var" => type_path = rest,
|
||||
_ => {}
|
||||
_ => {},
|
||||
}
|
||||
let var_type: VarType = type_path.iter().map(ToOwned::to_owned).collect();
|
||||
self.visit_var(location, &var_type, var_name, None);
|
||||
|
|
@ -327,7 +394,34 @@ impl<'o> WalkProc<'o> {
|
|||
Statement::Goto(_) => {},
|
||||
Statement::Crash(_) => {},
|
||||
Statement::Label { name: _, block } => self.visit_block(block),
|
||||
Statement::Del(expr) => { self.visit_expression(location, expr, None); },
|
||||
Statement::Del(expr) => {
|
||||
self.visit_expression(location, expr, None);
|
||||
},
|
||||
Statement::ForKeyValue(for_key_value) => {
|
||||
let ForKeyValueStatement {
|
||||
var_type,
|
||||
key,
|
||||
key_input_type: _,
|
||||
value,
|
||||
in_list,
|
||||
block,
|
||||
} = &**for_key_value;
|
||||
if let Some(in_list) = in_list {
|
||||
self.visit_expression(location, in_list, None);
|
||||
}
|
||||
if let Some(var_type) = var_type {
|
||||
self.visit_var(location, var_type, key, None);
|
||||
}
|
||||
// the "v" in a DM for (var/k, v) statement is essentially typeless.
|
||||
// There is currently no way to change that.
|
||||
let var_type_value = VarType {
|
||||
flags: VarTypeFlags::from_bits_truncate(0),
|
||||
type_path: Box::new([]),
|
||||
input_type: InputType::from_bits_truncate(0),
|
||||
};
|
||||
self.visit_var(location, &var_type_value, value, None);
|
||||
self.visit_block(block);
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -335,16 +429,25 @@ impl<'o> WalkProc<'o> {
|
|||
self.visit_var(location, &var.var_type, &var.name, var.value.as_ref())
|
||||
}
|
||||
|
||||
fn visit_var(&mut self, location: Location, var_type: &VarType, name: &str, value: Option<&'o Expression>) {
|
||||
fn visit_var(
|
||||
&mut self,
|
||||
location: Location,
|
||||
var_type: &VarType,
|
||||
name: &str,
|
||||
value: Option<&'o Expression>,
|
||||
) {
|
||||
let ty = self.static_type(location, &var_type.type_path);
|
||||
self.use_type(location, &ty);
|
||||
if let Some(ref expr) = value {
|
||||
if let Some(expr) = value {
|
||||
self.visit_expression(location, expr, ty.basic_type());
|
||||
}
|
||||
self.local_vars.insert(name.to_owned(), Local {
|
||||
ty,
|
||||
symbol: self.tab.new_symbol(location),
|
||||
});
|
||||
self.local_vars.insert(
|
||||
name.to_owned(),
|
||||
Local {
|
||||
ty,
|
||||
symbol: self.tab.new_symbol(location),
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
fn use_type(&mut self, location: Location, ty: &StaticType<'o>) {
|
||||
|
|
@ -354,25 +457,31 @@ impl<'o> WalkProc<'o> {
|
|||
StaticType::List { list, keys } => {
|
||||
self.tab.use_symbol(list.id, location);
|
||||
self.use_type(location, keys);
|
||||
}
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
fn visit_expression(&mut self, location: Location, expression: &'o Expression, type_hint: Option<TypeRef<'o>>) -> StaticType<'o> {
|
||||
#[allow(clippy::only_used_in_recursion)]
|
||||
fn visit_expression(
|
||||
&mut self,
|
||||
location: Location,
|
||||
expression: &'o Expression,
|
||||
type_hint: Option<TypeRef<'o>>,
|
||||
) -> StaticType<'o> {
|
||||
match expression {
|
||||
Expression::Base { term, follow } => {
|
||||
let base_type_hint = if follow.is_empty() {
|
||||
type_hint
|
||||
} else {
|
||||
None
|
||||
};
|
||||
let base_type_hint = if follow.is_empty() { type_hint } else { None };
|
||||
let mut ty = self.visit_term(term.location, &term.elem, base_type_hint);
|
||||
for each in follow.iter() {
|
||||
ty = self.visit_follow(each.location, ty, &each.elem);
|
||||
}
|
||||
ty
|
||||
},
|
||||
Expression::BinaryOp { op: BinaryOp::Or, lhs, rhs } => {
|
||||
Expression::BinaryOp {
|
||||
op: BinaryOp::Or,
|
||||
lhs,
|
||||
rhs,
|
||||
} => {
|
||||
// It appears that DM does this in more cases than this, but
|
||||
// this is the only case I've seen it used in the wild.
|
||||
// ex: var/datum/cache_entry/E = cache[key] || new
|
||||
|
|
@ -395,7 +504,7 @@ impl<'o> WalkProc<'o> {
|
|||
let ty = self.visit_expression(location, if_, type_hint);
|
||||
self.visit_expression(location, else_, type_hint);
|
||||
ty
|
||||
}
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -412,7 +521,12 @@ impl<'o> WalkProc<'o> {
|
|||
}
|
||||
}
|
||||
|
||||
fn visit_term(&mut self, location: Location, term: &'o Term, type_hint: Option<TypeRef<'o>>) -> StaticType<'o> {
|
||||
fn visit_term(
|
||||
&mut self,
|
||||
location: Location,
|
||||
term: &'o Term,
|
||||
type_hint: Option<TypeRef<'o>>,
|
||||
) -> StaticType<'o> {
|
||||
match term {
|
||||
Term::Null => StaticType::None,
|
||||
Term::Int(_) => StaticType::None,
|
||||
|
|
@ -464,9 +578,7 @@ impl<'o> WalkProc<'o> {
|
|||
}
|
||||
},
|
||||
|
||||
Term::NewImplicit { args } => {
|
||||
self.visit_new(location, type_hint, args)
|
||||
},
|
||||
Term::NewImplicit { args } => self.visit_new(location, type_hint, args),
|
||||
Term::NewPrefab { prefab, args } => {
|
||||
let typepath = self.visit_prefab(location, prefab);
|
||||
self.visit_new(location, typepath, args)
|
||||
|
|
@ -496,7 +608,11 @@ impl<'o> WalkProc<'o> {
|
|||
}
|
||||
StaticType::None
|
||||
},
|
||||
Term::Input { args, input_type: _, in_list } => {
|
||||
Term::Input {
|
||||
args,
|
||||
input_type: _,
|
||||
in_list,
|
||||
} => {
|
||||
// TODO: use /proc/input
|
||||
self.visit_arguments(location, args);
|
||||
if let Some(ref expr) = in_list {
|
||||
|
|
@ -520,10 +636,64 @@ impl<'o> WalkProc<'o> {
|
|||
self.visit_arguments(location, args_2);
|
||||
StaticType::None
|
||||
},
|
||||
Term::ExternalCall {
|
||||
library,
|
||||
function,
|
||||
args,
|
||||
} => {
|
||||
if let Some(library) = library {
|
||||
self.visit_expression(location, library, None);
|
||||
}
|
||||
self.visit_expression(location, function, None);
|
||||
self.visit_arguments(location, args);
|
||||
StaticType::None
|
||||
},
|
||||
|
||||
Term::GlobalCall(name, args) => {
|
||||
if let Some(proc) = self.objtree.root().get_proc(name) {
|
||||
self.visit_call(location, self.objtree.root(), proc, args, false)
|
||||
} else {
|
||||
self.visit_arguments(location, args);
|
||||
StaticType::None
|
||||
}
|
||||
},
|
||||
Term::GlobalIdent(name) => {
|
||||
if let Some(decl) = self.objtree.root().get_var_declaration(name) {
|
||||
self.tab.use_symbol(decl.id, location);
|
||||
self.static_type(location, &decl.var_type.type_path)
|
||||
} else {
|
||||
StaticType::None
|
||||
}
|
||||
},
|
||||
Term::__TYPE__ => {
|
||||
self.tab.use_symbol(self.ty.id, location);
|
||||
StaticType::None
|
||||
},
|
||||
Term::__PROC__ => {
|
||||
let Some(proc) = self.proc else {
|
||||
return StaticType::None;
|
||||
};
|
||||
if let Some(decl) = self.ty.get_proc_declaration(proc.name()) {
|
||||
self.tab.use_symbol(decl.id, location);
|
||||
}
|
||||
StaticType::None
|
||||
},
|
||||
Term::__IMPLIED_TYPE__ => {
|
||||
let Some(implied_type) = type_hint else {
|
||||
return StaticType::None;
|
||||
};
|
||||
self.tab.use_symbol(implied_type.id, location);
|
||||
StaticType::Type(implied_type)
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
fn visit_new(&mut self, location: Location, typepath: Option<TypeRef<'o>>, args: &'o Option<Box<[Expression]>>) -> StaticType<'o> {
|
||||
fn visit_new(
|
||||
&mut self,
|
||||
location: Location,
|
||||
typepath: Option<TypeRef<'o>>,
|
||||
args: &'o Option<Box<[Expression]>>,
|
||||
) -> StaticType<'o> {
|
||||
if let Some(typepath) = typepath {
|
||||
if let Some(new_proc) = typepath.get_proc("New") {
|
||||
self.visit_call(
|
||||
|
|
@ -533,7 +703,8 @@ impl<'o> WalkProc<'o> {
|
|||
args.as_ref().map_or(&[], |v| &v[..]),
|
||||
// New calls are exact: `new /datum()` will always call
|
||||
// `/datum/New()` and never an override.
|
||||
true);
|
||||
true,
|
||||
);
|
||||
}
|
||||
// If we had a diagnostic context here, we'd error for
|
||||
// types other than `/list`, which has no `New()`.
|
||||
|
|
@ -558,7 +729,9 @@ impl<'o> WalkProc<'o> {
|
|||
let mut type_hint = None;
|
||||
if let Some(decl) = nav.ty().get_var_declaration(key) {
|
||||
self.tab.use_symbol(decl.id, location);
|
||||
type_hint = self.static_type(location, &decl.var_type.type_path).basic_type();
|
||||
type_hint = self
|
||||
.static_type(location, &decl.var_type.type_path)
|
||||
.basic_type();
|
||||
}
|
||||
self.visit_expression(location, expr, type_hint);
|
||||
}
|
||||
|
|
@ -569,7 +742,12 @@ impl<'o> WalkProc<'o> {
|
|||
}
|
||||
}
|
||||
|
||||
fn visit_field(&mut self, location: Location, lhs: StaticType<'o>, name: &'o str) -> StaticType<'o> {
|
||||
fn visit_field(
|
||||
&mut self,
|
||||
location: Location,
|
||||
lhs: StaticType<'o>,
|
||||
name: &'o str,
|
||||
) -> StaticType<'o> {
|
||||
if let Some(ty) = lhs.basic_type() {
|
||||
if let Some(decl) = ty.get_var_declaration(name) {
|
||||
self.tab.use_symbol(decl.id, location);
|
||||
|
|
@ -582,7 +760,12 @@ impl<'o> WalkProc<'o> {
|
|||
}
|
||||
}
|
||||
|
||||
fn visit_follow(&mut self, location: Location, lhs: StaticType<'o>, rhs: &'o Follow) -> StaticType<'o> {
|
||||
fn visit_follow(
|
||||
&mut self,
|
||||
location: Location,
|
||||
lhs: StaticType<'o>,
|
||||
rhs: &'o Follow,
|
||||
) -> StaticType<'o> {
|
||||
match rhs {
|
||||
Follow::Unary(op) => self.visit_unary(lhs, *op),
|
||||
Follow::Index(_, expr) => {
|
||||
|
|
@ -595,6 +778,7 @@ impl<'o> WalkProc<'o> {
|
|||
}
|
||||
},
|
||||
Follow::Field(_, name) => self.visit_field(location, lhs, name),
|
||||
Follow::StaticField(name) => self.visit_field(location, lhs, name),
|
||||
Follow::Call(_, name, arguments) => {
|
||||
if let Some(ty) = lhs.basic_type() {
|
||||
if let Some(proc) = ty.get_proc(name) {
|
||||
|
|
@ -608,6 +792,14 @@ impl<'o> WalkProc<'o> {
|
|||
StaticType::None
|
||||
}
|
||||
},
|
||||
Follow::ProcReference(name) => {
|
||||
if let Some(ty) = lhs.basic_type() {
|
||||
if let Some(decl) = ty.get_proc_declaration(name) {
|
||||
self.tab.use_symbol(decl.id, location);
|
||||
}
|
||||
}
|
||||
StaticType::None
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -616,12 +808,24 @@ impl<'o> WalkProc<'o> {
|
|||
StaticType::None
|
||||
}
|
||||
|
||||
fn visit_binary(&mut self, _lhs: StaticType<'o>, _rhs: StaticType<'o>, _op: BinaryOp) -> StaticType<'o> {
|
||||
fn visit_binary(
|
||||
&mut self,
|
||||
_lhs: StaticType<'o>,
|
||||
_rhs: StaticType<'o>,
|
||||
_op: BinaryOp,
|
||||
) -> StaticType<'o> {
|
||||
// TODO: mark usage of operatorX procs
|
||||
StaticType::None
|
||||
}
|
||||
|
||||
fn visit_call(&mut self, location: Location, src: TypeRef<'o>, proc: ProcRef, args: &'o [Expression], is_exact: bool) -> StaticType<'o> {
|
||||
fn visit_call(
|
||||
&mut self,
|
||||
location: Location,
|
||||
src: TypeRef<'o>,
|
||||
proc: ProcRef,
|
||||
args: &'o [Expression],
|
||||
is_exact: bool,
|
||||
) -> StaticType<'o> {
|
||||
// register use of symbol
|
||||
if !is_exact {
|
||||
// Only include uses of the symbol by name, not `.()` or `..()`
|
||||
|
|
@ -634,17 +838,21 @@ impl<'o> WalkProc<'o> {
|
|||
// identify and register kwargs used
|
||||
for arg in args {
|
||||
let mut argument_value = arg;
|
||||
if let Expression::AssignOp { op: AssignOp::Assign, lhs, rhs } = arg {
|
||||
if let Expression::AssignOp {
|
||||
op: AssignOp::Assign,
|
||||
lhs,
|
||||
rhs,
|
||||
} = arg
|
||||
{
|
||||
match lhs.as_term() {
|
||||
Some(Term::Ident(_name)) |
|
||||
Some(Term::String(_name)) => {
|
||||
Some(Term::Ident(_name)) | Some(Term::String(_name)) => {
|
||||
// Don't visit_expression the kwarg key.
|
||||
argument_value = rhs;
|
||||
|
||||
// TODO: register a usage of the kwarg symbol here.
|
||||
// Recurse to children too?
|
||||
}
|
||||
_ => {}
|
||||
},
|
||||
_ => {},
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -657,14 +865,18 @@ impl<'o> WalkProc<'o> {
|
|||
fn visit_arguments(&mut self, location: Location, args: &'o [Expression]) {
|
||||
for arg in args {
|
||||
let mut argument_value = arg;
|
||||
if let Expression::AssignOp { op: AssignOp::Assign, lhs, rhs } = arg {
|
||||
if let Expression::AssignOp {
|
||||
op: AssignOp::Assign,
|
||||
lhs,
|
||||
rhs,
|
||||
} = arg
|
||||
{
|
||||
match lhs.as_term() {
|
||||
Some(Term::Ident(_name)) |
|
||||
Some(Term::String(_name)) => {
|
||||
Some(Term::Ident(_name)) | Some(Term::String(_name)) => {
|
||||
// Don't visit_expression the kwarg key.
|
||||
argument_value = rhs;
|
||||
}
|
||||
_ => {}
|
||||
},
|
||||
_ => {},
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -672,8 +884,21 @@ impl<'o> WalkProc<'o> {
|
|||
}
|
||||
}
|
||||
|
||||
#[allow(clippy::only_used_in_recursion)]
|
||||
fn static_type(&mut self, location: Location, mut of: &[String]) -> StaticType<'o> {
|
||||
while !of.is_empty() && ["static", "global", "const", "tmp"].contains(&&*of[0]) {
|
||||
while !of.is_empty()
|
||||
&& [
|
||||
"static",
|
||||
"global",
|
||||
"const",
|
||||
"tmp",
|
||||
"final",
|
||||
"SpacemanDMM_final",
|
||||
"SpacemanDMM_private",
|
||||
"SpacemanDMM_protected",
|
||||
]
|
||||
.contains(&&*of[0])
|
||||
{
|
||||
of = &of[1..];
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -28,14 +28,14 @@ fn read<R: BufRead>(input: &mut R) -> Result<Option<String>, Box<dyn std::error:
|
|||
let size = {
|
||||
let parts: Vec<&str> = buffer.split(' ').collect();
|
||||
if parts.len() != 2 {
|
||||
eprintln!("JSON-RPC read error: parts.len() != 2\n{:?}", parts);
|
||||
eprintln!("JSON-RPC read error: parts.len() != 2\n{parts:?}");
|
||||
return Ok(None);
|
||||
}
|
||||
if !parts[0].eq_ignore_ascii_case("content-length:") {
|
||||
eprintln!("JSON-RPC read error: !parts[0].eq_ignore_ascii_case(\"content-length:\")\n{:?}", parts);
|
||||
eprintln!("JSON-RPC read error: !parts[0].eq_ignore_ascii_case(\"content-length:\")\n{parts:?}");
|
||||
return Ok(None);
|
||||
}
|
||||
usize::from_str_radix(parts[1].trim(), 10)?
|
||||
parts[1].trim().parse::<usize>()?
|
||||
};
|
||||
|
||||
// skip blank line
|
||||
|
|
|
|||
|
|
@ -1,135 +1,4 @@
|
|||
//! Utility macros.
|
||||
|
||||
pub mod all_methods {
|
||||
pub use lsp_types::request::*;
|
||||
pub use crate::extras::*;
|
||||
}
|
||||
|
||||
pub mod all_notifications {
|
||||
pub use lsp_types::notification::*;
|
||||
pub use crate::extras::*;
|
||||
}
|
||||
|
||||
macro_rules! handle_method_call {
|
||||
($($(#[$attr:meta])* on $what:ident(&mut $self:ident, $p:pat) $b:block)*) => {
|
||||
impl<'a> Engine<'a> {
|
||||
fn handle_method_call_table(method: &str) -> Option<fn(&mut Self, serde_json::Value) -> Result<serde_json::Value, jsonrpc::Error>> {
|
||||
use macros::all_methods::*;
|
||||
$(if method == <$what>::METHOD {
|
||||
Some(|this, params_value| {
|
||||
let params: <$what as Request>::Params = serde_json::from_value(params_value).map_err(invalid_request)?;
|
||||
let result: <$what as Request>::Result = this.$what(params)?;
|
||||
Ok(serde_json::to_value(result).expect("encode problem"))
|
||||
})
|
||||
} else)* {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
$(
|
||||
#[allow(non_snake_case)]
|
||||
$(#[$attr])*
|
||||
fn $what(&mut $self, $p: <macros::all_methods::$what as lsp_types::request::Request>::Params)
|
||||
-> Result<<macros::all_methods::$what as lsp_types::request::Request>::Result, jsonrpc::Error>
|
||||
{
|
||||
#[allow(unused_imports)]
|
||||
use lsp_types::*;
|
||||
#[allow(unused_imports)]
|
||||
use lsp_types::request::*;
|
||||
let _v = $b;
|
||||
#[allow(unreachable_code)] { Ok(_v) }
|
||||
}
|
||||
)*
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
macro_rules! handle_notification {
|
||||
($(on $what:ident(&mut $self:ident, $p:pat) $b:block)*) => {
|
||||
impl<'a> Engine<'a> {
|
||||
fn handle_notification_table(method: &str) -> Option<fn(&mut Self, serde_json::Value) -> Result<(), jsonrpc::Error>> {
|
||||
use macros::all_notifications::*;
|
||||
$(if method == <$what>::METHOD {
|
||||
Some(|this, params_value| {
|
||||
let params: <$what as Notification>::Params = serde_json::from_value(params_value).map_err(invalid_request)?;
|
||||
this.$what(params)
|
||||
})
|
||||
} else)* {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
$(
|
||||
#[allow(non_snake_case)]
|
||||
fn $what(&mut $self, $p: <macros::all_notifications::$what as lsp_types::notification::Notification>::Params)
|
||||
-> Result<(), jsonrpc::Error>
|
||||
{
|
||||
#[allow(unused_imports)]
|
||||
use lsp_types::*;
|
||||
#[allow(unused_imports)]
|
||||
use lsp_types::notification::*;
|
||||
let _v = $b;
|
||||
#[allow(unreachable_code)] { Ok(_v) }
|
||||
}
|
||||
)*
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
macro_rules! handle_request {
|
||||
($(on $what:ident(&mut $self:ident, $p:pat) $b:block)*) => {
|
||||
impl Debugger {
|
||||
fn handle_request_table(command: &str) -> Option<fn(&mut Self, serde_json::Value) -> Result<serde_json::Value, Box<dyn Error>>> {
|
||||
use dap_types::*;
|
||||
$(if command == <$what>::COMMAND {
|
||||
Some(|this, arguments| {
|
||||
let params: <$what as Request>::Params = serde_json::from_value(arguments)?;
|
||||
let result: <$what as Request>::Result = this.$what(params)?;
|
||||
Ok(serde_json::to_value(result).expect("encode problem"))
|
||||
})
|
||||
} else)* {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
$(
|
||||
#[allow(non_snake_case)]
|
||||
fn $what(&mut $self, $p: <$what as dap_types::Request>::Params)
|
||||
-> Result<<$what as dap_types::Request>::Result, Box<dyn Error>>
|
||||
{
|
||||
let _v = $b;
|
||||
#[allow(unreachable_code)] { Ok(_v) }
|
||||
}
|
||||
)*
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
macro_rules! handle_extools {
|
||||
($(on $what:ident(&mut $self:ident, $p:pat) $b:block)*) => {
|
||||
impl ExtoolsThread {
|
||||
fn handle_response_table(type_: &str) -> Option<fn(&mut Self, serde_json::Value) -> Result<(), Box<dyn Error>>> {
|
||||
$(if type_ == <$what as Response>::TYPE {
|
||||
Some(|this, content| {
|
||||
let deserialized: $what = serde_json::from_value(content)?;
|
||||
this.$what(deserialized)
|
||||
})
|
||||
} else)* {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
$(
|
||||
#[allow(non_snake_case)]
|
||||
fn $what(&mut $self, $p: $what) -> Result<(), Box<dyn Error>> {
|
||||
let _v = $b;
|
||||
#[allow(unreachable_code)] { Ok(_v) }
|
||||
}
|
||||
)*
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
macro_rules! if_annotation {
|
||||
($p:pat in $a:expr; $b:block) => {
|
||||
for (_, thing) in $a.clone() {
|
||||
|
|
@ -142,10 +11,10 @@ macro_rules! if_annotation {
|
|||
}
|
||||
|
||||
macro_rules! match_annotation {
|
||||
($a:expr; $($($p:pat)|* => $b:block,)*) => {
|
||||
($a:expr; $($p:pat => $b:block,)*) => {
|
||||
for (_, thing) in $a.clone() {
|
||||
match thing {
|
||||
$($($p)|* => $b,)*
|
||||
$($p => $b,)*
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
File diff suppressed because it is too large
Load diff
|
|
@ -16,16 +16,14 @@ impl Query {
|
|||
if !any_alphanumeric(query) {
|
||||
return None;
|
||||
}
|
||||
Some(if query.starts_with('#') {
|
||||
Query::Define(query[1..].to_lowercase())
|
||||
} else if query.starts_with("var/") {
|
||||
let query = &query["var/".len()..];
|
||||
Some(if let Some(query) = query.strip_prefix('#') {
|
||||
Query::Define(query.to_lowercase())
|
||||
} else if let Some(query) = query.strip_prefix("var/") {
|
||||
if !any_alphanumeric(query) {
|
||||
return None;
|
||||
}
|
||||
Query::Var(query.to_lowercase())
|
||||
} else if query.starts_with("proc/") {
|
||||
let query = &query["proc/".len()..];
|
||||
} else if let Some(query) = query.strip_prefix("proc/") {
|
||||
if !any_alphanumeric(query) {
|
||||
return None;
|
||||
}
|
||||
|
|
@ -55,39 +53,37 @@ impl Query {
|
|||
}
|
||||
|
||||
pub fn matches_on_type(&self, _path: &str) -> bool {
|
||||
match *self {
|
||||
Query::Anything(_) |
|
||||
Query::Proc(_) |
|
||||
Query::Var(_) => true,
|
||||
_ => false,
|
||||
}
|
||||
matches!(*self, Query::Anything(_) | Query::Proc(_) | Query::Var(_))
|
||||
}
|
||||
|
||||
pub fn matches_var(&self, name: &str) -> bool {
|
||||
match *self {
|
||||
Query::Anything(ref q) |
|
||||
Query::Var(ref q) => starts_with(name, q),
|
||||
Query::Anything(ref q) | Query::Var(ref q) => starts_with(name, q),
|
||||
_ => false,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn matches_proc(&self, name: &str, _kind: dm::ast::ProcDeclKind) -> bool {
|
||||
match *self {
|
||||
Query::Anything(ref q) |
|
||||
Query::Proc(ref q) => starts_with(name, q),
|
||||
Query::Anything(ref q) | Query::Proc(ref q) => starts_with(name, q),
|
||||
_ => false,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn simplify<'a>(s: &'a str) -> impl Iterator<Item=char> + Clone + 'a {
|
||||
s.chars().flat_map(|c| c.to_lowercase()).filter(|c| c.is_alphanumeric())
|
||||
fn simplify(s: &str) -> impl Iterator<Item = char> + Clone + '_ {
|
||||
s.chars()
|
||||
.flat_map(|c| c.to_lowercase())
|
||||
.filter(|c| c.is_alphanumeric())
|
||||
}
|
||||
|
||||
// ignore case and underscores
|
||||
pub fn starts_with<'a>(fulltext: &'a str, query: &'a str) -> bool {
|
||||
let mut query_chars = simplify(query);
|
||||
simplify(fulltext).zip(&mut query_chars).all(|(a, b)| a == b) && query_chars.next().is_none()
|
||||
simplify(fulltext)
|
||||
.zip(&mut query_chars)
|
||||
.all(|(a, b)| a == b)
|
||||
&& query_chars.next().is_none()
|
||||
}
|
||||
|
||||
pub fn contains<'a>(fulltext: &'a str, query: &'a str) -> bool {
|
||||
|
|
@ -104,5 +100,7 @@ pub fn contains<'a>(fulltext: &'a str, query: &'a str) -> bool {
|
|||
}
|
||||
|
||||
fn any_alphanumeric(text: &str) -> bool {
|
||||
text.chars().flat_map(|c| c.to_lowercase()).any(|c| c.is_alphanumeric())
|
||||
text.chars()
|
||||
.flat_map(|c| c.to_lowercase())
|
||||
.any(|c| c.is_alphanumeric())
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,22 +1,21 @@
|
|||
[package]
|
||||
name = "dmdoc"
|
||||
version = "1.4.1"
|
||||
authors = ["Tad Hardesty <tad@platymuus.com>"]
|
||||
homepage = "https://github.com/SpaceManiac/SpacemanDMM/blob/master/src/dmdoc/README.md"
|
||||
edition = "2018"
|
||||
homepage = "https://github.com/SpaceManiac/SpacemanDMM/blob/master/crates/dmdoc/README.md"
|
||||
version.workspace = true
|
||||
authors.workspace = true
|
||||
edition.workspace = true
|
||||
|
||||
[dependencies]
|
||||
dreammaker = { path = "../dreammaker" }
|
||||
pulldown-cmark = "0.7.0"
|
||||
tera = "1.0.2"
|
||||
serde = "1.0.71"
|
||||
serde_derive = "1.0.27"
|
||||
walkdir = "2.2.0"
|
||||
git2 = { version = "0.13", default-features = false }
|
||||
pulldown-cmark = "0.9.6"
|
||||
walkdir = "2.5.0"
|
||||
git2 = { version = "0.20.2", default-features = false }
|
||||
maud = "0.27.0"
|
||||
foldhash = "0.2.0"
|
||||
|
||||
[dev-dependencies]
|
||||
walkdir = "2.2.0"
|
||||
walkdir = "2.5.0"
|
||||
|
||||
[build-dependencies]
|
||||
chrono = "0.4.0"
|
||||
git2 = { version = "0.13", default-features = false }
|
||||
chrono = "0.4.38"
|
||||
git2 = { version = "0.20.2", default-features = false }
|
||||
|
|
|
|||
|
|
@ -4,7 +4,7 @@
|
|||
of the [BYOND] game engine. It produces simple static HTML files based on
|
||||
documented files, macros, types, procs, and vars.
|
||||
|
||||
[BYOND]: https://secure.byond.com/
|
||||
[BYOND]: https://www.byond.com/
|
||||
|
||||
If dmdoc is run in a Git repository, web links to source code are placed next
|
||||
to item headings in the generated output; otherwise, file and line numbers are
|
||||
|
|
|
|||
|
|
@ -8,12 +8,12 @@ use std::path::PathBuf;
|
|||
|
||||
fn main() {
|
||||
let out_dir = PathBuf::from(env::var_os("OUT_DIR").unwrap());
|
||||
let mut f = File::create(&out_dir.join("build-info.txt")).unwrap();
|
||||
let mut f = File::create(out_dir.join("build-info.txt")).unwrap();
|
||||
|
||||
if let Ok(commit) = read_commit() {
|
||||
writeln!(f, "commit: {}", commit).unwrap();
|
||||
writeln!(f, "commit: {commit}").unwrap();
|
||||
}
|
||||
writeln!(f, "build date: {}", chrono::Utc::today()).unwrap();
|
||||
writeln!(f, "build date: {}", chrono::Utc::now().date_naive()).unwrap();
|
||||
}
|
||||
|
||||
fn read_commit() -> Result<String, git2::Error> {
|
||||
|
|
|
|||
File diff suppressed because it is too large
Load diff
|
|
@ -1,40 +1,47 @@
|
|||
//! Parser for "doc-block" markdown documents.
|
||||
|
||||
use std::ops::Range;
|
||||
use std::collections::VecDeque;
|
||||
use std::ops::Range;
|
||||
|
||||
use pulldown_cmark::{self, Parser, Tag, Event};
|
||||
use maud::PreEscaped;
|
||||
use pulldown_cmark::{self, BrokenLinkCallback, Event, HeadingLevel, Parser, Tag};
|
||||
|
||||
pub type BrokenLinkCallback<'a> = Option<&'a dyn Fn(&str, &str) -> Option<(String, String)>>;
|
||||
|
||||
pub fn render(markdown: &str, broken_link_callback: BrokenLinkCallback) -> String {
|
||||
pub fn render<'string>(
|
||||
markdown: &'string str,
|
||||
broken_link_callback: BrokenLinkCallback<'string, '_>,
|
||||
) -> PreEscaped<String> {
|
||||
let mut buf = String::new();
|
||||
push_html(&mut buf, parser(markdown, broken_link_callback));
|
||||
buf
|
||||
PreEscaped(buf)
|
||||
}
|
||||
|
||||
/// A rendered markdown document with the teaser identified.
|
||||
#[derive(Serialize)]
|
||||
pub struct DocBlock {
|
||||
pub html: String,
|
||||
pub html: PreEscaped<String>,
|
||||
pub has_description: bool,
|
||||
teaser: Range<usize>,
|
||||
}
|
||||
|
||||
impl DocBlock {
|
||||
pub fn parse(markdown: &str, broken_link_callback: BrokenLinkCallback) -> Self {
|
||||
pub fn parse<'string>(
|
||||
markdown: &'string str,
|
||||
broken_link_callback: BrokenLinkCallback<'string, '_>,
|
||||
) -> Self {
|
||||
parse_main(parser(markdown, broken_link_callback).peekable())
|
||||
}
|
||||
|
||||
pub fn parse_with_title(markdown: &str, broken_link_callback: BrokenLinkCallback) -> (Option<String>, Self) {
|
||||
pub fn parse_with_title<'string>(
|
||||
markdown: &'string str,
|
||||
broken_link_callback: BrokenLinkCallback<'string, '_>,
|
||||
) -> (Option<String>, Self) {
|
||||
let mut parser = parser(markdown, broken_link_callback).peekable();
|
||||
(
|
||||
if let Some(&Event::Start(Tag::Heading(1))) = parser.peek() {
|
||||
if let Some(&Event::Start(Tag::Heading(HeadingLevel::H1, _, _))) = parser.peek() {
|
||||
parser.next();
|
||||
let mut pieces = Vec::new();
|
||||
loop {
|
||||
match parser.next() {
|
||||
None | Some(Event::End(Tag::Heading(1))) => break,
|
||||
None | Some(Event::End(Tag::Heading(HeadingLevel::H1, _, _))) => break,
|
||||
Some(other) => pieces.push(other),
|
||||
}
|
||||
}
|
||||
|
|
@ -49,16 +56,19 @@ impl DocBlock {
|
|||
)
|
||||
}
|
||||
|
||||
pub fn teaser(&self) -> &str {
|
||||
&self.html[self.teaser.clone()]
|
||||
pub fn teaser(&self) -> PreEscaped<&str> {
|
||||
PreEscaped(&self.html.0[self.teaser.clone()])
|
||||
}
|
||||
}
|
||||
|
||||
fn parser<'a>(markdown: &'a str, broken_link_callback: BrokenLinkCallback<'a>) -> Parser<'a> {
|
||||
fn parser<'string, 'func>(
|
||||
markdown: &'string str,
|
||||
broken_link_callback: BrokenLinkCallback<'string, 'func>,
|
||||
) -> Parser<'string, 'func> {
|
||||
Parser::new_with_broken_link_callback(
|
||||
markdown,
|
||||
pulldown_cmark::Options::ENABLE_TABLES | pulldown_cmark::Options::ENABLE_STRIKETHROUGH,
|
||||
broken_link_callback
|
||||
broken_link_callback,
|
||||
)
|
||||
}
|
||||
|
||||
|
|
@ -85,14 +95,21 @@ fn parse_main(mut parser: std::iter::Peekable<Parser>) -> DocBlock {
|
|||
let has_description = parser.peek().is_some();
|
||||
push_html(&mut html, parser);
|
||||
trim_right(&mut html);
|
||||
DocBlock { html, teaser, has_description }
|
||||
DocBlock {
|
||||
html: PreEscaped(html),
|
||||
teaser,
|
||||
has_description,
|
||||
}
|
||||
}
|
||||
|
||||
fn push_html<'a, I: IntoIterator<Item=Event<'a>>>(buf: &mut String, iter: I) {
|
||||
pulldown_cmark::html::push_html(buf, HeadingLinker {
|
||||
inner: iter.into_iter(),
|
||||
output: Default::default(),
|
||||
});
|
||||
fn push_html<'a, I: IntoIterator<Item = Event<'a>>>(buf: &mut String, iter: I) {
|
||||
pulldown_cmark::html::push_html(
|
||||
buf,
|
||||
HeadingLinker {
|
||||
inner: iter.into_iter(),
|
||||
output: Default::default(),
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
fn trim_right(buf: &mut String) {
|
||||
|
|
@ -107,7 +124,7 @@ struct HeadingLinker<'a, I> {
|
|||
output: VecDeque<Event<'a>>,
|
||||
}
|
||||
|
||||
impl<'a, I: Iterator<Item=Event<'a>>> Iterator for HeadingLinker<'a, I> {
|
||||
impl<'a, I: Iterator<Item = Event<'a>>> Iterator for HeadingLinker<'a, I> {
|
||||
type Item = Event<'a>;
|
||||
|
||||
fn next(&mut self) -> Option<Event<'a>> {
|
||||
|
|
@ -116,23 +133,26 @@ impl<'a, I: Iterator<Item=Event<'a>>> Iterator for HeadingLinker<'a, I> {
|
|||
}
|
||||
|
||||
let original = self.inner.next();
|
||||
if let Some(Event::Start(Tag::Heading(heading))) = original {
|
||||
if let Some(Event::Start(Tag::Heading(heading, _, _))) = original {
|
||||
let mut text_buf = String::new();
|
||||
|
||||
while let Some(event) = self.inner.next() {
|
||||
for event in self.inner.by_ref() {
|
||||
if let Event::Text(ref text) = event {
|
||||
text_buf.push_str(text.as_ref());
|
||||
}
|
||||
|
||||
if let Event::End(Tag::Heading(_)) = event {
|
||||
if let Event::End(Tag::Heading(_, _, _)) = event {
|
||||
break;
|
||||
}
|
||||
|
||||
self.output.push_back(event);
|
||||
}
|
||||
|
||||
self.output.push_back(Event::Html(format!("</h{}>", heading).into()));
|
||||
return Some(Event::Html(format!("<h{} id=\"{}\">", heading, slugify(&text_buf)).into()));
|
||||
self.output
|
||||
.push_back(Event::Html(format!("</{heading}>").into()));
|
||||
return Some(Event::Html(
|
||||
format!("<{} id=\"{}\">", heading, slugify(&text_buf)).into(),
|
||||
));
|
||||
}
|
||||
original
|
||||
}
|
||||
|
|
|
|||
583
crates/dmdoc/src/template.rs
Normal file
583
crates/dmdoc/src/template.rs
Normal file
|
|
@ -0,0 +1,583 @@
|
|||
//! The built-in template.
|
||||
|
||||
use std::path::Path;
|
||||
|
||||
use dm::ast::{InputType, ProcReturnType};
|
||||
use maud::{display, html, Markup, PreEscaped, Render, DOCTYPE};
|
||||
|
||||
use crate::{markdown::DocBlock, Environment, Index, IndexTree, ModuleArgs, ModuleItem, Type};
|
||||
|
||||
pub(crate) fn base(
|
||||
env: &Environment,
|
||||
base_href: &str,
|
||||
title: &dyn Render,
|
||||
head: &dyn Render,
|
||||
header: &dyn Render,
|
||||
content: &dyn Render,
|
||||
) -> Markup {
|
||||
html! {
|
||||
(DOCTYPE)
|
||||
html lang="en" {
|
||||
head {
|
||||
meta charset="utf-8";
|
||||
@if !base_href.is_empty() {
|
||||
base href=(base_href);
|
||||
}
|
||||
link rel="stylesheet" href="dmdoc.css";
|
||||
title {
|
||||
(title) " - " (env.world_name)
|
||||
}
|
||||
(head)
|
||||
}
|
||||
body {
|
||||
header {
|
||||
a href="index.html" { (env.world_name) } " - "
|
||||
a href="index.html#modules" { "Modules" } " - "
|
||||
a href="index.html#types" { "Types" }
|
||||
(header)
|
||||
}
|
||||
main {
|
||||
(content)
|
||||
}
|
||||
footer {
|
||||
(env.filename)
|
||||
@if !env.git.revision.is_empty() {
|
||||
" "
|
||||
@if !env.git.web_url.is_empty() {
|
||||
a href=(format!("{}/tree/{}", env.git.web_url, env.git.revision)) {
|
||||
(env.git.revision[..7])
|
||||
}
|
||||
} @else {
|
||||
(env.git.revision)
|
||||
}
|
||||
@if !env.git.branch.is_empty() {
|
||||
" ("
|
||||
(env.git.branch)
|
||||
@if !env.git.remote_branch.is_empty() && env.git.remote_branch != env.git.branch {
|
||||
" → " (env.git.remote_branch)
|
||||
}
|
||||
")"
|
||||
}
|
||||
" — "
|
||||
@if !env.dmdoc.url.is_empty() {
|
||||
a href=(env.dmdoc.url) {
|
||||
"dmdoc " (env.dmdoc.version)
|
||||
}
|
||||
} @else {
|
||||
"dmdoc " (env.dmdoc.version)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn teaser(block: &DocBlock, prefix: &str) -> Markup {
|
||||
let teaser = block.teaser();
|
||||
html! {
|
||||
@if !teaser.0.is_empty() {
|
||||
(prefix)
|
||||
(teaser)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn git_link(env: &Environment, file: &str, line: u32) -> Markup {
|
||||
let z;
|
||||
let title = if line == 0 {
|
||||
file
|
||||
} else {
|
||||
z = format!("{file} {line}");
|
||||
&z
|
||||
};
|
||||
let icon = html! {
|
||||
img src="git.png" width="16" height="16" title=(title);
|
||||
};
|
||||
html! {
|
||||
@if !file.is_empty() && file != "(builtins)" {
|
||||
" "
|
||||
@if !env.git.web_url.is_empty() && !env.git.revision.is_empty() {
|
||||
a href=(
|
||||
if line == 0 {
|
||||
format!(
|
||||
"{}/blob/{}/{}",
|
||||
env.git.web_url,
|
||||
env.git.revision,
|
||||
file
|
||||
)
|
||||
} else {
|
||||
format!(
|
||||
"{}/blob/{}/{}#L{}",
|
||||
env.git.web_url,
|
||||
env.git.revision,
|
||||
file,
|
||||
line
|
||||
)
|
||||
}
|
||||
) {
|
||||
(icon)
|
||||
}
|
||||
} @else {
|
||||
(icon)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn index_tree(elems: &[IndexTree], extra_class: &str) -> Markup {
|
||||
html! {
|
||||
ul .index-tree .(extra_class) {
|
||||
@for tree in elems {
|
||||
(index_tree_elem(tree, ""))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn index_tree_elem(tree: &IndexTree, prefix: &str) -> Markup {
|
||||
html! {
|
||||
@if tree.children.len() == 1 && tree.htmlname.is_empty() && tree.teaser.0.is_empty() {
|
||||
(index_tree_elem(&tree.children[0], &format!("{}{}/", prefix, tree.self_name)))
|
||||
} @else {
|
||||
li .(if !tree.children.is_empty() { "has-children" } else { "" }) {
|
||||
@if !prefix.is_empty() {
|
||||
span class="no-substance" { (prefix) }
|
||||
}
|
||||
@if tree.htmlname.is_empty() {
|
||||
span .(if tree.no_substance { "no-substance" } else { "" }) title=(tree.full_name) { (tree.self_name) }
|
||||
} @else {
|
||||
a href=(format!("{}.html", tree.htmlname)) title=(tree.full_name) { (tree.self_name) }
|
||||
}
|
||||
@if !tree.teaser.0.is_empty() {
|
||||
" - " (tree.teaser)
|
||||
}
|
||||
@if !tree.children.is_empty() {
|
||||
(index_tree(&tree.children, ""))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn percentage(amt: usize, total: usize) -> Markup {
|
||||
if total > 0 {
|
||||
PreEscaped(format!(", {:.1}%", (amt as f32) * 100.0 / (total as f32)))
|
||||
} else {
|
||||
Markup::default()
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn dm_index(index: &Index) -> Markup {
|
||||
let Index {
|
||||
env,
|
||||
html,
|
||||
modules,
|
||||
types,
|
||||
} = index;
|
||||
base(
|
||||
env,
|
||||
"",
|
||||
&display("Index"),
|
||||
&html! {
|
||||
(maud::PreEscaped("\n<!-- produced by: \n"))
|
||||
(env.dmdoc.build_info)
|
||||
(maud::PreEscaped("\n-->\n"))
|
||||
script src="dmdoc.js" {}
|
||||
},
|
||||
&display(""),
|
||||
&html! {
|
||||
h1 { (env.title) }
|
||||
@if let Some(html) = html { (html) }
|
||||
|
||||
@if !modules.is_empty() {
|
||||
h3 id="modules" {
|
||||
"Modules "
|
||||
aside {
|
||||
"("
|
||||
(env.coverage.modules) " modules, "
|
||||
(env.coverage.macros_documented)"/"(env.coverage.macros_all)" macros"
|
||||
(percentage(env.coverage.macros_documented, env.coverage.macros_all))
|
||||
")"
|
||||
}
|
||||
}
|
||||
(index_tree(modules, "modules"))
|
||||
}
|
||||
|
||||
@if !types.is_empty() {
|
||||
h3 id="types" {
|
||||
"Types "
|
||||
aside {
|
||||
"("
|
||||
(env.coverage.types_detailed) " detailed/"
|
||||
(env.coverage.types_documented) " documented/"
|
||||
(env.coverage.types_all) " total"
|
||||
(percentage(env.coverage.types_documented, env.coverage.types_all))
|
||||
")"
|
||||
}
|
||||
}
|
||||
(index_tree(types, ""))
|
||||
}
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
pub(crate) fn dm_module(module: &ModuleArgs) -> Markup {
|
||||
let ModuleArgs {
|
||||
env,
|
||||
base_href,
|
||||
details,
|
||||
} = *module;
|
||||
base(
|
||||
env,
|
||||
base_href,
|
||||
&display(&details.orig_filename),
|
||||
&display(""),
|
||||
&html! {
|
||||
@if !details.defines.is_empty() {
|
||||
" — "
|
||||
a href=(format!("{}.html#define", details.htmlname)) { "Define Details" }
|
||||
}
|
||||
},
|
||||
&html! {
|
||||
h1 {
|
||||
@if let Some(ref name) = details.name {
|
||||
(name) " "
|
||||
aside {
|
||||
(details.orig_filename)
|
||||
}
|
||||
} @else {
|
||||
(details.orig_filename)
|
||||
}
|
||||
(git_link(env, &details.orig_filename, 0))
|
||||
}
|
||||
|
||||
table class="summary" cellspacing="0" {
|
||||
@for item in details.items.iter() {
|
||||
@match item {
|
||||
ModuleItem::Docs(docs) => {
|
||||
tr {
|
||||
td colspan="2" {
|
||||
(docs)
|
||||
}
|
||||
}
|
||||
},
|
||||
ModuleItem::Define { name, teaser } => {
|
||||
tr {
|
||||
th {
|
||||
a href=(format!("{}.html#define/{}", details.htmlname, name)) { (name) }
|
||||
}
|
||||
td {
|
||||
(teaser)
|
||||
}
|
||||
}
|
||||
},
|
||||
ModuleItem::Type { path, teaser, substance } => {
|
||||
tr {
|
||||
th {
|
||||
@if *substance {
|
||||
a href=(format!("{}.html", &path[1..])) {
|
||||
(path)
|
||||
}
|
||||
} @else {
|
||||
(env.linkify_type_str(path))
|
||||
}
|
||||
}
|
||||
td {
|
||||
(teaser)
|
||||
}
|
||||
}
|
||||
},
|
||||
ModuleItem::GlobalProc { name, teaser } => {
|
||||
tr {
|
||||
th {
|
||||
"/proc/"
|
||||
a href=(format!("global.html#proc/{}", name)) {
|
||||
(name)
|
||||
}
|
||||
}
|
||||
td {
|
||||
(teaser)
|
||||
}
|
||||
}
|
||||
},
|
||||
ModuleItem::GlobalVar { name, teaser } => {
|
||||
tr {
|
||||
th {
|
||||
"/var/"
|
||||
a href=(format!("global.html#var/{}", name)) {
|
||||
(name)
|
||||
}
|
||||
}
|
||||
td {
|
||||
(teaser)
|
||||
}
|
||||
}
|
||||
},
|
||||
ModuleItem::DocComment { .. } => {}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@if !details.defines.is_empty() {
|
||||
h2 id="define" { "Define Details" }
|
||||
@for (name, define) in details.defines.iter() {
|
||||
h3 id=(format!("define/{}", name)) {
|
||||
aside class="declaration" {
|
||||
"#define "
|
||||
}
|
||||
(name)
|
||||
@if define.has_params {
|
||||
aside {
|
||||
"("
|
||||
@for (i, param) in define.params.iter().enumerate() {
|
||||
@if i > 0 {
|
||||
", "
|
||||
}
|
||||
(param)
|
||||
}
|
||||
@if define.is_variadic {
|
||||
" ..."
|
||||
}
|
||||
")"
|
||||
}
|
||||
}
|
||||
(git_link(env, &details.orig_filename, define.line))
|
||||
}
|
||||
(define.docs.html)
|
||||
}
|
||||
}
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
pub(crate) fn dm_type(ty: &Type) -> Markup {
|
||||
let Type {
|
||||
env,
|
||||
base_href,
|
||||
path,
|
||||
details,
|
||||
} = *ty;
|
||||
base(
|
||||
env,
|
||||
base_href,
|
||||
&display(path),
|
||||
&display(""),
|
||||
&html! {
|
||||
@if !details.vars.is_empty() {
|
||||
" — "
|
||||
a href=(format!("{}.html#var", details.htmlname)) { "Var Details" }
|
||||
}
|
||||
@if !details.procs.is_empty() {
|
||||
@if !details.vars.is_empty() { " - " } @else { " — " }
|
||||
a href=(format!("{}.html#proc", details.htmlname)) { "Proc Details" }
|
||||
}
|
||||
},
|
||||
&html! {
|
||||
h1 {
|
||||
@if path == "global" {
|
||||
"(global)"
|
||||
} @else if !details.name.is_empty() {
|
||||
(details.name)
|
||||
" "
|
||||
aside {
|
||||
(env.linkify_type_str(path))
|
||||
}
|
||||
} @else {
|
||||
(env.linkify_type_str(path))
|
||||
}
|
||||
@if let Some(parent_type) = details.parent_type {
|
||||
aside {
|
||||
" inherits "
|
||||
(env.linkify_type_str(parent_type))
|
||||
}
|
||||
}
|
||||
(git_link(env, &details.file.to_string_lossy(), details.line))
|
||||
}
|
||||
@if let Some(ref docs) = details.docs {
|
||||
(docs.html)
|
||||
}
|
||||
|
||||
@if !details.vars.is_empty() || !details.procs.is_empty() {
|
||||
table class="summary" cellspacing="0" {
|
||||
@if !details.vars.is_empty() {
|
||||
tr { td colspan="2" { h2 { "Vars" } } }
|
||||
@for (name, var) in details.vars.iter() {
|
||||
tr {
|
||||
th { a href=(format!("{}.html#var/{}", details.htmlname, name)) { (name) } }
|
||||
td { (teaser(&var.docs, "")) }
|
||||
}
|
||||
}
|
||||
}
|
||||
@if !details.procs.is_empty() {
|
||||
tr { td colspan="2" { h2 { "Procs" } } }
|
||||
@for (name, proc) in details.procs.iter() {
|
||||
tr {
|
||||
th { a href=(format!("{}.html#proc/{}", details.htmlname, name)) { (name) } }
|
||||
td { (teaser(&proc.docs, "")) }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@if !details.vars.is_empty() {
|
||||
h2 id="var" { "Var Details" }
|
||||
@for (name, var) in details.vars.iter() {
|
||||
h3 id=(format!("var/{}", name)) {
|
||||
@if !var.decl.is_empty() {
|
||||
aside class="declaration" { (var.decl) " " }
|
||||
} @else if let Some(ref parent) = var.parent {
|
||||
aside class="parent" {
|
||||
a title=(format!("/{}", parent)) href=(format!("{}.html#var/{}", parent, name)) {
|
||||
"\u{2191}" // ↑
|
||||
}
|
||||
}
|
||||
}
|
||||
(name)
|
||||
@if let Some(ref ty) = var.type_ {
|
||||
@if ty.is_static || ty.is_const || ty.is_tmp || ty.is_final || !ty.path.is_empty() || !ty.input_type.is_empty() {
|
||||
" "
|
||||
aside {
|
||||
"\u{2013} " // –
|
||||
@if ty.is_static { "/static" }
|
||||
@if ty.is_const { "/const" }
|
||||
@if ty.is_tmp { "/tmp" }
|
||||
@if ty.is_final { "/final" }
|
||||
(env.linkify_type_array(ty.path))
|
||||
@if !ty.input_type.is_empty() {
|
||||
span class="as" { " as " }
|
||||
(render_input_type(env, ty.input_type))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
(git_link(env, &var.file.to_string_lossy(), var.line))
|
||||
}
|
||||
(var.docs.html)
|
||||
}
|
||||
}
|
||||
|
||||
@if !details.procs.is_empty() {
|
||||
h2 id="proc" { "Proc Details" }
|
||||
@for (name, proc) in details.procs.iter() {
|
||||
h3 id=(format!("proc/{}", name)) {
|
||||
@if !proc.decl.is_empty() {
|
||||
aside class="declaration" { (proc.decl) " " }
|
||||
} @else if let Some(ref parent) = proc.parent {
|
||||
aside class="parent" {
|
||||
a title=(format!("/{}", parent)) href=(format!("{}.html#proc/{}", parent, name)) {
|
||||
"\u{2191}" // ↑
|
||||
}
|
||||
}
|
||||
}
|
||||
(name)
|
||||
aside {
|
||||
"("
|
||||
@for (i, param) in proc.params.iter().enumerate() {
|
||||
@if i > 0 { ", " }
|
||||
@if !param.type_path.is_empty() {
|
||||
(env.linkify_type_str(¶m.type_path))
|
||||
"/"
|
||||
}
|
||||
(param.name)
|
||||
@if let Some(input_type) = param.input_type {
|
||||
@if !input_type.is_empty() {
|
||||
span class="as" { " as " }
|
||||
(render_input_type(env, input_type))
|
||||
}
|
||||
}
|
||||
}
|
||||
") "
|
||||
@match &proc.return_type {
|
||||
Some(ProcReturnType::InputType(i)) if !i.is_empty() => {
|
||||
span class="as" { " as " }
|
||||
(render_input_type(env, *i))
|
||||
},
|
||||
Some(ProcReturnType::TypePath(p)) => {
|
||||
span class="as" { " as " }
|
||||
(env.linkify_type_array(p))
|
||||
},
|
||||
_ => {},
|
||||
}
|
||||
(git_link(env, &proc.file.to_string_lossy(), proc.line))
|
||||
}
|
||||
}
|
||||
(proc.docs.html)
|
||||
}
|
||||
}
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
pub fn render_input_type(env: &Environment, input_type: InputType) -> Markup {
|
||||
html! {
|
||||
@for (i, &name) in matching_names(input_type).iter().enumerate() {
|
||||
@if i > 0 { " | " }
|
||||
@match name {
|
||||
"mob" => (linkify_input_type(env, "mob", "/mob")),
|
||||
"obj" => (linkify_input_type(env, "obj", "/obj")),
|
||||
"turf" => (linkify_input_type(env, "turf", "/turf")),
|
||||
"area" => (linkify_input_type(env, "area", "/area")),
|
||||
//"icon" => (linkify_input_type(env, "icon", "/icon")),
|
||||
//"sound" => (linkify_input_type(env, "sound", "/sound")),
|
||||
"movable" => (linkify_input_type(env, "movable", "/atom/movable")),
|
||||
"atom" => (linkify_input_type(env, "atom", "/atom")),
|
||||
"list" => (linkify_input_type(env, "list", "/list")),
|
||||
_ => (name),
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn linkify_input_type(env: &Environment, show: &str, typepath: &str) -> Markup {
|
||||
if env.all_type_names.contains(typepath) {
|
||||
html! {
|
||||
a href=(format!("{}.html", &typepath[1..])) { (show) }
|
||||
}
|
||||
} else {
|
||||
html! { (show) }
|
||||
}
|
||||
}
|
||||
|
||||
fn matching_names(mut input_type: InputType) -> Vec<&'static str> {
|
||||
let mut result = Vec::with_capacity(input_type.bits().count_ones() as usize);
|
||||
for &(name, value) in InputType::ENTRIES.iter().rev() {
|
||||
if input_type.contains(value) {
|
||||
input_type.remove(value);
|
||||
result.push(name);
|
||||
}
|
||||
}
|
||||
result.reverse();
|
||||
result
|
||||
}
|
||||
|
||||
pub fn save_resources(output_path: &Path) -> std::io::Result<()> {
|
||||
#[cfg(debug_assertions)]
|
||||
macro_rules! resources {
|
||||
($($name:expr,)*) => {
|
||||
let env = Path::new(concat!(env!("CARGO_MANIFEST_DIR"), "/src/template"));
|
||||
$(
|
||||
std::fs::copy(&env.join($name), &output_path.join($name))?;
|
||||
)*
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(not(debug_assertions))]
|
||||
macro_rules! resources {
|
||||
($($name:expr,)*) => {{
|
||||
use std::io::Write;
|
||||
$(
|
||||
crate::create(&output_path.join($name))?.write_all(include_bytes!(concat!("template/", $name)))?;
|
||||
)*
|
||||
}}
|
||||
}
|
||||
|
||||
resources! {
|
||||
"dmdoc.css",
|
||||
"dmdoc.js",
|
||||
"git.png",
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
|
@ -1,37 +0,0 @@
|
|||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>{% block head %}
|
||||
<meta charset="utf-8" />
|
||||
{% if base_href %}<base href="{{ base_href | safe }}" />{% endif %}
|
||||
<link rel="stylesheet" href="dmdoc.css" />
|
||||
<title>{% block title %}{% endblock title %} - {{ env.world_name }}</title>
|
||||
{% endblock head %}</head>
|
||||
<body>
|
||||
<header>{% block header %}
|
||||
<a href="index.html">{{ env.world_name }}</a> -
|
||||
<a href="index.html#modules">Modules</a> -
|
||||
<a href="index.html#types">Types</a>
|
||||
{% endblock header %}</header>
|
||||
<main>{% block content %}{% endblock content %}</main>
|
||||
<footer>{% block footer %}
|
||||
{{ env.filename }}
|
||||
{% if env.git.revision -%}
|
||||
{%- if env.git.web_url -%}
|
||||
<a href="{{ env.git.web_url | safe }}/tree/{{env.git.revision}}">{{ env.git.revision | substring(end=7) }}</a>
|
||||
{%- else -%}
|
||||
{{ env.git.revision }}
|
||||
{%- endif -%}
|
||||
{%- if env.git.branch %}
|
||||
({{ env.git.branch }}
|
||||
{%- if env.git.remote_branch and env.git.remote_branch != env.git.branch %}
|
||||
→ {{ env.git.remote_branch }}
|
||||
{%- endif -%}
|
||||
){% endif %}
|
||||
{%- endif %} — {% if env.dmdoc.url -%}
|
||||
<a href="{{ env.dmdoc.url | safe }}">dmdoc {{ env.dmdoc.version }}</a>
|
||||
{%- else -%}
|
||||
dmdoc {{ env.dmdoc.version }}
|
||||
{%- endif -%}
|
||||
{% endblock footer %}</footer>
|
||||
</body>
|
||||
</html>
|
||||
|
|
@ -1,30 +0,0 @@
|
|||
{% extends "base.html" %}
|
||||
{% import "macros.html" as macros %}
|
||||
{% block title %}Index{% endblock %}
|
||||
{% block head %}{{ super() }}
|
||||
<!-- produced by:
|
||||
{{ env.dmdoc.build_info }}
|
||||
-->
|
||||
<script src="dmdoc.js"></script>
|
||||
{% endblock head %}
|
||||
{% block content %}
|
||||
<h1>{{ env.title }}</h1>
|
||||
{% if html %}{{ html | safe }}{% endif %}
|
||||
|
||||
{% if modules %}
|
||||
<h3 id="modules">Modules
|
||||
<aside>({{ env.coverage.modules }} modules,
|
||||
{{ env.coverage.macros_documented }}/{{ env.coverage.macros_all }} macros{#
|
||||
#}{{ macros::percentage(amt=env.coverage.macros_documented, total=env.coverage.macros_all) }})</aside></h3>
|
||||
{{ macros::index_tree(elems=modules, extra_class="modules") }}
|
||||
{% endif %}
|
||||
|
||||
{% if types %}
|
||||
<h3 id="types">Types
|
||||
<aside>({{ env.coverage.types_detailed }} detailed/{#
|
||||
#}{{ env.coverage.types_documented }} documented/{{ env.coverage.types_all }} total{#
|
||||
#}{{ macros::percentage(amt=env.coverage.types_documented, total=env.coverage.types_all) }})</aside></h3>
|
||||
{{ macros::index_tree(elems=types) }}
|
||||
{% endif %}
|
||||
|
||||
{% endblock content %}
|
||||
|
|
@ -1,55 +0,0 @@
|
|||
{% extends "base.html" %}
|
||||
{% import "macros.html" as macros %}
|
||||
{% block title %}{{ details.orig_filename }}{% endblock %}
|
||||
{% block header -%}
|
||||
{{ super() }}
|
||||
{%- if details.defines %} — <a href="{{ details.htmlname | safe }}.html#define">Define Details</a>{% endif %}
|
||||
{%- endblock %}
|
||||
{% block content %}
|
||||
<h1>{% if details.name -%}
|
||||
{{ details.name }} <aside>{{ details.orig_filename | safe }}</aside>
|
||||
{%- else -%}
|
||||
{{ details.orig_filename | safe }}
|
||||
{%- endif %} {{ macros::git_link(env=env, file=details.orig_filename) }}</h1>
|
||||
|
||||
<table class="summary" cellspacing="0">
|
||||
{%- for item in details.items %}
|
||||
{% if item.docs -%}
|
||||
<tr><td colspan="2">{{ item.docs | safe }}</td></tr>
|
||||
{%- elif item.define -%}
|
||||
<tr><th><a href="{{ details.htmlname | safe }}.html#define/{{item.define.name}}">{{item.define.name}}</a></th><td>{{ item.define.teaser | safe }}</td></tr>
|
||||
{%- elif item.type -%}
|
||||
<tr><th>{% if item.type.substance -%}
|
||||
<a href="{{ item.type.path | safe | substring(start=1) }}.html">{{item.type.path}}</a>
|
||||
{%- else -%}
|
||||
{{ item.type.path | linkify_type | safe }}
|
||||
{%- endif %}</th><td>{{ item.type.teaser | safe }}</td></tr>
|
||||
{%- elif item.global_proc -%}
|
||||
<tr><th>/proc/<a href="global.html#proc/{{item.global_proc.name}}">{{item.global_proc.name}}</a></th>
|
||||
<td>{{ item.global_proc.teaser | safe }}</td></tr>
|
||||
{%- elif item.global_var -%}
|
||||
<tr><th>/var/<a href="global.html#var/{{item.global_var.name}}">{{item.global_var.name}}</a></th>
|
||||
<td>{{ item.global_var.teaser | safe }}</td></tr>
|
||||
{%- endif %}
|
||||
{%- endfor -%}
|
||||
</table>
|
||||
|
||||
{%- if details.defines -%}
|
||||
<h2 id="define">Define Details</h2>
|
||||
{% for name, define in details.defines -%}
|
||||
<h3 id="define/{{ name }}"><aside class="declaration">#define </aside>{{ name }}
|
||||
{%- if define.has_params %}
|
||||
<aside>(
|
||||
{%- for param in define.params -%}
|
||||
{% if not loop.first %}, {% endif -%}
|
||||
{{ param }}
|
||||
{%- endfor -%}
|
||||
{%- if define.is_variadic %} ...{% endif -%}
|
||||
)</aside>
|
||||
{%- endif -%}
|
||||
{{ macros::git_link(env=env, item=define, file=details.orig_filename) }}
|
||||
</h3>
|
||||
{{ define.docs.html | safe }}
|
||||
{%- endfor -%}
|
||||
{%- endif -%}
|
||||
{% endblock content %}
|
||||
|
|
@ -1,81 +0,0 @@
|
|||
{% extends "base.html" %}
|
||||
{% import "macros.html" as macros %}
|
||||
{% block title %}{{ path }}{% endblock %}
|
||||
{% block header -%}
|
||||
{{ super() }}
|
||||
{%- if details.vars %} — <a href="{{ details.htmlname | safe }}.html#var">Var Details</a>{% endif %}
|
||||
{%- if details.procs %}{% if details.vars %} - {% else %} — {% endif -%}
|
||||
<a href="{{ details.htmlname | safe }}.html#proc">Proc Details</a>{% endif %}
|
||||
{%- endblock %}
|
||||
{% block content %}
|
||||
<h1>{% if path == "(global)" -%}
|
||||
(global)
|
||||
{%- elif details.name -%}
|
||||
{{ details.name }} <aside>{{ path | linkify_type | safe }}</aside>
|
||||
{%- else -%}
|
||||
{{ path | linkify_type | safe }}
|
||||
{%- endif %}
|
||||
{%- if details.parent_type %}<aside> inherits {{ details.parent_type | linkify_type | safe }}</aside>{% endif -%}
|
||||
{{ macros::git_link(env=env, item=details) }}</h1>
|
||||
|
||||
{% if details.docs %}{{ details.docs.html | safe }}{% endif %}
|
||||
|
||||
{%- if details.vars or details.procs -%}
|
||||
<table class="summary" cellspacing="0">
|
||||
{%- if details.vars -%}
|
||||
<tr><td colspan="2"><h2>Vars</h2></td></tr>
|
||||
{%- for name, var in details.vars %}
|
||||
<tr><th><a href="{{details.htmlname|safe}}.html#var/{{name}}">{{ name }}</a></th><td>{{ macros::teaser(block=var.docs) }}</td></tr>
|
||||
{%- endfor %}
|
||||
{%- endif -%}
|
||||
|
||||
{%- if details.procs -%}
|
||||
<tr><td colspan="2"><h2>Procs</h2></td></tr>
|
||||
{%- for name, proc in details.procs %}
|
||||
<tr><th><a href="{{details.htmlname|safe}}.html#proc/{{name}}">{{ name }}</a></th><td>{{ macros::teaser(block=proc.docs) }}</td></tr>
|
||||
{%- endfor %}
|
||||
{%- endif -%}
|
||||
</table>
|
||||
{%- endif -%}
|
||||
|
||||
{% if details.vars %}
|
||||
<h2 id="var">Var Details</h2>
|
||||
{%- for name, var in details.vars -%}
|
||||
<h3 id="var/{{ name }}">
|
||||
{%- if var.decl -%}
|
||||
<aside class="declaration">{{ var.decl }} </aside>
|
||||
{%- elif var.parent -%}
|
||||
<aside class="parent"><a title="/{{ var.parent | safe }}" href="{{ var.parent | safe }}.html#var/{{ name }}">↑</a></aside>
|
||||
{%- endif -%}
|
||||
{{ name }}
|
||||
{%- if var.type %}
|
||||
<aside>– {% if var.type.is_static %}/static{% endif -%}
|
||||
{%- if var.type.is_const %}/const{% endif -%}
|
||||
{%- if var.type.is_tmp %}/tmp{% endif -%}
|
||||
{{ var.type.path | linkify_type | safe }}</aside>
|
||||
{%- endif -%}
|
||||
{{ macros::git_link(env=env, item=var) }}</h3>
|
||||
{{ var.docs.html | safe }}
|
||||
{%- endfor -%}
|
||||
{% endif %}
|
||||
|
||||
{%- if details.procs -%}
|
||||
<h2 id="proc">Proc Details</h2>
|
||||
{%- for name, proc in details.procs -%}
|
||||
<h3 id="proc/{{ name }}">
|
||||
{%- if proc.decl -%}
|
||||
<aside class="declaration">{{ proc.decl }} </aside>
|
||||
{%- elif proc.parent -%}
|
||||
<aside class="parent"><a title="/{{ proc.parent | safe }}" href="{{ proc.parent | safe }}.html#proc/{{ name }}">↑</a></aside>
|
||||
{%- endif -%}
|
||||
{{ name }}<aside>(
|
||||
{%- for param in proc.params -%}
|
||||
{% if not loop.first %}, {% endif -%}
|
||||
{% if param.type_path %}{{ param.type_path | linkify_type | safe }}/{% endif %}{{ param.name }}
|
||||
{%- endfor -%}
|
||||
) {{ macros::git_link(env=env, item=proc) }}</aside>
|
||||
</h3>
|
||||
{{ proc.docs.html | safe }}
|
||||
{%- endfor -%}
|
||||
{%- endif -%}
|
||||
{% endblock content %}
|
||||
|
|
@ -45,7 +45,7 @@ aside.declaration, aside.parent {
|
|||
margin-right: -100px;
|
||||
right: 105px;
|
||||
}
|
||||
aside.declaration {
|
||||
aside.declaration, span.as {
|
||||
font-style: italic;
|
||||
}
|
||||
table.summary tr:first-child > td > :first-child {
|
||||
|
|
|
|||
|
|
@ -1,53 +0,0 @@
|
|||
{% macro teaser(block, prefix="") -%}
|
||||
{%- if block -%}
|
||||
{%- set teaser = block.html | safe | substring(start=block.teaser.start, end=block.teaser.end) -%}
|
||||
{%- if teaser %}{{ prefix }}{{ teaser | safe }}{% endif -%}
|
||||
{%- endif -%}
|
||||
{%- endmacro teaser %}
|
||||
|
||||
{% macro git_link(env, item=false, file=false) -%}
|
||||
{% if item and item.file %}
|
||||
{% set file = item.file %}
|
||||
{% endif %}
|
||||
{%- if file -%}
|
||||
{%- if env.git.web_url and env.git.revision %}
|
||||
<a href="{{ env.git.web_url | safe }}/blob/{{ env.git.revision }}/{{ file | safe }}
|
||||
{%- if item.line %}#L{{ item.line }}{% endif %}">
|
||||
{%- endif %}
|
||||
<img src="git.png" width="16" height="16" title="{{ file }}{% if item.line %} {{ item.line }}{% endif %}"/>
|
||||
{%- if env.git.web_url and env.git.revision %}</a>{% endif %}
|
||||
{%- endif -%}
|
||||
{%- endmacro git_link %}
|
||||
|
||||
{% macro index_tree(elems, extra_class="") -%}
|
||||
<ul class="index-tree{% if extra_class %} {{extra_class}}{% endif %}">
|
||||
{% for tree in elems -%}
|
||||
{{ self::index_tree_elem(tree=tree) }}
|
||||
{% endfor -%}
|
||||
</ul>
|
||||
{%- endmacro %}
|
||||
|
||||
{% macro index_tree_elem(tree, prefix="") -%}
|
||||
{%- if tree.children | length == 1 and not tree.htmlname and not tree.teaser -%}
|
||||
{{ self::index_tree_elem(tree=tree.children[0], prefix=prefix ~ tree.self_name ~ "/") }}
|
||||
{%- else -%}
|
||||
<li{% if tree.children %} class="has-children"{% endif %}>
|
||||
{%- if prefix -%}
|
||||
<span class="no-substance">{{prefix}}</span>
|
||||
{%- endif -%}
|
||||
{%- if not tree.htmlname -%}
|
||||
<span{% if tree.no_substance %} class="no-substance"{% endif %} title="{{ tree.full_name }}">{{ tree.self_name }}</span>
|
||||
{%- else -%}
|
||||
<a href="{{ tree.htmlname | safe }}.html" title="{{ tree.full_name }}">{{ tree.self_name }}</a>
|
||||
{%- endif %}
|
||||
{%- if tree.teaser %} - {{ tree.teaser | safe }}{%- endif -%}
|
||||
{% if tree.children %}{{ self::index_tree(elems=tree.children) }}{% endif -%}
|
||||
</li>
|
||||
{%- endif -%}
|
||||
{%- endmacro %}
|
||||
|
||||
{% macro percentage(amt, total) -%}
|
||||
{%- if total -%}
|
||||
, {{ amt * 1000 / total | round / 10 }}%
|
||||
{%- endif -%}
|
||||
{%- endmacro %}
|
||||
|
|
@ -1,52 +0,0 @@
|
|||
//! The built-in template.
|
||||
|
||||
use std::path::Path;
|
||||
use tera::Tera;
|
||||
|
||||
pub fn builtin() -> Result<Tera, tera::Error> {
|
||||
#[cfg(debug_assertions)] {
|
||||
Tera::new(concat!(env!("CARGO_MANIFEST_DIR"), "/src/template/*.html"))
|
||||
}
|
||||
|
||||
#[cfg(not(debug_assertions))] {
|
||||
let mut tera = Tera::default();
|
||||
tera.add_raw_templates(vec![
|
||||
("macros.html", include_str!("macros.html")),
|
||||
("base.html", include_str!("base.html")),
|
||||
("dm_index.html", include_str!("dm_index.html")),
|
||||
("dm_type.html", include_str!("dm_type.html")),
|
||||
("dm_module.html", include_str!("dm_module.html")),
|
||||
])?;
|
||||
Ok(tera)
|
||||
}
|
||||
}
|
||||
|
||||
pub fn save_resources(output_path: &Path) -> std::io::Result<()> {
|
||||
#[cfg(debug_assertions)]
|
||||
macro_rules! resources {
|
||||
($($name:expr,)*) => {
|
||||
let env = Path::new(concat!(env!("CARGO_MANIFEST_DIR"), "/src/template"));
|
||||
$(
|
||||
std::fs::copy(&env.join($name), &output_path.join($name))?;
|
||||
)*
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(not(debug_assertions))]
|
||||
macro_rules! resources {
|
||||
($($name:expr,)*) => {{
|
||||
use std::io::Write;
|
||||
$(
|
||||
crate::create(&output_path.join($name))?.write_all(include_bytes!($name))?;
|
||||
)*
|
||||
}}
|
||||
}
|
||||
|
||||
resources! {
|
||||
"dmdoc.css",
|
||||
"dmdoc.js",
|
||||
"git.png",
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
|
@ -1,25 +1,24 @@
|
|||
[package]
|
||||
name = "dmm-tools-cli"
|
||||
version = "1.3.1"
|
||||
authors = ["Tad Hardesty <tad@platymuus.com>"]
|
||||
description = "BYOND map rendering and analysis tools powered by SpacemanDMM"
|
||||
edition = "2018"
|
||||
version.workspace = true
|
||||
authors.workspace = true
|
||||
edition.workspace = true
|
||||
|
||||
[[bin]]
|
||||
name = "dmm-tools"
|
||||
path = "src/main.rs"
|
||||
|
||||
[dependencies]
|
||||
structopt = "0.3.3"
|
||||
structopt-derive = "0.4.0"
|
||||
serde = "1.0.27"
|
||||
serde_derive = "1.0.27"
|
||||
serde_json = "1.0.9"
|
||||
rayon = "1.0.0"
|
||||
clap = { version = "4.5.20", features = ["derive"] }
|
||||
serde = "1.0.213"
|
||||
serde_derive = "1.0.213"
|
||||
serde_json = "1.0.132"
|
||||
rayon = "1.10.0"
|
||||
dreammaker = { path = "../dreammaker" }
|
||||
dmm-tools = { path = "../dmm-tools", features = ["png"] }
|
||||
ahash = "0.7.6"
|
||||
foldhash = "0.2.0"
|
||||
|
||||
[build-dependencies]
|
||||
chrono = "0.4.0"
|
||||
git2 = { version = "0.13", default-features = false }
|
||||
chrono = "0.4.38"
|
||||
git2 = { version = "0.20.2", default-features = false }
|
||||
|
|
|
|||
|
|
@ -8,12 +8,12 @@ use std::path::PathBuf;
|
|||
|
||||
fn main() {
|
||||
let out_dir = PathBuf::from(env::var_os("OUT_DIR").unwrap());
|
||||
let mut f = File::create(&out_dir.join("build-info.txt")).unwrap();
|
||||
let mut f = File::create(out_dir.join("build-info.txt")).unwrap();
|
||||
|
||||
if let Ok(commit) = read_commit() {
|
||||
writeln!(f, "commit: {}", commit).unwrap();
|
||||
writeln!(f, "commit: {commit}").unwrap();
|
||||
}
|
||||
writeln!(f, "build date: {}", chrono::Utc::today()).unwrap();
|
||||
write!(f, "build date: {}", chrono::Utc::now().date_naive()).unwrap();
|
||||
}
|
||||
|
||||
fn read_commit() -> Result<String, git2::Error> {
|
||||
|
|
|
|||
|
|
@ -1,46 +1,40 @@
|
|||
//! CLI tools, including a map renderer, using the same backend as the editor.
|
||||
#![forbid(unsafe_code)]
|
||||
#![doc(hidden)] // Don't interfere with lib docs.
|
||||
#![doc(hidden)] // Don't interfere with lib docs.
|
||||
|
||||
extern crate clap;
|
||||
extern crate rayon;
|
||||
extern crate structopt;
|
||||
|
||||
extern crate serde;
|
||||
extern crate serde_json;
|
||||
#[macro_use] extern crate serde_derive;
|
||||
#[macro_use]
|
||||
extern crate serde_derive;
|
||||
|
||||
extern crate dreammaker as dm;
|
||||
extern crate dmm_tools;
|
||||
extern crate dreammaker as dm;
|
||||
|
||||
use std::collections::HashMap;
|
||||
use foldhash::{HashMap, HashMapExt, HashSet};
|
||||
use std::fmt;
|
||||
use std::io::Write;
|
||||
use std::path::{Path, PathBuf};
|
||||
use std::sync::atomic::{AtomicIsize, Ordering};
|
||||
use std::sync::RwLock;
|
||||
use std::collections::HashSet;
|
||||
|
||||
use clap::{Parser, Subcommand};
|
||||
use rayon::iter::{IndexedParallelIterator, IntoParallelIterator, ParallelIterator};
|
||||
use structopt::StructOpt;
|
||||
|
||||
use dm::objtree::ObjectTree;
|
||||
use dmm_tools::*;
|
||||
|
||||
use ahash::RandomState;
|
||||
|
||||
// ----------------------------------------------------------------------------
|
||||
// Main driver
|
||||
|
||||
fn main() {
|
||||
let opt = Opt::from_clap(&Opt::clap()
|
||||
.long_version(concat!(
|
||||
env!("CARGO_PKG_VERSION"), "\n",
|
||||
include_str!(concat!(env!("OUT_DIR"), "/build-info.txt")),
|
||||
).trim_end())
|
||||
.get_matches());
|
||||
|
||||
let opt = Opt::parse();
|
||||
let mut context = Context::default();
|
||||
context.dm_context.set_print_severity(Some(dm::Severity::Error));
|
||||
context
|
||||
.dm_context
|
||||
.set_print_severity(Some(dm::Severity::Error));
|
||||
rayon::ThreadPoolBuilder::new()
|
||||
.num_threads(opt.jobs)
|
||||
.build_global()
|
||||
|
|
@ -73,16 +67,16 @@ impl Context {
|
|||
eprintln!("parsing {}", environment.display());
|
||||
|
||||
if let Some(parent) = environment.parent() {
|
||||
self.icon_cache.set_icons_root(&parent);
|
||||
self.icon_cache.set_icons_root(parent);
|
||||
}
|
||||
|
||||
self.dm_context.autodetect_config(&environment);
|
||||
let pp = match dm::preprocessor::Preprocessor::new(&self.dm_context, environment) {
|
||||
Ok(pp) => pp,
|
||||
Err(e) => {
|
||||
eprintln!("i/o error opening environment:\n{}", e);
|
||||
eprintln!("i/o error opening environment:\n{e}");
|
||||
std::process::exit(1);
|
||||
}
|
||||
},
|
||||
};
|
||||
let indents = dm::indents::IndentProcessor::new(&self.dm_context, pp);
|
||||
let parser = dm::parser::Parser::new(&self.dm_context, indents);
|
||||
|
|
@ -90,87 +84,90 @@ impl Context {
|
|||
}
|
||||
}
|
||||
|
||||
#[derive(StructOpt, Debug)]
|
||||
#[structopt(name="dmm-tools",
|
||||
author="Copyright (C) 2017-2021 Tad Hardesty",
|
||||
about="This program comes with ABSOLUTELY NO WARRANTY. This is free software,
|
||||
#[derive(Parser, Debug)]
|
||||
#[command(
|
||||
name="dmm-tools",
|
||||
version=concat!(
|
||||
env!("CARGO_PKG_VERSION"), "\n",
|
||||
include_str!(concat!(env!("OUT_DIR"), "/build-info.txt"))
|
||||
),
|
||||
author="Copyright (C) 2017-2025 Tad Hardesty",
|
||||
about="This program comes with ABSOLUTELY NO WARRANTY. This is free software,
|
||||
and you are welcome to redistribute it under the conditions of the GNU
|
||||
General Public License version 3.")]
|
||||
General Public License version 3.",
|
||||
)]
|
||||
struct Opt {
|
||||
/// The environment file to operate under.
|
||||
#[structopt(short="e", long="env")]
|
||||
#[arg(short = 'e', long = "env")]
|
||||
environment: Option<String>,
|
||||
|
||||
#[structopt(short="v", long="verbose")]
|
||||
#[arg(short = 'v', long = "verbose")]
|
||||
#[allow(dead_code)]
|
||||
verbose: bool,
|
||||
|
||||
/// Set the number of threads to be used for parallel execution when
|
||||
/// possible. A value of 0 will select automatically, and 1 will be serial.
|
||||
#[structopt(long="jobs", default_value="1")]
|
||||
#[arg(long = "jobs", default_value = "1")]
|
||||
jobs: usize,
|
||||
|
||||
#[structopt(subcommand)]
|
||||
#[command(subcommand)]
|
||||
command: Command,
|
||||
}
|
||||
|
||||
// ----------------------------------------------------------------------------
|
||||
// Subcommands
|
||||
|
||||
#[derive(StructOpt, Debug)]
|
||||
#[derive(Subcommand, Debug)]
|
||||
enum Command {
|
||||
/// Show information about the render-pass list.
|
||||
#[structopt(name = "list-passes")]
|
||||
#[command(name = "list-passes")]
|
||||
ListPasses {
|
||||
/// Output as JSON.
|
||||
#[structopt(short="j", long="json")]
|
||||
#[arg(short = 'j', long = "json")]
|
||||
json: bool,
|
||||
},
|
||||
/// Build minimaps of the specified maps.
|
||||
#[structopt(name = "minimap")]
|
||||
#[command(name = "minimap")]
|
||||
Minimap {
|
||||
/// The output directory.
|
||||
#[structopt(short="o", default_value="data/minimaps")]
|
||||
#[arg(short = 'o', default_value = "data/minimaps")]
|
||||
output: String,
|
||||
|
||||
/// Set the minimum x,y or x,y,z coordinate to act upon (1-indexed, inclusive).
|
||||
#[structopt(long="min")]
|
||||
#[arg(long = "min")]
|
||||
min: Option<CoordArg>,
|
||||
|
||||
/// Set the maximum x,y or x,y,z coordinate to act upon (1-indexed, inclusive).
|
||||
#[structopt(long="max")]
|
||||
#[arg(long = "max")]
|
||||
max: Option<CoordArg>,
|
||||
|
||||
/// Enable render-passes, or "all" to only exclude those passed to --disable.
|
||||
#[structopt(long="enable", default_value="")]
|
||||
#[arg(long = "enable", default_value = "")]
|
||||
enable: String,
|
||||
|
||||
/// Disable render-passes, or "all" to only use those passed to --enable.
|
||||
#[structopt(long="disable", default_value="")]
|
||||
#[arg(long = "disable", default_value = "")]
|
||||
disable: String,
|
||||
|
||||
/// Run output through pngcrush automatically. Requires pngcrush.
|
||||
#[structopt(long="pngcrush")]
|
||||
#[arg(long = "pngcrush")]
|
||||
pngcrush: bool,
|
||||
|
||||
/// Run output through optipng automatically. Requires optipng.
|
||||
#[structopt(long="optipng")]
|
||||
#[arg(long = "optipng")]
|
||||
optipng: bool,
|
||||
|
||||
/// The list of maps to process.
|
||||
files: Vec<String>,
|
||||
},
|
||||
/// List the differing coordinates between two maps.
|
||||
#[structopt(name="diff-maps")]
|
||||
DiffMaps {
|
||||
left: String,
|
||||
right: String,
|
||||
},
|
||||
#[command(name = "diff-maps")]
|
||||
DiffMaps { left: String, right: String },
|
||||
/// Show metadata information about the map.
|
||||
#[structopt(name="map-info")]
|
||||
#[command(name = "map-info")]
|
||||
MapInfo {
|
||||
/// Output as JSON.
|
||||
#[structopt(short="j", long="json")]
|
||||
#[arg(short = 'j', long = "json")]
|
||||
json: bool,
|
||||
|
||||
/// The list of maps to show info on.
|
||||
|
|
@ -194,9 +191,17 @@ fn run(opt: &Opt, command: &Command, context: &mut Context) {
|
|||
|
||||
let mut report = Vec::new();
|
||||
for &render_passes::RenderPassInfo {
|
||||
name, desc, default, new: _,
|
||||
} in render_passes::RENDER_PASSES {
|
||||
report.push(Pass { name, desc, default });
|
||||
name,
|
||||
desc,
|
||||
default,
|
||||
new: _,
|
||||
} in render_passes::RENDER_PASSES
|
||||
{
|
||||
report.push(Pass {
|
||||
name,
|
||||
desc,
|
||||
default,
|
||||
});
|
||||
}
|
||||
output_json(&report);
|
||||
} else {
|
||||
|
|
@ -219,17 +224,21 @@ fn run(opt: &Opt, command: &Command, context: &mut Context) {
|
|||
},
|
||||
// --------------------------------------------------------------------
|
||||
Command::Minimap {
|
||||
ref output, min, max, ref enable, ref disable, ref files,
|
||||
pngcrush, optipng,
|
||||
ref output,
|
||||
min,
|
||||
max,
|
||||
ref enable,
|
||||
ref disable,
|
||||
ref files,
|
||||
pngcrush,
|
||||
optipng,
|
||||
} => {
|
||||
context.objtree(opt);
|
||||
if context
|
||||
.dm_context
|
||||
.errors()
|
||||
.iter()
|
||||
.filter(|e| e.severity() <= dm::Severity::Error)
|
||||
.next()
|
||||
.is_some()
|
||||
.any(|e| e.severity() <= dm::Severity::Error)
|
||||
{
|
||||
println!("there were some parsing errors; render may be inaccurate")
|
||||
}
|
||||
|
|
@ -241,9 +250,13 @@ fn run(opt: &Opt, command: &Command, context: &mut Context) {
|
|||
..
|
||||
} = *context;
|
||||
|
||||
let render_passes = &dmm_tools::render_passes::configure(&context.dm_context.config().map_renderer, enable, disable);
|
||||
let render_passes = &dmm_tools::render_passes::configure(
|
||||
&context.dm_context.config().map_renderer,
|
||||
enable,
|
||||
disable,
|
||||
);
|
||||
let paths: Vec<&Path> = files.iter().map(|p| p.as_ref()).collect();
|
||||
let errors: RwLock<HashSet<String, RandomState>> = Default::default();
|
||||
let errors: RwLock<HashSet<String>> = Default::default();
|
||||
|
||||
let perform_job = move |path: &Path| {
|
||||
let mut filename;
|
||||
|
|
@ -263,7 +276,7 @@ fn run(opt: &Opt, command: &Command, context: &mut Context) {
|
|||
eprintln!("Failed to load {}:\n{}", path.display(), e);
|
||||
exit_status.fetch_add(1, Ordering::Relaxed);
|
||||
return;
|
||||
}
|
||||
},
|
||||
};
|
||||
|
||||
let (dim_x, dim_y, dim_z) = map.dim_xyz();
|
||||
|
|
@ -279,24 +292,24 @@ fn run(opt: &Opt, command: &Command, context: &mut Context) {
|
|||
max.x = clamp(max.x, min.x, dim_x);
|
||||
max.y = clamp(max.y, min.y, dim_y);
|
||||
max.z = clamp(max.z, min.z, dim_z);
|
||||
println!("{}rendering from {} to {}", prefix, min, max);
|
||||
println!("{prefix}rendering from {min} to {max}");
|
||||
|
||||
let do_z_level = |z| {
|
||||
println!("{}generating z={}", prefix, 1 + z);
|
||||
let bump = Default::default();
|
||||
let minimap_context = minimap::Context {
|
||||
objtree: &objtree,
|
||||
objtree,
|
||||
map: &map,
|
||||
level: map.z_level(z),
|
||||
min: (min.x - 1, min.y - 1),
|
||||
max: (max.x - 1, max.y - 1),
|
||||
render_passes: &render_passes,
|
||||
render_passes,
|
||||
errors: &errors,
|
||||
bump: &bump,
|
||||
};
|
||||
let image = minimap::generate(minimap_context, icon_cache).unwrap();
|
||||
if let Err(e) = std::fs::create_dir_all(output) {
|
||||
eprintln!("Failed to create output directory {}:\n{}", output, e);
|
||||
eprintln!("Failed to create output directory {output}:\n{e}");
|
||||
exit_status.fetch_add(1, Ordering::Relaxed);
|
||||
return;
|
||||
}
|
||||
|
|
@ -306,35 +319,41 @@ fn run(opt: &Opt, command: &Command, context: &mut Context) {
|
|||
path.file_stem().unwrap().to_string_lossy(),
|
||||
1 + z
|
||||
);
|
||||
println!("{}saving {}", prefix, outfile);
|
||||
println!("{prefix}saving {outfile}");
|
||||
image.to_file(outfile.as_ref()).unwrap();
|
||||
if pngcrush {
|
||||
println!(" pngcrush {}", outfile);
|
||||
let temp = format!("{}.temp", outfile);
|
||||
assert!(std::process::Command::new("pngcrush")
|
||||
.arg("-ow")
|
||||
.arg(&outfile)
|
||||
.arg(&temp)
|
||||
.stderr(std::process::Stdio::null())
|
||||
.status()
|
||||
.unwrap()
|
||||
.success(), "pngcrush failed");
|
||||
println!(" pngcrush {outfile}");
|
||||
let temp = format!("{outfile}.temp");
|
||||
assert!(
|
||||
std::process::Command::new("pngcrush")
|
||||
.arg("-ow")
|
||||
.arg(&outfile)
|
||||
.arg(&temp)
|
||||
.stderr(std::process::Stdio::null())
|
||||
.status()
|
||||
.unwrap()
|
||||
.success(),
|
||||
"pngcrush failed"
|
||||
);
|
||||
}
|
||||
if optipng {
|
||||
println!("{}optipng {}", prefix, outfile);
|
||||
assert!(std::process::Command::new("optipng")
|
||||
.arg(&outfile)
|
||||
.stderr(std::process::Stdio::null())
|
||||
.status()
|
||||
.unwrap()
|
||||
.success(), "optipng failed");
|
||||
println!("{prefix}optipng {outfile}");
|
||||
assert!(
|
||||
std::process::Command::new("optipng")
|
||||
.arg(&outfile)
|
||||
.stderr(std::process::Stdio::null())
|
||||
.status()
|
||||
.unwrap()
|
||||
.success(),
|
||||
"optipng failed"
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
if parallel {
|
||||
((min.z - 1)..(max.z)).into_par_iter().for_each(do_z_level);
|
||||
} else {
|
||||
((min.z - 1)..(max.z)).into_iter().for_each(do_z_level);
|
||||
((min.z - 1)..(max.z)).for_each(do_z_level);
|
||||
}
|
||||
};
|
||||
|
||||
|
|
@ -348,7 +367,8 @@ fn run(opt: &Opt, command: &Command, context: &mut Context) {
|
|||
},
|
||||
// --------------------------------------------------------------------
|
||||
Command::DiffMaps {
|
||||
ref left, ref right,
|
||||
ref left,
|
||||
ref right,
|
||||
} => {
|
||||
use std::cmp::min;
|
||||
|
||||
|
|
@ -362,14 +382,16 @@ fn run(opt: &Opt, command: &Command, context: &mut Context) {
|
|||
let left_dims = left_map.dim_xyz();
|
||||
let right_dims = right_map.dim_xyz();
|
||||
if left_dims != right_dims {
|
||||
println!(" different size: {:?} {:?}", left_dims, right_dims);
|
||||
println!(" different size: {left_dims:?} {right_dims:?}");
|
||||
}
|
||||
|
||||
for z in 0..min(left_dims.2, right_dims.2) {
|
||||
for y in 0..min(left_dims.1, right_dims.1) {
|
||||
for x in 0..min(left_dims.0, right_dims.0) {
|
||||
let left_tile = &left_map.dictionary[&left_map.grid[(z, left_dims.1 - y - 1, x)]];
|
||||
let right_tile = &right_map.dictionary[&right_map.grid[(z, right_dims.1 - y - 1, x)]];
|
||||
let left_tile =
|
||||
&left_map.dictionary[&left_map.grid[(z, left_dims.1 - y - 1, x)]];
|
||||
let right_tile =
|
||||
&right_map.dictionary[&right_map.grid[(z, right_dims.1 - y - 1, x)]];
|
||||
if left_tile != right_tile {
|
||||
println!(" different tile: ({}, {}, {})", x + 1, y + 1, z + 1);
|
||||
}
|
||||
|
|
@ -378,9 +400,7 @@ fn run(opt: &Opt, command: &Command, context: &mut Context) {
|
|||
}
|
||||
},
|
||||
// --------------------------------------------------------------------
|
||||
Command::MapInfo {
|
||||
json, ref files,
|
||||
} => {
|
||||
Command::MapInfo { json, ref files } => {
|
||||
if !json {
|
||||
eprintln!("non-JSON output is not yet supported");
|
||||
}
|
||||
|
|
@ -392,15 +412,18 @@ fn run(opt: &Opt, command: &Command, context: &mut Context) {
|
|||
num_keys: usize,
|
||||
}
|
||||
|
||||
let mut report = HashMap::with_hasher(RandomState::default());
|
||||
let mut report = HashMap::new();
|
||||
for path in files.iter() {
|
||||
let path = std::path::Path::new(path);
|
||||
let map = dmm::Map::from_file(path).unwrap();
|
||||
report.insert(path, Map {
|
||||
size: map.dim_xyz(),
|
||||
key_length: map.key_length(),
|
||||
num_keys: map.dictionary.len(),
|
||||
});
|
||||
report.insert(
|
||||
path,
|
||||
Map {
|
||||
size: map.dim_xyz(),
|
||||
key_length: map.key_length(),
|
||||
num_keys: map.dictionary.len(),
|
||||
},
|
||||
);
|
||||
}
|
||||
output_json(&report);
|
||||
},
|
||||
|
|
@ -412,8 +435,7 @@ fn run(opt: &Opt, command: &Command, context: &mut Context) {
|
|||
let result = render_many(context, command);
|
||||
let stdout = std::io::stdout();
|
||||
serde_json::to_writer(stdout.lock(), &result).unwrap();
|
||||
}
|
||||
// --------------------------------------------------------------------
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -443,7 +465,7 @@ impl std::str::FromStr for CoordArg {
|
|||
|
||||
fn from_str(s: &str) -> Result<Self, String> {
|
||||
match s
|
||||
.split(",")
|
||||
.split(',')
|
||||
.map(|x| x.parse())
|
||||
.collect::<Result<Vec<_>, std::num::ParseIntError>>()
|
||||
{
|
||||
|
|
@ -551,9 +573,7 @@ fn render_many(context: &Context, command: RenderManyCommand) -> RenderManyComma
|
|||
.dm_context
|
||||
.errors()
|
||||
.iter()
|
||||
.filter(|e| e.severity() <= dm::Severity::Error)
|
||||
.next()
|
||||
.is_some()
|
||||
.any(|e| e.severity() <= dm::Severity::Error)
|
||||
{
|
||||
eprintln!("there were some parsing errors; render may be inaccurate")
|
||||
}
|
||||
|
|
@ -563,72 +583,92 @@ fn render_many(context: &Context, command: RenderManyCommand) -> RenderManyComma
|
|||
ref exit_status,
|
||||
..
|
||||
} = *context;
|
||||
let render_passes = &dmm_tools::render_passes::configure_list(&context.dm_context.config().map_renderer, &command.enable, &command.disable);
|
||||
let errors: RwLock<HashSet<String, RandomState>> = Default::default();
|
||||
let render_passes = &dmm_tools::render_passes::configure_list(
|
||||
&context.dm_context.config().map_renderer,
|
||||
&command.enable,
|
||||
&command.disable,
|
||||
);
|
||||
let errors: RwLock<HashSet<String>> = Default::default();
|
||||
|
||||
// Prepare output directory.
|
||||
let output_directory = command.output_directory;
|
||||
if let Err(e) = std::fs::create_dir_all(&output_directory) {
|
||||
eprintln!("failed to create output directory {}:\n{}", output_directory.display(), e);
|
||||
eprintln!(
|
||||
"failed to create output directory {}:\n{}",
|
||||
output_directory.display(),
|
||||
e
|
||||
);
|
||||
exit_status.fetch_add(1, Ordering::Relaxed);
|
||||
panic!();
|
||||
}
|
||||
|
||||
|
||||
// Iterate over the maps
|
||||
let result_files: Vec<_> = command.files.into_par_iter().enumerate().map(|(file_idx, file)| {
|
||||
eprintln!("{}: load {}", file_idx, file.path.display());
|
||||
let stem = file.path.file_stem().unwrap().to_string_lossy();
|
||||
let map = dmm::Map::from_file(&file.path).unwrap(); // TODO: error handling
|
||||
let (dim_x, dim_y, dim_z) = map.dim_xyz();
|
||||
let result_files: Vec<_> = command
|
||||
.files
|
||||
.into_par_iter()
|
||||
.enumerate()
|
||||
.map(|(file_idx, file)| {
|
||||
eprintln!("{}: load {}", file_idx, file.path.display());
|
||||
let stem = file.path.file_stem().unwrap().to_string_lossy();
|
||||
let map = dmm::Map::from_file(&file.path).unwrap(); // TODO: error handling
|
||||
let (dim_x, dim_y, dim_z) = map.dim_xyz();
|
||||
|
||||
// If `chunks` was not specified, render one chunk per z-level.
|
||||
let chunks = file.chunks.unwrap_or_else(|| (1..=dim_z).map(|z| RenderManyChunk {
|
||||
z,
|
||||
min_x: None,
|
||||
min_y: None,
|
||||
max_x: None,
|
||||
max_y: None,
|
||||
}).collect());
|
||||
// If `chunks` was not specified, render one chunk per z-level.
|
||||
let chunks = file.chunks.unwrap_or_else(|| {
|
||||
(1..=dim_z)
|
||||
.map(|z| RenderManyChunk {
|
||||
z,
|
||||
min_x: None,
|
||||
min_y: None,
|
||||
max_x: None,
|
||||
max_y: None,
|
||||
})
|
||||
.collect()
|
||||
});
|
||||
|
||||
let result_chunks: Vec<_> = chunks.into_par_iter().enumerate().map(|(chunk_idx, chunk)| {
|
||||
eprintln!("{}/{}: render {:?}", file_idx, chunk_idx, chunk);
|
||||
let result_chunks: Vec<_> = chunks
|
||||
.into_par_iter()
|
||||
.enumerate()
|
||||
.map(|(chunk_idx, chunk)| {
|
||||
eprintln!("{file_idx}/{chunk_idx}: render {chunk:?}");
|
||||
|
||||
// Render the image.
|
||||
let bump = Default::default();
|
||||
let minimap_context = minimap::Context {
|
||||
objtree,
|
||||
map: &map,
|
||||
level: map.z_level(chunk.z - 1),
|
||||
// Default and clamp to [1, max].
|
||||
min: (chunk.min_x.unwrap_or(1).max(1) - 1, chunk.min_y.unwrap_or(1).max(1) - 1),
|
||||
max: (chunk.max_x.unwrap_or(dim_x).min(dim_x) - 1, chunk.max_y.unwrap_or(dim_y).min(dim_y) - 1),
|
||||
render_passes,
|
||||
errors: &errors,
|
||||
bump: &bump,
|
||||
};
|
||||
let image = minimap::generate(minimap_context, icon_cache).unwrap(); // TODO: error handling
|
||||
// Render the image.
|
||||
let bump = Default::default();
|
||||
let minimap_context = minimap::Context {
|
||||
objtree,
|
||||
map: &map,
|
||||
level: map.z_level(chunk.z - 1),
|
||||
// Default and clamp to [1, max].
|
||||
min: (
|
||||
chunk.min_x.unwrap_or(1).max(1) - 1,
|
||||
chunk.min_y.unwrap_or(1).max(1) - 1,
|
||||
),
|
||||
max: (
|
||||
chunk.max_x.unwrap_or(dim_x).min(dim_x) - 1,
|
||||
chunk.max_y.unwrap_or(dim_y).min(dim_y) - 1,
|
||||
),
|
||||
render_passes,
|
||||
errors: &errors,
|
||||
bump: &bump,
|
||||
};
|
||||
let image = minimap::generate(minimap_context, icon_cache).unwrap(); // TODO: error handling
|
||||
|
||||
// Write it to file.
|
||||
let filename = PathBuf::from(format!(
|
||||
"{}_z{}_chunk{}.png",
|
||||
stem,
|
||||
chunk.z,
|
||||
chunk_idx,
|
||||
));
|
||||
eprintln!("{}/{}: save {}", file_idx, chunk_idx, filename.display());
|
||||
let outfile = output_directory.join(&filename);
|
||||
image.to_file(&outfile).unwrap(); // TODO: error handling
|
||||
// Write it to file.
|
||||
let filename =
|
||||
PathBuf::from(format!("{}_z{}_chunk{}.png", stem, chunk.z, chunk_idx,));
|
||||
eprintln!("{}/{}: save {}", file_idx, chunk_idx, filename.display());
|
||||
let outfile = output_directory.join(&filename);
|
||||
image.to_file(&outfile).unwrap(); // TODO: error handling
|
||||
|
||||
RenderManyChunkResult {
|
||||
filename,
|
||||
RenderManyChunkResult { filename }
|
||||
})
|
||||
.collect();
|
||||
|
||||
RenderManyFileResult {
|
||||
chunks: result_chunks,
|
||||
}
|
||||
}).collect();
|
||||
|
||||
RenderManyFileResult {
|
||||
chunks: result_chunks,
|
||||
}
|
||||
}).collect();
|
||||
})
|
||||
.collect();
|
||||
|
||||
RenderManyCommandResult {
|
||||
files: result_files,
|
||||
|
|
|
|||
|
|
@ -2,32 +2,37 @@
|
|||
name = "dmm-tools"
|
||||
version = "0.1.0"
|
||||
authors = ["Tad Hardesty <tad@platymuus.com>"]
|
||||
edition = "2018"
|
||||
edition = "2021"
|
||||
|
||||
[dependencies]
|
||||
inflate = "0.4.1"
|
||||
ndarray = "0.15.3"
|
||||
rand = "0.8.4"
|
||||
inflate = "0.4.5"
|
||||
ndarray = "0.15.6"
|
||||
rand = "0.8.5"
|
||||
dreammaker = { path = "../dreammaker" }
|
||||
lodepng = "3.0.0"
|
||||
indexmap = "1.7.0"
|
||||
ahash = "0.7.6"
|
||||
lodepng = "3.10.7"
|
||||
indexmap = "2.6.0"
|
||||
foldhash = "0.2.0"
|
||||
either = "1.13.0"
|
||||
|
||||
[dependencies.bytemuck]
|
||||
version = "1.5"
|
||||
version = "1.19.0"
|
||||
features = ["derive"]
|
||||
|
||||
[dependencies.bumpalo]
|
||||
version = "3.0.0"
|
||||
version = "3.16.0"
|
||||
features = ["collections"]
|
||||
|
||||
[dependencies.png]
|
||||
version = "0.17.2"
|
||||
version = "0.17.14"
|
||||
optional = true
|
||||
|
||||
[dependencies.gfx_core]
|
||||
version = "0.9.2"
|
||||
optional = true
|
||||
|
||||
[dependencies.gif]
|
||||
version = "0.11.4"
|
||||
optional = true
|
||||
|
||||
[dev-dependencies]
|
||||
walkdir = "2.0.1"
|
||||
walkdir = "2.5.0"
|
||||
|
|
|
|||
|
|
@ -1,13 +1,13 @@
|
|||
extern crate dreammaker as dm;
|
||||
extern crate dmm_tools;
|
||||
extern crate walkdir;
|
||||
extern crate dreammaker as dm;
|
||||
extern crate ndarray;
|
||||
extern crate walkdir;
|
||||
|
||||
use std::path::Path;
|
||||
use std::collections::HashMap;
|
||||
use walkdir::{DirEntry, WalkDir};
|
||||
use dmm_tools::dmi::*;
|
||||
use foldhash::{HashMap, HashMapExt};
|
||||
use ndarray::s;
|
||||
use std::path::Path;
|
||||
use walkdir::{DirEntry, WalkDir};
|
||||
|
||||
fn is_visible(entry: &DirEntry) -> bool {
|
||||
entry
|
||||
|
|
@ -15,7 +15,7 @@ fn is_visible(entry: &DirEntry) -> bool {
|
|||
.file_name()
|
||||
.unwrap_or("".as_ref())
|
||||
.to_str()
|
||||
.map(|s| !s.starts_with("."))
|
||||
.map(|s| !s.starts_with('.'))
|
||||
.unwrap_or(true)
|
||||
}
|
||||
|
||||
|
|
@ -23,9 +23,9 @@ fn files_with_extension<F: FnMut(&Path)>(ext: &str, mut f: F) {
|
|||
let dir = match std::env::var_os("TEST_DME") {
|
||||
Some(dme) => Path::new(&dme).parent().unwrap().to_owned(),
|
||||
None => {
|
||||
println!("Set TEST_DME to check .{} files", ext);
|
||||
println!("Set TEST_DME to check .{ext} files");
|
||||
return;
|
||||
}
|
||||
},
|
||||
};
|
||||
for entry in WalkDir::new(dir).into_iter().filter_entry(is_visible) {
|
||||
let entry = entry.unwrap();
|
||||
|
|
|
|||
|
|
@ -2,22 +2,29 @@
|
|||
//!
|
||||
//! Includes re-exports from `dreammaker::dmi`.
|
||||
|
||||
use bytemuck::Pod;
|
||||
use std::io;
|
||||
use std::path::Path;
|
||||
use bytemuck::Pod;
|
||||
|
||||
use lodepng::{self, RGBA, Decoder, ColorType};
|
||||
use lodepng::{self, ColorType, Decoder, RGBA};
|
||||
use ndarray::Array2;
|
||||
|
||||
pub use dm::dmi::*;
|
||||
use std::ops::{Index, IndexMut};
|
||||
|
||||
type Rect = (u32, u32, u32, u32);
|
||||
/// Absolute x and y.
|
||||
pub type Coordinate = (u32, u32);
|
||||
/// Start x, Start y, End x, End y - relative to Coordinate.
|
||||
pub type Rect = (u32, u32, u32, u32);
|
||||
|
||||
// ----------------------------------------------------------------------------
|
||||
// Icon file and metadata handling
|
||||
|
||||
#[cfg(all(feature = "png", feature = "gif"))]
|
||||
pub mod render;
|
||||
|
||||
/// An image with associated DMI metadata.
|
||||
#[derive(Debug)]
|
||||
pub struct IconFile {
|
||||
/// The icon's metadata.
|
||||
pub metadata: Metadata,
|
||||
|
|
@ -27,7 +34,11 @@ pub struct IconFile {
|
|||
|
||||
impl IconFile {
|
||||
pub fn from_file(path: &Path) -> io::Result<IconFile> {
|
||||
let (bitmap, metadata) = Metadata::from_file(path)?;
|
||||
Self::from_bytes(&std::fs::read(path)?)
|
||||
}
|
||||
|
||||
pub fn from_bytes(data: &[u8]) -> io::Result<IconFile> {
|
||||
let (bitmap, metadata) = Metadata::from_bytes(data)?;
|
||||
Ok(IconFile {
|
||||
metadata,
|
||||
image: Image::from_rgba(bitmap),
|
||||
|
|
@ -35,7 +46,7 @@ impl IconFile {
|
|||
}
|
||||
|
||||
#[inline]
|
||||
pub fn rect_of(&self, icon_state: &str, dir: Dir) -> Option<Rect> {
|
||||
pub fn rect_of(&self, icon_state: &StateIndex, dir: Dir) -> Option<Rect> {
|
||||
self.metadata.rect_of(self.image.width, icon_state, dir, 0)
|
||||
}
|
||||
|
||||
|
|
@ -49,9 +60,18 @@ impl IconFile {
|
|||
self.metadata.height,
|
||||
)
|
||||
}
|
||||
|
||||
pub fn get_icon_state(&self, icon_state: &StateIndex) -> io::Result<&State> {
|
||||
self.metadata.get_icon_state(icon_state).ok_or_else(|| {
|
||||
io::Error::new(
|
||||
io::ErrorKind::NotFound,
|
||||
format!("icon_state {icon_state} not found"),
|
||||
)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Default, Clone, Copy, Pod, Zeroable, Eq, PartialEq)]
|
||||
#[derive(Default, Debug, Clone, Copy, Pod, Zeroable, Eq, PartialEq)]
|
||||
#[repr(C)]
|
||||
pub struct Rgba8 {
|
||||
pub r: u8,
|
||||
|
|
@ -92,6 +112,7 @@ impl IndexMut<u8> for Rgba8 {
|
|||
// Image manipulation
|
||||
|
||||
/// A two-dimensional RGBA image.
|
||||
#[derive(Clone, Debug, PartialEq, Eq)]
|
||||
pub struct Image {
|
||||
pub width: u32,
|
||||
pub height: u32,
|
||||
|
|
@ -103,9 +124,7 @@ impl Image {
|
|||
Image {
|
||||
width,
|
||||
height,
|
||||
data: {
|
||||
Array2::default((width as usize, height as usize))
|
||||
},
|
||||
data: { Array2::default((width as usize, height as usize)) },
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -122,18 +141,17 @@ impl Image {
|
|||
}
|
||||
}
|
||||
|
||||
/// Read an `Image` from a file.
|
||||
/// Read an `Image` from a [u8] array.
|
||||
///
|
||||
/// Prefer to call `IconFile::from_file`, which can read both metadata and
|
||||
/// Prefer to call `IconFile::from_bytes`, which can read both metadata and
|
||||
/// image contents at one time.
|
||||
pub fn from_file(path: &Path) -> io::Result<Image> {
|
||||
let path = &::dm::fix_case(path);
|
||||
pub fn from_bytes(data: &[u8]) -> io::Result<Image> {
|
||||
let mut decoder = Decoder::new();
|
||||
decoder.info_raw_mut().colortype = ColorType::RGBA;
|
||||
decoder.info_raw_mut().set_bitdepth(8);
|
||||
decoder.read_text_chunks(false);
|
||||
decoder.remember_unknown_chunks(false);
|
||||
let bitmap = match decoder.decode_file(path) {
|
||||
let bitmap = match decoder.decode(data) {
|
||||
Ok(::lodepng::Image::RGBA(bitmap)) => bitmap,
|
||||
Ok(_) => return Err(io::Error::new(io::ErrorKind::InvalidData, "not RGBA")),
|
||||
Err(e) => return Err(io::Error::new(io::ErrorKind::InvalidData, e)),
|
||||
|
|
@ -142,21 +160,45 @@ impl Image {
|
|||
Ok(Image::from_rgba(bitmap))
|
||||
}
|
||||
|
||||
/// Read an `Image` from a file.
|
||||
///
|
||||
/// Prefer to call `IconFile::from_file`, which can read both metadata and
|
||||
/// image contents at one time.
|
||||
pub fn from_file(path: &Path) -> io::Result<Image> {
|
||||
let path = &::dm::fix_case(path);
|
||||
Self::from_bytes(&std::fs::read(path)?)
|
||||
}
|
||||
|
||||
pub fn clear(&mut self) {
|
||||
self.data.fill(Default::default())
|
||||
}
|
||||
|
||||
#[cfg(feature = "png")]
|
||||
pub fn to_file(&self, path: &Path) -> io::Result<()> {
|
||||
use std::fs::File;
|
||||
|
||||
let mut encoder = png::Encoder::new(File::create(path)?, self.width, self.height);
|
||||
encoder.set_color(::png::ColorType::Rgba);
|
||||
encoder.set_depth(::png::BitDepth::Eight);
|
||||
let mut writer = encoder.write_header()?;
|
||||
// TODO: metadata with write_chunk()
|
||||
|
||||
writer.write_image_data(bytemuck::cast_slice(self.data.as_slice().unwrap()))?;
|
||||
pub fn to_write<W: std::io::Write>(&self, writer: W) -> io::Result<()> {
|
||||
{
|
||||
let mut encoder = png::Encoder::new(writer, self.width, self.height);
|
||||
encoder.set_color(::png::ColorType::Rgba);
|
||||
encoder.set_depth(::png::BitDepth::Eight);
|
||||
let mut writer = encoder.write_header()?;
|
||||
// TODO: metadata with write_chunk()
|
||||
writer.write_image_data(bytemuck::cast_slice(self.data.as_slice().unwrap()))?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn composite(&mut self, other: &Image, pos: (u32, u32), crop: Rect, color: [u8; 4]) {
|
||||
#[cfg(feature = "png")]
|
||||
pub fn to_file(&self, path: &Path) -> io::Result<()> {
|
||||
self.to_write(std::fs::File::create(path)?)
|
||||
}
|
||||
|
||||
#[cfg(feature = "png")]
|
||||
pub fn to_bytes(&self) -> io::Result<Vec<u8>> {
|
||||
let mut vector = Vec::new();
|
||||
self.to_write(&mut vector)?;
|
||||
Ok(vector)
|
||||
}
|
||||
|
||||
pub fn composite(&mut self, other: &Image, pos: Coordinate, crop: Rect, color: [u8; 4]) {
|
||||
let other_dat = other.data.as_slice().unwrap();
|
||||
let self_dat = self.data.as_slice_mut().unwrap();
|
||||
let mut sy = crop.1;
|
||||
|
|
@ -166,13 +208,10 @@ impl Image {
|
|||
let src = other_dat[(sy * other.width + sx) as usize];
|
||||
macro_rules! tint {
|
||||
($i:expr) => {
|
||||
mul255(
|
||||
src[$i],
|
||||
color[$i],
|
||||
)
|
||||
mul255(src[$i], color[$i])
|
||||
};
|
||||
}
|
||||
let mut dst = &mut self_dat[(y * self.width + x) as usize];
|
||||
let dst = &mut self_dat[(y * self.width + x) as usize];
|
||||
let src_tint = Rgba8::new(tint!(0), tint!(1), tint!(2), tint!(3));
|
||||
|
||||
// out_A = src_A + dst_A (1 - src_A)
|
||||
|
|
@ -189,7 +228,7 @@ impl Image {
|
|||
dst[i] = 0;
|
||||
}
|
||||
}
|
||||
dst.a = out_a as u8;
|
||||
dst.a = out_a;
|
||||
|
||||
sx += 1;
|
||||
}
|
||||
|
|
|
|||
210
crates/dmm-tools/src/dmi/render.rs
Normal file
210
crates/dmm-tools/src/dmi/render.rs
Normal file
|
|
@ -0,0 +1,210 @@
|
|||
use super::*;
|
||||
use either::Either;
|
||||
use gif::DisposalMethod;
|
||||
|
||||
static NO_TINT: [u8; 4] = [0xff, 0xff, 0xff, 0xff];
|
||||
|
||||
/// Used to render an IconFile to a .gif/.png easily.
|
||||
#[derive(Debug)]
|
||||
pub struct IconRenderer<'a> {
|
||||
/// The IconFile we render from.
|
||||
source: &'a IconFile,
|
||||
}
|
||||
|
||||
/// [`IconRenderer::render`] will return this to indicate if it wrote to the stream using
|
||||
/// [`gif::Encoder`] or `[`png::Encoder`].
|
||||
#[derive(Debug, Clone, Copy)]
|
||||
pub enum RenderType {
|
||||
Png,
|
||||
Gif,
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct RenderStateGuard<'a> {
|
||||
pub render_type: RenderType,
|
||||
renderer: &'a IconRenderer<'a>,
|
||||
state: &'a State,
|
||||
}
|
||||
|
||||
impl<'a> RenderStateGuard<'a> {
|
||||
pub fn render<W: std::io::Write>(self, target: W) -> io::Result<()> {
|
||||
match self.render_type {
|
||||
RenderType::Png => self.renderer.render_to_png(self.state, target),
|
||||
RenderType::Gif => self.renderer.render_gif(self.state, target),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Public API
|
||||
impl<'a> IconRenderer<'a> {
|
||||
pub fn new(source: &'a IconFile) -> Self {
|
||||
Self { source }
|
||||
}
|
||||
|
||||
/// Renders with either [`gif::Encoder`] or [`png::Encoder`] depending on whether the icon state is animated
|
||||
/// or not.
|
||||
/// Returns a [`RenderType`] to help you determine how to treat the written data.
|
||||
pub fn prepare_render(&self, icon_state: &StateIndex) -> io::Result<RenderStateGuard> {
|
||||
self.prepare_render_state(self.source.get_icon_state(icon_state)?)
|
||||
}
|
||||
|
||||
/// This is here so that duplicate icon states can be handled by not relying on the btreemap
|
||||
/// of state names in [`Metadata`].
|
||||
pub fn prepare_render_state(&'a self, icon_state: &'a State) -> io::Result<RenderStateGuard> {
|
||||
match icon_state.is_animated() {
|
||||
false => Ok(RenderStateGuard {
|
||||
renderer: self,
|
||||
state: icon_state,
|
||||
render_type: RenderType::Png,
|
||||
}),
|
||||
true => Ok(RenderStateGuard {
|
||||
renderer: self,
|
||||
state: icon_state,
|
||||
render_type: RenderType::Gif,
|
||||
}),
|
||||
}
|
||||
}
|
||||
|
||||
/// Instead of writing to a file, this gives a Vec<Image> of each frame/dir as it would be composited
|
||||
/// for a file.
|
||||
pub fn render_to_images(&self, icon_state: &StateIndex) -> io::Result<Vec<Image>> {
|
||||
let state = self.source.get_icon_state(icon_state)?;
|
||||
Ok(self.render_frames(state))
|
||||
}
|
||||
}
|
||||
|
||||
/// Private helpers
|
||||
impl<'a> IconRenderer<'a> {
|
||||
/// Helper for render_to_images- not used for render_gif because it's less efficient.
|
||||
fn render_frames(&self, icon_state: &State) -> Vec<Image> {
|
||||
let frames = match &icon_state.frames {
|
||||
Frames::One => 1,
|
||||
Frames::Count(count) => *count,
|
||||
Frames::Delays(delays) => delays.len(),
|
||||
};
|
||||
let mut canvas = self.get_canvas(icon_state.dirs);
|
||||
let mut vec = Vec::new();
|
||||
let range = if icon_state.rewind {
|
||||
Either::Left((0..frames).chain((0..frames).rev()))
|
||||
} else {
|
||||
Either::Right(0..frames)
|
||||
};
|
||||
for frame in range {
|
||||
self.render_dirs(icon_state, &mut canvas, frame as u32);
|
||||
vec.push(canvas.clone());
|
||||
canvas.clear();
|
||||
}
|
||||
vec
|
||||
}
|
||||
|
||||
/// Returns a new canvas of the appropriate size
|
||||
fn get_canvas(&self, dirs: Dirs) -> Image {
|
||||
match dirs {
|
||||
Dirs::One => Image::new_rgba(self.source.metadata.width, self.source.metadata.height),
|
||||
Dirs::Four => {
|
||||
Image::new_rgba(self.source.metadata.width * 4, self.source.metadata.height)
|
||||
},
|
||||
Dirs::Eight => {
|
||||
Image::new_rgba(self.source.metadata.width * 8, self.source.metadata.height)
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
/// Gives a [`Vec<Dir>`] of each [`Dir`] matching our [`Dirs`] setting,
|
||||
/// in the same order BYOND uses.
|
||||
fn ordered_dirs(dirs: Dirs) -> Vec<Dir> {
|
||||
match dirs {
|
||||
Dirs::One => [Dir::South].to_vec(),
|
||||
Dirs::Four => [Dir::South, Dir::North, Dir::East, Dir::West].to_vec(),
|
||||
Dirs::Eight => [
|
||||
Dir::South,
|
||||
Dir::North,
|
||||
Dir::East,
|
||||
Dir::West,
|
||||
Dir::Southeast,
|
||||
Dir::Southwest,
|
||||
Dir::Northeast,
|
||||
Dir::Northwest,
|
||||
]
|
||||
.to_vec(),
|
||||
}
|
||||
}
|
||||
|
||||
/// Renders each direction to the same canvas, offsetting them to the right
|
||||
fn render_dirs(&self, icon_state: &State, canvas: &mut Image, frame: u32) {
|
||||
for (dir_no, dir) in Self::ordered_dirs(icon_state.dirs).iter().enumerate() {
|
||||
let frame_idx = icon_state.index_of_frame(*dir, frame as u32);
|
||||
let frame_rect = self.source.rect_of_index(frame_idx);
|
||||
canvas.composite(
|
||||
&self.source.image,
|
||||
(self.source.metadata.width * (dir_no as u32), 0),
|
||||
frame_rect,
|
||||
NO_TINT,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// Renders the whole file to a gif, animated states becoming frames
|
||||
fn render_gif<W: std::io::Write>(&self, icon_state: &State, target: W) -> io::Result<()> {
|
||||
if !icon_state.is_animated() {
|
||||
return Err(io::Error::new(
|
||||
io::ErrorKind::InvalidData,
|
||||
"Tried to render gif with one frame",
|
||||
));
|
||||
}
|
||||
|
||||
let (frames, delays) = match &icon_state.frames {
|
||||
Frames::Count(frames) => (*frames, None),
|
||||
Frames::Delays(delays) => (delays.len(), Some(delays)),
|
||||
_ => unreachable!(),
|
||||
};
|
||||
|
||||
let mut canvas = self.get_canvas(icon_state.dirs);
|
||||
|
||||
let mut encoder = gif::Encoder::new(target, canvas.width as u16, canvas.height as u16, &[])
|
||||
.map_err(|e| io::Error::new(io::ErrorKind::Other, format!("{e}")))?;
|
||||
|
||||
encoder
|
||||
.set_repeat(gif::Repeat::Infinite)
|
||||
.map_err(|e| io::Error::new(io::ErrorKind::Other, format!("{e}")))?;
|
||||
|
||||
let range = if icon_state.rewind {
|
||||
Either::Left((0..frames).chain((0..frames).rev()))
|
||||
} else {
|
||||
Either::Right(0..frames)
|
||||
};
|
||||
|
||||
for frame in range {
|
||||
self.render_dirs(icon_state, &mut canvas, frame as u32);
|
||||
|
||||
let mut pixels = bytemuck::cast_slice(canvas.data.as_slice().unwrap()).to_owned();
|
||||
let mut gif_frame =
|
||||
gif::Frame::from_rgba(canvas.width as u16, canvas.height as u16, &mut pixels);
|
||||
// gif::Frame delays are measured in "Frame delay in units of 10 ms."
|
||||
// aka centiseconds. We're measuring in BYOND ticks, aka deciseconds.
|
||||
// And it's a u16 for some reason so we just SHRUG and floor it.
|
||||
gif_frame.delay =
|
||||
(delays.map_or_else(|| 1.0, |f| *f.get(frame).unwrap_or(&1.0)) * 10.0) as u16;
|
||||
// the disposal method by default is "keep the previous frame under the alpha mask"
|
||||
// wtf
|
||||
gif_frame.dispose = DisposalMethod::Background;
|
||||
encoder.write_frame(&gif_frame).unwrap();
|
||||
// Clear the canvas.
|
||||
canvas.clear();
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Renders the whole file to a png, discarding all but the first frame of animations
|
||||
fn render_to_png<W: std::io::Write>(&self, icon_state: &State, target: W) -> io::Result<()> {
|
||||
let mut canvas = self.get_canvas(icon_state.dirs);
|
||||
|
||||
self.render_dirs(icon_state, &mut canvas, 0);
|
||||
|
||||
canvas.to_write(target)?;
|
||||
// Always remember to clear the canvas for the next guy!
|
||||
canvas.clear();
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
|
@ -1,16 +1,16 @@
|
|||
use std::collections::BTreeMap;
|
||||
use std::path::Path;
|
||||
use std::fmt;
|
||||
use std::fs::File;
|
||||
use std::io;
|
||||
use std::fmt;
|
||||
use std::path::Path;
|
||||
|
||||
use ndarray::{self, Array3, Axis};
|
||||
use foldhash::fast::RandomState;
|
||||
use indexmap::IndexMap;
|
||||
use ahash::RandomState;
|
||||
use ndarray::{self, Array3, Axis};
|
||||
|
||||
use dm::DMError;
|
||||
use dm::constants::Constant;
|
||||
use crate::dmi::Dir;
|
||||
use dm::constants::Constant;
|
||||
use dm::DMError;
|
||||
|
||||
mod read;
|
||||
mod save_tgm;
|
||||
|
|
@ -18,7 +18,7 @@ mod save_tgm;
|
|||
const MAX_KEY_LENGTH: u8 = 3;
|
||||
|
||||
/// BYOND is currently limited to 65534 keys.
|
||||
/// https://secure.byond.com/forum/?post=2340796#comment23770802
|
||||
/// https://www.byond.com/forum/?post=2340796#comment23770802
|
||||
type KeyType = u16;
|
||||
|
||||
/// An opaque map key.
|
||||
|
|
@ -28,7 +28,7 @@ pub struct Key(KeyType);
|
|||
/// An XY coordinate pair in the BYOND coordinate system.
|
||||
///
|
||||
/// The lower-left corner is `{ x: 1, y: 1 }`.
|
||||
#[derive(Clone, Copy, Debug, Eq, PartialEq, Hash)]
|
||||
#[derive(Clone, Copy, Debug, Eq, PartialEq, Ord, PartialOrd, Hash)]
|
||||
pub struct Coord2 {
|
||||
pub x: i32,
|
||||
pub y: i32,
|
||||
|
|
@ -42,17 +42,34 @@ impl Coord2 {
|
|||
|
||||
#[inline]
|
||||
pub fn z(self, z: i32) -> Coord3 {
|
||||
Coord3 { x: self.x, y: self.y, z }
|
||||
Coord3 {
|
||||
x: self.x,
|
||||
y: self.y,
|
||||
z,
|
||||
}
|
||||
}
|
||||
|
||||
fn to_raw(self, (dim_y, dim_x): (usize, usize)) -> (usize, usize) {
|
||||
assert!(self.x >= 1 && self.x <= dim_x as i32, "x={} not in [1, {}]", self.x, dim_x);
|
||||
assert!(self.y >= 1 && self.y <= dim_y as i32, "y={} not in [1, {}]", self.y, dim_y);
|
||||
assert!(
|
||||
self.x >= 1 && self.x <= dim_x as i32,
|
||||
"x={} not in [1, {}]",
|
||||
self.x,
|
||||
dim_x
|
||||
);
|
||||
assert!(
|
||||
self.y >= 1 && self.y <= dim_y as i32,
|
||||
"y={} not in [1, {}]",
|
||||
self.y,
|
||||
dim_y
|
||||
);
|
||||
(dim_y - self.y as usize, self.x as usize - 1)
|
||||
}
|
||||
|
||||
fn from_raw((y, x): (usize, usize), (dim_y, _dim_x): (usize, usize)) -> Coord2 {
|
||||
Coord2 { x: x as i32 + 1, y: (dim_y - y) as i32 }
|
||||
Coord2 {
|
||||
x: x as i32 + 1,
|
||||
y: (dim_y - y) as i32,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -61,7 +78,10 @@ impl std::ops::Add<Dir> for Coord2 {
|
|||
|
||||
fn add(self, rhs: Dir) -> Coord2 {
|
||||
let (x, y) = rhs.offset();
|
||||
Coord2 { x: self.x + x, y: self.y + y }
|
||||
Coord2 {
|
||||
x: self.x + x,
|
||||
y: self.y + y,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -71,7 +91,7 @@ impl std::ops::Add<Dir> for Coord2 {
|
|||
///
|
||||
/// Note that BYOND by default considers "UP" to be Z+1, but this does not
|
||||
/// necessarily apply to a given game's logic.
|
||||
#[derive(Clone, Copy, Debug, Eq, PartialEq, Hash)]
|
||||
#[derive(Clone, Copy, Debug, Eq, PartialEq, Ord, PartialOrd, Hash)]
|
||||
pub struct Coord3 {
|
||||
pub x: i32,
|
||||
pub y: i32,
|
||||
|
|
@ -86,19 +106,48 @@ impl Coord3 {
|
|||
|
||||
#[inline]
|
||||
pub fn xy(self) -> Coord2 {
|
||||
Coord2 { x: self.x, y: self.y }
|
||||
Coord2 {
|
||||
x: self.x,
|
||||
y: self.y,
|
||||
}
|
||||
}
|
||||
|
||||
fn to_raw(self, (dim_z, dim_y, dim_x): (usize, usize, usize)) -> (usize, usize, usize) {
|
||||
assert!(self.x >= 1 && self.x <= dim_x as i32, "x={} not in [1, {}]", self.x, dim_x);
|
||||
assert!(self.y >= 1 && self.y <= dim_y as i32, "y={} not in [1, {}]", self.y, dim_y);
|
||||
assert!(self.z >= 1 && self.z <= dim_z as i32, "y={} not in [1, {}]", self.z, dim_z);
|
||||
(self.z as usize - 1, dim_y - self.y as usize, self.x as usize - 1)
|
||||
assert!(
|
||||
self.x >= 1 && self.x <= dim_x as i32,
|
||||
"x={} not in [1, {}]",
|
||||
self.x,
|
||||
dim_x
|
||||
);
|
||||
assert!(
|
||||
self.y >= 1 && self.y <= dim_y as i32,
|
||||
"y={} not in [1, {}]",
|
||||
self.y,
|
||||
dim_y
|
||||
);
|
||||
assert!(
|
||||
self.z >= 1 && self.z <= dim_z as i32,
|
||||
"y={} not in [1, {}]",
|
||||
self.z,
|
||||
dim_z
|
||||
);
|
||||
(
|
||||
self.z as usize - 1,
|
||||
dim_y - self.y as usize,
|
||||
self.x as usize - 1,
|
||||
)
|
||||
}
|
||||
|
||||
#[allow(dead_code)]
|
||||
fn from_raw((z, y, x): (usize, usize, usize), (_dim_z, dim_y, _dim_x): (usize, usize, usize)) -> Coord3 {
|
||||
Coord3 { x: x as i32 + 1, y: (dim_y - y) as i32, z: z as i32 + 1 }
|
||||
fn from_raw(
|
||||
(z, y, x): (usize, usize, usize),
|
||||
(_dim_z, dim_y, _dim_x): (usize, usize, usize),
|
||||
) -> Coord3 {
|
||||
Coord3 {
|
||||
x: x as i32 + 1,
|
||||
y: (dim_y - y) as i32,
|
||||
z: z as i32 + 1,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -118,32 +167,43 @@ pub struct ZLevel<'a> {
|
|||
pub grid: ndarray::ArrayView<'a, Key, ndarray::Dim<[usize; 2]>>,
|
||||
}
|
||||
|
||||
// TODO: port to ast::Prefab<Constant>
|
||||
#[derive(Debug, Default, Eq, PartialEq, Clone)]
|
||||
#[derive(Debug, Default, Clone)]
|
||||
pub struct Prefab {
|
||||
pub path: String,
|
||||
// insertion order, sort of most of the time alphabetical but not quite
|
||||
pub vars: IndexMap<String, Constant, RandomState>,
|
||||
}
|
||||
|
||||
impl PartialEq for Prefab {
|
||||
fn eq(&self, other: &Self) -> bool {
|
||||
self.path == other.path && self.vars == other.vars
|
||||
}
|
||||
}
|
||||
|
||||
impl Eq for Prefab {}
|
||||
|
||||
impl std::hash::Hash for Prefab {
|
||||
fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
|
||||
self.path.hash(state);
|
||||
self.vars.keys().for_each(|key| {key.hash(state)});
|
||||
let mut items: Vec<_> = self.vars.iter().collect();
|
||||
items.sort_by_key(|&(k, _)| k);
|
||||
for kvp in items {
|
||||
kvp.hash(state);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Map {
|
||||
pub fn new(x: usize, y: usize, z: usize, turf: String, area: String) -> Map {
|
||||
assert!(x > 0 && y > 0 && z > 0, "({}, {}, {})", x, y, z);
|
||||
assert!(x > 0 && y > 0 && z > 0, "({x}, {y}, {z})");
|
||||
|
||||
let mut dictionary = BTreeMap::new();
|
||||
dictionary.insert(Key(0), vec![
|
||||
Prefab::from_path(turf),
|
||||
Prefab::from_path(area),
|
||||
]);
|
||||
dictionary.insert(
|
||||
Key(0),
|
||||
vec![Prefab::from_path(turf), Prefab::from_path(area)],
|
||||
);
|
||||
|
||||
let grid = Array3::default((z, y, x)); // default = 0
|
||||
let grid = Array3::default((z, y, x)); // default = 0
|
||||
|
||||
Map {
|
||||
key_length: 1,
|
||||
|
|
@ -170,9 +230,12 @@ impl Map {
|
|||
Ok(map)
|
||||
}
|
||||
|
||||
pub fn to_writer(&self, writer: &mut impl std::io::Write) -> io::Result<()> {
|
||||
save_tgm::save_tgm(self, writer)
|
||||
}
|
||||
|
||||
pub fn to_file(&self, path: &Path) -> io::Result<()> {
|
||||
// DMM saver later
|
||||
save_tgm::save_tgm(self, File::create(path)?)
|
||||
self.to_writer(&mut File::create(path)?)
|
||||
}
|
||||
|
||||
pub fn key_length(&self) -> u8 {
|
||||
|
|
@ -180,12 +243,15 @@ impl Map {
|
|||
}
|
||||
|
||||
pub fn adjust_key_length(&mut self) {
|
||||
if self.dictionary.len() > 2704 {
|
||||
self.key_length = 3;
|
||||
} else if self.dictionary.len() > 52 {
|
||||
self.key_length = 2;
|
||||
} else {
|
||||
self.key_length = 1;
|
||||
if let Some(max_key) = self.dictionary.keys().max() {
|
||||
let max_key = max_key.0;
|
||||
if max_key >= 2704 {
|
||||
self.key_length = 3;
|
||||
} else if max_key >= 52 {
|
||||
self.key_length = 2;
|
||||
} else {
|
||||
self.key_length = 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -201,12 +267,17 @@ impl Map {
|
|||
}
|
||||
|
||||
#[inline]
|
||||
pub fn z_level(&self, z: usize) -> ZLevel {
|
||||
ZLevel { grid: self.grid.index_axis(Axis(0), z) }
|
||||
pub fn z_level(&self, z: usize) -> ZLevel<'_> {
|
||||
ZLevel {
|
||||
grid: self.grid.index_axis(Axis(0), z),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn iter_levels<'a>(&'a self) -> impl Iterator<Item=(i32, ZLevel<'a>)> + 'a {
|
||||
self.grid.axis_iter(Axis(0)).enumerate().map(|(i, grid)| (i as i32 + 1, ZLevel { grid }))
|
||||
pub fn iter_levels(&self) -> impl Iterator<Item = (i32, ZLevel<'_>)> + '_ {
|
||||
self.grid
|
||||
.axis_iter(Axis(0))
|
||||
.enumerate()
|
||||
.map(|(i, grid)| (i as i32 + 1, ZLevel { grid }))
|
||||
}
|
||||
|
||||
#[inline]
|
||||
|
|
@ -226,9 +297,11 @@ impl std::ops::Index<Coord3> for Map {
|
|||
|
||||
impl<'a> ZLevel<'a> {
|
||||
/// Iterate over the z-level in row-major order starting at the top-left.
|
||||
pub fn iter_top_down<'b>(&'b self) -> impl Iterator<Item=(Coord2, Key)> + 'b {
|
||||
pub fn iter_top_down(&self) -> impl Iterator<Item = (Coord2, Key)> + '_ {
|
||||
let dim = self.grid.dim();
|
||||
self.grid.indexed_iter().map(move |(c, k)| (Coord2::from_raw(c, dim), *k))
|
||||
self.grid
|
||||
.indexed_iter()
|
||||
.map(move |(c, k)| (Coord2::from_raw(c, dim), *k))
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -264,7 +337,7 @@ impl fmt::Display for Prefab {
|
|||
if f.alternate() {
|
||||
f.write_str("\n ")?;
|
||||
}
|
||||
write!(f, "{} = {}", k, v)?;
|
||||
write!(f, "{k} = {v}")?;
|
||||
}
|
||||
if f.alternate() {
|
||||
f.write_str("\n")?;
|
||||
|
|
@ -289,7 +362,7 @@ impl fmt::Display for Coord3 {
|
|||
|
||||
impl Key {
|
||||
pub fn invalid() -> Key {
|
||||
Key(KeyType::max_value())
|
||||
Key(KeyType::MAX)
|
||||
}
|
||||
|
||||
pub fn next(self) -> Key {
|
||||
|
|
@ -322,9 +395,9 @@ impl fmt::Display for FormatKey {
|
|||
const BASE_52: &[u8] = b"abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ";
|
||||
|
||||
fn base_52_reverse(ch: u8) -> Result<KeyType, String> {
|
||||
if ch >= b'a' && ch <= b'z' {
|
||||
if ch.is_ascii_lowercase() {
|
||||
Ok(ch as KeyType - b'a' as KeyType)
|
||||
} else if ch >= b'A' && ch <= b'Z' {
|
||||
} else if ch.is_ascii_uppercase() {
|
||||
Ok(26 + ch as KeyType - b'A' as KeyType)
|
||||
} else {
|
||||
Err(format!("Not a base-52 character: {:?}", ch as char))
|
||||
|
|
@ -332,8 +405,11 @@ fn base_52_reverse(ch: u8) -> Result<KeyType, String> {
|
|||
}
|
||||
|
||||
fn advance_key(current: KeyType, next_digit: KeyType) -> Result<KeyType, &'static str> {
|
||||
current.checked_mul(52).and_then(|b| b.checked_add(next_digit)).ok_or_else(|| {
|
||||
// https://secure.byond.com/forum/?post=2340796#comment23770802
|
||||
"Key overflow, max is 'ymo'"
|
||||
})
|
||||
current
|
||||
.checked_mul(52)
|
||||
.and_then(|b| b.checked_add(next_digit))
|
||||
.ok_or({
|
||||
// https://www.byond.com/forum/?post=2340796#comment23770802
|
||||
"Key overflow, max is 'ymo'"
|
||||
})
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,18 +1,14 @@
|
|||
//! Map parser, supporting standard DMM or TGM-format files.
|
||||
use std::collections::BTreeMap;
|
||||
use std::cmp::max;
|
||||
use std::collections::BTreeMap;
|
||||
use std::mem::take;
|
||||
|
||||
use ndarray::Array3;
|
||||
|
||||
use dm::lexer::{from_utf8_or_latin1, LocationTracker};
|
||||
use dm::{DMError, Location};
|
||||
use dm::lexer::{LocationTracker, from_utf8_or_latin1};
|
||||
|
||||
use super::{Map, Key, KeyType, Prefab};
|
||||
|
||||
#[inline]
|
||||
fn take<T: Default>(t: &mut T) -> T {
|
||||
std::mem::replace(t, T::default())
|
||||
}
|
||||
use super::{Key, KeyType, Map, Prefab};
|
||||
|
||||
pub fn parse_map(map: &mut Map, path: &std::path::Path) -> Result<(), DMError> {
|
||||
let file_id = Default::default();
|
||||
|
|
@ -37,7 +33,38 @@ pub fn parse_map(map: &mut Map, path: &std::path::Path) -> Result<(), DMError> {
|
|||
let mut escaping = false;
|
||||
let mut skip_whitespace = false;
|
||||
|
||||
let mut curr_key_start_location = Location::default();
|
||||
|
||||
let mut curr_datum_start_location = Location::default();
|
||||
macro_rules! set_curr_datum_start_location {
|
||||
() => {
|
||||
if curr_datum.is_empty() {
|
||||
curr_datum_start_location = chars.location();
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
macro_rules! insert_current_var {
|
||||
() => {
|
||||
curr_prefab.vars.insert(
|
||||
from_utf8_or_latin1(take(&mut curr_var)),
|
||||
dm::constants::evaluate_str(curr_datum_start_location, &take(&mut curr_datum))
|
||||
.map_err(|e| {
|
||||
e.with_note(
|
||||
curr_key_start_location,
|
||||
format!(
|
||||
"within key: \"{}\"",
|
||||
super::FormatKey(curr_key_length, super::Key(curr_key))
|
||||
),
|
||||
)
|
||||
})?,
|
||||
);
|
||||
};
|
||||
}
|
||||
|
||||
while let Some(ch) = chars.next() {
|
||||
// Readability, simple elif chain isn't duplicate code
|
||||
#[allow(clippy::if_same_then_else)]
|
||||
if ch == b'\n' || ch == b'\r' {
|
||||
in_comment_line = false;
|
||||
comment_trigger = false;
|
||||
|
|
@ -63,18 +90,23 @@ pub fn parse_map(map: &mut Map, path: &std::path::Path) -> Result<(), DMError> {
|
|||
if in_varedit_block {
|
||||
if in_quote_block {
|
||||
if ch == b'\\' {
|
||||
set_curr_datum_start_location!();
|
||||
curr_datum.push(ch);
|
||||
escaping = true;
|
||||
} else if escaping {
|
||||
set_curr_datum_start_location!();
|
||||
curr_datum.push(ch);
|
||||
escaping = false;
|
||||
} else if ch == b'"' {
|
||||
set_curr_datum_start_location!();
|
||||
curr_datum.push(ch);
|
||||
in_quote_block = false;
|
||||
} else {
|
||||
set_curr_datum_start_location!();
|
||||
curr_datum.push(ch);
|
||||
}
|
||||
} else { // in_quote_block
|
||||
} else {
|
||||
// in_quote_block
|
||||
if skip_whitespace && ch == b' ' {
|
||||
skip_whitespace = false;
|
||||
continue;
|
||||
|
|
@ -82,6 +114,7 @@ pub fn parse_map(map: &mut Map, path: &std::path::Path) -> Result<(), DMError> {
|
|||
skip_whitespace = false;
|
||||
|
||||
if ch == b'"' {
|
||||
set_curr_datum_start_location!();
|
||||
curr_datum.push(ch);
|
||||
in_quote_block = true;
|
||||
} else if ch == b'=' && curr_var.is_empty() {
|
||||
|
|
@ -93,20 +126,15 @@ pub fn parse_map(map: &mut Map, path: &std::path::Path) -> Result<(), DMError> {
|
|||
curr_var.truncate(length);
|
||||
skip_whitespace = true;
|
||||
} else if ch == b';' {
|
||||
curr_prefab.vars.insert(
|
||||
from_utf8_or_latin1(take(&mut curr_var)),
|
||||
dm::constants::evaluate_str(chars.location(), &take(&mut curr_datum))?,
|
||||
);
|
||||
insert_current_var!();
|
||||
skip_whitespace = true;
|
||||
} else if ch == b'}' {
|
||||
if !curr_var.is_empty() {
|
||||
curr_prefab.vars.insert(
|
||||
from_utf8_or_latin1(take(&mut curr_var)),
|
||||
dm::constants::evaluate_str(chars.location(), &take(&mut curr_datum))?,
|
||||
);
|
||||
insert_current_var!();
|
||||
}
|
||||
in_varedit_block = false;
|
||||
} else {
|
||||
set_curr_datum_start_location!();
|
||||
curr_datum.push(ch);
|
||||
}
|
||||
}
|
||||
|
|
@ -130,6 +158,7 @@ pub fn parse_map(map: &mut Map, path: &std::path::Path) -> Result<(), DMError> {
|
|||
in_data_block = false;
|
||||
after_data_block = true;
|
||||
} else {
|
||||
set_curr_datum_start_location!();
|
||||
curr_datum.push(ch);
|
||||
}
|
||||
} else if in_key_block {
|
||||
|
|
@ -138,6 +167,9 @@ pub fn parse_map(map: &mut Map, path: &std::path::Path) -> Result<(), DMError> {
|
|||
assert!(map.key_length == 0 || map.key_length == curr_key_length);
|
||||
map.key_length = curr_key_length;
|
||||
} else {
|
||||
if curr_key == 0 {
|
||||
curr_key_start_location = chars.location();
|
||||
}
|
||||
curr_key = advance_key(chars.location(), curr_key, ch)?;
|
||||
curr_key_length += 1;
|
||||
}
|
||||
|
|
@ -188,7 +220,10 @@ pub fn parse_map(map: &mut Map, path: &std::path::Path) -> Result<(), DMError> {
|
|||
max_y = max(max_y, curr_y);
|
||||
reading_coord = Coord::Z;
|
||||
} else {
|
||||
return Err(DMError::new(chars.location(), "Incorrect number of coordinates"));
|
||||
return Err(DMError::new(
|
||||
chars.location(),
|
||||
"Incorrect number of coordinates",
|
||||
));
|
||||
}
|
||||
} else if ch == b')' {
|
||||
assert_eq!(reading_coord, Coord::Z);
|
||||
|
|
@ -199,7 +234,12 @@ pub fn parse_map(map: &mut Map, path: &std::path::Path) -> Result<(), DMError> {
|
|||
} else {
|
||||
match (ch as char).to_digit(10) {
|
||||
Some(x) => curr_num = 10 * curr_num + x as usize,
|
||||
None => return Err(DMError::new(chars.location(), format!("bad digit {:?} in map coordinate", ch))),
|
||||
None => {
|
||||
return Err(DMError::new(
|
||||
chars.location(),
|
||||
format!("bad digit {ch:?} in map coordinate"),
|
||||
))
|
||||
},
|
||||
}
|
||||
}
|
||||
} else if in_map_string {
|
||||
|
|
@ -223,9 +263,10 @@ pub fn parse_map(map: &mut Map, path: &std::path::Path) -> Result<(), DMError> {
|
|||
let key = take(&mut curr_key);
|
||||
curr_key_length = 0;
|
||||
if grid.insert((curr_x, curr_y, curr_z), Key(key)).is_some() {
|
||||
return Err(DMError::new(chars.location(), format!(
|
||||
"multiple entries for ({}, {}, {})",
|
||||
curr_x, curr_y, curr_z)))
|
||||
return Err(DMError::new(
|
||||
chars.location(),
|
||||
format!("multiple entries for ({curr_x}, {curr_y}, {curr_z})"),
|
||||
));
|
||||
}
|
||||
max_x = max(max_x, curr_x);
|
||||
curr_x += 1;
|
||||
|
|
@ -244,9 +285,10 @@ pub fn parse_map(map: &mut Map, path: &std::path::Path) -> Result<(), DMError> {
|
|||
if let Some(&tile) = grid.get(&(x + 1, y + 1, z + 1)) {
|
||||
tile
|
||||
} else {
|
||||
result = Err(DMError::new(chars.location(), format!(
|
||||
"no value for tile ({}, {}, {})",
|
||||
x + 1, y + 1, z + 1)));
|
||||
result = Err(DMError::new(
|
||||
chars.location(),
|
||||
format!("no value for tile ({}, {}, {})", x + 1, y + 1, z + 1),
|
||||
));
|
||||
Key(0)
|
||||
}
|
||||
});
|
||||
|
|
@ -260,6 +302,6 @@ fn advance_key(loc: Location, curr_key: KeyType, ch: u8) -> Result<KeyType, DMEr
|
|||
Ok(single) => match super::advance_key(curr_key, single) {
|
||||
Err(err) => Err(DMError::new(loc, err)),
|
||||
Ok(key) => Ok(key),
|
||||
}
|
||||
},
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,26 +1,28 @@
|
|||
//! TGM map writer.
|
||||
use std::fs::File;
|
||||
use std::io::{self, Write, BufWriter};
|
||||
use std::io::{self, BufWriter, Write};
|
||||
|
||||
use ndarray::Axis;
|
||||
|
||||
use super::Map;
|
||||
|
||||
const TGM_HEADER: &str = "//MAP CONVERTED BY dmm2tgm.py THIS HEADER COMMENT PREVENTS RECONVERSION, DO NOT REMOVE";
|
||||
const TGM_HEADER: &str =
|
||||
"//MAP CONVERTED BY dmm2tgm.py THIS HEADER COMMENT PREVENTS RECONVERSION, DO NOT REMOVE";
|
||||
|
||||
pub fn save_tgm(map: &Map, f: File) -> io::Result<()> {
|
||||
let mut f = BufWriter::new(f);
|
||||
write!(f, "{}\n", TGM_HEADER)?;
|
||||
// Note: writeln! currently (2022-04-30) writes the \n character alone on all platforms
|
||||
// If that changes, this will break.
|
||||
pub fn save_tgm(map: &Map, w: &mut impl Write) -> io::Result<()> {
|
||||
let mut f = BufWriter::new(w);
|
||||
writeln!(f, "{TGM_HEADER}")?;
|
||||
|
||||
// dictionary
|
||||
for (&key, prefabs) in map.dictionary.iter() {
|
||||
write!(f, "\"{}\" = (\n", map.format_key(key))?;
|
||||
writeln!(f, "\"{}\" = (", map.format_key(key))?;
|
||||
for (i, fab) in prefabs.iter().enumerate() {
|
||||
write!(f, "{}", fab.path)?;
|
||||
if !fab.vars.is_empty() {
|
||||
write!(f, "{{")?;
|
||||
for (i, (var, value)) in fab.vars.iter().enumerate() {
|
||||
write!(f, "\n\t{} = {}", var, value)?;
|
||||
write!(f, "\n\t{var} = {value}")?;
|
||||
if i + 1 != fab.vars.len() {
|
||||
write!(f, ";")?;
|
||||
}
|
||||
|
|
@ -28,21 +30,21 @@ pub fn save_tgm(map: &Map, f: File) -> io::Result<()> {
|
|||
write!(f, "\n\t}}")?;
|
||||
}
|
||||
if i + 1 != prefabs.len() {
|
||||
write!(f, ",\n")?;
|
||||
writeln!(f, ",")?;
|
||||
}
|
||||
}
|
||||
write!(f, ")\n")?;
|
||||
writeln!(f, ")")?;
|
||||
}
|
||||
|
||||
// grid in Y-major
|
||||
for (z, z_grid) in map.grid.axis_iter(Axis(0)).enumerate() {
|
||||
write!(f, "\n")?;
|
||||
writeln!(f)?;
|
||||
for (x, x_col) in z_grid.axis_iter(Axis(1)).enumerate() {
|
||||
write!(f, "({},1,{}) = {{\"\n", x + 1, z + 1)?;
|
||||
writeln!(f, "({},1,{}) = {{\"", x + 1, z + 1)?;
|
||||
for &elem in x_col.iter() {
|
||||
write!(f, "{}\n", map.format_key(elem))?;
|
||||
writeln!(f, "{}", map.format_key(elem))?;
|
||||
}
|
||||
write!(f, "\"}}\n")?;
|
||||
writeln!(f, "\"}}")?;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -1,12 +1,13 @@
|
|||
use std::sync::{Arc, RwLock};
|
||||
use foldhash::fast::RandomState;
|
||||
use std::collections::{hash_map, HashMap};
|
||||
use std::path::{Path, PathBuf};
|
||||
use std::collections::{HashMap, hash_map};
|
||||
use std::sync::{Arc, RwLock};
|
||||
|
||||
use super::dmi::IconFile;
|
||||
|
||||
#[derive(Default)]
|
||||
pub struct IconCache {
|
||||
lock: RwLock<HashMap<PathBuf, Option<Arc<IconFile>>>>,
|
||||
lock: RwLock<HashMap<PathBuf, Option<Arc<IconFile>>, RandomState>>,
|
||||
icons_root: Option<PathBuf>,
|
||||
}
|
||||
|
||||
|
|
@ -15,12 +16,17 @@ impl IconCache {
|
|||
let map = self.lock.get_mut().unwrap();
|
||||
(match map.entry(path.to_owned()) {
|
||||
hash_map::Entry::Occupied(entry) => entry.into_mut().as_mut(),
|
||||
hash_map::Entry::Vacant(entry) => entry.insert(
|
||||
match &self.icons_root {
|
||||
Some(root) => load(&root.join(path)),
|
||||
_ => load(path),
|
||||
}.map(Arc::new)).as_mut(),
|
||||
}).map(|x| &**x)
|
||||
hash_map::Entry::Vacant(entry) => entry
|
||||
.insert(
|
||||
match &self.icons_root {
|
||||
Some(root) => load(&root.join(path)),
|
||||
_ => load(path),
|
||||
}
|
||||
.map(Arc::new),
|
||||
)
|
||||
.as_mut(),
|
||||
})
|
||||
.map(|x| &**x)
|
||||
}
|
||||
|
||||
pub fn retrieve_shared(&self, path: &Path) -> Option<Arc<IconFile>> {
|
||||
|
|
@ -31,9 +37,13 @@ impl IconCache {
|
|||
None => {
|
||||
let arc = match &self.icons_root {
|
||||
Some(root) => load(&root.join(path)),
|
||||
None => load(&path),
|
||||
}.map(Arc::new);
|
||||
self.lock.write().unwrap().insert(path.to_owned(), arc.clone());
|
||||
None => load(path),
|
||||
}
|
||||
.map(Arc::new);
|
||||
self.lock
|
||||
.write()
|
||||
.unwrap()
|
||||
.insert(path.to_owned(), arc.clone());
|
||||
arc
|
||||
},
|
||||
}
|
||||
|
|
@ -50,6 +60,6 @@ fn load(path: &Path) -> Option<IconFile> {
|
|||
Err(err) => {
|
||||
eprintln!("error loading icon: {}\n {}", path.display(), err);
|
||||
None
|
||||
}
|
||||
},
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,22 +1,25 @@
|
|||
//! SS13 minimap generation tool
|
||||
#![deny(unsafe_code)] // NB deny rather than forbid, ndarray macros use unsafe
|
||||
#![deny(unsafe_code)] // NB deny rather than forbid, ndarray macros use unsafe
|
||||
|
||||
extern crate dreammaker as dm;
|
||||
|
||||
#[cfg(feature="png")] extern crate png;
|
||||
extern crate lodepng;
|
||||
extern crate inflate;
|
||||
extern crate lodepng;
|
||||
#[cfg(feature = "png")]
|
||||
extern crate png;
|
||||
|
||||
#[macro_use] extern crate bytemuck;
|
||||
extern crate rand;
|
||||
#[macro_use]
|
||||
extern crate bytemuck;
|
||||
extern crate bumpalo;
|
||||
extern crate rand;
|
||||
|
||||
#[cfg(feature="gfx_core")] extern crate gfx_core;
|
||||
#[cfg(feature = "gfx_core")]
|
||||
extern crate gfx_core;
|
||||
|
||||
pub mod dmi;
|
||||
pub mod dmm;
|
||||
mod icon_cache;
|
||||
pub mod minimap;
|
||||
pub mod render_passes;
|
||||
pub mod dmi;
|
||||
|
||||
pub use icon_cache::IconCache;
|
||||
|
|
|
|||
|
|
@ -1,16 +1,16 @@
|
|||
use std::collections::BTreeMap;
|
||||
use std::sync::RwLock;
|
||||
use std::collections::{HashSet, BTreeMap};
|
||||
|
||||
use ndarray::Axis;
|
||||
|
||||
use dm::objtree::*;
|
||||
use dm::constants::Constant;
|
||||
use crate::dmm::{Map, ZLevel, Prefab};
|
||||
use crate::dmi::{Dir, Image};
|
||||
use crate::render_passes::RenderPass;
|
||||
use crate::dmi::{self, Dir, Image};
|
||||
use crate::dmm::{Map, Prefab, ZLevel};
|
||||
use crate::icon_cache::IconCache;
|
||||
use crate::render_passes::RenderPass;
|
||||
use dm::constants::Constant;
|
||||
use dm::objtree::*;
|
||||
|
||||
use ahash::RandomState;
|
||||
use foldhash::HashSet;
|
||||
|
||||
const TILE_SIZE: u32 = 32;
|
||||
|
||||
|
|
@ -25,10 +25,12 @@ pub struct Context<'a> {
|
|||
pub min: (usize, usize),
|
||||
pub max: (usize, usize),
|
||||
pub render_passes: &'a [Box<dyn RenderPass>],
|
||||
pub errors: &'a RwLock<HashSet<String, RandomState>>,
|
||||
pub errors: &'a RwLock<HashSet<String>>,
|
||||
pub bump: &'a bumpalo::Bump,
|
||||
}
|
||||
|
||||
// This should eventually be faliable and not just shrug it's shoulders at errors and log them.
|
||||
#[allow(clippy::result_unit_err)]
|
||||
pub fn generate(ctx: Context, icon_cache: &IconCache) -> Result<Image, ()> {
|
||||
let Context {
|
||||
objtree,
|
||||
|
|
@ -48,7 +50,10 @@ pub fn generate(ctx: Context, icon_cache: &IconCache) -> Result<Image, ()> {
|
|||
// create atom arrays from the map dictionary
|
||||
let mut atoms = BTreeMap::new();
|
||||
for (key, prefabs) in map.dictionary.iter() {
|
||||
atoms.insert(key, get_atom_list(objtree, prefabs, render_passes, ctx.errors));
|
||||
atoms.insert(
|
||||
key,
|
||||
get_atom_list(objtree, prefabs, render_passes, ctx.errors),
|
||||
);
|
||||
}
|
||||
|
||||
// loads atoms from the prefabs on the map and adds overlays and smoothing
|
||||
|
|
@ -70,19 +75,19 @@ pub fn generate(ctx: Context, icon_cache: &IconCache) -> Result<Image, ()> {
|
|||
'atom: for atom in atoms.get(e).expect("bad key").iter() {
|
||||
for pass in render_passes.iter() {
|
||||
// Note that late_filter is NOT called during smoothing lookups.
|
||||
if !pass.late_filter(&atom, objtree) {
|
||||
if !pass.late_filter(atom, objtree) {
|
||||
continue 'atom;
|
||||
}
|
||||
}
|
||||
let mut sprite = Sprite::from_vars(objtree, atom);
|
||||
for pass in render_passes {
|
||||
pass.adjust_sprite(&atom, &mut sprite, objtree, bump);
|
||||
pass.adjust_sprite(atom, &mut sprite, objtree, bump);
|
||||
}
|
||||
if sprite.icon.is_empty() {
|
||||
println!("no icon: {}", atom.type_.path);
|
||||
continue;
|
||||
}
|
||||
let atom = Atom { sprite, .. *atom };
|
||||
let atom = Atom { sprite, ..*atom };
|
||||
|
||||
for pass in render_passes {
|
||||
pass.overlays(&atom, objtree, &mut underlays, &mut overlays, bump);
|
||||
|
|
@ -90,11 +95,13 @@ pub fn generate(ctx: Context, icon_cache: &IconCache) -> Result<Image, ()> {
|
|||
|
||||
// smoothing time
|
||||
let mut neighborhood = [&[][..]; 9];
|
||||
for (i, (dx, dy)) in [
|
||||
#[rustfmt::skip]
|
||||
const GRID: [(i32, i32); 9] = [
|
||||
(-1, 1), (0, 1), (1, 1),
|
||||
(-1, 0), (0, 0), (1, 0),
|
||||
(-1, -1), (0, -1), (1, -1),
|
||||
].iter().enumerate() {
|
||||
];
|
||||
for (i, (dx, dy)) in GRID.iter().enumerate() {
|
||||
let new_x = x as i32 + dx;
|
||||
let new_y = y as i32 - dy;
|
||||
let (dim_y, dim_x) = ctx.level.grid.dim();
|
||||
|
|
@ -107,7 +114,13 @@ pub fn generate(ctx: Context, icon_cache: &IconCache) -> Result<Image, ()> {
|
|||
|
||||
let mut normal_appearance = true;
|
||||
for pass in render_passes {
|
||||
if !pass.neighborhood_appearance(&atom, objtree, &neighborhood, &mut underlays, bump) {
|
||||
if !pass.neighborhood_appearance(
|
||||
&atom,
|
||||
objtree,
|
||||
&neighborhood,
|
||||
&mut underlays,
|
||||
bump,
|
||||
) {
|
||||
normal_appearance = false;
|
||||
}
|
||||
}
|
||||
|
|
@ -123,37 +136,45 @@ pub fn generate(ctx: Context, icon_cache: &IconCache) -> Result<Image, ()> {
|
|||
drop(underlays);
|
||||
drop(overlays);
|
||||
|
||||
// sorts the atom list and renders them onto the output image
|
||||
sprites.sort_by_key(|(_, s)| (s.plane, s.layer));
|
||||
|
||||
let mut map_image = Image::new_rgba(len_x as u32 * TILE_SIZE, len_y as u32 * TILE_SIZE);
|
||||
'sprite: for (loc, sprite) in sprites {
|
||||
// Drop sprites rejected by any render pass.
|
||||
sprites.retain(|(_, sprite)| {
|
||||
for pass in render_passes.iter() {
|
||||
if !pass.sprite_filter(&sprite) {
|
||||
continue 'sprite;
|
||||
if !pass.sprite_filter(sprite) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
true
|
||||
});
|
||||
|
||||
// Sort the sprite list by depth.
|
||||
sprites.sort_by_key(|(_, s)| (s.plane, s.layer));
|
||||
|
||||
// Composite the sorted sprites onto the output image.
|
||||
let mut map_image = Image::new_rgba(len_x as u32 * TILE_SIZE, len_y as u32 * TILE_SIZE);
|
||||
for ((x, y), sprite) in sprites {
|
||||
let icon_file = match icon_cache.retrieve_shared(sprite.icon.as_ref()) {
|
||||
Some(icon_file) => icon_file,
|
||||
None => continue,
|
||||
};
|
||||
|
||||
if let Some(rect) = icon_file.rect_of(sprite.icon_state, sprite.dir) {
|
||||
if let Some(rect) = icon_file.rect_of(&sprite.icon_state.into(), sprite.dir) {
|
||||
let pixel_x = sprite.ofs_x;
|
||||
let pixel_y = sprite.ofs_y + icon_file.metadata.height as i32;
|
||||
let loc = (
|
||||
((loc.0 - ctx.min.0 as u32) * TILE_SIZE) as i32 + pixel_x,
|
||||
((loc.1 + 1 - min_y as u32) * TILE_SIZE) as i32 - pixel_y,
|
||||
((x - ctx.min.0 as u32) * TILE_SIZE) as i32 + pixel_x,
|
||||
((y + 1 - min_y as u32) * TILE_SIZE) as i32 - pixel_y,
|
||||
);
|
||||
|
||||
if let Some((loc, rect)) = clip((map_image.width, map_image.height), loc, rect) {
|
||||
map_image.composite(&icon_file.image, loc, rect, sprite.color);
|
||||
}
|
||||
} else {
|
||||
let key = format!("bad icon: {:?}, state: {:?}", sprite.icon, sprite.icon_state);
|
||||
let key = format!(
|
||||
"bad icon: {:?}, state: {:?}",
|
||||
sprite.icon, sprite.icon_state
|
||||
);
|
||||
if !ctx.errors.read().unwrap().contains(&key) {
|
||||
eprintln!("{}", key);
|
||||
eprintln!("{key}");
|
||||
ctx.errors.write().unwrap().insert(key);
|
||||
}
|
||||
}
|
||||
|
|
@ -163,34 +184,28 @@ pub fn generate(ctx: Context, icon_cache: &IconCache) -> Result<Image, ()> {
|
|||
}
|
||||
|
||||
// OOB handling
|
||||
fn clip(bounds: (u32, u32), mut loc: (i32, i32), mut rect: (u32, u32, u32, u32)) -> Option<((u32, u32), (u32, u32, u32, u32))> {
|
||||
fn clip(
|
||||
bounds: dmi::Coordinate,
|
||||
mut loc: (i32, i32),
|
||||
mut rect: dmi::Rect,
|
||||
) -> Option<(dmi::Coordinate, dmi::Rect)> {
|
||||
if loc.0 < 0 {
|
||||
rect.0 += (-loc.0) as u32;
|
||||
match rect.2.checked_sub((-loc.0) as u32) {
|
||||
Some(s) => rect.2 = s,
|
||||
None => return None, // out of the viewport
|
||||
}
|
||||
rect.2 = rect.2.checked_sub((-loc.0) as u32)?;
|
||||
loc.0 = 0;
|
||||
}
|
||||
while loc.0 + rect.2 as i32 > bounds.0 as i32 {
|
||||
rect.2 -= 1;
|
||||
if rect.2 == 0 {
|
||||
return None;
|
||||
}
|
||||
let overhang = loc.0 + rect.2 as i32 - bounds.0 as i32;
|
||||
if overhang > 0 {
|
||||
rect.2 = rect.2.checked_sub(overhang as u32)?;
|
||||
}
|
||||
if loc.1 < 0 {
|
||||
rect.1 += (-loc.1) as u32;
|
||||
match rect.3.checked_sub((-loc.1) as u32) {
|
||||
Some(s) => rect.3 = s,
|
||||
None => return None, // out of the viewport
|
||||
}
|
||||
rect.3 = rect.3.checked_sub((-loc.1) as u32)?;
|
||||
loc.1 = 0;
|
||||
}
|
||||
while loc.1 + rect.3 as i32 > bounds.1 as i32 {
|
||||
rect.3 -= 1;
|
||||
if rect.3 == 0 {
|
||||
return None;
|
||||
}
|
||||
let overhang = loc.1 + rect.3 as i32 - bounds.1 as i32;
|
||||
if overhang > 0 {
|
||||
rect.3 = rect.3.checked_sub(overhang as u32)?;
|
||||
}
|
||||
Some(((loc.0 as u32, loc.1 as u32), rect))
|
||||
}
|
||||
|
|
@ -199,7 +214,7 @@ fn get_atom_list<'a>(
|
|||
objtree: &'a ObjectTree,
|
||||
prefabs: &'a [Prefab],
|
||||
render_passes: &[Box<dyn RenderPass>],
|
||||
errors: &RwLock<HashSet<String, RandomState>>,
|
||||
errors: &RwLock<HashSet<String>>,
|
||||
) -> Vec<Atom<'a>> {
|
||||
let mut result = Vec::new();
|
||||
|
||||
|
|
@ -216,11 +231,11 @@ fn get_atom_list<'a>(
|
|||
None => {
|
||||
let key = format!("bad path: {}", fab.path);
|
||||
if !errors.read().unwrap().contains(&key) {
|
||||
println!("{}", key);
|
||||
println!("{key}");
|
||||
errors.write().unwrap().insert(key);
|
||||
}
|
||||
continue;
|
||||
}
|
||||
},
|
||||
};
|
||||
|
||||
for pass in render_passes {
|
||||
|
|
@ -261,14 +276,14 @@ impl<'a> Atom<'a> {
|
|||
}
|
||||
|
||||
pub fn istype(&self, parent: &str) -> bool {
|
||||
subpath(&self.type_.path, parent)
|
||||
ispath(&self.type_.path, parent)
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> From<&'a Type> for Atom<'a> {
|
||||
fn from(type_: &'a Type) -> Self {
|
||||
Atom {
|
||||
type_: type_,
|
||||
type_,
|
||||
prefab: None,
|
||||
sprite: Sprite::default(),
|
||||
}
|
||||
|
|
@ -318,7 +333,8 @@ pub trait GetVar<'a> {
|
|||
fn get_path(&self) -> &str;
|
||||
|
||||
fn get_var(&self, key: &str, objtree: &'a ObjectTree) -> &'a Constant {
|
||||
self.get_var_inner(key, objtree).unwrap_or(Constant::null())
|
||||
self.get_var_inner(key, objtree)
|
||||
.unwrap_or_else(Constant::null)
|
||||
}
|
||||
|
||||
fn get_var_notnull(&self, key: &str, objtree: &'a ObjectTree) -> Option<&'a Constant> {
|
||||
|
|
@ -337,7 +353,7 @@ impl<'a> GetVar<'a> for Atom<'a> {
|
|||
}
|
||||
|
||||
fn get_var_inner(&self, key: &str, objtree: &'a ObjectTree) -> Option<&'a Constant> {
|
||||
if let Some(ref prefab) = self.prefab {
|
||||
if let Some(prefab) = self.prefab {
|
||||
if let Some(v) = prefab.get(key) {
|
||||
return Some(v);
|
||||
}
|
||||
|
|
@ -345,7 +361,7 @@ impl<'a> GetVar<'a> for Atom<'a> {
|
|||
let mut current = Some(self.type_);
|
||||
while let Some(t) = current.take() {
|
||||
if let Some(v) = t.vars.get(key) {
|
||||
return Some(v.value.constant.as_ref().unwrap_or(Constant::null()));
|
||||
return Some(v.value.constant.as_ref().unwrap_or_else(Constant::null));
|
||||
}
|
||||
current = objtree.parent_of(t);
|
||||
}
|
||||
|
|
@ -365,7 +381,7 @@ impl<'a> GetVar<'a> for &'a Prefab {
|
|||
let mut current = objtree.find(&self.path);
|
||||
while let Some(t) = current.take() {
|
||||
if let Some(v) = t.get().vars.get(key) {
|
||||
return Some(v.value.constant.as_ref().unwrap_or(Constant::null()));
|
||||
return Some(v.value.constant.as_ref().unwrap_or_else(Constant::null));
|
||||
}
|
||||
current = t.parent_type();
|
||||
}
|
||||
|
|
@ -382,7 +398,7 @@ impl<'a> GetVar<'a> for &'a Type {
|
|||
let mut current = Some(*self);
|
||||
while let Some(t) = current.take() {
|
||||
if let Some(v) = t.vars.get(key) {
|
||||
return Some(v.value.constant.as_ref().unwrap_or(Constant::null()));
|
||||
return Some(v.value.constant.as_ref().unwrap_or_else(Constant::null));
|
||||
}
|
||||
current = objtree.parent_of(t);
|
||||
}
|
||||
|
|
@ -399,7 +415,7 @@ impl<'a> GetVar<'a> for TypeRef<'a> {
|
|||
let mut current = Some(*self);
|
||||
while let Some(t) = current.take() {
|
||||
if let Some(v) = t.get().vars.get(key) {
|
||||
return Some(v.value.constant.as_ref().unwrap_or(Constant::null()));
|
||||
return Some(v.value.constant.as_ref().unwrap_or_else(Constant::null));
|
||||
}
|
||||
current = t.parent_type();
|
||||
}
|
||||
|
|
@ -410,46 +426,6 @@ impl<'a> GetVar<'a> for TypeRef<'a> {
|
|||
// ----------------------------------------------------------------------------
|
||||
// Renderer-agnostic sprite structure
|
||||
|
||||
/// Information about when a sprite should be shown or hidden.
|
||||
#[derive(Default, Debug, Clone, Copy, Eq, PartialEq, Hash)]
|
||||
pub struct Category {
|
||||
raw: u32,
|
||||
}
|
||||
|
||||
impl Category {
|
||||
const AREA: Category = Category { raw: 1 };
|
||||
const TURF: Category = Category { raw: 2 };
|
||||
const OBJ: Category = Category { raw: 3 };
|
||||
const MOB: Category = Category { raw: 4 };
|
||||
|
||||
///
|
||||
pub fn from_path(path: &str) -> Category {
|
||||
if path.starts_with("/area") {
|
||||
Category::AREA
|
||||
} else if path.starts_with("/turf") {
|
||||
Category::TURF
|
||||
} else if path.starts_with("/obj") {
|
||||
Category::OBJ
|
||||
} else if path.starts_with("/mob") {
|
||||
Category::MOB
|
||||
} else {
|
||||
Category { raw: 0 }
|
||||
}
|
||||
}
|
||||
|
||||
/// Encode this category for FFI representation.
|
||||
pub fn matches_basic_layers(self, visible: &[bool]) -> bool {
|
||||
visible.get(self.raw as usize).copied().unwrap_or(false)
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature="gfx_core")]
|
||||
impl gfx_core::shade::BaseTyped for Category {
|
||||
fn get_base_type() -> gfx_core::shade::BaseType {
|
||||
u32::get_base_type()
|
||||
}
|
||||
}
|
||||
|
||||
/// A guaranteed sortable representation of a `layer` float.
|
||||
#[derive(Default, Debug, Clone, Copy, Ord, PartialOrd, Eq, PartialEq, Hash)]
|
||||
pub struct Layer {
|
||||
|
|
@ -466,17 +442,23 @@ impl From<i16> for Layer {
|
|||
impl From<i32> for Layer {
|
||||
fn from(whole: i32) -> Layer {
|
||||
use std::convert::TryFrom;
|
||||
Layer { whole: i16::try_from(whole).expect("layer out of range"), frac: 0 }
|
||||
Layer {
|
||||
whole: i16::try_from(whole).expect("layer out of range"),
|
||||
frac: 0,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<f32> for Layer {
|
||||
fn from(f: f32) -> Layer {
|
||||
Layer { whole: f.floor() as i16, frac: ((f.fract() + 1.).fract() * 65536.) as u16 }
|
||||
Layer {
|
||||
whole: f.floor() as i16,
|
||||
frac: ((f.fract() + 1.).fract() * 65536.) as u16,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature="gfx_core")]
|
||||
#[cfg(feature = "gfx_core")]
|
||||
impl gfx_core::shade::BaseTyped for Layer {
|
||||
fn get_base_type() -> gfx_core::shade::BaseType {
|
||||
i32::get_base_type()
|
||||
|
|
@ -489,18 +471,15 @@ impl gfx_core::shade::BaseTyped for Layer {
|
|||
/// overlays.
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct Sprite<'s> {
|
||||
// filtering
|
||||
pub category: Category,
|
||||
|
||||
// visual appearance
|
||||
pub icon: &'s str,
|
||||
pub icon_state: &'s str,
|
||||
pub dir: Dir,
|
||||
pub color: [u8; 4], // [r, g, b, a]
|
||||
pub color: [u8; 4], // [r, g, b, a]
|
||||
|
||||
// position
|
||||
pub ofs_x: i32, // pixel_x + pixel_w + step_x
|
||||
pub ofs_y: i32, // pixel_y + pixel_z + step_y
|
||||
pub ofs_x: i32, // pixel_x + pixel_w + step_x
|
||||
pub ofs_y: i32, // pixel_y + pixel_z + step_y
|
||||
|
||||
// sorting
|
||||
pub plane: i32,
|
||||
|
|
@ -517,10 +496,13 @@ impl<'s> Sprite<'s> {
|
|||
let step_y = vars.get_var("step_y", objtree).to_int().unwrap_or(0);
|
||||
|
||||
Sprite {
|
||||
category: Category::from_path(vars.get_path()),
|
||||
icon: vars.get_var("icon", objtree).as_path_str().unwrap_or(""),
|
||||
icon_state: vars.get_var("icon_state", objtree).as_str().unwrap_or(""),
|
||||
dir: vars.get_var("dir", objtree).to_int().and_then(Dir::from_int).unwrap_or(Dir::default()),
|
||||
dir: vars
|
||||
.get_var("dir", objtree)
|
||||
.to_int()
|
||||
.and_then(Dir::from_int)
|
||||
.unwrap_or_default(),
|
||||
color: color_of(objtree, vars),
|
||||
ofs_x: pixel_x + pixel_w + step_x,
|
||||
ofs_y: pixel_y + pixel_z + step_y,
|
||||
|
|
@ -533,7 +515,6 @@ impl<'s> Sprite<'s> {
|
|||
impl<'s> Default for Sprite<'s> {
|
||||
fn default() -> Self {
|
||||
Sprite {
|
||||
category: Category::default(),
|
||||
icon: "",
|
||||
icon_state: "",
|
||||
dir: Dir::default(),
|
||||
|
|
@ -552,7 +533,7 @@ fn plane_of<'s, T: GetVar<'s> + ?Sized>(objtree: &'s ObjectTree, atom: &T) -> i3
|
|||
other => {
|
||||
eprintln!("not a plane: {:?} on {:?}", other, atom.get_path());
|
||||
0
|
||||
}
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -562,25 +543,27 @@ pub(crate) fn layer_of<'s, T: GetVar<'s> + ?Sized>(objtree: &'s ObjectTree, atom
|
|||
other => {
|
||||
eprintln!("not a layer: {:?} on {:?}", other, atom.get_path());
|
||||
Layer::from(2)
|
||||
}
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
pub fn color_of<'s, T: GetVar<'s> + ?Sized>(objtree: &'s ObjectTree, atom: &T) -> [u8; 4] {
|
||||
let alpha = match atom.get_var("alpha", objtree) {
|
||||
&Constant::Float(i) if i >= 0. && i <= 255. => i as u8,
|
||||
&Constant::Float(i) if (0. ..=255.).contains(&i) => i as u8,
|
||||
_ => 255,
|
||||
};
|
||||
|
||||
match atom.get_var("color", objtree) {
|
||||
&Constant::String(ref color) if color.starts_with("#") => {
|
||||
match *atom.get_var("color", objtree) {
|
||||
Constant::String(ref color) if color.starts_with('#') => {
|
||||
let mut sum = 0;
|
||||
for ch in color[1..color.len()].chars() {
|
||||
sum = 16 * sum + ch.to_digit(16).unwrap_or(0);
|
||||
}
|
||||
if color.len() == 7 { // #rrggbb
|
||||
if color.len() == 7 {
|
||||
// #rrggbb
|
||||
[(sum >> 16) as u8, (sum >> 8) as u8, sum as u8, alpha]
|
||||
} else if color.len() == 4 { // #rgb
|
||||
} else if color.len() == 4 {
|
||||
// #rgb
|
||||
[
|
||||
(0x11 * ((sum >> 8) & 0xf)) as u8,
|
||||
(0x11 * ((sum >> 4) & 0xf)) as u8,
|
||||
|
|
@ -588,13 +571,13 @@ pub fn color_of<'s, T: GetVar<'s> + ?Sized>(objtree: &'s ObjectTree, atom: &T) -
|
|||
alpha,
|
||||
]
|
||||
} else {
|
||||
[255, 255, 255, alpha] // invalid
|
||||
[255, 255, 255, alpha] // invalid
|
||||
}
|
||||
}
|
||||
&Constant::String(ref color) => match html_color(color) {
|
||||
},
|
||||
Constant::String(ref color) => match html_color(color) {
|
||||
Some([r, g, b]) => [r, g, b, alpha],
|
||||
None => [255, 255, 255, alpha],
|
||||
}
|
||||
},
|
||||
// TODO: color matrix support?
|
||||
_ => [255, 255, 255, alpha],
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,9 +1,9 @@
|
|||
//! Port of icon smoothing subsystem.
|
||||
|
||||
use dm::objtree::ObjectTree;
|
||||
use dm::constants::Constant;
|
||||
use crate::dmi::Dir;
|
||||
use crate::minimap::{Sprite, Atom, GetVar, Neighborhood};
|
||||
use crate::minimap::{Atom, GetVar, Neighborhood, Sprite};
|
||||
use dm::constants::Constant;
|
||||
use dm::objtree::ObjectTree;
|
||||
|
||||
use super::RenderPass;
|
||||
|
||||
|
|
@ -17,10 +17,10 @@ const N_NORTHWEST: i32 = 512;
|
|||
const N_SOUTHEAST: i32 = 64;
|
||||
const N_SOUTHWEST: i32 = 1024;
|
||||
|
||||
const SMOOTH_TRUE: i32 = 1; // smooth with exact specified types or just itself
|
||||
const SMOOTH_MORE: i32 = 2; // smooth with all subtypes thereof
|
||||
const SMOOTH_DIAGONAL: i32 = 4; // smooth diagonally
|
||||
const SMOOTH_BORDER: i32 = 8; // smooth with the borders of the map
|
||||
const SMOOTH_TRUE: i32 = 1; // smooth with exact specified types or just itself
|
||||
const SMOOTH_MORE: i32 = 2; // smooth with all subtypes thereof
|
||||
const SMOOTH_DIAGONAL: i32 = 4; // smooth diagonally
|
||||
const SMOOTH_BORDER: i32 = 8; // smooth with the borders of the map
|
||||
|
||||
pub struct IconSmoothing {
|
||||
pub mask: i32,
|
||||
|
|
@ -33,7 +33,8 @@ impl Default for IconSmoothing {
|
|||
}
|
||||
|
||||
impl RenderPass for IconSmoothing {
|
||||
fn adjust_sprite<'a>(&self,
|
||||
fn adjust_sprite<'a>(
|
||||
&self,
|
||||
atom: &Atom<'a>,
|
||||
sprite: &mut Sprite<'a>,
|
||||
_objtree: &'a ObjectTree,
|
||||
|
|
@ -45,7 +46,8 @@ impl RenderPass for IconSmoothing {
|
|||
}
|
||||
}
|
||||
|
||||
fn neighborhood_appearance<'a>(&self,
|
||||
fn neighborhood_appearance<'a>(
|
||||
&self,
|
||||
atom: &Atom<'a>,
|
||||
objtree: &'a ObjectTree,
|
||||
neighborhood: &Neighborhood<'a, '_>,
|
||||
|
|
@ -67,7 +69,12 @@ impl RenderPass for IconSmoothing {
|
|||
}
|
||||
}
|
||||
|
||||
fn calculate_adjacencies(objtree: &ObjectTree, neighborhood: &Neighborhood, atom: &Atom, smooth_flags: i32) -> i32 {
|
||||
fn calculate_adjacencies(
|
||||
objtree: &ObjectTree,
|
||||
neighborhood: &Neighborhood,
|
||||
atom: &Atom,
|
||||
smooth_flags: i32,
|
||||
) -> i32 {
|
||||
// TODO: anchored check
|
||||
|
||||
let mut adjacencies = 0;
|
||||
|
|
@ -103,29 +110,37 @@ fn calculate_adjacencies(objtree: &ObjectTree, neighborhood: &Neighborhood, atom
|
|||
adjacencies
|
||||
}
|
||||
|
||||
fn find_type_in_direction(objtree: &ObjectTree, adjacency: &Neighborhood, source: &Atom, direction: Dir, smooth_flags: i32) -> bool {
|
||||
fn find_type_in_direction(
|
||||
objtree: &ObjectTree,
|
||||
adjacency: &Neighborhood,
|
||||
source: &Atom,
|
||||
direction: Dir,
|
||||
smooth_flags: i32,
|
||||
) -> bool {
|
||||
let atom_list = adjacency.offset(direction);
|
||||
if atom_list.is_empty() {
|
||||
return smooth_flags & SMOOTH_BORDER != 0;
|
||||
}
|
||||
|
||||
match source.get_var("canSmoothWith", objtree) {
|
||||
&Constant::List(ref elements) => if smooth_flags & SMOOTH_MORE != 0 {
|
||||
// smooth with canSmoothWith + subtypes
|
||||
for atom in atom_list {
|
||||
let mut path = atom.get_path();
|
||||
while !path.is_empty() {
|
||||
if smoothlist_contains(elements, path) {
|
||||
Constant::List(elements) => {
|
||||
if smooth_flags & SMOOTH_MORE != 0 {
|
||||
// smooth with canSmoothWith + subtypes
|
||||
for atom in atom_list {
|
||||
let mut path = atom.get_path();
|
||||
while !path.is_empty() {
|
||||
if smoothlist_contains(elements, path) {
|
||||
return true;
|
||||
}
|
||||
path = &path[..path.rfind('/').unwrap()];
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// smooth only with exact types in canSmoothWith
|
||||
for atom in atom_list {
|
||||
if smoothlist_contains(elements, atom.get_path()) {
|
||||
return true;
|
||||
}
|
||||
path = &path[..path.rfind("/").unwrap()];
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// smooth only with exact types in canSmoothWith
|
||||
for atom in atom_list {
|
||||
if smoothlist_contains(elements, atom.get_path()) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
},
|
||||
|
|
@ -142,16 +157,21 @@ fn find_type_in_direction(objtree: &ObjectTree, adjacency: &Neighborhood, source
|
|||
}
|
||||
|
||||
fn smoothlist_contains(list: &[(Constant, Option<Constant>)], desired: &str) -> bool {
|
||||
for &(ref key, _) in list {
|
||||
// TODO: be more specific than to_string
|
||||
if key.to_string() == desired {
|
||||
for (key, _) in list {
|
||||
if key == desired {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
false
|
||||
}
|
||||
|
||||
fn cardinal_smooth<'a>(output: &mut Vec<Sprite<'a>>, objtree: &'a ObjectTree, bump: &'a bumpalo::Bump, source: &Atom<'a>, adjacencies: i32) {
|
||||
fn cardinal_smooth<'a>(
|
||||
output: &mut Vec<Sprite<'a>>,
|
||||
objtree: &'a ObjectTree,
|
||||
bump: &'a bumpalo::Bump,
|
||||
source: &Atom<'a>,
|
||||
adjacencies: i32,
|
||||
) {
|
||||
for &(what, f1, n1, f2, n2, f3) in &[
|
||||
("1", N_NORTH, "n", N_WEST, "w", N_NORTHWEST),
|
||||
("2", N_NORTH, "n", N_EAST, "e", N_NORTHEAST),
|
||||
|
|
@ -174,7 +194,7 @@ fn cardinal_smooth<'a>(output: &mut Vec<Sprite<'a>>, objtree: &'a ObjectTree, bu
|
|||
|
||||
let mut sprite = Sprite {
|
||||
icon_state: name.into_bump_str(),
|
||||
.. source.sprite
|
||||
..source.sprite
|
||||
};
|
||||
if let Some(icon) = source.get_var("smooth_icon", objtree).as_path_str() {
|
||||
sprite.icon = icon;
|
||||
|
|
@ -183,7 +203,14 @@ fn cardinal_smooth<'a>(output: &mut Vec<Sprite<'a>>, objtree: &'a ObjectTree, bu
|
|||
}
|
||||
}
|
||||
|
||||
fn diagonal_smooth<'a>(output: &mut Vec<Sprite<'a>>, objtree: &'a ObjectTree, bump: &'a bumpalo::Bump, neighborhood: &Neighborhood<'a, '_>, source: &Atom<'a>, adjacencies: i32) {
|
||||
fn diagonal_smooth<'a>(
|
||||
output: &mut Vec<Sprite<'a>>,
|
||||
objtree: &'a ObjectTree,
|
||||
bump: &'a bumpalo::Bump,
|
||||
neighborhood: &Neighborhood<'a, '_>,
|
||||
source: &Atom<'a>,
|
||||
adjacencies: i32,
|
||||
) {
|
||||
let presets = if adjacencies == N_NORTH | N_WEST {
|
||||
["d-se", "d-se-0"]
|
||||
} else if adjacencies == N_NORTH | N_EAST {
|
||||
|
|
@ -212,7 +239,10 @@ fn diagonal_smooth<'a>(output: &mut Vec<Sprite<'a>>, objtree: &'a ObjectTree, bu
|
|||
.index(&Constant::string("space"))
|
||||
.is_some()
|
||||
{
|
||||
output.push(Sprite::from_vars(objtree, &objtree.expect("/turf/open/space/basic")));
|
||||
output.push(Sprite::from_vars(
|
||||
objtree,
|
||||
&objtree.expect("/turf/open/space/basic"),
|
||||
));
|
||||
} else {
|
||||
let dir = reverse_ndir(adjacencies).flip();
|
||||
let mut needs_plating = true;
|
||||
|
|
@ -228,7 +258,10 @@ fn diagonal_smooth<'a>(output: &mut Vec<Sprite<'a>>, objtree: &'a ObjectTree, bu
|
|||
}
|
||||
}
|
||||
if needs_plating {
|
||||
output.push(Sprite::from_vars(objtree, &objtree.expect("/turf/open/floor/plating")));
|
||||
output.push(Sprite::from_vars(
|
||||
objtree,
|
||||
&objtree.expect("/turf/open/floor/plating"),
|
||||
));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -237,7 +270,7 @@ fn diagonal_smooth<'a>(output: &mut Vec<Sprite<'a>>, objtree: &'a ObjectTree, bu
|
|||
for &each in presets.iter() {
|
||||
let mut copy = Sprite {
|
||||
icon_state: each,
|
||||
.. source.sprite
|
||||
..source.sprite
|
||||
};
|
||||
if let Some(icon) = source.get_var("smooth_icon", objtree).as_path_str() {
|
||||
copy.icon = icon;
|
||||
|
|
|
|||
|
|
@ -4,10 +4,11 @@
|
|||
//! followed by
|
||||
//! https://github.com/tgstation/tgstation/pull/53906
|
||||
|
||||
use dm::objtree::ObjectTree;
|
||||
use dm::constants::Constant;
|
||||
use crate::dmi::Dir;
|
||||
use crate::minimap::{Sprite, Atom, GetVar, Neighborhood};
|
||||
use crate::minimap::{Atom, GetVar, Neighborhood, Sprite};
|
||||
use dm::constants::Constant;
|
||||
use dm::objtree::ObjectTree;
|
||||
use foldhash::HashSet;
|
||||
|
||||
use super::RenderPass;
|
||||
|
||||
|
|
@ -40,7 +41,8 @@ impl Default for IconSmoothing {
|
|||
}
|
||||
|
||||
impl RenderPass for IconSmoothing {
|
||||
fn adjust_sprite<'a>(&self,
|
||||
fn adjust_sprite<'a>(
|
||||
&self,
|
||||
atom: &Atom<'a>,
|
||||
sprite: &mut Sprite<'a>,
|
||||
_objtree: &'a ObjectTree,
|
||||
|
|
@ -52,14 +54,19 @@ impl RenderPass for IconSmoothing {
|
|||
}
|
||||
}
|
||||
|
||||
fn neighborhood_appearance<'a>(&self,
|
||||
fn neighborhood_appearance<'a>(
|
||||
&self,
|
||||
atom: &Atom<'a>,
|
||||
objtree: &'a ObjectTree,
|
||||
neighborhood: &Neighborhood<'a, '_>,
|
||||
output: &mut Vec<Sprite<'a>>,
|
||||
bump: &'a bumpalo::Bump,
|
||||
) -> bool {
|
||||
let smooth_flags = self.mask & atom.get_var("smoothing_flags", objtree).to_int().unwrap_or(0);
|
||||
let smooth_flags = self.mask
|
||||
& atom
|
||||
.get_var("smoothing_flags", objtree)
|
||||
.to_int()
|
||||
.unwrap_or(0);
|
||||
if smooth_flags & SMOOTH_CORNERS != 0 {
|
||||
let adjacencies = calculate_adjacencies(objtree, neighborhood, atom, smooth_flags);
|
||||
if smooth_flags & SMOOTH_DIAGONAL_CORNERS != 0 {
|
||||
|
|
@ -70,7 +77,15 @@ impl RenderPass for IconSmoothing {
|
|||
false
|
||||
} else if smooth_flags & SMOOTH_BITMASK != 0 {
|
||||
let adjacencies = calculate_adjacencies(objtree, neighborhood, atom, smooth_flags);
|
||||
bitmask_smooth(output, objtree, bump, neighborhood, atom, adjacencies, smooth_flags)
|
||||
bitmask_smooth(
|
||||
output,
|
||||
objtree,
|
||||
bump,
|
||||
neighborhood,
|
||||
atom,
|
||||
adjacencies,
|
||||
smooth_flags,
|
||||
)
|
||||
} else {
|
||||
true
|
||||
}
|
||||
|
|
@ -80,9 +95,18 @@ impl RenderPass for IconSmoothing {
|
|||
// ----------------------------------------------------------------------------
|
||||
// Older cardinal smoothing system
|
||||
|
||||
fn calculate_adjacencies(objtree: &ObjectTree, neighborhood: &Neighborhood, atom: &Atom, smooth_flags: i32) -> i32 {
|
||||
fn calculate_adjacencies(
|
||||
objtree: &ObjectTree,
|
||||
neighborhood: &Neighborhood,
|
||||
atom: &Atom,
|
||||
smooth_flags: i32,
|
||||
) -> i32 {
|
||||
// Easier to read as a nested conditional
|
||||
#[allow(clippy::collapsible_if)]
|
||||
if atom.istype("/atom/movable/") {
|
||||
if atom.get_var("can_be_unanchored", objtree).to_bool() && !atom.get_var("anchored", objtree).to_bool() {
|
||||
if atom.get_var("can_be_unanchored", objtree).to_bool()
|
||||
&& !atom.get_var("anchored", objtree).to_bool()
|
||||
{
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
|
|
@ -121,19 +145,25 @@ fn calculate_adjacencies(objtree: &ObjectTree, neighborhood: &Neighborhood, atom
|
|||
adjacencies
|
||||
}
|
||||
|
||||
fn find_type_in_direction(objtree: &ObjectTree, adjacency: &Neighborhood, source: &Atom, direction: Dir, smooth_flags: i32) -> bool {
|
||||
fn find_type_in_direction(
|
||||
objtree: &ObjectTree,
|
||||
adjacency: &Neighborhood,
|
||||
source: &Atom,
|
||||
direction: Dir,
|
||||
smooth_flags: i32,
|
||||
) -> bool {
|
||||
let atom_list = adjacency.offset(direction);
|
||||
if atom_list.is_empty() {
|
||||
return smooth_flags & SMOOTH_BORDER != 0;
|
||||
}
|
||||
|
||||
match source.get_var("canSmoothWith", objtree) {
|
||||
&Constant::List(ref elements) => {
|
||||
Constant::List(elements) => {
|
||||
// smooth with anything for which their smoothing_groups overlaps our canSmoothWith
|
||||
let set: std::collections::HashSet<_> = elements.iter().map(|x| &x.0).collect();
|
||||
let set: HashSet<_> = elements.iter().map(|x| &x.0).collect();
|
||||
for atom in atom_list {
|
||||
if let &Constant::List(ref elements2) = atom.get_var("smoothing_groups", objtree) {
|
||||
let set2: std::collections::HashSet<_> = elements2.iter().map(|x| &x.0).collect();
|
||||
if let Constant::List(elements2) = atom.get_var("smoothing_groups", objtree) {
|
||||
let set2: HashSet<_> = elements2.iter().map(|x| &x.0).collect();
|
||||
if set.intersection(&set2).next().is_some() {
|
||||
return true;
|
||||
}
|
||||
|
|
@ -152,12 +182,46 @@ fn find_type_in_direction(objtree: &ObjectTree, adjacency: &Neighborhood, source
|
|||
false
|
||||
}
|
||||
|
||||
fn cardinal_smooth<'a>(output: &mut Vec<Sprite<'a>>, objtree: &'a ObjectTree, bump: &'a bumpalo::Bump, source: &Atom<'a>, adjacencies: i32) {
|
||||
fn cardinal_smooth<'a>(
|
||||
output: &mut Vec<Sprite<'a>>,
|
||||
objtree: &'a ObjectTree,
|
||||
bump: &'a bumpalo::Bump,
|
||||
source: &Atom<'a>,
|
||||
adjacencies: i32,
|
||||
) {
|
||||
for &(what, f1, n1, f2, n2, f3) in &[
|
||||
("1", NORTH_JUNCTION, "n", WEST_JUNCTION, "w", NORTHWEST_JUNCTION),
|
||||
("2", NORTH_JUNCTION, "n", EAST_JUNCTION, "e", NORTHEAST_JUNCTION),
|
||||
("3", SOUTH_JUNCTION, "s", WEST_JUNCTION, "w", SOUTHWEST_JUNCTION),
|
||||
("4", SOUTH_JUNCTION, "s", EAST_JUNCTION, "e", SOUTHEAST_JUNCTION),
|
||||
(
|
||||
"1",
|
||||
NORTH_JUNCTION,
|
||||
"n",
|
||||
WEST_JUNCTION,
|
||||
"w",
|
||||
NORTHWEST_JUNCTION,
|
||||
),
|
||||
(
|
||||
"2",
|
||||
NORTH_JUNCTION,
|
||||
"n",
|
||||
EAST_JUNCTION,
|
||||
"e",
|
||||
NORTHEAST_JUNCTION,
|
||||
),
|
||||
(
|
||||
"3",
|
||||
SOUTH_JUNCTION,
|
||||
"s",
|
||||
WEST_JUNCTION,
|
||||
"w",
|
||||
SOUTHWEST_JUNCTION,
|
||||
),
|
||||
(
|
||||
"4",
|
||||
SOUTH_JUNCTION,
|
||||
"s",
|
||||
EAST_JUNCTION,
|
||||
"e",
|
||||
SOUTHEAST_JUNCTION,
|
||||
),
|
||||
] {
|
||||
let name = if (adjacencies & f1 != 0) && (adjacencies & f2 != 0) {
|
||||
if (adjacencies & f3) != 0 {
|
||||
|
|
@ -175,7 +239,7 @@ fn cardinal_smooth<'a>(output: &mut Vec<Sprite<'a>>, objtree: &'a ObjectTree, bu
|
|||
|
||||
let mut sprite = Sprite {
|
||||
icon_state: name.into_bump_str(),
|
||||
.. source.sprite
|
||||
..source.sprite
|
||||
};
|
||||
if let Some(icon) = source.get_var("smooth_icon", objtree).as_path_str() {
|
||||
sprite.icon = icon;
|
||||
|
|
@ -184,7 +248,14 @@ fn cardinal_smooth<'a>(output: &mut Vec<Sprite<'a>>, objtree: &'a ObjectTree, bu
|
|||
}
|
||||
}
|
||||
|
||||
fn diagonal_smooth<'a>(output: &mut Vec<Sprite<'a>>, objtree: &'a ObjectTree, bump: &'a bumpalo::Bump, neighborhood: &Neighborhood<'a, '_>, source: &Atom<'a>, adjacencies: i32) {
|
||||
fn diagonal_smooth<'a>(
|
||||
output: &mut Vec<Sprite<'a>>,
|
||||
objtree: &'a ObjectTree,
|
||||
bump: &'a bumpalo::Bump,
|
||||
neighborhood: &Neighborhood<'a, '_>,
|
||||
source: &Atom<'a>,
|
||||
adjacencies: i32,
|
||||
) {
|
||||
let presets = if adjacencies == NORTH_JUNCTION | WEST_JUNCTION {
|
||||
["d-se", "d-se-0"]
|
||||
} else if adjacencies == NORTH_JUNCTION | EAST_JUNCTION {
|
||||
|
|
@ -214,7 +285,7 @@ fn diagonal_smooth<'a>(output: &mut Vec<Sprite<'a>>, objtree: &'a ObjectTree, bu
|
|||
for &each in presets.iter() {
|
||||
let mut copy = Sprite {
|
||||
icon_state: each,
|
||||
.. source.sprite
|
||||
..source.sprite
|
||||
};
|
||||
if let Some(icon) = source.get_var("smooth_icon", objtree).as_path_str() {
|
||||
copy.icon = icon;
|
||||
|
|
@ -223,14 +294,23 @@ fn diagonal_smooth<'a>(output: &mut Vec<Sprite<'a>>, objtree: &'a ObjectTree, bu
|
|||
}
|
||||
}
|
||||
|
||||
fn diagonal_underlay<'a>(output: &mut Vec<Sprite<'a>>, objtree: &'a ObjectTree, neighborhood: &Neighborhood<'a, '_>, source: &Atom<'a>, adjacencies: i32) {
|
||||
fn diagonal_underlay<'a>(
|
||||
output: &mut Vec<Sprite<'a>>,
|
||||
objtree: &'a ObjectTree,
|
||||
neighborhood: &Neighborhood<'a, '_>,
|
||||
source: &Atom<'a>,
|
||||
adjacencies: i32,
|
||||
) {
|
||||
// BYOND memes
|
||||
if source
|
||||
.get_var("fixed_underlay", objtree)
|
||||
.index(&Constant::string("space"))
|
||||
.is_some()
|
||||
{
|
||||
output.push(Sprite::from_vars(objtree, &objtree.expect("/turf/open/space/basic")));
|
||||
output.push(Sprite::from_vars(
|
||||
objtree,
|
||||
&objtree.expect("/turf/open/space/basic"),
|
||||
));
|
||||
} else if let Some(dir) = reverse_ndir(adjacencies) {
|
||||
let dir = dir.flip();
|
||||
let mut needs_plating = true;
|
||||
|
|
@ -246,7 +326,10 @@ fn diagonal_underlay<'a>(output: &mut Vec<Sprite<'a>>, objtree: &'a ObjectTree,
|
|||
}
|
||||
}
|
||||
if needs_plating {
|
||||
output.push(Sprite::from_vars(objtree, &objtree.expect("/turf/open/floor/plating")));
|
||||
output.push(Sprite::from_vars(
|
||||
objtree,
|
||||
&objtree.expect("/turf/open/floor/plating"),
|
||||
));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -288,18 +371,27 @@ fn bitmask_smooth<'a>(
|
|||
) -> bool {
|
||||
let mut diagonal = "";
|
||||
if source.istype("/turf/open/floor/") {
|
||||
if source.get_var("broken", objtree).to_bool() || source.get_var("burnt", objtree).to_bool() {
|
||||
return true; // use original appearance
|
||||
if source.get_var("broken", objtree).to_bool() || source.get_var("burnt", objtree).to_bool()
|
||||
{
|
||||
return true; // use original appearance
|
||||
}
|
||||
} else if source.istype("/turf/closed/") && (smooth_flags & SMOOTH_DIAGONAL_CORNERS != 0) && reverse_ndir(smoothing_junction).is_some() {
|
||||
} else if source.istype("/turf/closed/")
|
||||
&& (smooth_flags & SMOOTH_DIAGONAL_CORNERS != 0)
|
||||
&& reverse_ndir(smoothing_junction).is_some()
|
||||
{
|
||||
diagonal_underlay(output, objtree, neighborhood, source, smoothing_junction);
|
||||
diagonal = "-d";
|
||||
}
|
||||
|
||||
let base_icon_state = source.get_var("base_icon_state", objtree).as_str().unwrap_or("");
|
||||
let base_icon_state = source
|
||||
.get_var("base_icon_state", objtree)
|
||||
.as_str()
|
||||
.unwrap_or("");
|
||||
let mut sprite = Sprite {
|
||||
icon_state: bumpalo::format!(in bump, "{}-{}{}", base_icon_state, smoothing_junction, diagonal).into_bump_str(),
|
||||
.. source.sprite
|
||||
icon_state:
|
||||
bumpalo::format!(in bump, "{}-{}{}", base_icon_state, smoothing_junction, diagonal)
|
||||
.into_bump_str(),
|
||||
..source.sprite
|
||||
};
|
||||
if let Some(icon) = source.get_var("smooth_icon", objtree).as_path_str() {
|
||||
sprite.icon = icon;
|
||||
|
|
|
|||
|
|
@ -1,20 +1,20 @@
|
|||
use dm::objtree::*;
|
||||
use crate::minimap::{Atom, GetVar, Layer, Neighborhood, Sprite};
|
||||
use dm::constants::Constant;
|
||||
use crate::minimap::{Atom, GetVar, Sprite, Layer, Neighborhood};
|
||||
use dm::objtree::*;
|
||||
|
||||
mod transit_tube;
|
||||
mod random;
|
||||
mod structures;
|
||||
mod icon_smoothing;
|
||||
mod icon_smoothing_2020;
|
||||
mod random;
|
||||
mod smart_cables;
|
||||
mod structures;
|
||||
mod transit_tube;
|
||||
|
||||
pub use self::transit_tube::TransitTube;
|
||||
pub use self::random::Random;
|
||||
pub use self::structures::{GravityGen, Spawners};
|
||||
pub use self::icon_smoothing::IconSmoothing as IconSmoothing2016;
|
||||
pub use self::icon_smoothing_2020::IconSmoothing;
|
||||
pub use self::random::Random;
|
||||
pub use self::smart_cables::SmartCables;
|
||||
pub use self::structures::{GravityGen, Spawners};
|
||||
pub use self::transit_tube::TransitTube;
|
||||
|
||||
/// A map rendering pass.
|
||||
///
|
||||
|
|
@ -26,61 +26,69 @@ pub trait RenderPass: Sync {
|
|||
fn configure(&mut self, renderer_config: &dm::config::MapRenderer) {}
|
||||
|
||||
/// Filter atoms based solely on their typepath.
|
||||
fn path_filter(&self,
|
||||
path: &str,
|
||||
) -> bool { true }
|
||||
fn path_filter(&self, path: &str) -> bool {
|
||||
true
|
||||
}
|
||||
|
||||
/// Filter atoms at the beginning of the process.
|
||||
///
|
||||
/// Return `false` to discard the atom.
|
||||
fn early_filter(&self,
|
||||
atom: &Atom,
|
||||
objtree: &ObjectTree,
|
||||
) -> bool { true }
|
||||
fn early_filter(&self, atom: &Atom, objtree: &ObjectTree) -> bool {
|
||||
true
|
||||
}
|
||||
|
||||
/// Expand atoms, such as spawners into the atoms they spawn.
|
||||
///
|
||||
/// Return `false` to discard the original atom.
|
||||
fn expand<'a>(&self,
|
||||
fn expand<'a>(
|
||||
&self,
|
||||
atom: &Atom<'a>,
|
||||
objtree: &'a ObjectTree,
|
||||
output: &mut Vec<Atom<'a>>,
|
||||
) -> bool { true }
|
||||
) -> bool {
|
||||
true
|
||||
}
|
||||
|
||||
fn adjust_sprite<'a>(&self,
|
||||
fn adjust_sprite<'a>(
|
||||
&self,
|
||||
atom: &Atom<'a>,
|
||||
sprite: &mut Sprite<'a>,
|
||||
objtree: &'a ObjectTree,
|
||||
bump: &'a bumpalo::Bump, // TODO: kind of a hacky way to pass this
|
||||
) {}
|
||||
bump: &'a bumpalo::Bump, // TODO: kind of a hacky way to pass this
|
||||
) {
|
||||
}
|
||||
|
||||
/// Apply overlays and underlays to an atom, in the form of pseudo-atoms.
|
||||
fn overlays<'a>(&self,
|
||||
fn overlays<'a>(
|
||||
&self,
|
||||
atom: &Atom<'a>,
|
||||
objtree: &'a ObjectTree,
|
||||
underlays: &mut Vec<Sprite<'a>>,
|
||||
overlays: &mut Vec<Sprite<'a>>,
|
||||
bump: &'a bumpalo::Bump, // TODO: kind of a hacky way to pass this
|
||||
) {}
|
||||
bump: &'a bumpalo::Bump, // TODO: kind of a hacky way to pass this
|
||||
) {
|
||||
}
|
||||
|
||||
fn neighborhood_appearance<'a>(&self,
|
||||
fn neighborhood_appearance<'a>(
|
||||
&self,
|
||||
atom: &Atom<'a>,
|
||||
objtree: &'a ObjectTree,
|
||||
neighborhood: &Neighborhood<'a, '_>,
|
||||
output: &mut Vec<Sprite<'a>>,
|
||||
bump: &'a bumpalo::Bump, // TODO: kind of a hacky way to pass this
|
||||
) -> bool { true }
|
||||
bump: &'a bumpalo::Bump, // TODO: kind of a hacky way to pass this
|
||||
) -> bool {
|
||||
true
|
||||
}
|
||||
|
||||
/// Filter atoms at the end of the process, after they have been taken into
|
||||
/// account by their neighbors.
|
||||
fn late_filter(&self,
|
||||
atom: &Atom,
|
||||
objtree: &ObjectTree,
|
||||
) -> bool { true }
|
||||
fn late_filter(&self, atom: &Atom, objtree: &ObjectTree) -> bool {
|
||||
true
|
||||
}
|
||||
|
||||
fn sprite_filter(&self,
|
||||
sprite: &Sprite,
|
||||
) -> bool { true }
|
||||
fn sprite_filter(&self, sprite: &Sprite) -> bool {
|
||||
true
|
||||
}
|
||||
}
|
||||
|
||||
pub struct RenderPassInfo {
|
||||
|
|
@ -91,40 +99,120 @@ pub struct RenderPassInfo {
|
|||
}
|
||||
|
||||
macro_rules! pass {
|
||||
($typ:ty, $name:expr, $desc:expr, $def:expr) => (RenderPassInfo {
|
||||
name: $name,
|
||||
desc: $desc,
|
||||
default: $def,
|
||||
new: || Box::new(<$typ>::default())
|
||||
})
|
||||
($typ:ty, $name:expr, $desc:expr, $def:expr) => {
|
||||
RenderPassInfo {
|
||||
name: $name,
|
||||
desc: $desc,
|
||||
default: $def,
|
||||
new: || Box::<$typ>::default(),
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
pub const RENDER_PASSES: &[RenderPassInfo] = &[
|
||||
pass!(HideSpace, "hide-space", "Do not render space tiles, instead leaving transparency.", true),
|
||||
pass!(
|
||||
HideSpace,
|
||||
"hide-space",
|
||||
"Do not render space tiles, instead leaving transparency.",
|
||||
true
|
||||
),
|
||||
pass!(HideAreas, "hide-areas", "Do not render area icons.", true),
|
||||
pass!(HideInvisible, "hide-invisible", "Do not render invisible or ephemeral objects such as mapping helpers.", true),
|
||||
pass!(Random, "random", "Replace random spawners with one of their possibilities.", true),
|
||||
pass!(Pretty, "pretty", "Add the minor cosmetic overlays for various objects.", true),
|
||||
pass!(Spawners, "spawners", "Replace object spawners with their spawned objects.", true),
|
||||
pass!(Overlays, "overlays", "Add overlays and underlays to atoms which usually have them.", true),
|
||||
pass!(TransitTube, "transit-tube", "Add overlays to connect transit tubes together.", true),
|
||||
pass!(GravityGen, "gravity-gen", "Expand the gravity generator to the full structure.", true),
|
||||
pass!(
|
||||
HideInvisible,
|
||||
"hide-invisible",
|
||||
"Do not render invisible or ephemeral objects such as mapping helpers.",
|
||||
true
|
||||
),
|
||||
pass!(
|
||||
Random,
|
||||
"random",
|
||||
"Replace random spawners with one of their possibilities.",
|
||||
true
|
||||
),
|
||||
pass!(
|
||||
Pretty,
|
||||
"pretty",
|
||||
"Add the minor cosmetic overlays for various objects.",
|
||||
true
|
||||
),
|
||||
pass!(
|
||||
Spawners,
|
||||
"spawners",
|
||||
"Replace object spawners with their spawned objects.",
|
||||
true
|
||||
),
|
||||
pass!(
|
||||
Overlays,
|
||||
"overlays",
|
||||
"Add overlays and underlays to atoms which usually have them.",
|
||||
true
|
||||
),
|
||||
pass!(
|
||||
TransitTube,
|
||||
"transit-tube",
|
||||
"Add overlays to connect transit tubes together.",
|
||||
true
|
||||
),
|
||||
pass!(
|
||||
GravityGen,
|
||||
"gravity-gen",
|
||||
"Expand the gravity generator to the full structure.",
|
||||
true
|
||||
),
|
||||
pass!(Wires, "only-powernet", "Render only power cables.", false),
|
||||
pass!(Pipes, "only-pipenet", "Render only atmospheric pipes.", false),
|
||||
pass!(FancyLayers, "fancy-layers", "Layer atoms according to in-game rules.", true),
|
||||
pass!(IconSmoothing2016, "icon-smoothing-2016", "Emulate the icon smoothing subsystem (xxalpha, 2016).", false),
|
||||
pass!(IconSmoothing, "icon-smoothing", "Emulate the icon smoothing subsystem (Rohesie, 2020).", true),
|
||||
pass!(SmartCables, "smart-cables", "Handle smart cable layout.", true),
|
||||
pass!(WiresAndPipes, "only-wires-and-pipes", "Renders only power cables and atmospheric pipes.", false),
|
||||
pass!(
|
||||
Pipes,
|
||||
"only-pipenet",
|
||||
"Render only atmospheric pipes.",
|
||||
false
|
||||
),
|
||||
pass!(
|
||||
FancyLayers,
|
||||
"fancy-layers",
|
||||
"Layer atoms according to in-game rules.",
|
||||
true
|
||||
),
|
||||
pass!(
|
||||
IconSmoothing2016,
|
||||
"icon-smoothing-2016",
|
||||
"Emulate the icon smoothing subsystem (xxalpha, 2016).",
|
||||
false
|
||||
),
|
||||
pass!(
|
||||
IconSmoothing,
|
||||
"icon-smoothing",
|
||||
"Emulate the icon smoothing subsystem (Rohesie, 2020).",
|
||||
true
|
||||
),
|
||||
pass!(
|
||||
SmartCables,
|
||||
"smart-cables",
|
||||
"Handle smart cable layout.",
|
||||
true
|
||||
),
|
||||
pass!(
|
||||
WiresAndPipes,
|
||||
"only-wires-and-pipes",
|
||||
"Renders only power cables and atmospheric pipes.",
|
||||
false
|
||||
),
|
||||
];
|
||||
|
||||
pub fn configure(renderer_config: &dm::config::MapRenderer, include: &str, exclude: &str) -> Vec<Box<dyn RenderPass>> {
|
||||
let include: Vec<&str> = include.split(",").collect();
|
||||
let exclude: Vec<&str> = exclude.split(",").collect();
|
||||
pub fn configure(
|
||||
renderer_config: &dm::config::MapRenderer,
|
||||
include: &str,
|
||||
exclude: &str,
|
||||
) -> Vec<Box<dyn RenderPass>> {
|
||||
let include: Vec<&str> = include.split(',').collect();
|
||||
let exclude: Vec<&str> = exclude.split(',').collect();
|
||||
configure_list(renderer_config, &include, &exclude)
|
||||
}
|
||||
|
||||
pub fn configure_list<T: AsRef<str>>(renderer_config: &dm::config::MapRenderer, include: &[T], exclude: &[T]) -> Vec<Box<dyn RenderPass>> {
|
||||
pub fn configure_list<T: AsRef<str>>(
|
||||
renderer_config: &dm::config::MapRenderer,
|
||||
include: &[T],
|
||||
exclude: &[T],
|
||||
) -> Vec<Box<dyn RenderPass>> {
|
||||
let include_all = include.iter().any(|name| name.as_ref() == "all");
|
||||
let exclude_all = exclude.iter().any(|name| name.as_ref() == "all");
|
||||
|
||||
|
|
@ -155,14 +243,15 @@ pub fn configure_list<T: AsRef<str>>(renderer_config: &dm::config::MapRenderer,
|
|||
fn add_to<'a>(target: &mut Vec<Sprite<'a>>, atom: &Atom<'a>, icon_state: &'a str) {
|
||||
target.push(Sprite {
|
||||
icon_state,
|
||||
.. atom.sprite
|
||||
..atom.sprite
|
||||
});
|
||||
}
|
||||
|
||||
#[derive(Default)]
|
||||
pub struct HideSpace;
|
||||
impl RenderPass for HideSpace {
|
||||
fn expand<'a>(&self,
|
||||
fn expand<'a>(
|
||||
&self,
|
||||
atom: &Atom<'a>,
|
||||
objtree: &'a ObjectTree,
|
||||
output: &mut Vec<Atom<'a>>,
|
||||
|
|
@ -188,7 +277,7 @@ impl RenderPass for HideSpace {
|
|||
pub struct HideAreas;
|
||||
impl RenderPass for HideAreas {
|
||||
fn path_filter(&self, path: &str) -> bool {
|
||||
!subpath(path, "/area/")
|
||||
!ispath(path, "/area/")
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -199,9 +288,10 @@ pub struct HideInvisible {
|
|||
|
||||
impl RenderPass for HideInvisible {
|
||||
fn configure(&mut self, renderer_config: &dm::config::MapRenderer) {
|
||||
self.overrides = renderer_config.hide_invisible.clone();
|
||||
self.overrides.clone_from(&renderer_config.hide_invisible);
|
||||
// Put longer typepaths earlier in the list so that `/foo/bar` can override `/foo`.
|
||||
self.overrides.sort_unstable_by_key(|k| usize::MAX - k.len());
|
||||
self.overrides
|
||||
.sort_unstable_by_key(|k| usize::MAX - k.len());
|
||||
// Append `/` to each typepath for faster starts_with later.
|
||||
for key in self.overrides.iter_mut() {
|
||||
if !key.ends_with('/') {
|
||||
|
|
@ -211,7 +301,7 @@ impl RenderPass for HideInvisible {
|
|||
}
|
||||
|
||||
fn path_filter(&self, path: &str) -> bool {
|
||||
!subpath(path, "/obj/effect/spawner/xmastree/")
|
||||
!ispath(path, "/obj/effect/spawner/xmastree/")
|
||||
}
|
||||
|
||||
fn early_filter(&self, atom: &Atom, objtree: &ObjectTree) -> bool {
|
||||
|
|
@ -224,14 +314,18 @@ impl RenderPass for HideInvisible {
|
|||
}
|
||||
}
|
||||
// invisible objects and syndicate balloons are not to show
|
||||
if atom.get_var("invisibility", objtree).to_float().unwrap_or(0.) > 60. ||
|
||||
atom.istype("/obj/effect/mapping_helpers/")
|
||||
if atom
|
||||
.get_var("invisibility", objtree)
|
||||
.to_float()
|
||||
.unwrap_or(0.)
|
||||
> 60.
|
||||
|| atom.istype("/obj/effect/mapping_helpers/")
|
||||
{
|
||||
return false;
|
||||
}
|
||||
if atom.get_var("icon", objtree) == "icons/obj/items_and_weapons.dmi" &&
|
||||
atom.get_var("icon_state", objtree) == "syndballoon" &&
|
||||
!atom.istype("/obj/item/toy/syndicateballoon/")
|
||||
if atom.get_var("icon", objtree) == "icons/obj/items_and_weapons.dmi"
|
||||
&& atom.get_var("icon_state", objtree) == "syndballoon"
|
||||
&& !atom.istype("/obj/item/toy/syndicateballoon/")
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
|
@ -242,7 +336,8 @@ impl RenderPass for HideInvisible {
|
|||
#[derive(Default)]
|
||||
pub struct Overlays;
|
||||
impl RenderPass for Overlays {
|
||||
fn adjust_sprite<'a>(&self,
|
||||
fn adjust_sprite<'a>(
|
||||
&self,
|
||||
atom: &Atom<'a>,
|
||||
sprite: &mut Sprite<'a>,
|
||||
objtree: &'a ObjectTree,
|
||||
|
|
@ -252,12 +347,16 @@ impl RenderPass for Overlays {
|
|||
|
||||
if atom.istype("/obj/machinery/power/apc/") {
|
||||
// auto-set pixel location
|
||||
match atom.get_var("dir", objtree).to_int().and_then(Dir::from_int) {
|
||||
match atom
|
||||
.get_var("dir", objtree)
|
||||
.to_int()
|
||||
.and_then(Dir::from_int)
|
||||
{
|
||||
Some(Dir::North) => sprite.ofs_y = 23,
|
||||
Some(Dir::South) => sprite.ofs_y = -23,
|
||||
Some(Dir::East) => sprite.ofs_x = 24,
|
||||
Some(Dir::West) => sprite.ofs_x = -25,
|
||||
_ => {}
|
||||
_ => {},
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -275,12 +374,12 @@ impl RenderPass for Overlays {
|
|||
underlays.push(Sprite {
|
||||
icon: "icons/turf/floors.dmi",
|
||||
icon_state: "plating",
|
||||
.. atom.sprite
|
||||
..atom.sprite
|
||||
});
|
||||
underlays.push(Sprite {
|
||||
icon: "icons/obj/structures.dmi",
|
||||
icon_state: "grille",
|
||||
.. atom.sprite
|
||||
..atom.sprite
|
||||
});
|
||||
} else if atom.istype("/obj/structure/closet/") {
|
||||
// closet doors
|
||||
|
|
@ -290,20 +389,30 @@ impl RenderPass for Overlays {
|
|||
} else {
|
||||
"icon_state"
|
||||
};
|
||||
if let &Constant::String(ref door) = atom.get_var(var, objtree) {
|
||||
add_to(overlays, atom, bumpalo::format!(in bump, "{}_open", door).into_bump_str());
|
||||
if let Constant::String(door) = atom.get_var(var, objtree) {
|
||||
add_to(
|
||||
overlays,
|
||||
atom,
|
||||
bumpalo::format!(in bump, "{}_open", door).into_bump_str(),
|
||||
);
|
||||
}
|
||||
} else {
|
||||
if let &Constant::String(ref door) = atom
|
||||
if let Constant::String(door) = atom
|
||||
.get_var_notnull("icon_door", objtree)
|
||||
.unwrap_or_else(|| atom.get_var("icon_state", objtree))
|
||||
{
|
||||
add_to(overlays, atom, bumpalo::format!(in bump, "{}_door", door).into_bump_str());
|
||||
add_to(
|
||||
overlays,
|
||||
atom,
|
||||
bumpalo::format!(in bump, "{}_door", door).into_bump_str(),
|
||||
);
|
||||
}
|
||||
if atom.get_var("welded", objtree).to_bool() {
|
||||
add_to(overlays, atom, "welded");
|
||||
}
|
||||
if atom.get_var("secure", objtree).to_bool() && !atom.get_var("broken", objtree).to_bool() {
|
||||
if atom.get_var("secure", objtree).to_bool()
|
||||
&& !atom.get_var("broken", objtree).to_bool()
|
||||
{
|
||||
if atom.get_var("locked", objtree).to_bool() {
|
||||
add_to(overlays, atom, "locked");
|
||||
} else {
|
||||
|
|
@ -311,7 +420,9 @@ impl RenderPass for Overlays {
|
|||
}
|
||||
}
|
||||
}
|
||||
} else if atom.istype("/obj/machinery/computer/") || atom.istype("/obj/machinery/power/solar_control/") {
|
||||
} else if atom.istype("/obj/machinery/computer/")
|
||||
|| atom.istype("/obj/machinery/power/solar_control/")
|
||||
{
|
||||
// computer screens and keyboards
|
||||
if let Some(screen) = atom.get_var("icon_screen", objtree).as_str() {
|
||||
add_to(overlays, atom, screen);
|
||||
|
|
@ -325,7 +436,7 @@ impl RenderPass for Overlays {
|
|||
overlays.push(Sprite {
|
||||
icon: overlays_file,
|
||||
icon_state: "glass_closed",
|
||||
.. atom.sprite
|
||||
..atom.sprite
|
||||
})
|
||||
}
|
||||
} else {
|
||||
|
|
@ -338,10 +449,12 @@ impl RenderPass for Overlays {
|
|||
}
|
||||
|
||||
// APC terminals
|
||||
let mut terminal = Sprite::from_vars(objtree, &objtree.expect("/obj/machinery/power/terminal"));
|
||||
let mut terminal =
|
||||
Sprite::from_vars(objtree, &objtree.expect("/obj/machinery/power/terminal"));
|
||||
terminal.dir = atom.sprite.dir;
|
||||
// TODO: un-hack this
|
||||
FancyLayers::default().apply_fancy_layer("/obj/machinery/power/terminal", &mut terminal);
|
||||
FancyLayers::default()
|
||||
.apply_fancy_layer("/obj/machinery/power/terminal", &mut terminal);
|
||||
underlays.push(terminal);
|
||||
}
|
||||
}
|
||||
|
|
@ -350,7 +463,8 @@ impl RenderPass for Overlays {
|
|||
#[derive(Default)]
|
||||
pub struct Pretty;
|
||||
impl RenderPass for Pretty {
|
||||
fn adjust_sprite<'a>(&self,
|
||||
fn adjust_sprite<'a>(
|
||||
&self,
|
||||
atom: &Atom<'a>,
|
||||
sprite: &mut Sprite<'a>,
|
||||
_: &'a ObjectTree,
|
||||
|
|
@ -361,18 +475,20 @@ impl RenderPass for Pretty {
|
|||
}
|
||||
}
|
||||
|
||||
fn overlays<'a>(&self,
|
||||
fn overlays<'a>(
|
||||
&self,
|
||||
atom: &Atom<'a>,
|
||||
objtree: &'a ObjectTree,
|
||||
_: &mut Vec<Sprite<'a>>,
|
||||
overlays: &mut Vec<Sprite<'a>>,
|
||||
_: &bumpalo::Bump,
|
||||
) {
|
||||
if atom.istype("/obj/item/storage/box/") && !atom.istype("/obj/item/storage/box/papersack/") {
|
||||
if atom.istype("/obj/item/storage/box/") && !atom.istype("/obj/item/storage/box/papersack/")
|
||||
{
|
||||
if let Some(icon_state) = atom.get_var("illustration", objtree).as_str() {
|
||||
overlays.push(Sprite {
|
||||
icon_state,
|
||||
.. atom.sprite
|
||||
..atom.sprite
|
||||
});
|
||||
}
|
||||
} else if atom.istype("/obj/machinery/firealarm/") {
|
||||
|
|
@ -382,21 +498,21 @@ impl RenderPass for Pretty {
|
|||
} else if atom.istype("/obj/structure/tank_dispenser/") {
|
||||
if let &Constant::Float(oxygen) = atom.get_var("oxygentanks", objtree) {
|
||||
match oxygen as i32 {
|
||||
4..=std::i32::MAX => add_to(overlays, atom, "oxygen-4"),
|
||||
4..=i32::MAX => add_to(overlays, atom, "oxygen-4"),
|
||||
3 => add_to(overlays, atom, "oxygen-3"),
|
||||
2 => add_to(overlays, atom, "oxygen-2"),
|
||||
1 => add_to(overlays, atom, "oxygen-1"),
|
||||
_ => {}
|
||||
_ => {},
|
||||
}
|
||||
}
|
||||
if let &Constant::Float(plasma) = atom.get_var("plasmatanks", objtree) {
|
||||
match plasma as i32 {
|
||||
5..=std::i32::MAX => add_to(overlays, atom, "plasma-5"),
|
||||
5..=i32::MAX => add_to(overlays, atom, "plasma-5"),
|
||||
4 => add_to(overlays, atom, "plasma-4"),
|
||||
3 => add_to(overlays, atom, "plasma-3"),
|
||||
2 => add_to(overlays, atom, "plasma-2"),
|
||||
1 => add_to(overlays, atom, "plasma-1"),
|
||||
_ => {}
|
||||
_ => {},
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -434,9 +550,14 @@ pub struct FancyLayers {
|
|||
|
||||
impl RenderPass for FancyLayers {
|
||||
fn configure(&mut self, renderer_config: &dm::config::MapRenderer) {
|
||||
self.overrides = renderer_config.fancy_layers.clone().into_iter().collect::<Vec<_>>();
|
||||
self.overrides = renderer_config
|
||||
.fancy_layers
|
||||
.clone()
|
||||
.into_iter()
|
||||
.collect::<Vec<_>>();
|
||||
// Put longer typepaths earlier in the list so that `/foo/bar` can override `/foo`.
|
||||
self.overrides.sort_unstable_by_key(|(k, _)| usize::MAX - k.len());
|
||||
self.overrides
|
||||
.sort_unstable_by_key(|(k, _)| usize::MAX - k.len());
|
||||
// Append `/` to each typepath for faster starts_with later.
|
||||
for (key, _) in self.overrides.iter_mut() {
|
||||
if !key.ends_with('/') {
|
||||
|
|
@ -445,7 +566,8 @@ impl RenderPass for FancyLayers {
|
|||
}
|
||||
}
|
||||
|
||||
fn adjust_sprite<'a>(&self,
|
||||
fn adjust_sprite<'a>(
|
||||
&self,
|
||||
atom: &Atom<'a>,
|
||||
sprite: &mut Sprite<'a>,
|
||||
objtree: &'a ObjectTree,
|
||||
|
|
@ -454,14 +576,15 @@ impl RenderPass for FancyLayers {
|
|||
self.apply_fancy_layer(atom.get_path(), sprite);
|
||||
|
||||
// dual layering of vents 1: hide original sprite underfloor
|
||||
if atom.istype("/obj/machinery/atmospherics/components/unary/") {
|
||||
if unary_aboveground(atom, objtree).is_some() {
|
||||
sprite.layer = Layer::from(-5);
|
||||
}
|
||||
if atom.istype("/obj/machinery/atmospherics/components/unary/")
|
||||
&& unary_aboveground(atom, objtree).is_some()
|
||||
{
|
||||
sprite.layer = Layer::from(-5);
|
||||
}
|
||||
}
|
||||
|
||||
fn overlays<'a>(&self,
|
||||
fn overlays<'a>(
|
||||
&self,
|
||||
atom: &Atom<'a>,
|
||||
objtree: &'a ObjectTree,
|
||||
_underlays: &mut Vec<Sprite<'a>>,
|
||||
|
|
@ -475,7 +598,7 @@ impl RenderPass for FancyLayers {
|
|||
icon_state: aboveground,
|
||||
// use original layer, not modified layer above
|
||||
layer: crate::minimap::layer_of(objtree, atom),
|
||||
.. atom.sprite
|
||||
..atom.sprite
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
@ -484,10 +607,13 @@ impl RenderPass for FancyLayers {
|
|||
|
||||
fn unary_aboveground(atom: &Atom, objtree: &ObjectTree) -> Option<&'static str> {
|
||||
Some(match atom.get_var("icon_state", objtree) {
|
||||
&Constant::String(ref text) => match &**text {
|
||||
Constant::String(text) => match &**text {
|
||||
"vent_map-1" | "vent_map-2" | "vent_map-3" | "vent_map-4" => "vent_off",
|
||||
"vent_map_on-1" | "vent_map_on-2" | "vent_map_on-3" | "vent_map_on-4" => "vent_out",
|
||||
"vent_map_siphon_on-1" | "vent_map_siphon_on-2" | "vent_map_siphon_on-3" | "vent_map_siphon_on-4" => "vent_in",
|
||||
"vent_map_siphon_on-1"
|
||||
| "vent_map_siphon_on-2"
|
||||
| "vent_map_siphon_on-3"
|
||||
| "vent_map_siphon_on-4" => "vent_in",
|
||||
"scrub_map-1" | "scrub_map-2" | "scrub_map-3" | "scrub_map-4" => "scrub_off",
|
||||
"scrub_map_on-1" | "scrub_map_on-2" | "scrub_map_on-3" | "scrub_map_on-4" => "scrub_on",
|
||||
_ => return None,
|
||||
|
|
@ -499,32 +625,32 @@ fn unary_aboveground(atom: &Atom, objtree: &ObjectTree) -> Option<&'static str>
|
|||
impl FancyLayers {
|
||||
fn fancy_layer_for_path(&self, p: &str) -> Option<Layer> {
|
||||
for &(ref key, val) in self.overrides.iter() {
|
||||
if subpath(p, key) {
|
||||
if ispath(p, key) {
|
||||
return Some(Layer::from(val));
|
||||
}
|
||||
}
|
||||
|
||||
if subpath(p, "/turf/open/floor/plating/") || subpath(p, "/turf/open/space/") {
|
||||
Some(Layer::from(-10)) // under everything
|
||||
} else if subpath(p, "/turf/closed/mineral/") {
|
||||
Some(Layer::from(-3)) // above hidden stuff and plating but below walls
|
||||
} else if subpath(p, "/turf/open/floor/") || subpath(p, "/turf/closed/") {
|
||||
Some(Layer::from(-2)) // above hidden pipes and wires
|
||||
} else if subpath(p, "/turf/") {
|
||||
Some(Layer::from(-10)) // under everything
|
||||
} else if subpath(p, "/obj/effect/turf_decal/") {
|
||||
Some(Layer::from(-1)) // above turfs
|
||||
} else if subpath(p, "/obj/structure/disposalpipe/") {
|
||||
if ispath(p, "/turf/open/floor/plating/") || ispath(p, "/turf/open/space/") {
|
||||
Some(Layer::from(-10)) // under everything
|
||||
} else if ispath(p, "/turf/closed/mineral/") {
|
||||
Some(Layer::from(-3)) // above hidden stuff and plating but below walls
|
||||
} else if ispath(p, "/turf/open/floor/") || ispath(p, "/turf/closed/") {
|
||||
Some(Layer::from(-2)) // above hidden pipes and wires
|
||||
} else if ispath(p, "/turf/") {
|
||||
Some(Layer::from(-10)) // under everything
|
||||
} else if ispath(p, "/obj/effect/turf_decal/") {
|
||||
Some(Layer::from(-1)) // above turfs
|
||||
} else if ispath(p, "/obj/structure/disposalpipe/") {
|
||||
Some(Layer::from(-6))
|
||||
} else if subpath(p, "/obj/machinery/atmospherics/pipe/") && !p.contains("visible") {
|
||||
} else if ispath(p, "/obj/machinery/atmospherics/pipe/") && !p.contains("visible") {
|
||||
Some(Layer::from(-5))
|
||||
} else if subpath(p, "/obj/structure/cable/") {
|
||||
} else if ispath(p, "/obj/structure/cable/") {
|
||||
Some(Layer::from(-4))
|
||||
} else if subpath(p, "/obj/machinery/power/terminal/") {
|
||||
} else if ispath(p, "/obj/machinery/power/terminal/") {
|
||||
Some(Layer::from(-3.5))
|
||||
} else if subpath(p, "/obj/structure/lattice/") {
|
||||
} else if ispath(p, "/obj/structure/lattice/") {
|
||||
Some(Layer::from(-8))
|
||||
} else if subpath(p, "/obj/machinery/navbeacon/") {
|
||||
} else if ispath(p, "/obj/machinery/navbeacon/") {
|
||||
Some(Layer::from(-3))
|
||||
} else {
|
||||
None
|
||||
|
|
|
|||
|
|
@ -1,12 +1,13 @@
|
|||
use super::*;
|
||||
|
||||
use rand::Rng;
|
||||
use rand::seq::SliceRandom;
|
||||
use rand::Rng;
|
||||
|
||||
#[derive(Default)]
|
||||
pub struct Random;
|
||||
impl RenderPass for Random {
|
||||
fn expand<'a>(&self,
|
||||
fn expand<'a>(
|
||||
&self,
|
||||
atom: &Atom<'a>,
|
||||
objtree: &'a ObjectTree,
|
||||
output: &mut Vec<Atom<'a>>,
|
||||
|
|
@ -23,7 +24,7 @@ impl RenderPass for Random {
|
|||
}
|
||||
if let Some(&replacement) = machines.choose(&mut rng) {
|
||||
output.push(Atom::from(replacement));
|
||||
return false; // consumed
|
||||
return false; // consumed
|
||||
}
|
||||
}
|
||||
} else if atom.istype("/obj/machinery/vending/cola/random/") {
|
||||
|
|
@ -36,12 +37,12 @@ impl RenderPass for Random {
|
|||
}
|
||||
if let Some(&replacement) = machines.choose(&mut rng) {
|
||||
output.push(Atom::from(replacement));
|
||||
return false; // consumed
|
||||
return false; // consumed
|
||||
}
|
||||
}
|
||||
} else if atom.istype("/obj/item/bedsheet/random/") {
|
||||
if let Some(root) = objtree.find("/obj/item/bedsheet") {
|
||||
let mut sheets = vec![root.get()]; // basic bedsheet is included
|
||||
let mut sheets = vec![root.get()]; // basic bedsheet is included
|
||||
for child in root.children() {
|
||||
if child.name() != "random" {
|
||||
sheets.push(child.get());
|
||||
|
|
@ -49,7 +50,7 @@ impl RenderPass for Random {
|
|||
}
|
||||
if let Some(&replacement) = sheets.choose(&mut rng) {
|
||||
output.push(Atom::from(replacement));
|
||||
return false; // consumed
|
||||
return false; // consumed
|
||||
}
|
||||
}
|
||||
} else if atom.istype("/obj/effect/spawner/lootdrop/") {
|
||||
|
|
@ -83,12 +84,13 @@ impl RenderPass for Random {
|
|||
loot_spawned += 1;
|
||||
}
|
||||
}
|
||||
return false; // consumed
|
||||
return false; // consumed
|
||||
}
|
||||
true
|
||||
}
|
||||
|
||||
fn adjust_sprite<'a>(&self,
|
||||
fn adjust_sprite<'a>(
|
||||
&self,
|
||||
atom: &Atom<'a>,
|
||||
sprite: &mut Sprite<'a>,
|
||||
objtree: &'a ObjectTree,
|
||||
|
|
@ -100,30 +102,39 @@ impl RenderPass for Random {
|
|||
const LEGIT_POSTERS: u32 = 35;
|
||||
|
||||
if atom.istype("/obj/structure/sign/poster/contraband/random/") {
|
||||
sprite.icon_state = bumpalo::format!(in bump, "poster{}", rng.gen_range(1..=CONTRABAND_POSTERS)).into_bump_str();
|
||||
sprite.icon_state =
|
||||
bumpalo::format!(in bump, "poster{}", rng.gen_range(1..=CONTRABAND_POSTERS))
|
||||
.into_bump_str();
|
||||
} else if atom.istype("/obj/structure/sign/poster/official/random/") {
|
||||
sprite.icon_state = bumpalo::format!(in bump, "poster{}_legit", rng.gen_range(1..=LEGIT_POSTERS)).into_bump_str();
|
||||
sprite.icon_state =
|
||||
bumpalo::format!(in bump, "poster{}_legit", rng.gen_range(1..=LEGIT_POSTERS))
|
||||
.into_bump_str();
|
||||
} else if atom.istype("/obj/structure/sign/poster/random/") {
|
||||
let i = 1 + rng.gen_range(0..CONTRABAND_POSTERS + LEGIT_POSTERS);
|
||||
if i <= CONTRABAND_POSTERS {
|
||||
sprite.icon_state = bumpalo::format!(in bump, "poster{}", i).into_bump_str();
|
||||
} else {
|
||||
sprite.icon_state = bumpalo::format!(in bump, "poster{}_legit", i - CONTRABAND_POSTERS).into_bump_str();
|
||||
sprite.icon_state =
|
||||
bumpalo::format!(in bump, "poster{}_legit", i - CONTRABAND_POSTERS)
|
||||
.into_bump_str();
|
||||
}
|
||||
} else if atom.istype("/obj/item/kirbyplants/random/") || atom.istype("/obj/item/twohanded/required/kirbyplants/random/") {
|
||||
} else if atom.istype("/obj/item/kirbyplants/random/")
|
||||
|| atom.istype("/obj/item/twohanded/required/kirbyplants/random/")
|
||||
{
|
||||
sprite.icon = "icons/obj/flora/plants.dmi";
|
||||
let random = rng.gen_range(0..26);
|
||||
if random == 0 {
|
||||
sprite.icon_state = "applebush";
|
||||
} else {
|
||||
sprite.icon_state = bumpalo::format!(in bump, "plant-{:02}", random).into_bump_str();
|
||||
sprite.icon_state =
|
||||
bumpalo::format!(in bump, "plant-{:02}", random).into_bump_str();
|
||||
}
|
||||
} else if atom.istype("/obj/structure/sign/barsign/") {
|
||||
if let Some(root) = objtree.find("/datum/barsign") {
|
||||
let mut signs = Vec::new();
|
||||
for child in root.children() {
|
||||
if let Some(v) = child.vars.get("hidden") {
|
||||
if !v.value.constant.as_ref().map_or(false, |c| c.to_bool()) {
|
||||
if !v.value.constant.as_ref().is_some_and(|c| c.to_bool()) {
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
|
@ -140,7 +151,7 @@ impl RenderPass for Random {
|
|||
}
|
||||
}
|
||||
} else if atom.istype("/obj/item/relic/") {
|
||||
sprite.icon_state = [
|
||||
sprite.icon_state = [
|
||||
"shock_kit",
|
||||
"armor-igniter-analyzer",
|
||||
"infra-igniter0",
|
||||
|
|
@ -150,7 +161,9 @@ impl RenderPass for Random {
|
|||
"radio-radio",
|
||||
"timer-multitool0",
|
||||
"radio-igniter-tank",
|
||||
].choose(&mut rng).unwrap();
|
||||
]
|
||||
.choose(&mut rng)
|
||||
.unwrap();
|
||||
}
|
||||
|
||||
if atom.istype("/obj/item/lipstick/random/") {
|
||||
|
|
@ -163,16 +176,30 @@ impl RenderPass for Random {
|
|||
"tape_red",
|
||||
"tape_yellow",
|
||||
"tape_purple",
|
||||
].choose(&mut rng).unwrap();
|
||||
]
|
||||
.choose(&mut rng)
|
||||
.unwrap();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn pickweight<'a>(list: &[&'a (Constant, Option<Constant>)]) -> &'a Constant {
|
||||
let mut total: i32 = list.iter().map(|(_, v)| v.as_ref().unwrap_or(Constant::null()).to_int().unwrap_or(1)).sum();
|
||||
let mut total: i32 = list
|
||||
.iter()
|
||||
.map(|(_, v)| {
|
||||
v.as_ref()
|
||||
.unwrap_or_else(Constant::null)
|
||||
.to_int()
|
||||
.unwrap_or(1)
|
||||
})
|
||||
.sum();
|
||||
total = rand::thread_rng().gen_range(1..=total);
|
||||
for (k, v) in list.iter() {
|
||||
total -= v.as_ref().unwrap_or(Constant::null()).to_int().unwrap_or(1);
|
||||
total -= v
|
||||
.as_ref()
|
||||
.unwrap_or_else(Constant::null)
|
||||
.to_int()
|
||||
.unwrap_or(1);
|
||||
if total <= 0 {
|
||||
return k;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,12 +1,13 @@
|
|||
use std::fmt::Write;
|
||||
use crate::dmi::Dir;
|
||||
use super::*;
|
||||
use crate::dmi::Dir;
|
||||
use std::fmt::Write;
|
||||
|
||||
#[derive(Default)]
|
||||
pub struct SmartCables;
|
||||
|
||||
impl RenderPass for SmartCables {
|
||||
fn neighborhood_appearance<'a>(&self,
|
||||
fn neighborhood_appearance<'a>(
|
||||
&self,
|
||||
atom: &Atom<'a>,
|
||||
objtree: &'a ObjectTree,
|
||||
neighborhood: &Neighborhood<'a, '_>,
|
||||
|
|
@ -17,7 +18,10 @@ impl RenderPass for SmartCables {
|
|||
return true;
|
||||
}
|
||||
|
||||
let cable_layer = atom.get_var("cable_layer", objtree).as_str().unwrap_or("l2");
|
||||
let cable_layer = atom
|
||||
.get_var("cable_layer", objtree)
|
||||
.as_str()
|
||||
.unwrap_or("l2");
|
||||
|
||||
let mut under_smes = false;
|
||||
let mut under_terminal = false;
|
||||
|
|
@ -50,11 +54,15 @@ impl RenderPass for SmartCables {
|
|||
}
|
||||
|
||||
for atom in turf {
|
||||
if atom.istype("/obj/structure/cable/") {
|
||||
if atom.get_var("cable_layer", objtree).as_str().unwrap_or("l2") == cable_layer {
|
||||
linked_dirs |= check_dir.to_int();
|
||||
break;
|
||||
}
|
||||
if atom.istype("/obj/structure/cable/")
|
||||
&& atom
|
||||
.get_var("cable_layer", objtree)
|
||||
.as_str()
|
||||
.unwrap_or("l2")
|
||||
== cable_layer
|
||||
{
|
||||
linked_dirs |= check_dir.to_int();
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -79,7 +87,7 @@ impl RenderPass for SmartCables {
|
|||
|
||||
output.push(Sprite {
|
||||
icon_state: icon_state.into_bump_str(),
|
||||
.. atom.sprite
|
||||
..atom.sprite
|
||||
});
|
||||
false
|
||||
}
|
||||
|
|
@ -87,6 +95,8 @@ impl RenderPass for SmartCables {
|
|||
|
||||
fn should_have_node(turf: &[Atom]) -> bool {
|
||||
for atom in turf {
|
||||
// Readability, simple elif chain isn't duplicate code
|
||||
#[allow(clippy::if_same_then_else)]
|
||||
if atom.istype("/obj/structure/grille/") || atom.istype("/obj/structure/cable_bridge/") {
|
||||
return true;
|
||||
} else if atom.istype("/obj/machinery/power/") {
|
||||
|
|
|
|||
|
|
@ -3,7 +3,8 @@ use super::*;
|
|||
#[derive(Default)]
|
||||
pub struct Spawners;
|
||||
impl RenderPass for Spawners {
|
||||
fn expand<'a>(&self,
|
||||
fn expand<'a>(
|
||||
&self,
|
||||
atom: &Atom<'a>,
|
||||
objtree: &'a ObjectTree,
|
||||
output: &mut Vec<Atom<'a>>,
|
||||
|
|
@ -12,13 +13,13 @@ impl RenderPass for Spawners {
|
|||
return true;
|
||||
}
|
||||
match atom.get_var("spawn_list", objtree) {
|
||||
&Constant::List(ref elements) => {
|
||||
for &(ref key, _) in elements.iter() {
|
||||
Constant::List(elements) => {
|
||||
for (key, _) in elements.iter() {
|
||||
// TODO: use a more civilized lookup method
|
||||
let type_key;
|
||||
let reference = match key {
|
||||
&Constant::String(ref s) => s,
|
||||
&Constant::Prefab(ref fab) => {
|
||||
let reference = match *key {
|
||||
Constant::String(ref s) => s,
|
||||
Constant::Prefab(ref fab) => {
|
||||
type_key = dm::ast::FormatTreePath(&fab.path).to_string();
|
||||
type_key.as_str()
|
||||
},
|
||||
|
|
@ -26,9 +27,9 @@ impl RenderPass for Spawners {
|
|||
};
|
||||
output.push(Atom::from(objtree.expect(reference)));
|
||||
}
|
||||
false // don't include the original atom
|
||||
}
|
||||
_ => { true } // TODO: complain?
|
||||
false // don't include the original atom
|
||||
},
|
||||
_ => true, // TODO: complain?
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -36,14 +37,15 @@ impl RenderPass for Spawners {
|
|||
#[derive(Default)]
|
||||
pub struct GravityGen;
|
||||
impl RenderPass for GravityGen {
|
||||
fn overlays<'a>(&self,
|
||||
fn overlays<'a>(
|
||||
&self,
|
||||
atom: &Atom<'a>,
|
||||
objtree: &'a ObjectTree,
|
||||
_underlays: &mut Vec<Sprite<'a>>,
|
||||
overlays: &mut Vec<Sprite<'a>>,
|
||||
_: &bumpalo::Bump,
|
||||
) {
|
||||
if !atom.istype("/obj/machinery/gravity_generator/main/station/") {
|
||||
if !atom.istype("/obj/machinery/gravity_generator/main/") {
|
||||
return;
|
||||
}
|
||||
|
||||
|
|
@ -57,13 +59,16 @@ impl RenderPass for GravityGen {
|
|||
(7, "on_7", 1, 0),
|
||||
(9, "on_9", -1, 0),
|
||||
] {
|
||||
let mut sprite = Sprite::from_vars(objtree, &objtree.expect("/obj/machinery/gravity_generator/part"));
|
||||
let mut sprite = Sprite::from_vars(
|
||||
objtree,
|
||||
&objtree.expect("/obj/machinery/gravity_generator/part"),
|
||||
);
|
||||
sprite.ofs_x += 32 * x;
|
||||
sprite.ofs_y += 32 * y;
|
||||
sprite.icon_state = icon_state;
|
||||
sprite.plane = 0; // TODO: figure out plane handling for real
|
||||
sprite.plane = 0; // TODO: figure out plane handling for real
|
||||
if count <= 3 {
|
||||
sprite.layer = Layer::from(4.25); // WALL_OBJ_LAYER
|
||||
sprite.layer = Layer::from(4.25); // WALL_OBJ_LAYER
|
||||
}
|
||||
if count == 5 {
|
||||
// energy overlay goes above the middle part
|
||||
|
|
|
|||
|
|
@ -4,7 +4,8 @@ use crate::dmi::Dir;
|
|||
#[derive(Default)]
|
||||
pub struct TransitTube;
|
||||
impl RenderPass for TransitTube {
|
||||
fn overlays<'a>(&self,
|
||||
fn overlays<'a>(
|
||||
&self,
|
||||
atom: &Atom<'a>,
|
||||
objtree: &'a ObjectTree,
|
||||
_: &mut Vec<Sprite<'a>>,
|
||||
|
|
@ -34,7 +35,11 @@ impl RenderPass for TransitTube {
|
|||
}
|
||||
};
|
||||
|
||||
let dir = atom.get_var("dir", objtree).to_int().and_then(Dir::from_int).unwrap_or(Dir::default());
|
||||
let dir = atom
|
||||
.get_var("dir", objtree)
|
||||
.to_int()
|
||||
.and_then(Dir::from_int)
|
||||
.unwrap_or_default();
|
||||
if atom.istype("/obj/structure/transit_tube/station/reverse/") {
|
||||
fulfill(&match dir {
|
||||
North => [East],
|
||||
|
|
@ -109,7 +114,7 @@ fn create_tube_overlay<'a>(
|
|||
icon: source.sprite.icon,
|
||||
layer: source.sprite.layer,
|
||||
icon_state: "decorative",
|
||||
.. Default::default()
|
||||
..Default::default()
|
||||
};
|
||||
if let Some(shift) = shift {
|
||||
sprite.icon_state = "decorative_diag";
|
||||
|
|
@ -118,7 +123,7 @@ fn create_tube_overlay<'a>(
|
|||
Dir::South => sprite.ofs_y -= 32,
|
||||
Dir::East => sprite.ofs_x += 32,
|
||||
Dir::West => sprite.ofs_x -= 32,
|
||||
_ => {}
|
||||
_ => {},
|
||||
}
|
||||
}
|
||||
output.push(sprite);
|
||||
|
|
|
|||
|
|
@ -1,9 +1,9 @@
|
|||
extern crate walkdir;
|
||||
extern crate dmm_tools;
|
||||
extern crate walkdir;
|
||||
|
||||
use dmm_tools::*;
|
||||
use std::path::Path;
|
||||
use walkdir::{DirEntry, WalkDir};
|
||||
use dmm_tools::*;
|
||||
|
||||
fn is_visible(entry: &DirEntry) -> bool {
|
||||
entry
|
||||
|
|
@ -11,7 +11,7 @@ fn is_visible(entry: &DirEntry) -> bool {
|
|||
.file_name()
|
||||
.unwrap_or("".as_ref())
|
||||
.to_str()
|
||||
.map(|s| !s.starts_with("."))
|
||||
.map(|s| !s.starts_with('.'))
|
||||
.unwrap_or(true)
|
||||
}
|
||||
|
||||
|
|
@ -19,9 +19,9 @@ fn files_with_extension<F: FnMut(&Path)>(ext: &str, mut f: F) {
|
|||
let dir = match std::env::var_os("TEST_DME") {
|
||||
Some(dme) => Path::new(&dme).parent().unwrap().to_owned(),
|
||||
None => {
|
||||
println!("Set TEST_DME to check .{} files", ext);
|
||||
println!("Set TEST_DME to check .{ext} files");
|
||||
return;
|
||||
}
|
||||
},
|
||||
};
|
||||
for entry in WalkDir::new(dir).into_iter().filter_entry(is_visible) {
|
||||
let entry = entry.unwrap();
|
||||
|
|
|
|||
|
|
@ -1,15 +1,14 @@
|
|||
[package]
|
||||
name = "dreamchecker"
|
||||
version = "1.7.1"
|
||||
authors = ["Tad Hardesty <tad@platymuus.com>"]
|
||||
edition = "2018"
|
||||
version.workspace = true
|
||||
authors.workspace = true
|
||||
edition.workspace = true
|
||||
|
||||
[dependencies]
|
||||
dreammaker = { path = "../dreammaker" }
|
||||
guard = "0.5.0"
|
||||
serde_json = "1.0"
|
||||
ahash = "0.7.6"
|
||||
foldhash = "0.2.0"
|
||||
|
||||
[build-dependencies]
|
||||
chrono = "0.4.0"
|
||||
git2 = { version = "0.13", default-features = false }
|
||||
chrono = "0.4.38"
|
||||
git2 = { version = "0.20.2", default-features = false }
|
||||
|
|
|
|||
|
|
@ -3,7 +3,7 @@
|
|||
**DreamChecker** is a robust whole-program static analysis and type checking
|
||||
engine for DreamMaker, the scripting language of the [BYOND] game engine.
|
||||
|
||||
[BYOND]: https://secure.byond.com/
|
||||
[BYOND]: https://www.byond.com/
|
||||
|
||||
## Running DreamChecker
|
||||
|
||||
|
|
|
|||
|
|
@ -8,12 +8,12 @@ use std::path::PathBuf;
|
|||
|
||||
fn main() {
|
||||
let out_dir = PathBuf::from(env::var_os("OUT_DIR").unwrap());
|
||||
let mut f = File::create(&out_dir.join("build-info.txt")).unwrap();
|
||||
let mut f = File::create(out_dir.join("build-info.txt")).unwrap();
|
||||
|
||||
if let Ok(commit) = read_commit() {
|
||||
writeln!(f, "commit: {}", commit).unwrap();
|
||||
writeln!(f, "commit: {commit}").unwrap();
|
||||
}
|
||||
writeln!(f, "build date: {}", chrono::Utc::today()).unwrap();
|
||||
writeln!(f, "build date: {}", chrono::Utc::now().date_naive()).unwrap();
|
||||
}
|
||||
|
||||
fn read_commit() -> Result<String, git2::Error> {
|
||||
|
|
|
|||
File diff suppressed because it is too large
Load diff
|
|
@ -1,8 +1,8 @@
|
|||
//! DreamChecker, a robust static analysis and typechecking engine for
|
||||
//! DreamMaker.
|
||||
|
||||
extern crate dreammaker as dm;
|
||||
extern crate dreamchecker;
|
||||
extern crate dreammaker as dm;
|
||||
#[macro_use]
|
||||
extern crate serde_json;
|
||||
|
||||
|
|
@ -17,14 +17,17 @@ fn main() {
|
|||
let mut parse_only = false;
|
||||
|
||||
let mut args = std::env::args();
|
||||
let _ = args.next(); // skip executable name
|
||||
let _ = args.next(); // skip executable name
|
||||
while let Some(arg) = args.next() {
|
||||
if arg == "-V" || arg == "--version" {
|
||||
println!(
|
||||
"dreamchecker {} Copyright (C) 2017-2021 Tad Hardesty",
|
||||
"dreamchecker {} Copyright (C) 2017-2025 Tad Hardesty",
|
||||
env!("CARGO_PKG_VERSION")
|
||||
);
|
||||
println!("{}", include_str!(concat!(env!("OUT_DIR"), "/build-info.txt")));
|
||||
println!(
|
||||
"{}",
|
||||
include_str!(concat!(env!("OUT_DIR"), "/build-info.txt"))
|
||||
);
|
||||
println!("This program comes with ABSOLUTELY NO WARRANTY. This is free software,");
|
||||
println!("and you are welcome to redistribute it under the conditions of the GNU");
|
||||
println!("General Public License version 3.");
|
||||
|
|
@ -38,29 +41,30 @@ fn main() {
|
|||
} else if arg == "--parse-only" {
|
||||
parse_only = true;
|
||||
} else {
|
||||
eprintln!("unknown argument: {}", arg);
|
||||
eprintln!("unknown argument: {arg}");
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
let dme = environment
|
||||
.map(std::path::PathBuf::from)
|
||||
.unwrap_or_else(|| dm::detect_environment_default()
|
||||
.expect("error detecting .dme")
|
||||
.expect("no .dme found"));
|
||||
.unwrap_or_else(|| {
|
||||
dm::detect_environment_default()
|
||||
.expect("error detecting .dme")
|
||||
.expect("no .dme found")
|
||||
});
|
||||
|
||||
let mut context = dm::Context::default();
|
||||
context.set_print_severity(Some(dm::Severity::Info));
|
||||
if let Some(filepath) = config_file {
|
||||
context.force_config(filepath.as_ref());
|
||||
} else {
|
||||
context.autodetect_config(&dme);
|
||||
}
|
||||
context.set_print_severity(Some(dm::Severity::Info));
|
||||
|
||||
println!("============================================================");
|
||||
println!("Parsing {}...\n", dme.display());
|
||||
let pp = dm::preprocessor::Preprocessor::new(&context, dme)
|
||||
.expect("i/o error opening .dme");
|
||||
let pp = dm::preprocessor::Preprocessor::new(&context, dme).expect("i/o error opening .dme");
|
||||
let indents = dm::indents::IndentProcessor::new(&context, pp);
|
||||
let mut parser = dm::parser::Parser::new(&context, indents);
|
||||
parser.enable_procs();
|
||||
|
|
@ -71,8 +75,12 @@ fn main() {
|
|||
}
|
||||
|
||||
println!("============================================================");
|
||||
let errors = context.errors().iter().filter(|each| each.severity() <= dm::Severity::Info).count();
|
||||
println!("Found {} diagnostics", errors);
|
||||
let errors = context
|
||||
.errors()
|
||||
.iter()
|
||||
.filter(|each| each.severity() <= dm::Severity::Info)
|
||||
.count();
|
||||
println!("Found {errors} diagnostics");
|
||||
|
||||
if json {
|
||||
serde_json::to_writer(std::io::stdout().lock(), &json! {{
|
||||
|
|
|
|||
112
crates/dreamchecker/src/switch_rand_range.rs
Normal file
112
crates/dreamchecker/src/switch_rand_range.rs
Normal file
|
|
@ -0,0 +1,112 @@
|
|||
use std::borrow::Borrow;
|
||||
|
||||
use dm::ast::*;
|
||||
use dm::{Context, DMError, Location, Severity};
|
||||
|
||||
/**
|
||||
* Checks for mistakes in switches of the form `switch(rand(L, H))`.
|
||||
* If some cases lie outside of the [L, H] range or the whole [L, H] range is not covered by all the cases a warning is issued.
|
||||
*/
|
||||
pub fn check_switch_rand_range(
|
||||
input: &Expression,
|
||||
cases: &SwitchCases,
|
||||
default: &Option<Block>,
|
||||
location: Location,
|
||||
context: &Context,
|
||||
) {
|
||||
let (rand_start, rand_end) = if let Some(range) = get_rand_range(input) {
|
||||
range
|
||||
} else {
|
||||
return;
|
||||
};
|
||||
|
||||
let mut case_ranges = Vec::with_capacity(cases.len());
|
||||
for case_block in cases.iter() {
|
||||
let location = case_block.0.location;
|
||||
for case in case_block.0.elem.iter() {
|
||||
if let Some((start, end)) = get_case_range(case, location) {
|
||||
let start = start.ceil() as i32;
|
||||
let end = end.floor() as i32;
|
||||
if start <= rand_end && end >= rand_start {
|
||||
case_ranges.push((start, end));
|
||||
} else {
|
||||
DMError::new(location, format!("Case range '{start} to {end}' will never trigger as it is outside the rand() range {rand_start} to {rand_end}"))
|
||||
.with_component(dm::Component::DreamChecker)
|
||||
.set_severity(Severity::Warning)
|
||||
.register(context);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if default.is_some() {
|
||||
// covers the whole range directly so no need to check for gaps
|
||||
return;
|
||||
}
|
||||
|
||||
case_ranges.sort_by(|a, b| a.0.cmp(&b.0));
|
||||
let mut first_uncovered = rand_start;
|
||||
for (start, end) in case_ranges.iter() {
|
||||
if *start > first_uncovered {
|
||||
break;
|
||||
} else {
|
||||
first_uncovered = std::cmp::max(first_uncovered, end + 1);
|
||||
}
|
||||
}
|
||||
|
||||
if first_uncovered <= rand_end {
|
||||
DMError::new(
|
||||
location,
|
||||
format!(
|
||||
"Switch branches on rand() with range {rand_start} to {rand_end} but no case branch triggers for {first_uncovered}"
|
||||
),
|
||||
)
|
||||
.with_component(dm::Component::DreamChecker)
|
||||
.set_severity(Severity::Warning)
|
||||
.register(context);
|
||||
}
|
||||
}
|
||||
|
||||
fn get_case_range(case: &Case, location: Location) -> Option<(f32, f32)> {
|
||||
match case {
|
||||
Case::Exact(ref value) => {
|
||||
let value = value
|
||||
.to_owned()
|
||||
.simple_evaluate(location)
|
||||
.ok()?
|
||||
.to_float()?;
|
||||
Some((value, value))
|
||||
},
|
||||
Case::Range(ref min, ref max) => {
|
||||
let min = min.to_owned().simple_evaluate(location).ok()?.to_float()?;
|
||||
let max = max.to_owned().simple_evaluate(location).ok()?.to_float()?;
|
||||
Some((min, max))
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
fn get_rand_range(maybe_rand: &Expression) -> Option<(i32, i32)> {
|
||||
let (term, location) = match maybe_rand {
|
||||
Expression::Base { term, follow } if follow.is_empty() => (&term.elem, term.location),
|
||||
_ => return None,
|
||||
};
|
||||
|
||||
let rand_args: &[Expression] = match term {
|
||||
Term::Call(proc_name, args) if proc_name.as_str() == "rand" => args.borrow(),
|
||||
_ => return None,
|
||||
};
|
||||
|
||||
let (min, max) = match rand_args {
|
||||
[min, max] => (
|
||||
min.to_owned().simple_evaluate(location).ok()?.to_float()?,
|
||||
max.to_owned().simple_evaluate(location).ok()?.to_float()?,
|
||||
),
|
||||
[max] => (
|
||||
0.,
|
||||
max.to_owned().simple_evaluate(location).ok()?.to_float()?,
|
||||
),
|
||||
_ => return None,
|
||||
};
|
||||
|
||||
Some((min.ceil() as i32, max.floor() as i32))
|
||||
}
|
||||
|
|
@ -1,14 +1,18 @@
|
|||
use dm::Context;
|
||||
use std::borrow::Cow;
|
||||
|
||||
use crate::{run_inner};
|
||||
use crate::run_inner;
|
||||
|
||||
pub const NO_ERRORS: &[(u32, u16, &str)] = &[];
|
||||
|
||||
pub fn parse_a_file_for_test<S: Into<Cow<'static, str>>>(buffer: S) -> Context {
|
||||
let context = Context::default();
|
||||
|
||||
let pp = dm::preprocessor::Preprocessor::from_buffer(&context, "unit_tests.rs".into(), buffer.into());
|
||||
let pp = dm::preprocessor::Preprocessor::from_buffer(
|
||||
&context,
|
||||
"unit_tests.rs".into(),
|
||||
buffer.into(),
|
||||
);
|
||||
|
||||
let indents = dm::indents::IndentProcessor::new(&context, pp);
|
||||
|
||||
|
|
@ -32,7 +36,7 @@ pub fn check_errors_match<S: Into<Cow<'static, str>>>(buffer: S, errorlist: &[(u
|
|||
|| nexterror.description() != *desc
|
||||
{
|
||||
panic!(
|
||||
"possible feature regression in dreamchecker, expected {}:{}:{}, found {}:{}:{}",
|
||||
"possible feature regression in dreamchecker:\nexpected error: {}:{}:{}\nfound error: {}:{}:{}",
|
||||
*line,
|
||||
*column,
|
||||
*desc,
|
||||
|
|
|
|||
|
|
@ -1,21 +1,19 @@
|
|||
//! Support for "type expressions", used in evaluating dynamic/generic return
|
||||
//! types.
|
||||
|
||||
use std::collections::HashMap;
|
||||
use foldhash::HashMap;
|
||||
|
||||
use dm::ast::*;
|
||||
use dm::constants::Constant;
|
||||
use dm::objtree::{ObjectTree, ProcRef};
|
||||
use dm::{DMError, Location};
|
||||
|
||||
use ahash::RandomState;
|
||||
|
||||
use crate::{Analysis, StaticType};
|
||||
|
||||
pub struct TypeExprContext<'o, 't> {
|
||||
pub objtree: &'o ObjectTree,
|
||||
pub param_name_map: HashMap<&'t str, Analysis<'o>, RandomState>,
|
||||
pub param_idx_map: HashMap<usize, Analysis<'o>, RandomState>,
|
||||
pub param_name_map: HashMap<&'t str, Analysis<'o>>,
|
||||
pub param_idx_map: HashMap<usize, Analysis<'o>>,
|
||||
}
|
||||
|
||||
impl<'o, 't> TypeExprContext<'o, 't> {
|
||||
|
|
@ -86,7 +84,7 @@ impl<'o> TypeExpr<'o> {
|
|||
} else {
|
||||
else_.evaluate(location, ec)
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
TypeExpr::ParamTypepath {
|
||||
name,
|
||||
|
|
@ -102,7 +100,7 @@ impl<'o> TypeExpr<'o> {
|
|||
} else {
|
||||
Ok(StaticType::None)
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
TypeExpr::ParamStaticType {
|
||||
name,
|
||||
|
|
@ -114,7 +112,7 @@ impl<'o> TypeExpr<'o> {
|
|||
} else {
|
||||
Ok(StaticType::None)
|
||||
}
|
||||
}
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -137,16 +135,13 @@ impl<'o> TypeExprCompiler<'o> {
|
|||
expr: &Expression,
|
||||
) -> Result<TypeExpr<'o>, DMError> {
|
||||
match expr {
|
||||
Expression::Base {
|
||||
term,
|
||||
follow,
|
||||
} => {
|
||||
Expression::Base { term, follow } => {
|
||||
let mut ty = self.visit_term(term.location, &term.elem)?;
|
||||
for each in follow.iter() {
|
||||
ty = self.visit_follow(each.location, ty, &each.elem)?;
|
||||
}
|
||||
Ok(ty)
|
||||
}
|
||||
},
|
||||
Expression::BinaryOp {
|
||||
op: BinaryOp::Or,
|
||||
lhs,
|
||||
|
|
@ -160,7 +155,7 @@ impl<'o> TypeExprCompiler<'o> {
|
|||
if_: Box::new(lty),
|
||||
else_: Box::new(rty),
|
||||
})
|
||||
}
|
||||
},
|
||||
Expression::BinaryOp {
|
||||
op: BinaryOp::And,
|
||||
lhs,
|
||||
|
|
@ -174,7 +169,7 @@ impl<'o> TypeExprCompiler<'o> {
|
|||
if_: Box::new(rty),
|
||||
else_: Box::new(lty),
|
||||
})
|
||||
}
|
||||
},
|
||||
Expression::TernaryOp { cond, if_, else_ } => Ok(TypeExpr::Condition {
|
||||
cond: Box::new(self.visit_expression(location, cond)?),
|
||||
if_: Box::new(self.visit_expression(location, if_)?),
|
||||
|
|
@ -200,9 +195,9 @@ impl<'o> TypeExprCompiler<'o> {
|
|||
}
|
||||
Err(DMError::new(
|
||||
location,
|
||||
format!("type expr: no such parameter {:?}", unscoped_name),
|
||||
format!("type expr: no such parameter {unscoped_name:?}"),
|
||||
))
|
||||
}
|
||||
},
|
||||
|
||||
Term::Expr(expr) => self.visit_expression(location, expr),
|
||||
|
||||
|
|
@ -210,7 +205,7 @@ impl<'o> TypeExprCompiler<'o> {
|
|||
let bits: Vec<_> = fab.path.iter().map(|(_, name)| name.to_owned()).collect();
|
||||
let ty = crate::static_type(self.objtree, location, &bits)?;
|
||||
Ok(TypeExpr::from(ty))
|
||||
}
|
||||
},
|
||||
|
||||
_ => Err(DMError::new(location, "type expr: bad term node")),
|
||||
}
|
||||
|
|
@ -263,7 +258,10 @@ impl<'o> TypeExprCompiler<'o> {
|
|||
)),
|
||||
},
|
||||
|
||||
_ => Err(DMError::new(location, format!("type expr: bad follow node {:?}", rhs))),
|
||||
_ => Err(DMError::new(
|
||||
location,
|
||||
format!("type expr: bad follow node {rhs:?}"),
|
||||
)),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,4 +1,3 @@
|
|||
|
||||
extern crate dreamchecker as dc;
|
||||
|
||||
use dc::test_helpers::*;
|
||||
|
|
@ -15,13 +14,12 @@ fn const_eval() {
|
|||
if(1)
|
||||
return
|
||||
return
|
||||
"##.trim();
|
||||
"##
|
||||
.trim();
|
||||
check_errors_match(code, CONST_EVAL_ERRORS);
|
||||
}
|
||||
|
||||
pub const IF_ELSE_ERRORS: &[(u32, u16, &str)] = &[
|
||||
(6, 5, "possible unreachable code here"),
|
||||
];
|
||||
pub const IF_ELSE_ERRORS: &[(u32, u16, &str)] = &[(6, 5, "possible unreachable code here")];
|
||||
|
||||
#[test]
|
||||
fn if_else() {
|
||||
|
|
@ -32,16 +30,25 @@ fn if_else() {
|
|||
else
|
||||
return
|
||||
return
|
||||
"##.trim();
|
||||
"##
|
||||
.trim();
|
||||
check_errors_match(code, IF_ELSE_ERRORS);
|
||||
}
|
||||
|
||||
pub const IF_ARMS_ERRORS: &[(u32, u16, &str)] = &[
|
||||
(2, 7, "control flow condition is a static term"),
|
||||
(2, 7, "if condition is always true"),
|
||||
(4, 12, "unreachable if block, preceeding if/elseif condition(s) are always true"),
|
||||
(
|
||||
4,
|
||||
12,
|
||||
"unreachable if block, preceeding if/elseif condition(s) are always true",
|
||||
),
|
||||
// TODO: fix location reporting on this
|
||||
(7, 9, "unreachable else block, preceeding if/elseif condition(s) are always true"),
|
||||
(
|
||||
7,
|
||||
9,
|
||||
"unreachable else block, preceeding if/elseif condition(s) are always true",
|
||||
),
|
||||
];
|
||||
|
||||
#[test]
|
||||
|
|
@ -54,13 +61,13 @@ fn if_arms() {
|
|||
return
|
||||
else
|
||||
return
|
||||
"##.trim();
|
||||
"##
|
||||
.trim();
|
||||
check_errors_match(code, IF_ARMS_ERRORS);
|
||||
}
|
||||
|
||||
pub const DO_WHILE_ERRORS: &[(u32, u16, &str)] = &[
|
||||
(2, 5, "do while terminates without ever reaching condition"),
|
||||
];
|
||||
pub const DO_WHILE_ERRORS: &[(u32, u16, &str)] =
|
||||
&[(2, 5, "do while terminates without ever reaching condition")];
|
||||
|
||||
#[test]
|
||||
fn do_while() {
|
||||
|
|
@ -69,7 +76,8 @@ fn do_while() {
|
|||
do
|
||||
return
|
||||
while(prob(50))
|
||||
"##.trim();
|
||||
"##
|
||||
.trim();
|
||||
check_errors_match(code, DO_WHILE_ERRORS);
|
||||
}
|
||||
|
||||
|
|
@ -92,6 +100,74 @@ fn for_loop_condition() {
|
|||
for(var/z = 1; z <= 6; z++) // Legit, should have no error
|
||||
break
|
||||
return
|
||||
"##.trim();
|
||||
"##
|
||||
.trim();
|
||||
check_errors_match(code, FOR_LOOP_CONDITION_ERRORS);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn for_kv_check() {
|
||||
let code = r##"
|
||||
/proc/test()
|
||||
var/alist/A = alist()
|
||||
for (var/k, v in A)
|
||||
world.log << k
|
||||
world.log << v
|
||||
|
||||
"##
|
||||
.trim();
|
||||
check_errors_match(code, NO_ERRORS);
|
||||
}
|
||||
|
||||
pub const FOR_KV_VAR_ERROR: &[(u32, u16, &str)] =
|
||||
&[(3, 19, "for (var/key, value) requires a 'var' keyword")];
|
||||
|
||||
#[test]
|
||||
fn for_kv_var_check() {
|
||||
let code = r##"
|
||||
/proc/test()
|
||||
var/alist/A = alist()
|
||||
for (k, v in A)
|
||||
world.log << k
|
||||
world.log << v
|
||||
|
||||
"##
|
||||
.trim();
|
||||
check_errors_match(code, FOR_KV_VAR_ERROR);
|
||||
}
|
||||
|
||||
pub const FOR_KV_VALUE_ERROR: &[(u32, u16, &str)] = &[(
|
||||
3,
|
||||
23,
|
||||
"value must be a variable in a for (var/key, value) statement",
|
||||
)];
|
||||
|
||||
#[test]
|
||||
fn for_kv_value_check() {
|
||||
let code = r##"
|
||||
/proc/test()
|
||||
var/alist/A = alist()
|
||||
for (var/k, 0 in A)
|
||||
world.log << k
|
||||
"##
|
||||
.trim();
|
||||
check_errors_match(code, FOR_KV_VALUE_ERROR);
|
||||
}
|
||||
|
||||
pub const FOR_KV_KEY_ERROR: &[(u32, u16, &str)] = &[(
|
||||
3,
|
||||
27,
|
||||
"cannot assigned a value to var/key in a for(var/key, value) statement",
|
||||
)];
|
||||
|
||||
#[test]
|
||||
fn for_kv_key_check() {
|
||||
let code = r##"
|
||||
/proc/test()
|
||||
var/alist/A = alist()
|
||||
for (var/k = 5, v in A)
|
||||
world.log << k
|
||||
"##
|
||||
.trim();
|
||||
check_errors_match(code, FOR_KV_KEY_ERROR);
|
||||
}
|
||||
|
|
|
|||
16
crates/dreamchecker/tests/call_ext_tests.rs
Normal file
16
crates/dreamchecker/tests/call_ext_tests.rs
Normal file
|
|
@ -0,0 +1,16 @@
|
|||
extern crate dreamchecker as dc;
|
||||
|
||||
use dc::test_helpers::*;
|
||||
|
||||
pub const CALL_EXT_MISSING_CALL_ERRORS: &[(u32, u16, &str)] =
|
||||
&[(2, 19, "got ';', expected one of: '('")];
|
||||
|
||||
#[test]
|
||||
fn call_ext_missing_call() {
|
||||
let code = r##"
|
||||
/proc/f()
|
||||
call_ext(1, 2)
|
||||
"##
|
||||
.trim();
|
||||
check_errors_match(code, CALL_EXT_MISSING_CALL_ERRORS);
|
||||
}
|
||||
|
|
@ -1,11 +1,9 @@
|
|||
|
||||
extern crate dreamchecker as dc;
|
||||
|
||||
use dc::test_helpers::*;
|
||||
|
||||
pub const TRUE_SUB_ERRORS: &[(u32, u16, &str)] = &[
|
||||
(4, 18, "proc never calls parent, required by /mob/proc/test"),
|
||||
];
|
||||
pub const TRUE_SUB_ERRORS: &[(u32, u16, &str)] =
|
||||
&[(4, 18, "proc never calls parent, required by /mob/proc/test")];
|
||||
|
||||
#[test]
|
||||
fn true_substitution() {
|
||||
|
|
@ -15,7 +13,8 @@ fn true_substitution() {
|
|||
|
||||
/mob/subtype/test()
|
||||
return
|
||||
"##.trim();
|
||||
"##
|
||||
.trim();
|
||||
check_errors_match(code, TRUE_SUB_ERRORS);
|
||||
}
|
||||
|
||||
|
|
@ -29,7 +28,8 @@ fn call_parent() {
|
|||
return
|
||||
/mob/anothertype/test()
|
||||
..()
|
||||
"##.trim();
|
||||
"##
|
||||
.trim();
|
||||
check_errors_match(code, TRUE_SUB_ERRORS);
|
||||
}
|
||||
|
||||
|
|
@ -42,13 +42,13 @@ fn call_parent_disable() {
|
|||
/mob/subtype/test()
|
||||
set SpacemanDMM_should_call_parent = 0
|
||||
return
|
||||
"##.trim();
|
||||
"##
|
||||
.trim();
|
||||
check_errors_match(code, NO_ERRORS);
|
||||
}
|
||||
|
||||
pub const NO_OVERRIDE_ERRORS: &[(u32, u16, &str)] = &[
|
||||
(4, 18, "proc overrides parent, prohibited by /mob/proc/test"),
|
||||
];
|
||||
pub const NO_OVERRIDE_ERRORS: &[(u32, u16, &str)] =
|
||||
&[(4, 18, "proc overrides parent, prohibited by /mob/proc/test")];
|
||||
|
||||
#[test]
|
||||
fn no_override() {
|
||||
|
|
@ -58,7 +58,21 @@ fn no_override() {
|
|||
|
||||
/mob/subtype/test()
|
||||
return
|
||||
"##.trim();
|
||||
"##
|
||||
.trim();
|
||||
check_errors_match(code, NO_OVERRIDE_ERRORS);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn final_proc() {
|
||||
let code = r##"
|
||||
/mob/proc/final/test()
|
||||
return
|
||||
|
||||
/mob/subtype/test()
|
||||
return
|
||||
"##
|
||||
.trim();
|
||||
check_errors_match(code, NO_OVERRIDE_ERRORS);
|
||||
}
|
||||
|
||||
|
|
@ -76,7 +90,22 @@ fn no_override_disable() {
|
|||
/mob/subtype/test()
|
||||
set SpacemanDMM_should_not_override = 0
|
||||
return
|
||||
"##.trim();
|
||||
"##
|
||||
.trim();
|
||||
check_errors_match(code, NO_OVERRIDE_DISABLE_ERRORS);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn final_proc_intermix() {
|
||||
let code = r##"
|
||||
/mob/proc/final/test()
|
||||
return
|
||||
|
||||
/mob/subtype/test()
|
||||
set SpacemanDMM_should_not_override = 0
|
||||
return
|
||||
"##
|
||||
.trim();
|
||||
check_errors_match(code, NO_OVERRIDE_DISABLE_ERRORS);
|
||||
}
|
||||
|
||||
|
|
@ -89,13 +118,12 @@ fn can_be_redefined() {
|
|||
|
||||
/mob/test()
|
||||
return
|
||||
"##.trim();
|
||||
"##
|
||||
.trim();
|
||||
check_errors_match(code, NO_ERRORS);
|
||||
}
|
||||
|
||||
pub const NO_CAN_BE_REDEFINED_ERRORS: &[(u32, u16, &str)] = &[
|
||||
(4, 10, "redefining proc /mob/test"),
|
||||
];
|
||||
pub const NO_CAN_BE_REDEFINED_ERRORS: &[(u32, u16, &str)] = &[(4, 10, "redefining proc /mob/test")];
|
||||
|
||||
#[test]
|
||||
fn no_can_be_redefined() {
|
||||
|
|
@ -105,6 +133,7 @@ fn no_can_be_redefined() {
|
|||
|
||||
/mob/test()
|
||||
return
|
||||
"##.trim();
|
||||
"##
|
||||
.trim();
|
||||
check_errors_match(code, NO_CAN_BE_REDEFINED_ERRORS);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,11 +1,9 @@
|
|||
|
||||
extern crate dreamchecker as dc;
|
||||
|
||||
use dc::test_helpers::*;
|
||||
|
||||
pub const AFTER_KWARG_ERRORS: &[(u32, u16, &str)] = &[
|
||||
(3, 5, "proc called with non-kwargs after kwargs: foo()"),
|
||||
];
|
||||
pub const AFTER_KWARG_ERRORS: &[(u32, u16, &str)] =
|
||||
&[(3, 5, "proc called with non-kwargs after kwargs: foo()")];
|
||||
|
||||
#[test]
|
||||
fn after_kwarg() {
|
||||
|
|
@ -13,23 +11,52 @@ fn after_kwarg() {
|
|||
/proc/foo(arg1, arg2, arg3)
|
||||
/proc/test()
|
||||
foo(arg2=1, 1)
|
||||
"##.trim();
|
||||
"##
|
||||
.trim();
|
||||
check_errors_match(code, AFTER_KWARG_ERRORS);
|
||||
}
|
||||
|
||||
pub const FILTER_KWARGS_ERRORS: &[(u32, u16, &str)] = &[
|
||||
(4, 5, "filter(type=\"color\") called with invalid 'space' value 'Null'"),
|
||||
(15, 5, "filter(type=\"alpha\") called with invalid keyword parameter 'color'"),
|
||||
(16, 5, "filter(type=\"blur\") called with invalid keyword parameter 'x'"),
|
||||
(17, 5, "filter() called with invalid type keyword parameter value 'fakename'"),
|
||||
(18, 5, "filter() called without mandatory keyword parameter 'type'"),
|
||||
(19, 5, "filter() called without mandatory keyword parameter 'type'"),
|
||||
(20, 5, "filter(type=\"wave\") called with invalid keyword parameter 'color'"),
|
||||
(
|
||||
4,
|
||||
5,
|
||||
"filter(type=\"color\") called with invalid 'space' value 'Null'",
|
||||
),
|
||||
(
|
||||
15,
|
||||
5,
|
||||
"filter(type=\"alpha\") called with invalid keyword parameter 'color'",
|
||||
),
|
||||
(
|
||||
16,
|
||||
5,
|
||||
"filter(type=\"blur\") called with invalid keyword parameter 'x'",
|
||||
),
|
||||
(
|
||||
17,
|
||||
5,
|
||||
"filter() called with invalid type keyword parameter value 'fakename'",
|
||||
),
|
||||
(
|
||||
18,
|
||||
5,
|
||||
"filter() called without mandatory keyword parameter 'type'",
|
||||
),
|
||||
(
|
||||
19,
|
||||
5,
|
||||
"filter() called without mandatory keyword parameter 'type'",
|
||||
),
|
||||
(
|
||||
20,
|
||||
5,
|
||||
"filter(type=\"wave\") called with invalid keyword parameter 'color'",
|
||||
),
|
||||
];
|
||||
|
||||
#[test]
|
||||
fn filter_kwarg() {
|
||||
let code = r##"
|
||||
let code = r#"
|
||||
/proc/test()
|
||||
filter(type="alpha", x=1, y=2, icon=null, render_source=null, flags=0)
|
||||
filter(type="angular_blur", x=1, y=2, size=null)
|
||||
|
|
@ -50,6 +77,6 @@ fn filter_kwarg() {
|
|||
filter(x=4)
|
||||
filter("alpha", x=1, flags=MASK_INVERSE|MASK_INVERSE|MASK_INVERSE|MASK_INVERSE|MASK_INVERSE|MASK_INVERSE)
|
||||
filter(type="wave", color=null)
|
||||
"##.trim();
|
||||
"#.trim();
|
||||
check_errors_match(code, FILTER_KWARGS_ERRORS);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,4 +1,3 @@
|
|||
|
||||
extern crate dreamchecker as dc;
|
||||
|
||||
use dc::test_helpers::check_errors_match;
|
||||
|
|
@ -25,6 +24,7 @@ fn local_scope() {
|
|||
alabel:
|
||||
var/bar
|
||||
bar++
|
||||
"##.trim();
|
||||
"##
|
||||
.trim();
|
||||
check_errors_match(code, LOCAL_SCOPE_ERRORS);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,15 +1,16 @@
|
|||
|
||||
extern crate dreamchecker as dc;
|
||||
|
||||
use dc::test_helpers::*;
|
||||
|
||||
pub const NEW_DOT_ERRORS: &[(u32, u16, &str)] = &[
|
||||
(12, 14, "got '(', expected one of: operator, field access, ';'"),
|
||||
];
|
||||
pub const NEW_DOT_ERRORS: &[(u32, u16, &str)] = &[(
|
||||
12,
|
||||
14,
|
||||
"got '(', expected one of: operator, field access, ';'",
|
||||
)];
|
||||
|
||||
#[test]
|
||||
fn new_dot() {
|
||||
let code = r##"
|
||||
let code = r#"
|
||||
/mob/subtype
|
||||
/mob/proc/foo()
|
||||
/mob/proc/test()
|
||||
|
|
@ -24,13 +25,16 @@ fn new_dot() {
|
|||
new foo()()
|
||||
new /obj[0]() // TODO: see parser.rs
|
||||
new 2 + 2() // TODO: see parser.rs
|
||||
"##.trim();
|
||||
"#
|
||||
.trim();
|
||||
check_errors_match(code, NEW_DOT_ERRORS);
|
||||
}
|
||||
|
||||
pub const NEW_PRECEDENCE_ERRORS: &[(u32, u16, &str)] = &[
|
||||
(4, 13, "got '(', expected one of: operator, field access, ';'"),
|
||||
];
|
||||
pub const NEW_PRECEDENCE_ERRORS: &[(u32, u16, &str)] = &[(
|
||||
4,
|
||||
13,
|
||||
"got '(', expected one of: operator, field access, ';'",
|
||||
)];
|
||||
|
||||
#[test]
|
||||
fn new_precedence() {
|
||||
|
|
@ -39,6 +43,7 @@ fn new_precedence() {
|
|||
/mob/proc/foo()
|
||||
/mob/proc/test()
|
||||
new L[1]()
|
||||
"##.trim();
|
||||
"##
|
||||
.trim();
|
||||
check_errors_match(code, NEW_PRECEDENCE_ERRORS);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -30,13 +30,16 @@ fn in_ambig() {
|
|||
return
|
||||
if((i ? 1 : 2) in list())
|
||||
return
|
||||
"##.trim();
|
||||
"##
|
||||
.trim();
|
||||
check_errors_match(code, IN_AMBIG_ERRORS);
|
||||
}
|
||||
|
||||
pub const TERNARY_IN_AMBIG_ERRORS: &[(u32, u16, &str)] = &[
|
||||
(2, 14, "got \'in\', expected one of: operator, field access, \':\'"),
|
||||
];
|
||||
pub const TERNARY_IN_AMBIG_ERRORS: &[(u32, u16, &str)] = &[(
|
||||
2,
|
||||
14,
|
||||
"got \'in\', expected one of: operator, field access, \':\'",
|
||||
)];
|
||||
|
||||
#[test]
|
||||
fn ambig_in_ternary_cond() {
|
||||
|
|
@ -44,13 +47,16 @@ fn ambig_in_ternary_cond() {
|
|||
/proc/test()
|
||||
if(i ? 1 in list() : 2)
|
||||
return
|
||||
"##.trim();
|
||||
"##
|
||||
.trim();
|
||||
check_errors_match(code, TERNARY_IN_AMBIG_ERRORS);
|
||||
}
|
||||
|
||||
pub const OP_OVERLOAD_ERRORS: &[(u32, u16, &str)] = &[
|
||||
(6, 6, "Attempting operator++ on a /mob which does not overload operator++"),
|
||||
];
|
||||
pub const OP_OVERLOAD_ERRORS: &[(u32, u16, &str)] = &[(
|
||||
6,
|
||||
6,
|
||||
"Attempting operator++ on a /mob which does not overload operator++",
|
||||
)];
|
||||
|
||||
#[test]
|
||||
fn operator_overload() {
|
||||
|
|
@ -63,7 +69,8 @@ fn operator_overload() {
|
|||
M++
|
||||
var/mob/test/T = new
|
||||
T++
|
||||
"##.trim();
|
||||
"##
|
||||
.trim();
|
||||
check_errors_match(code, OP_OVERLOAD_ERRORS);
|
||||
}
|
||||
|
||||
|
|
@ -87,6 +94,7 @@ fn ambigous_not_bitwise() {
|
|||
return
|
||||
if (1++ & 1)
|
||||
return
|
||||
"##.trim();
|
||||
"##
|
||||
.trim();
|
||||
check_errors_match(code, NOT_AMBIG_BITWISE_ERRORS);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,4 +1,3 @@
|
|||
|
||||
extern crate dreamchecker as dc;
|
||||
|
||||
use dc::test_helpers::*;
|
||||
|
|
@ -31,14 +30,19 @@ fn private_proc() {
|
|||
M.private2()
|
||||
var/mob/subtype/S = new
|
||||
S.private()
|
||||
"##.trim();
|
||||
"##
|
||||
.trim();
|
||||
check_errors_match(code, PRIVATE_PROC_ERRORS);
|
||||
}
|
||||
|
||||
pub const PRIVATE_VAR_ERRORS: &[(u32, u16, &str)] = &[
|
||||
(5, 9, "/mob/subtype overrides private var \"foo\""),
|
||||
(12, 6, "field \"bar\" on /mob is declared as private"),
|
||||
(14, 6, "field \"foo\" on /mob/subtype is declared as private"),
|
||||
(
|
||||
14,
|
||||
6,
|
||||
"field \"foo\" on /mob/subtype is declared as private",
|
||||
),
|
||||
];
|
||||
|
||||
#[test]
|
||||
|
|
@ -58,13 +62,22 @@ fn private_var() {
|
|||
M.bar = TRUE
|
||||
var/mob/subtype/S = new
|
||||
S.foo = TRUE
|
||||
"##.trim();
|
||||
"##
|
||||
.trim();
|
||||
check_errors_match(code, PRIVATE_VAR_ERRORS);
|
||||
}
|
||||
|
||||
pub const PROTECTED_PROC_ERRORS: &[(u32, u16, &str)] = &[
|
||||
(15, 6, "/obj/proc/test attempting to call protected proc /mob/proc/protected2"),
|
||||
(17, 6, "/obj/proc/test attempting to call protected proc /mob/proc/protected"),
|
||||
(
|
||||
15,
|
||||
6,
|
||||
"/obj/proc/test attempting to call protected proc /mob/proc/protected2",
|
||||
),
|
||||
(
|
||||
17,
|
||||
6,
|
||||
"/obj/proc/test attempting to call protected proc /mob/proc/protected",
|
||||
),
|
||||
];
|
||||
|
||||
#[test]
|
||||
|
|
@ -87,13 +100,18 @@ fn protected_proc() {
|
|||
M.protected2()
|
||||
var/mob/subtype/S = new
|
||||
S.protected()
|
||||
"##.trim();
|
||||
"##
|
||||
.trim();
|
||||
check_errors_match(code, PROTECTED_PROC_ERRORS);
|
||||
}
|
||||
|
||||
pub const PROTECTED_VAR_ERRORS: &[(u32, u16, &str)] = &[
|
||||
(12, 6, "field \"bar\" on /mob is declared as protected"),
|
||||
(14, 6, "field \"foo\" on /mob/subtype is declared as protected"),
|
||||
(
|
||||
14,
|
||||
6,
|
||||
"field \"foo\" on /mob/subtype is declared as protected",
|
||||
),
|
||||
];
|
||||
|
||||
#[test]
|
||||
|
|
@ -113,6 +131,7 @@ fn protected_var() {
|
|||
M.bar = TRUE
|
||||
var/mob/subtype/S = new
|
||||
S.foo = TRUE
|
||||
"##.trim();
|
||||
"##
|
||||
.trim();
|
||||
check_errors_match(code, PROTECTED_VAR_ERRORS);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,11 +1,9 @@
|
|||
|
||||
extern crate dreamchecker as dc;
|
||||
|
||||
use dc::test_helpers::check_errors_match;
|
||||
use dc::test_helpers::parse_a_file_for_test;
|
||||
|
||||
pub const NO_PARENT_ERRORS: &[(u32, u16, &str)] = &[
|
||||
(2, 5, "proc has no parent: /mob/proc/test"),
|
||||
];
|
||||
pub const NO_PARENT_ERRORS: &[(u32, u16, &str)] = &[(2, 5, "proc has no parent: /mob/proc/test")];
|
||||
|
||||
#[test]
|
||||
fn no_parent() {
|
||||
|
|
@ -13,6 +11,49 @@ fn no_parent() {
|
|||
/mob/proc/test()
|
||||
..()
|
||||
return
|
||||
"##.trim();
|
||||
"##
|
||||
.trim();
|
||||
check_errors_match(code, NO_PARENT_ERRORS);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn return_type() {
|
||||
let code = r##"
|
||||
/mob/proc/test() as /datum
|
||||
return
|
||||
|
||||
/mob/proc/test2() as num
|
||||
return
|
||||
"##
|
||||
.trim();
|
||||
let context = parse_a_file_for_test(code);
|
||||
let error_text: Vec<String> = context
|
||||
.errors()
|
||||
.iter()
|
||||
.map(|error| format!("{error}"))
|
||||
.collect();
|
||||
if !error_text.is_empty() {
|
||||
panic!("\n{}", error_text.join("\n"))
|
||||
}
|
||||
}
|
||||
|
||||
pub const RETURN_TYPE_FAILURE_ERRORS: &[(u32, u16, &str)] = &[
|
||||
(4, 13, "cannot specify a return type for a proc override"),
|
||||
(7, 22, "bad input type: 'incorrect'"),
|
||||
];
|
||||
|
||||
#[test]
|
||||
fn return_type_failure() {
|
||||
let code = r##"
|
||||
/datum/proc/test() as /datum
|
||||
return
|
||||
|
||||
/mob/test() as /mob
|
||||
return
|
||||
|
||||
/mob/proc/test2() as incorrect
|
||||
return
|
||||
"##
|
||||
.trim();
|
||||
check_errors_match(code, RETURN_TYPE_FAILURE_ERRORS);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,11 +1,12 @@
|
|||
|
||||
extern crate dreamchecker as dc;
|
||||
|
||||
use dc::test_helpers::check_errors_match;
|
||||
|
||||
pub const SLEEP_ERRORS: &[(u32, u16, &str)] = &[
|
||||
(16, 16, "/mob/proc/test3 sets SpacemanDMM_should_not_sleep but calls blocking proc /proc/sleepingproc"),
|
||||
];
|
||||
pub const SLEEP_ERRORS: &[(u32, u16, &str)] = &[(
|
||||
16,
|
||||
16,
|
||||
"/mob/proc/test3 sets SpacemanDMM_should_not_sleep but calls blocking proc /proc/sleepingproc",
|
||||
)];
|
||||
|
||||
#[test]
|
||||
fn sleep() {
|
||||
|
|
@ -41,7 +42,8 @@ fn sleep() {
|
|||
/mob/proc/test6()
|
||||
set SpacemanDMM_should_not_sleep = TRUE
|
||||
spawnthensleepproc()
|
||||
"##.trim();
|
||||
"##
|
||||
.trim();
|
||||
check_errors_match(code, SLEEP_ERRORS);
|
||||
}
|
||||
|
||||
|
|
@ -75,7 +77,8 @@ fn sleep2() {
|
|||
sleep(1)
|
||||
/mob/living/thing()
|
||||
. = ..()
|
||||
"##.trim();
|
||||
"##
|
||||
.trim();
|
||||
check_errors_match(code, SLEEP_ERRORS2);
|
||||
}
|
||||
|
||||
|
|
@ -106,7 +109,8 @@ fn sleep3() {
|
|||
sleep(1)
|
||||
/atom/movable/thing()
|
||||
. = ..()
|
||||
"##.trim();
|
||||
"##
|
||||
.trim();
|
||||
check_errors_match(code, &[
|
||||
(8, 23, "/atom/movable/proc/bar calls /atom/movable/proc/foo which has override child proc that sleeps /mob/proc/foo"),
|
||||
]);
|
||||
|
|
@ -145,17 +149,45 @@ fn sleep4() {
|
|||
/mob/proc/test2()
|
||||
var/client/C = new /client
|
||||
C.MeasureText()
|
||||
"##.trim();
|
||||
"##
|
||||
.trim();
|
||||
check_errors_match(code, SLEEP_ERROR4);
|
||||
}
|
||||
|
||||
// Test overrides and for regression of issue #267
|
||||
pub const SLEEP_ERROR5: &[(u32, u16, &str)] = &[
|
||||
(7, 19, "/datum/sub/proc/checker sets SpacemanDMM_should_not_sleep but calls blocking proc /proc/sleeper"),
|
||||
];
|
||||
|
||||
#[test]
|
||||
fn sleep5() {
|
||||
let code = r##"
|
||||
/datum/proc/checker()
|
||||
set SpacemanDMM_should_not_sleep = 1
|
||||
|
||||
/datum/proc/proxy()
|
||||
sleeper()
|
||||
|
||||
/datum/sub/checker()
|
||||
proxy()
|
||||
|
||||
/proc/sleeper()
|
||||
sleep(1)
|
||||
|
||||
/datum/hijack/proxy()
|
||||
sleep(1)
|
||||
"##
|
||||
.trim();
|
||||
check_errors_match(code, SLEEP_ERROR5);
|
||||
}
|
||||
|
||||
pub const PURE_ERRORS: &[(u32, u16, &str)] = &[
|
||||
(12, 16, "/mob/proc/test2 sets SpacemanDMM_should_be_pure but calls a /proc/impure that does impure operations"),
|
||||
];
|
||||
|
||||
#[test]
|
||||
fn pure() {
|
||||
let code = r##"
|
||||
let code = r#"
|
||||
/proc/pure()
|
||||
return 1
|
||||
/proc/impure()
|
||||
|
|
@ -170,15 +202,15 @@ fn pure() {
|
|||
/mob/proc/test2()
|
||||
set SpacemanDMM_should_be_pure = TRUE
|
||||
bar()
|
||||
"##.trim();
|
||||
"#
|
||||
.trim();
|
||||
check_errors_match(code, PURE_ERRORS);
|
||||
}
|
||||
|
||||
// these tests are separate because the ordering the errors are reported in isn't determinate and I CBF figuring out why -spookydonut Jan 2020
|
||||
// TODO: find out why
|
||||
pub const PURE2_ERRORS: &[(u32, u16, &str)] = &[
|
||||
(5, 5, "call to pure proc test discards return value"),
|
||||
];
|
||||
pub const PURE2_ERRORS: &[(u32, u16, &str)] =
|
||||
&[(5, 5, "call to pure proc test discards return value")];
|
||||
|
||||
#[test]
|
||||
fn pure2() {
|
||||
|
|
@ -190,6 +222,7 @@ fn pure2() {
|
|||
test()
|
||||
/mob/proc/test3()
|
||||
return test()
|
||||
"##.trim();
|
||||
"##
|
||||
.trim();
|
||||
check_errors_match(code, PURE2_ERRORS);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,4 +1,3 @@
|
|||
|
||||
extern crate dreamchecker as dc;
|
||||
|
||||
use dc::test_helpers::*;
|
||||
|
|
@ -17,7 +16,8 @@ fn field_access() {
|
|||
L?[1].name
|
||||
var/atom/movable/particle_holder = new
|
||||
particle_holder.particles.height
|
||||
"##.trim();
|
||||
"##
|
||||
.trim();
|
||||
check_errors_match(code, FIELD_ACCESS_ERRORS);
|
||||
}
|
||||
|
||||
|
|
@ -34,13 +34,12 @@ fn proc_call() {
|
|||
L[1].foo()
|
||||
L?[1].foo()
|
||||
/mob/proc/foo()
|
||||
"##.trim();
|
||||
"##
|
||||
.trim();
|
||||
check_errors_match(code, PROC_CALL_ERRORS);
|
||||
}
|
||||
|
||||
pub const RETURN_TYPE_ERRORS: &[(u32, u16, &str)] = &[
|
||||
(3, 16, "undefined proc: \"foo\" on /atom"),
|
||||
];
|
||||
pub const RETURN_TYPE_ERRORS: &[(u32, u16, &str)] = &[(3, 16, "undefined proc: \"foo\" on /atom")];
|
||||
|
||||
#[test]
|
||||
fn return_type() {
|
||||
|
|
@ -49,6 +48,7 @@ fn return_type() {
|
|||
viewers()[1].foo()
|
||||
orange()[1].foo()
|
||||
/mob/proc/foo()
|
||||
"##.trim();
|
||||
"##
|
||||
.trim();
|
||||
check_errors_match(code, RETURN_TYPE_ERRORS);
|
||||
}
|
||||
|
|
|
|||
118
crates/dreamchecker/tests/switch_rand_range_tests.rs
Normal file
118
crates/dreamchecker/tests/switch_rand_range_tests.rs
Normal file
|
|
@ -0,0 +1,118 @@
|
|||
extern crate dreamchecker as dc;
|
||||
|
||||
use dc::test_helpers::*;
|
||||
|
||||
pub const SWITCH_RAND_INCOMPLETE_ERRORS: &[(u32, u16, &str)] = &[
|
||||
(
|
||||
4,
|
||||
19,
|
||||
"Case range '0 to 0' will never trigger as it is outside the rand() range 1 to 3",
|
||||
),
|
||||
(
|
||||
2,
|
||||
5,
|
||||
"Switch branches on rand() with range 1 to 3 but no case branch triggers for 3",
|
||||
),
|
||||
];
|
||||
|
||||
#[test]
|
||||
fn switch_rand_incomplete() {
|
||||
let code = r##"
|
||||
/proc/test()
|
||||
switch(rand(1, 3))
|
||||
if(0)
|
||||
return
|
||||
if(1)
|
||||
return
|
||||
if(2)
|
||||
return
|
||||
"##
|
||||
.trim();
|
||||
check_errors_match(code, SWITCH_RAND_INCOMPLETE_ERRORS);
|
||||
}
|
||||
|
||||
pub const SWITCH_RAND_WITH_EVALUATION_ERRORS: &[(u32, u16, &str)] = &[(
|
||||
2,
|
||||
5,
|
||||
"Switch branches on rand() with range 2 to 3 but no case branch triggers for 3",
|
||||
)];
|
||||
|
||||
#[test]
|
||||
fn switch_rand_with_evaluation() {
|
||||
let code = r##"
|
||||
/proc/test()
|
||||
switch(rand(1 + 1, 4 - 1))
|
||||
if(3 - 1)
|
||||
return
|
||||
"##
|
||||
.trim();
|
||||
check_errors_match(code, SWITCH_RAND_WITH_EVALUATION_ERRORS);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn switch_rand_case_ranges() {
|
||||
let code = r##"
|
||||
/proc/test()
|
||||
switch(rand(1, 4))
|
||||
if(1 to 2)
|
||||
return
|
||||
if(3, 4)
|
||||
return
|
||||
"##
|
||||
.trim();
|
||||
check_errors_match(code, &[]);
|
||||
}
|
||||
|
||||
pub const SWITCH_RAND_DEFAULT_ERRORS: &[(u32, u16, &str)] = &[(
|
||||
4,
|
||||
19,
|
||||
"Case range '5 to 5' will never trigger as it is outside the rand() range 1 to 4",
|
||||
)];
|
||||
|
||||
#[test]
|
||||
fn switch_rand_default() {
|
||||
let code = r##"
|
||||
/proc/test()
|
||||
switch(rand(1, 4))
|
||||
if(5)
|
||||
return
|
||||
if(2)
|
||||
return
|
||||
else
|
||||
return
|
||||
"##
|
||||
.trim();
|
||||
check_errors_match(code, SWITCH_RAND_DEFAULT_ERRORS);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn switch_rand_floats() {
|
||||
let code = r##"
|
||||
/proc/test()
|
||||
switch(rand(1, 4))
|
||||
if(0.5 to 1.5)
|
||||
return
|
||||
if(2)
|
||||
return
|
||||
if(2.5 to 400)
|
||||
return
|
||||
"##
|
||||
.trim();
|
||||
check_errors_match(code, &[]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn switch_rand_out_of_order() {
|
||||
let code = r##"
|
||||
/proc/test()
|
||||
switch(rand(1, 4))
|
||||
if(3 to 4)
|
||||
return
|
||||
if(2)
|
||||
return
|
||||
if(1)
|
||||
return
|
||||
"##
|
||||
.trim();
|
||||
check_errors_match(code, &[]);
|
||||
}
|
||||
|
|
@ -1,11 +1,8 @@
|
|||
|
||||
extern crate dreamchecker as dc;
|
||||
|
||||
use dc::test_helpers::*;
|
||||
|
||||
pub const VAR_DEC_ERRORS: &[(u32, u16, &str)] = &[
|
||||
(5, 12, "/mob/subtype redeclares var \"foo\""),
|
||||
];
|
||||
pub const VAR_DEC_ERRORS: &[(u32, u16, &str)] = &[(5, 12, "/mob/subtype redeclares var \"foo\"")];
|
||||
|
||||
#[test]
|
||||
fn var_redec() {
|
||||
|
|
@ -15,29 +12,41 @@ fn var_redec() {
|
|||
|
||||
/mob/subtype
|
||||
var/foo
|
||||
"##.trim();
|
||||
"##
|
||||
.trim();
|
||||
check_errors_match(code, VAR_DEC_ERRORS);
|
||||
}
|
||||
|
||||
pub const VAR_FINAL_ERRORS: &[(u32, u16, &str)] = &[
|
||||
(5, 9, "/mob/subtype overrides final var \"foo\""),
|
||||
];
|
||||
pub const VAR_FINAL_ERRORS: &[(u32, u16, &str)] =
|
||||
&[(5, 9, "/mob/subtype overrides final var \"foo\"")];
|
||||
|
||||
#[test]
|
||||
fn var_final() {
|
||||
fn var_spaceman_final() {
|
||||
let code = r##"
|
||||
/mob
|
||||
var/SpacemanDMM_final/foo = 0
|
||||
|
||||
/mob/subtype
|
||||
foo = 1
|
||||
"##.trim();
|
||||
"##
|
||||
.trim();
|
||||
check_errors_match(code, VAR_FINAL_ERRORS);
|
||||
}
|
||||
|
||||
pub const VAR_UNDECL_ERRORS: &[(u32, u16, &str)] = &[
|
||||
(6, 5, "undefined var: \"bar\""),
|
||||
];
|
||||
#[test]
|
||||
fn var_final() {
|
||||
let code = r##"
|
||||
/mob
|
||||
var/final/foo = 0
|
||||
|
||||
/mob/subtype
|
||||
foo = 1
|
||||
"##
|
||||
.trim();
|
||||
check_errors_match(code, VAR_FINAL_ERRORS);
|
||||
}
|
||||
|
||||
pub const VAR_UNDECL_ERRORS: &[(u32, u16, &str)] = &[(6, 5, "undefined var: \"bar\"")];
|
||||
|
||||
#[test]
|
||||
fn var_undecl() {
|
||||
|
|
@ -48,7 +57,7 @@ fn var_undecl() {
|
|||
/mob/proc/test()
|
||||
foo++
|
||||
bar++
|
||||
"##.trim();
|
||||
"##
|
||||
.trim();
|
||||
check_errors_match(code, VAR_UNDECL_ERRORS);
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -2,23 +2,25 @@
|
|||
name = "dreammaker"
|
||||
version = "0.1.0"
|
||||
authors = ["Tad Hardesty <tad@platymuus.com>"]
|
||||
edition = "2018"
|
||||
edition = "2021"
|
||||
|
||||
[dependencies]
|
||||
interval-tree = { path = "../interval-tree" }
|
||||
builtins-proc-macro = { path = "../builtins-proc-macro" }
|
||||
lodepng = "3.1.0"
|
||||
bitflags = "1.0.3"
|
||||
termcolor = "1.0.4"
|
||||
ordered-float = "2.0.0"
|
||||
serde = { version = "1.0.103", features = ["derive"] }
|
||||
serde_derive = "1.0.103"
|
||||
toml = "0.5.5"
|
||||
guard = "0.5.0"
|
||||
phf = { version = "0.10.0", features = ["macros"] }
|
||||
color_space = "0.5.3"
|
||||
ahash = "0.7.6"
|
||||
indexmap = "1.7.0"
|
||||
lodepng = "3.10.7"
|
||||
bitflags = "1.3.2"
|
||||
termcolor = "1.4.1"
|
||||
ordered-float = "3.9.2"
|
||||
serde = { version = "1.0.213", features = ["derive"] }
|
||||
serde_derive = "1.0.213"
|
||||
toml = "0.5.11"
|
||||
phf = { version = "0.11.2", features = ["macros"] }
|
||||
color_space = "0.5.4"
|
||||
foldhash = "0.2.0"
|
||||
indexmap = "2.6.0"
|
||||
derivative = "2.2.0"
|
||||
get-size = "0.1.4"
|
||||
get-size-derive = "0.1.3"
|
||||
|
||||
[dev-dependencies]
|
||||
walkdir = "2.0.1"
|
||||
walkdir = "2.5.0"
|
||||
|
|
|
|||
|
|
@ -15,4 +15,4 @@ core component of SpacemanDMM and powers the rest of the tooling.
|
|||
* Non-constant initial values for object variables.
|
||||
* Integer constants which are outside of range.
|
||||
|
||||
[2072419]: https://secure.byond.com/forum/?post=2072419
|
||||
[2072419]: https://www.byond.com/forum/?post=2072419
|
||||
|
|
|
|||
|
|
@ -7,8 +7,7 @@ fn main() {
|
|||
let env = dm::detect_environment_default()
|
||||
.expect("error detecting .dme")
|
||||
.expect("no .dme found");
|
||||
let pp = dm::preprocessor::Preprocessor::new(&context, env)
|
||||
.expect("i/o error opening .dme");
|
||||
let pp = dm::preprocessor::Preprocessor::new(&context, env).expect("i/o error opening .dme");
|
||||
let indents = dm::indents::IndentProcessor::new(&context, pp);
|
||||
let mut parser = dm::parser::Parser::new(&context, indents);
|
||||
parser.enable_procs();
|
||||
|
|
@ -25,5 +24,10 @@ fn main() {
|
|||
}
|
||||
}
|
||||
});
|
||||
println!("decls: {}\noverrides: {}\ntotal: {}", decls, overrides, decls + overrides);
|
||||
println!(
|
||||
"decls: {}\noverrides: {}\ntotal: {}",
|
||||
decls,
|
||||
overrides,
|
||||
decls + overrides
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -32,31 +32,24 @@ fn main() {
|
|||
.unwrap_or(0);
|
||||
|
||||
// Migrate old flags_1 values to their item_flags equivalents
|
||||
let lhs =
|
||||
if flags_1 & (1<<1) != 0 { 1<<8 } else { 0 } |
|
||||
if flags_1 & (1<<2) != 0 { 1<<7 } else { 0 } |
|
||||
if flags_1 & (1<<6) != 0 { 1<<9 } else { 0 } |
|
||||
if flags_1 & (1<<10) != 0 { 1<<6 } else { 0 };
|
||||
flags_1 &= !((1<<1) | (1<<2) | (1<<6) | (1<<10));
|
||||
let lhs = if flags_1 & (1 << 1) != 0 { 1 << 8 } else { 0 }
|
||||
| if flags_1 & (1 << 2) != 0 { 1 << 7 } else { 0 }
|
||||
| if flags_1 & (1 << 6) != 0 { 1 << 9 } else { 0 }
|
||||
| if flags_1 & (1 << 10) != 0 { 1 << 6 } else { 0 };
|
||||
flags_1 &= !((1 << 1) | (1 << 2) | (1 << 6) | (1 << 10));
|
||||
|
||||
let rhs = item_flags & ((1<<6) | (1<<7) | (1<<8) | (1<<9));
|
||||
let rhs = item_flags & ((1 << 6) | (1 << 7) | (1 << 8) | (1 << 9));
|
||||
item_flags &= !rhs;
|
||||
|
||||
let crossover = if rhs != 0 && lhs != 0 {
|
||||
panic!(
|
||||
"flags_1={}, item_flags={}, lhs={}, rhs={}",
|
||||
flags_1, item_flags, lhs, rhs
|
||||
);
|
||||
panic!("flags_1={flags_1}, item_flags={item_flags}, lhs={lhs}, rhs={rhs}");
|
||||
} else if lhs != 0 {
|
||||
lhs
|
||||
} else {
|
||||
rhs
|
||||
};
|
||||
|
||||
println!(
|
||||
"flags_1={}, item_flags={}, crossover={}",
|
||||
flags_1, item_flags, crossover
|
||||
);
|
||||
println!("flags_1={flags_1}, item_flags={item_flags}, crossover={crossover}");
|
||||
});
|
||||
|
||||
println!("---- anchored example ----");
|
||||
|
|
@ -70,17 +63,22 @@ fn main() {
|
|||
println!("{} -> {}", ty.path, anch);
|
||||
|
||||
// print location info for any type with a redundant `anchored = TRUE`
|
||||
if anch && ty
|
||||
.parent_type()
|
||||
.unwrap()
|
||||
.get_value("anchored")
|
||||
.unwrap()
|
||||
.constant
|
||||
.as_ref()
|
||||
.unwrap()
|
||||
.to_bool()
|
||||
if anch
|
||||
&& ty
|
||||
.parent_type()
|
||||
.unwrap()
|
||||
.get_value("anchored")
|
||||
.unwrap()
|
||||
.constant
|
||||
.as_ref()
|
||||
.unwrap()
|
||||
.to_bool()
|
||||
{
|
||||
println!("{}:{}", ctx.file_path(var.location.file).display(), var.location.line);
|
||||
println!(
|
||||
"{}:{}",
|
||||
ctx.file_path(var.location.file).display(),
|
||||
var.location.line
|
||||
);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
|
|
|||
|
|
@ -9,8 +9,7 @@ fn main() {
|
|||
let env = dm::detect_environment_default()
|
||||
.expect("error detecting .dme")
|
||||
.expect("no .dme found");
|
||||
let pp = dm::preprocessor::Preprocessor::new(&context, env)
|
||||
.expect("i/o error opening .dme");
|
||||
let pp = dm::preprocessor::Preprocessor::new(&context, env).expect("i/o error opening .dme");
|
||||
let indents = dm::indents::IndentProcessor::new(&context, pp);
|
||||
let mut parser = dm::parser::Parser::new(&context, indents);
|
||||
parser.enable_procs();
|
||||
|
|
|
|||
|
|
@ -1,9 +1,13 @@
|
|||
//! Data structures for the parser to output mappings from input ranges to AST
|
||||
//! elements at those positions.
|
||||
|
||||
use interval_tree::{IntervalTree, RangePairIter, RangeInclusive, range};
|
||||
use super::Location;
|
||||
use std::rc::Rc;
|
||||
|
||||
use crate::docs::DocCollection;
|
||||
use interval_tree::{range, IntervalTree, RangeInclusive, RangePairIter};
|
||||
|
||||
use super::ast::*;
|
||||
use super::Location;
|
||||
|
||||
pub type Iter<'a> = RangePairIter<'a, Location, Annotation>;
|
||||
|
||||
|
|
@ -23,24 +27,28 @@ pub enum Annotation {
|
|||
UnscopedVar(Ident),
|
||||
ScopedCall(Vec<Ident>, Ident),
|
||||
ScopedVar(Vec<Ident>, Ident),
|
||||
ParentCall, // ..
|
||||
ReturnVal, // .
|
||||
InSequence(usize), // where in TreePath or TypePath is this ident
|
||||
ParentCall, // ..
|
||||
ReturnVal, // .
|
||||
InSequence(usize), // where in TreePath or TypePath is this ident
|
||||
|
||||
// a macro is called here, which is defined at this location
|
||||
MacroDefinition(Ident),
|
||||
MacroUse(String, Location),
|
||||
MacroUse {
|
||||
name: String,
|
||||
definition_location: Location,
|
||||
docs: Option<Rc<DocCollection>>,
|
||||
},
|
||||
|
||||
Include(std::path::PathBuf),
|
||||
Resource(std::path::PathBuf),
|
||||
|
||||
// error annotations, mostly for autocompletion
|
||||
ScopedMissingIdent(Vec<Ident>), // when a . is followed by a non-ident
|
||||
ScopedMissingIdent(Vec<Ident>), // when a . is followed by a non-ident
|
||||
IncompleteTypePath(TypePath, PathOp),
|
||||
IncompleteTreePath(bool, Vec<Ident>),
|
||||
|
||||
ProcArguments(Vec<Ident>, String, usize), // Vec empty for unscoped call
|
||||
ProcArgument(usize), // where in the prog arguments we are
|
||||
ProcArguments(Vec<Ident>, String, usize), // Vec empty for unscoped call
|
||||
ProcArgument(usize), // where in the prog arguments we are
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
|
|
@ -60,7 +68,8 @@ impl Default for AnnotationTree {
|
|||
|
||||
impl AnnotationTree {
|
||||
pub fn insert(&mut self, place: std::ops::Range<Location>, value: Annotation) {
|
||||
self.tree.insert(range(place.start, place.end.pred()), value);
|
||||
self.tree
|
||||
.insert(range(place.start, place.end.pred()), value);
|
||||
self.len += 1;
|
||||
}
|
||||
|
||||
|
|
@ -77,19 +86,19 @@ impl AnnotationTree {
|
|||
self.len == 0
|
||||
}
|
||||
|
||||
pub fn iter(&self) -> Iter {
|
||||
pub fn iter(&self) -> Iter<'_> {
|
||||
self.tree.iter()
|
||||
}
|
||||
|
||||
pub fn get_location(&self, loc: Location) -> Iter {
|
||||
pub fn get_location(&self, loc: Location) -> Iter<'_> {
|
||||
self.tree.range(range(loc.pred(), loc))
|
||||
}
|
||||
|
||||
pub fn get_range(&self, place: std::ops::Range<Location>) -> Iter {
|
||||
pub fn get_range(&self, place: std::ops::Range<Location>) -> Iter<'_> {
|
||||
self.tree.range(range(place.start, place.end.pred()))
|
||||
}
|
||||
|
||||
pub fn get_range_raw(&self, place: RangeInclusive<Location>) -> Iter {
|
||||
pub fn get_range_raw(&self, place: RangeInclusive<Location>) -> Iter<'_> {
|
||||
self.tree.range(place)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -3,15 +3,24 @@
|
|||
//! Most AST types can be pretty-printed using the `Display` trait.
|
||||
use std::fmt;
|
||||
use std::iter::FromIterator;
|
||||
|
||||
use get_size::GetSize;
|
||||
use get_size_derive::GetSize;
|
||||
use phf::phf_map;
|
||||
|
||||
use crate::error::Location;
|
||||
|
||||
/// Arguments for [`Term::Pick`]
|
||||
pub type PickArgs = [(Option<Expression>, Expression)];
|
||||
|
||||
/// Cases for [`Statement::Switch`]
|
||||
pub type SwitchCases = [(Spanned<Vec<Case>>, Block)];
|
||||
|
||||
// ----------------------------------------------------------------------------
|
||||
// Simple enums
|
||||
|
||||
/// The unary operators, both prefix and postfix.
|
||||
#[derive(Copy, Clone, PartialEq, Eq, Debug)]
|
||||
#[derive(Copy, Clone, PartialEq, Eq, Debug, GetSize)]
|
||||
pub enum UnaryOp {
|
||||
Neg,
|
||||
Not,
|
||||
|
|
@ -20,12 +29,14 @@ pub enum UnaryOp {
|
|||
PostIncr,
|
||||
PreDecr,
|
||||
PostDecr,
|
||||
Reference,
|
||||
Dereference,
|
||||
}
|
||||
|
||||
impl UnaryOp {
|
||||
/// Prepare to display this unary operator around (to the left or right of)
|
||||
/// its operand.
|
||||
pub fn around<'a, T: fmt::Display + ?Sized>(self, expr: &'a T) -> impl fmt::Display + 'a {
|
||||
pub fn around<T: fmt::Display + ?Sized>(self, expr: &'_ T) -> impl fmt::Display + '_ {
|
||||
/// A formatting wrapper created by `UnaryOp::around`.
|
||||
struct Around<'a, T: 'a + ?Sized> {
|
||||
op: UnaryOp,
|
||||
|
|
@ -43,6 +54,8 @@ impl UnaryOp {
|
|||
PostIncr => write!(f, "{}++", self.expr),
|
||||
PreDecr => write!(f, "--{}", self.expr),
|
||||
PostDecr => write!(f, "{}--", self.expr),
|
||||
Reference => write!(f, "&{}", self.expr),
|
||||
Dereference => write!(f, "*{}", self.expr),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -59,6 +72,8 @@ impl UnaryOp {
|
|||
BitNot => "~",
|
||||
PreIncr | PostIncr => "++",
|
||||
PreDecr | PostDecr => "--",
|
||||
Reference => "&",
|
||||
Dereference => "*",
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -66,7 +81,7 @@ impl UnaryOp {
|
|||
/// The DM path operators.
|
||||
///
|
||||
/// Which path operator is used typically only matters at the start of a path.
|
||||
#[derive(Copy, Clone, Hash, PartialEq, Eq, Debug)]
|
||||
#[derive(Copy, Clone, Hash, PartialEq, Eq, Debug, GetSize)]
|
||||
pub enum PathOp {
|
||||
/// `/` for absolute pathing.
|
||||
Slash,
|
||||
|
|
@ -93,7 +108,7 @@ impl fmt::Display for PathOp {
|
|||
}
|
||||
|
||||
/// The binary operators.
|
||||
#[derive(Copy, Clone, PartialEq, Eq, Debug)]
|
||||
#[derive(Copy, Clone, PartialEq, Eq, Debug, GetSize)]
|
||||
pub enum BinaryOp {
|
||||
Add,
|
||||
Sub,
|
||||
|
|
@ -101,12 +116,14 @@ pub enum BinaryOp {
|
|||
Div,
|
||||
Pow,
|
||||
Mod,
|
||||
FloatMod,
|
||||
Eq,
|
||||
NotEq,
|
||||
Less,
|
||||
Greater,
|
||||
LessEq,
|
||||
GreaterEq,
|
||||
LessOrGreater,
|
||||
Equiv,
|
||||
NotEquiv,
|
||||
BitAnd,
|
||||
|
|
@ -117,7 +134,7 @@ pub enum BinaryOp {
|
|||
And,
|
||||
Or,
|
||||
In,
|
||||
To, // only appears in RHS of `In`
|
||||
To, // only appears in RHS of `In`
|
||||
}
|
||||
|
||||
impl fmt::Display for BinaryOp {
|
||||
|
|
@ -130,11 +147,13 @@ impl fmt::Display for BinaryOp {
|
|||
Div => "/",
|
||||
Pow => "**",
|
||||
Mod => "%",
|
||||
FloatMod => "%%",
|
||||
Eq => "==",
|
||||
NotEq => "!=",
|
||||
Less => "<",
|
||||
Greater => ">",
|
||||
LessEq => "<=",
|
||||
LessOrGreater => "<=>",
|
||||
GreaterEq => ">=",
|
||||
Equiv => "~=",
|
||||
NotEquiv => "~!",
|
||||
|
|
@ -152,7 +171,7 @@ impl fmt::Display for BinaryOp {
|
|||
}
|
||||
|
||||
/// The assignment operators, including augmented assignment.
|
||||
#[derive(Copy, Clone, PartialEq, Eq, Debug)]
|
||||
#[derive(Copy, Clone, PartialEq, Eq, Debug, GetSize)]
|
||||
pub enum AssignOp {
|
||||
Assign,
|
||||
AddAssign,
|
||||
|
|
@ -160,6 +179,7 @@ pub enum AssignOp {
|
|||
MulAssign,
|
||||
DivAssign,
|
||||
ModAssign,
|
||||
FloatModAssign,
|
||||
AssignInto,
|
||||
BitAndAssign,
|
||||
AndAssign,
|
||||
|
|
@ -180,6 +200,7 @@ impl fmt::Display for AssignOp {
|
|||
MulAssign => "*=",
|
||||
DivAssign => "/=",
|
||||
ModAssign => "%=",
|
||||
FloatModAssign => "%%=",
|
||||
AssignInto => ":=",
|
||||
BitAndAssign => "&=",
|
||||
AndAssign => "&&=",
|
||||
|
|
@ -221,6 +242,7 @@ augmented! {
|
|||
Mul = MulAssign;
|
||||
Div = DivAssign;
|
||||
Mod = ModAssign;
|
||||
FloatMod = FloatModAssign;
|
||||
BitAnd = BitAndAssign;
|
||||
BitOr = BitOrAssign;
|
||||
BitXor = BitXorAssign;
|
||||
|
|
@ -237,7 +259,7 @@ pub enum TernaryOp {
|
|||
}
|
||||
|
||||
/// The possible kinds of access operators for lists
|
||||
#[derive(Debug, Copy, Clone, Eq, PartialEq)]
|
||||
#[derive(Debug, Copy, Clone, Eq, PartialEq, GetSize)]
|
||||
pub enum ListAccessKind {
|
||||
/// `[]`
|
||||
Normal,
|
||||
|
|
@ -246,7 +268,7 @@ pub enum ListAccessKind {
|
|||
}
|
||||
|
||||
/// The possible kinds of index operators, for both fields and methods.
|
||||
#[derive(Debug, Copy, Clone, Eq, PartialEq)]
|
||||
#[derive(Debug, Copy, Clone, Eq, PartialEq, GetSize)]
|
||||
pub enum PropertyAccessKind {
|
||||
/// `a.b`
|
||||
Dot,
|
||||
|
|
@ -256,6 +278,8 @@ pub enum PropertyAccessKind {
|
|||
SafeDot,
|
||||
/// `a?:b`
|
||||
SafeColon,
|
||||
/// 'a::b'
|
||||
Scope,
|
||||
}
|
||||
|
||||
impl PropertyAccessKind {
|
||||
|
|
@ -265,6 +289,7 @@ impl PropertyAccessKind {
|
|||
PropertyAccessKind::Colon => ":",
|
||||
PropertyAccessKind::SafeDot => "?.",
|
||||
PropertyAccessKind::SafeColon => "?:",
|
||||
PropertyAccessKind::Scope => "::",
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -275,12 +300,64 @@ impl fmt::Display for PropertyAccessKind {
|
|||
}
|
||||
}
|
||||
|
||||
/// Description of a proc's return type (`as` phrase).
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Hash, GetSize)]
|
||||
pub enum ProcReturnType {
|
||||
InputType(InputType),
|
||||
TypePath(Vec<Ident>),
|
||||
}
|
||||
|
||||
impl ProcReturnType {
|
||||
pub fn is_empty(&self) -> bool {
|
||||
matches!(self, ProcReturnType::InputType(InputType { bits: 0 }))
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for ProcReturnType {
|
||||
fn default() -> Self {
|
||||
ProcReturnType::InputType(InputType::empty())
|
||||
}
|
||||
}
|
||||
|
||||
/// Information about a proc declaration
|
||||
///
|
||||
/// Holds what sort of decl it was (did it use /proc or /verb), alongside a set of flags
|
||||
/// That describe extra info pulled from the path
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Copy, Hash)]
|
||||
pub struct ProcDeclBuilder {
|
||||
pub kind: ProcDeclKind,
|
||||
pub flags: ProcFlags,
|
||||
}
|
||||
|
||||
impl ProcDeclBuilder {
|
||||
pub fn new(kind: ProcDeclKind, flags: Option<ProcFlags>) -> ProcDeclBuilder {
|
||||
ProcDeclBuilder {
|
||||
kind,
|
||||
flags: flags.unwrap_or_default(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn kind(self) -> &'static str {
|
||||
self.kind.name()
|
||||
}
|
||||
|
||||
pub fn is_final(self) -> bool {
|
||||
self.flags.is_final()
|
||||
}
|
||||
}
|
||||
|
||||
impl fmt::Display for ProcDeclBuilder {
|
||||
fn fmt(&self, fmt: &mut fmt::Formatter) -> fmt::Result {
|
||||
write!(fmt, "{}{}", self.kind, self.flags)
|
||||
}
|
||||
}
|
||||
|
||||
/// The proc declaration kind, either `proc` or `verb`.
|
||||
///
|
||||
/// DM requires referencing proc paths to include whether the target is
|
||||
/// declared as a proc or verb, even though the two modes are functionally
|
||||
/// identical in many other respects.
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Copy, Hash)]
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Copy, Hash, GetSize)]
|
||||
pub enum ProcDeclKind {
|
||||
Proc,
|
||||
Verb,
|
||||
|
|
@ -316,7 +393,48 @@ impl fmt::Display for ProcDeclKind {
|
|||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq)]
|
||||
bitflags! {
|
||||
#[derive(Default, GetSize)]
|
||||
pub struct ProcFlags: u8 {
|
||||
// DM flags
|
||||
const FINAL = 1 << 0;
|
||||
}
|
||||
}
|
||||
|
||||
impl ProcFlags {
|
||||
pub fn from_name(name: &str) -> Option<ProcFlags> {
|
||||
match name {
|
||||
// DM flags
|
||||
"final" => Some(ProcFlags::FINAL),
|
||||
// Fallback
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
#[inline]
|
||||
pub fn is_final(&self) -> bool {
|
||||
self.contains(ProcFlags::FINAL)
|
||||
}
|
||||
|
||||
pub fn to_vec(&self) -> Vec<&'static str> {
|
||||
let mut v = Vec::new();
|
||||
if self.is_final() {
|
||||
v.push("final");
|
||||
}
|
||||
v
|
||||
}
|
||||
}
|
||||
|
||||
impl fmt::Display for ProcFlags {
|
||||
fn fmt(&self, fmt: &mut fmt::Formatter) -> fmt::Result {
|
||||
if self.is_final() {
|
||||
fmt.write_str("/final")?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, GetSize)]
|
||||
pub enum SettingMode {
|
||||
/// As in `set name = "Use"`.
|
||||
Assign,
|
||||
|
|
@ -354,12 +472,20 @@ macro_rules! type_table {
|
|||
}
|
||||
|
||||
impl $name {
|
||||
pub fn from_str(text: &str) -> Option<Self> {
|
||||
match text {
|
||||
pub const ENTRIES: &'static [(&'static str, $name)] = &[
|
||||
$(($txt, $name::$i),)*
|
||||
];
|
||||
}
|
||||
|
||||
impl std::str::FromStr for $name {
|
||||
type Err = ();
|
||||
|
||||
fn from_str(s: &str) -> Result<Self, Self::Err> {
|
||||
match s {
|
||||
$(
|
||||
$txt => Some($name::$i),
|
||||
$txt => Ok($name::$i),
|
||||
)*
|
||||
_ => None,
|
||||
_ => Err(()),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -384,6 +510,7 @@ macro_rules! type_table {
|
|||
|
||||
type_table! {
|
||||
/// A type specifier for verb arguments and input() calls.
|
||||
#[derive(GetSize)]
|
||||
pub struct InputType;
|
||||
|
||||
// These values can be known with an invocation such as:
|
||||
|
|
@ -404,10 +531,48 @@ type_table! {
|
|||
"password", PASSWORD, 1 << 15;
|
||||
"command_text", COMMAND_TEXT, 1 << 16;
|
||||
"color", COLOR, 1 << 17;
|
||||
// Non-primitive combinations that are still valid as(X) calls:
|
||||
"movable", MOVABLE, Self::OBJ.bits | Self::MOB.bits;
|
||||
"atom", ATOM, Self::AREA.bits | Self::TURF.bits | Self::OBJ.bits | Self::MOB.bits;
|
||||
// Placeholder value for `as list` that's technically only legal as a proc return type, but whatever.
|
||||
"list", LIST, 1 << 31;
|
||||
}
|
||||
|
||||
impl Default for InputType {
|
||||
fn default() -> Self {
|
||||
Self::empty()
|
||||
}
|
||||
}
|
||||
|
||||
impl InputType {
|
||||
/// Get a typepath that approximates this input type, if possible.
|
||||
pub fn to_typepath(&self) -> Option<&'static str> {
|
||||
if self.is_empty() {
|
||||
None
|
||||
} else if *self == InputType::MOB {
|
||||
Some("/mob")
|
||||
} else if *self == InputType::OBJ {
|
||||
Some("/obj")
|
||||
} else if *self == InputType::TURF {
|
||||
Some("/turf")
|
||||
} else if *self == InputType::AREA {
|
||||
Some("/area")
|
||||
} else if *self == InputType::LIST {
|
||||
Some("/list")
|
||||
} else if self.difference(InputType::MOVABLE).is_empty() {
|
||||
// Only applies to exactly movable = mob | obj
|
||||
Some("/atom/movable")
|
||||
} else if self.difference(InputType::ATOM).is_empty() {
|
||||
// Might apply to area|turf or turf|mob or similar combos
|
||||
Some("/atom")
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
bitflags! {
|
||||
#[derive(Default)]
|
||||
#[derive(Default, GetSize)]
|
||||
pub struct VarTypeFlags: u8 {
|
||||
// DM flags
|
||||
const STATIC = 1 << 0;
|
||||
|
|
@ -427,6 +592,7 @@ impl VarTypeFlags {
|
|||
"global" | "static" => Some(VarTypeFlags::STATIC),
|
||||
"const" => Some(VarTypeFlags::CONST),
|
||||
"tmp" => Some(VarTypeFlags::TMP),
|
||||
"final" => Some(VarTypeFlags::FINAL),
|
||||
// SpacemanDMM flags
|
||||
"SpacemanDMM_final" => Some(VarTypeFlags::FINAL),
|
||||
"SpacemanDMM_private" => Some(VarTypeFlags::PRIVATE),
|
||||
|
|
@ -468,7 +634,8 @@ impl VarTypeFlags {
|
|||
|
||||
#[inline]
|
||||
pub fn is_const_evaluable(&self) -> bool {
|
||||
self.contains(VarTypeFlags::CONST) || !self.intersects(VarTypeFlags::STATIC | VarTypeFlags::PROTECTED)
|
||||
self.contains(VarTypeFlags::CONST)
|
||||
|| !self.intersects(VarTypeFlags::STATIC | VarTypeFlags::PROTECTED)
|
||||
}
|
||||
|
||||
#[inline]
|
||||
|
|
@ -478,12 +645,24 @@ impl VarTypeFlags {
|
|||
|
||||
pub fn to_vec(&self) -> Vec<&'static str> {
|
||||
let mut v = Vec::new();
|
||||
if self.is_static() { v.push("static"); }
|
||||
if self.is_const() { v.push("const"); }
|
||||
if self.is_tmp() { v.push("tmp"); }
|
||||
if self.is_final() { v.push("SpacemanDMM_final"); }
|
||||
if self.is_private() { v.push("SpacemanDMM_private"); }
|
||||
if self.is_protected() { v.push("SpacemanDMM_protected"); }
|
||||
if self.is_static() {
|
||||
v.push("static");
|
||||
}
|
||||
if self.is_const() {
|
||||
v.push("const");
|
||||
}
|
||||
if self.is_tmp() {
|
||||
v.push("tmp");
|
||||
}
|
||||
if self.is_final() {
|
||||
v.push("final");
|
||||
}
|
||||
if self.is_private() {
|
||||
v.push("SpacemanDMM_private");
|
||||
}
|
||||
if self.is_protected() {
|
||||
v.push("SpacemanDMM_protected");
|
||||
}
|
||||
v
|
||||
}
|
||||
}
|
||||
|
|
@ -500,7 +679,7 @@ impl fmt::Display for VarTypeFlags {
|
|||
fmt.write_str("tmp/")?;
|
||||
}
|
||||
if self.is_final() {
|
||||
fmt.write_str("SpacemanDMM_final/")?;
|
||||
fmt.write_str("final/")?;
|
||||
}
|
||||
if self.is_private() {
|
||||
fmt.write_str("SpacemanDMM_private/")?;
|
||||
|
|
@ -528,7 +707,7 @@ pub struct Ident2 {
|
|||
|
||||
impl Ident2 {
|
||||
pub fn as_str(&self) -> &str {
|
||||
&*self.inner
|
||||
&self.inner
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -559,7 +738,7 @@ impl From<Ident2> for String {
|
|||
impl std::ops::Deref for Ident2 {
|
||||
type Target = str;
|
||||
fn deref(&self) -> &str {
|
||||
&*self.inner
|
||||
&self.inner
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -575,8 +754,14 @@ impl fmt::Debug for Ident2 {
|
|||
}
|
||||
}
|
||||
|
||||
impl GetSize for Ident2 {
|
||||
fn get_heap_size(&self) -> usize {
|
||||
self.inner.len()
|
||||
}
|
||||
}
|
||||
|
||||
/// An AST element with an additional location attached.
|
||||
#[derive(Copy, Clone, Eq, Debug)]
|
||||
#[derive(Copy, Clone, Eq, Debug, GetSize)]
|
||||
pub struct Spanned<T> {
|
||||
// TODO: add a Span type and use it here
|
||||
pub location: Location,
|
||||
|
|
@ -604,7 +789,7 @@ pub struct FormatTreePath<'a, T>(pub &'a [T]);
|
|||
impl<'a, T: fmt::Display> fmt::Display for FormatTreePath<'a, T> {
|
||||
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
|
||||
for each in self.0.iter() {
|
||||
write!(f, "/{}", each)?;
|
||||
write!(f, "/{each}")?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
|
@ -628,7 +813,7 @@ impl<'a> fmt::Display for FormatTypePath<'a> {
|
|||
// Terms and Expressions
|
||||
|
||||
/// A typepath optionally followed by a set of variables.
|
||||
#[derive(Clone, PartialEq, Debug)]
|
||||
#[derive(Clone, PartialEq, Debug, GetSize)]
|
||||
pub struct Prefab {
|
||||
pub path: TypePath,
|
||||
pub vars: Box<[(Ident2, Expression)]>,
|
||||
|
|
@ -648,7 +833,7 @@ pub struct FormatVars<'a, T>(pub &'a T);
|
|||
|
||||
impl<'a, T, K, V> fmt::Display for FormatVars<'a, T>
|
||||
where
|
||||
&'a T: IntoIterator<Item=(K, V)>,
|
||||
&'a T: IntoIterator<Item = (K, V)>,
|
||||
K: fmt::Display,
|
||||
V: fmt::Display,
|
||||
{
|
||||
|
|
@ -666,7 +851,7 @@ where
|
|||
}
|
||||
|
||||
/// The structure of an expression, a tree of terms and operators.
|
||||
#[derive(Clone, PartialEq, Debug)]
|
||||
#[derive(Clone, PartialEq, Debug, GetSize)]
|
||||
pub enum Expression {
|
||||
/// An expression containing a term directly. The term is evaluated first,
|
||||
/// then its follows, then its unary operators in reverse order.
|
||||
|
|
@ -702,14 +887,14 @@ pub enum Expression {
|
|||
if_: Box<Expression>,
|
||||
/// The value otherwise.
|
||||
else_: Box<Expression>,
|
||||
}
|
||||
},
|
||||
}
|
||||
|
||||
impl Expression {
|
||||
/// If this expression consists of a single term, return it.
|
||||
pub fn as_term(&self) -> Option<&Term> {
|
||||
match self {
|
||||
&Expression::Base { ref term, ref follow } if follow.is_empty() => Some(&term.elem),
|
||||
Expression::Base { term, follow } if follow.is_empty() => Some(&term.elem),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
|
@ -731,29 +916,29 @@ impl Expression {
|
|||
pub fn is_const_eval(&self) -> bool {
|
||||
match self {
|
||||
Expression::BinaryOp { op, lhs, rhs } => {
|
||||
guard!(let Some(lhterm) = lhs.as_term() else {
|
||||
return false
|
||||
});
|
||||
guard!(let Some(rhterm) = rhs.as_term() else {
|
||||
return false
|
||||
});
|
||||
let Some(lhterm) = lhs.as_term() else {
|
||||
return false;
|
||||
};
|
||||
let Some(rhterm) = rhs.as_term() else {
|
||||
return false;
|
||||
};
|
||||
if !lhterm.is_static() {
|
||||
return false
|
||||
return false;
|
||||
}
|
||||
if !rhterm.is_static() {
|
||||
return false
|
||||
}
|
||||
match op {
|
||||
BinaryOp::Eq |
|
||||
BinaryOp::NotEq |
|
||||
BinaryOp::Less |
|
||||
BinaryOp::Greater |
|
||||
BinaryOp::LessEq |
|
||||
BinaryOp::GreaterEq |
|
||||
BinaryOp::And |
|
||||
BinaryOp::Or => true,
|
||||
_ => false,
|
||||
return false;
|
||||
}
|
||||
matches!(
|
||||
op,
|
||||
BinaryOp::Eq
|
||||
| BinaryOp::NotEq
|
||||
| BinaryOp::Less
|
||||
| BinaryOp::Greater
|
||||
| BinaryOp::LessEq
|
||||
| BinaryOp::GreaterEq
|
||||
| BinaryOp::And
|
||||
| BinaryOp::Or
|
||||
)
|
||||
},
|
||||
_ => false,
|
||||
}
|
||||
|
|
@ -762,9 +947,7 @@ impl Expression {
|
|||
pub fn is_truthy(&self) -> Option<bool> {
|
||||
match self {
|
||||
Expression::Base { term, follow } => {
|
||||
guard!(let Some(mut truthy) = term.elem.is_truthy() else {
|
||||
return None;
|
||||
});
|
||||
let mut truthy = term.elem.is_truthy()?;
|
||||
for follow in follow.iter() {
|
||||
match follow.elem {
|
||||
Follow::Unary(UnaryOp::Not) => truthy = !truthy,
|
||||
|
|
@ -774,12 +957,8 @@ impl Expression {
|
|||
Some(truthy)
|
||||
},
|
||||
Expression::BinaryOp { op, lhs, rhs } => {
|
||||
guard!(let Some(lhtruth) = lhs.is_truthy() else {
|
||||
return None
|
||||
});
|
||||
guard!(let Some(rhtruth) = rhs.is_truthy() else {
|
||||
return None
|
||||
});
|
||||
let lhtruth = lhs.is_truthy()?;
|
||||
let rhtruth = rhs.is_truthy()?;
|
||||
match op {
|
||||
BinaryOp::And => Some(lhtruth && rhtruth),
|
||||
BinaryOp::Or => Some(lhtruth || rhtruth),
|
||||
|
|
@ -788,24 +967,35 @@ impl Expression {
|
|||
},
|
||||
Expression::AssignOp { op, lhs: _, rhs } => {
|
||||
if let AssignOp::Assign = op {
|
||||
return match rhs.as_term() {
|
||||
match rhs.as_term() {
|
||||
Some(term) => term.is_truthy(),
|
||||
_ => None,
|
||||
}
|
||||
} else {
|
||||
return None
|
||||
None
|
||||
}
|
||||
},
|
||||
Expression::TernaryOp { cond, if_, else_ } => {
|
||||
guard!(let Some(condtruth) = cond.is_truthy() else {
|
||||
return None
|
||||
});
|
||||
let condtruth = cond.is_truthy()?;
|
||||
if condtruth {
|
||||
if_.is_truthy()
|
||||
} else {
|
||||
else_.is_truthy()
|
||||
}
|
||||
}
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
pub fn nameof(&self) -> Option<&str> {
|
||||
match self {
|
||||
Expression::Base { term, follow } => {
|
||||
if let Some(last) = follow.last() {
|
||||
last.elem.nameof()
|
||||
} else {
|
||||
term.elem.nameof()
|
||||
}
|
||||
},
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -823,7 +1013,8 @@ impl From<Term> for Expression {
|
|||
}
|
||||
|
||||
/// The structure of a term, the basic building block of the AST.
|
||||
#[derive(Clone, PartialEq, Debug)]
|
||||
#[allow(non_camel_case_types)]
|
||||
#[derive(Clone, PartialEq, Debug, GetSize)]
|
||||
pub enum Term {
|
||||
// Terms with no recursive contents ---------------------------------------
|
||||
/// The literal `null`.
|
||||
|
|
@ -840,6 +1031,15 @@ pub enum Term {
|
|||
Resource(String),
|
||||
/// An `as()` call, with an input type. Undocumented.
|
||||
As(InputType),
|
||||
/// A reference to our current proc's name
|
||||
__PROC__,
|
||||
/// A reference to the current proc/scope's type
|
||||
__TYPE__,
|
||||
/// If rhs of an assignment op, this is a reference to the lhs var's type
|
||||
/// If we're used as the second arg of an istype then it's the implied type of the first arg
|
||||
/// Second case takes precedence over the first, but we don't properly implement because it would be impossible to
|
||||
/// Tell. You can't DO anything to the __IMPLIED_TYPE__ so we don't really need to care about it
|
||||
__IMPLIED_TYPE__,
|
||||
|
||||
// Non-function calls with recursive contents -----------------------------
|
||||
/// An expression contained in a term.
|
||||
|
|
@ -880,7 +1080,7 @@ pub enum Term {
|
|||
/// An `input` call.
|
||||
Input {
|
||||
args: Box<[Expression]>,
|
||||
input_type: Option<InputType>, // as
|
||||
input_type: Option<InputType>, // as
|
||||
in_list: Option<Box<Expression>>, // in
|
||||
},
|
||||
/// A `locate` call.
|
||||
|
|
@ -889,24 +1089,31 @@ pub enum Term {
|
|||
in_list: Option<Box<Expression>>, // in
|
||||
},
|
||||
/// A `pick` call, possibly with weights.
|
||||
Pick(Box<[(Option<Expression>, Expression)]>),
|
||||
Pick(Box<PickArgs>),
|
||||
/// A use of the `call()()` primitive.
|
||||
DynamicCall(Box<[Expression]>, Box<[Expression]>),
|
||||
/// A use of the `call_ext()()` primitive.
|
||||
ExternalCall {
|
||||
library: Option<Box<Expression>>,
|
||||
function: Box<Expression>,
|
||||
args: Box<[Expression]>,
|
||||
},
|
||||
/// Unscoped `::A` is a shorthand for `global.A`
|
||||
GlobalIdent(Ident2),
|
||||
/// Unscoped `::A(...)` is a shorthand for `global.A(...)`
|
||||
GlobalCall(Ident2, Box<[Expression]>),
|
||||
}
|
||||
|
||||
impl Term {
|
||||
pub fn is_static(&self) -> bool {
|
||||
matches!(self,
|
||||
Term::Null
|
||||
| Term::Int(_)
|
||||
| Term::Float(_)
|
||||
| Term::String(_)
|
||||
| Term::Prefab(_)
|
||||
matches!(
|
||||
self,
|
||||
Term::Null | Term::Int(_) | Term::Float(_) | Term::String(_) | Term::Prefab(_)
|
||||
)
|
||||
}
|
||||
|
||||
pub fn is_truthy(&self) -> Option<bool> {
|
||||
return match self {
|
||||
match self {
|
||||
// `null`, `0`, and empty strings are falsey.
|
||||
Term::Null => Some(false),
|
||||
Term::Int(i) => Some(*i != 0),
|
||||
|
|
@ -935,7 +1142,7 @@ impl Term {
|
|||
Term::Expr(e) => e.is_truthy(),
|
||||
|
||||
_ => None,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
pub fn valid_for_range(&self, other: &Term, step: Option<&Expression>) -> Option<bool> {
|
||||
|
|
@ -943,48 +1150,60 @@ impl Term {
|
|||
if let Term::Int(o) = *other {
|
||||
// edge case
|
||||
if i == 0 && o == 0 {
|
||||
return Some(false)
|
||||
return Some(false);
|
||||
}
|
||||
if let Some(stepexp) = step {
|
||||
if let Some(stepterm) = stepexp.as_term() {
|
||||
if let Term::Int(_s) = stepterm {
|
||||
return Some(true)
|
||||
return Some(true);
|
||||
}
|
||||
} else {
|
||||
return Some(true)
|
||||
return Some(true);
|
||||
}
|
||||
}
|
||||
return Some(i <= o)
|
||||
return Some(i <= o);
|
||||
}
|
||||
}
|
||||
None
|
||||
}
|
||||
|
||||
pub fn nameof(&self) -> Option<&str> {
|
||||
match self {
|
||||
Term::Expr(e) => e.nameof(),
|
||||
Term::Ident(i) => Some(i),
|
||||
Term::Prefab(fab) if fab.vars.is_empty() => Some(&fab.path.last()?.1),
|
||||
Term::GlobalIdent(i) => Some(i),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<Expression> for Term {
|
||||
fn from(expr: Expression) -> Term {
|
||||
match expr {
|
||||
Expression::Base { term, follow } => if follow.is_empty() {
|
||||
match term.elem {
|
||||
Term::Expr(expr) => Term::from(*expr),
|
||||
other => other,
|
||||
Expression::Base { term, follow } => {
|
||||
if follow.is_empty() {
|
||||
match term.elem {
|
||||
Term::Expr(expr) => Term::from(*expr),
|
||||
other => other,
|
||||
}
|
||||
} else {
|
||||
Term::Expr(Box::new(Expression::Base { term, follow }))
|
||||
}
|
||||
} else {
|
||||
Term::Expr(Box::new(Expression::Base { term, follow }))
|
||||
},
|
||||
other => Term::Expr(Box::new(other)),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, PartialEq, Debug)]
|
||||
#[derive(Clone, PartialEq, Debug, GetSize)]
|
||||
pub struct MiniExpr {
|
||||
pub ident: Ident2,
|
||||
pub fields: Box<[Field]>,
|
||||
}
|
||||
|
||||
/// An expression part which is applied to a term or another follow.
|
||||
#[derive(Debug, Clone, PartialEq)]
|
||||
#[derive(Debug, Clone, PartialEq, GetSize)]
|
||||
pub enum Follow {
|
||||
/// Index the value by an expression.
|
||||
Index(ListAccessKind, Box<Expression>),
|
||||
|
|
@ -994,10 +1213,31 @@ pub enum Follow {
|
|||
Call(PropertyAccessKind, Ident2, Box<[Expression]>),
|
||||
/// Apply a unary operator to the value.
|
||||
Unary(UnaryOp),
|
||||
/// Any of:
|
||||
/// - `/typepath::static_var` to read/write any type's static variables.
|
||||
/// - `/typepath::normal_var` gets the initial value of any type var.
|
||||
/// - `parent_type::normal_var` gets the initial value on the parent type. Only works outside procs.
|
||||
/// - `type::normal_var` gets the initial value on the current type. Only works outside procs. Beware loops.
|
||||
StaticField(Ident2),
|
||||
/// `foo::bar()` is a proc reference.
|
||||
/// If the LHS is a constant typepath, that is used.
|
||||
/// Otherwise the **static** type of LHS is used.
|
||||
ProcReference(Ident2),
|
||||
}
|
||||
|
||||
impl Follow {
|
||||
pub fn nameof(&self) -> Option<&str> {
|
||||
match self {
|
||||
Follow::Field(_, i) => Some(i),
|
||||
Follow::StaticField(i) => Some(i),
|
||||
Follow::ProcReference(i) => Some(i),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Like a `Follow` but only supports field accesses.
|
||||
#[derive(Debug, Clone, PartialEq)]
|
||||
#[derive(Debug, Clone, PartialEq, GetSize)]
|
||||
pub struct Field {
|
||||
pub kind: PropertyAccessKind,
|
||||
pub ident: Ident2,
|
||||
|
|
@ -1010,7 +1250,7 @@ impl From<Field> for Follow {
|
|||
}
|
||||
|
||||
/// A parameter declaration in the header of a proc.
|
||||
#[derive(Debug, Clone, PartialEq, Default)]
|
||||
#[derive(Debug, Clone, PartialEq, Default, GetSize)]
|
||||
pub struct Parameter {
|
||||
pub var_type: VarType,
|
||||
pub name: Ident,
|
||||
|
|
@ -1024,17 +1264,18 @@ impl fmt::Display for Parameter {
|
|||
fn fmt(&self, fmt: &mut fmt::Formatter) -> fmt::Result {
|
||||
write!(fmt, "{}{}", self.var_type, self.name)?;
|
||||
if let Some(input_type) = self.input_type {
|
||||
write!(fmt, " as {}", input_type)?;
|
||||
write!(fmt, " as {input_type}")?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
/// A type which may be ascribed to a `var`.
|
||||
#[derive(Debug, Clone, PartialEq, Default)]
|
||||
#[derive(Debug, Clone, PartialEq, Default, GetSize)]
|
||||
pub struct VarType {
|
||||
pub flags: VarTypeFlags,
|
||||
pub type_path: TreePath,
|
||||
pub input_type: InputType,
|
||||
}
|
||||
|
||||
impl VarType {
|
||||
|
|
@ -1050,7 +1291,7 @@ impl VarType {
|
|||
}
|
||||
|
||||
impl FromIterator<String> for VarType {
|
||||
fn from_iter<T: IntoIterator<Item=String>>(iter: T) -> Self {
|
||||
fn from_iter<T: IntoIterator<Item = String>>(iter: T) -> Self {
|
||||
VarTypeBuilder::from_iter(iter).build()
|
||||
}
|
||||
}
|
||||
|
|
@ -1071,6 +1312,7 @@ impl fmt::Display for VarType {
|
|||
pub struct VarTypeBuilder {
|
||||
pub flags: VarTypeFlags,
|
||||
pub type_path: Vec<Ident>,
|
||||
pub input_type: Option<InputType>,
|
||||
}
|
||||
|
||||
impl VarTypeBuilder {
|
||||
|
|
@ -1084,12 +1326,13 @@ impl VarTypeBuilder {
|
|||
VarType {
|
||||
flags: self.flags,
|
||||
type_path: self.type_path.into_boxed_slice(),
|
||||
input_type: self.input_type.unwrap_or_default(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl FromIterator<String> for VarTypeBuilder {
|
||||
fn from_iter<T: IntoIterator<Item=String>>(iter: T) -> Self {
|
||||
fn from_iter<T: IntoIterator<Item = String>>(iter: T) -> Self {
|
||||
let mut flags = VarTypeFlags::default();
|
||||
let type_path = iter
|
||||
.into_iter()
|
||||
|
|
@ -1105,6 +1348,7 @@ impl FromIterator<String> for VarTypeBuilder {
|
|||
VarTypeBuilder {
|
||||
flags,
|
||||
type_path,
|
||||
input_type: None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1124,7 +1368,7 @@ impl VarSuffix {
|
|||
pub fn into_initializer(self) -> Option<Expression> {
|
||||
// `var/L[10]` is equivalent to `var/list/L = new /list(10)`
|
||||
// `var/L[2][][3]` is equivalent to `var/list/list/list = new /list(2, 3)`
|
||||
let args: Vec<_> = self.list.into_iter().filter_map(|x| x).collect();
|
||||
let args: Vec<_> = self.list.into_iter().flatten().collect();
|
||||
if args.is_empty() {
|
||||
None
|
||||
} else {
|
||||
|
|
@ -1143,7 +1387,7 @@ impl VarSuffix {
|
|||
pub type Block = Box<[Spanned<Statement>]>;
|
||||
|
||||
/// A statement in a proc body.
|
||||
#[derive(Debug, Clone, PartialEq)]
|
||||
#[derive(Debug, Clone, PartialEq, GetSize)]
|
||||
pub enum Statement {
|
||||
Expr(Expression),
|
||||
Return(Option<Expression>),
|
||||
|
|
@ -1158,7 +1402,7 @@ pub enum Statement {
|
|||
},
|
||||
If {
|
||||
arms: Vec<(Spanned<Expression>, Block)>,
|
||||
else_arm: Option<Block>
|
||||
else_arm: Option<Block>,
|
||||
},
|
||||
ForInfinite {
|
||||
block: Block,
|
||||
|
|
@ -1170,13 +1414,14 @@ pub enum Statement {
|
|||
block: Block,
|
||||
},
|
||||
ForList(Box<ForListStatement>),
|
||||
ForKeyValue(Box<ForKeyValueStatement>),
|
||||
ForRange(Box<ForRangeStatement>),
|
||||
Var(Box<VarStatement>),
|
||||
Vars(Vec<VarStatement>),
|
||||
Setting {
|
||||
name: Ident2,
|
||||
mode: SettingMode,
|
||||
value: Expression
|
||||
value: Expression,
|
||||
},
|
||||
Spawn {
|
||||
delay: Option<Expression>,
|
||||
|
|
@ -1184,7 +1429,7 @@ pub enum Statement {
|
|||
},
|
||||
Switch {
|
||||
input: Box<Expression>,
|
||||
cases: Box<[(Spanned<Vec<Case>>, Block)]>,
|
||||
cases: Box<SwitchCases>,
|
||||
default: Option<Block>,
|
||||
},
|
||||
TryCatch {
|
||||
|
|
@ -1203,20 +1448,20 @@ pub enum Statement {
|
|||
Crash(Option<Expression>),
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq)]
|
||||
#[derive(Debug, Clone, PartialEq, GetSize)]
|
||||
pub struct VarStatement {
|
||||
pub var_type: VarType,
|
||||
pub name: Ident,
|
||||
pub value: Option<Expression>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq)]
|
||||
#[derive(Debug, Clone, PartialEq, GetSize)]
|
||||
pub enum Case {
|
||||
Exact(Expression),
|
||||
Range(Expression, Expression),
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq)]
|
||||
#[derive(Debug, Clone, PartialEq, GetSize)]
|
||||
pub struct ForListStatement {
|
||||
pub var_type: Option<VarType>,
|
||||
pub name: Ident2,
|
||||
|
|
@ -1227,7 +1472,18 @@ pub struct ForListStatement {
|
|||
pub block: Block,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq)]
|
||||
#[derive(Debug, Clone, PartialEq, GetSize)]
|
||||
pub struct ForKeyValueStatement {
|
||||
pub var_type: Option<VarType>,
|
||||
pub key: Ident2,
|
||||
pub key_input_type: Option<InputType>,
|
||||
pub value: Ident2,
|
||||
/// Defaults to 'world'.
|
||||
pub in_list: Option<Expression>,
|
||||
pub block: Block,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, GetSize)]
|
||||
pub struct ForRangeStatement {
|
||||
pub var_type: Option<VarType>,
|
||||
pub name: Ident2,
|
||||
|
|
@ -1259,7 +1515,7 @@ pub static VALID_FILTER_TYPES: phf::Map<&'static str, &[&str]> = phf_map! {
|
|||
"angular_blur" => &[ "x", "y", "size" ],
|
||||
"bloom" => &[ "threshold", "size", "offset", "alpha" ],
|
||||
"color" => &[ "color", "space" ],
|
||||
"displace" => &[ "x", "y", "size", "icon", "render_source" ],
|
||||
"displace" => &[ "x", "y", "size", "icon", "render_source", "flags" ],
|
||||
"drop_shadow" => &[ "x", "y", "size", "offset", "color"],
|
||||
"blur" => &[ "size" ],
|
||||
"layer" => &[ "x", "y", "icon", "render_source", "flags", "color", "transform", "blend_mode" ],
|
||||
|
|
@ -1275,9 +1531,16 @@ pub static VALID_FILTER_TYPES: phf::Map<&'static str, &[&str]> = phf_map! {
|
|||
pub static VALID_FILTER_FLAGS: phf::Map<&'static str, (&str, bool, bool, &[&str])> = phf_map! {
|
||||
"alpha" => ("flags", false, true, &[ "MASK_INVERSE", "MASK_SWAP" ]),
|
||||
"color" => ("space", true, false, &[ "FILTER_COLOR_RGB", "FILTER_COLOR_HSV", "FILTER_COLOR_HSL", "FILTER_COLOR_HCY" ]),
|
||||
"displace" => ("flags", false, true, &[ "FILTER_OVERLAY" ]),
|
||||
"layer" => ("flags", true, true, &[ "FILTER_OVERLAY", "FILTER_UNDERLAY" ]),
|
||||
"rays" => ("flags", false, true, &[ "FILTER_OVERLAY", "FILTER_UNDERLAY" ]),
|
||||
"outline" => ("flags", false, true, &[ "OUTLINE_SHARP", "OUTLINE_SQUARE" ]),
|
||||
"ripple" => ("flags", false, true, &[ "WAVE_BOUNDED" ]),
|
||||
"wave" => ("flags", false, true, &[ "WAVE_SIDEWAYS", "WAVE_BOUNDED" ]),
|
||||
};
|
||||
|
||||
// ----------------------------------------------------------------------------
|
||||
// Guard against sizeof regression.
|
||||
const _: [(); 0 - !(std::mem::size_of::<Statement>() <= 56) as usize] = [];
|
||||
const _: [(); 0 - !(std::mem::size_of::<Expression>() <= 32) as usize] = [];
|
||||
const _: [(); 0 - !(std::mem::size_of::<Term>() <= 40) as usize] = [];
|
||||
|
|
|
|||
|
|
@ -2,64 +2,76 @@
|
|||
|
||||
use builtins_proc_macro::builtins_table;
|
||||
|
||||
use super::objtree::*;
|
||||
use super::Location;
|
||||
use super::preprocessor::{DefineMap, Define};
|
||||
use super::constants::Constant;
|
||||
use super::docs::{BuiltinDocs, DocCollection};
|
||||
use super::objtree::*;
|
||||
use super::preprocessor::{Define, DefineMap};
|
||||
use super::Location;
|
||||
|
||||
const DM_VERSION: i32 = 514;
|
||||
const DM_BUILD: i32 = 1556;
|
||||
const DM_VERSION: i32 = 516;
|
||||
const DM_BUILD: i32 = 1666;
|
||||
|
||||
/// Register BYOND builtin macros to the given define map.
|
||||
pub fn default_defines(defines: &mut DefineMap) {
|
||||
use super::lexer::*;
|
||||
use super::lexer::Token::*;
|
||||
use super::lexer::*;
|
||||
let location = Location::builtins();
|
||||
|
||||
// #define EXCEPTION(value) new /exception(value)
|
||||
defines.insert("EXCEPTION".to_owned(), (location, Define::Function {
|
||||
params: vec!["value".to_owned()],
|
||||
variadic: false,
|
||||
subst: vec![
|
||||
Ident("new".to_owned(), true),
|
||||
Punct(Punctuation::Slash),
|
||||
Ident("exception".to_owned(), false),
|
||||
Punct(Punctuation::LParen),
|
||||
Ident("value".to_owned(), false),
|
||||
Punct(Punctuation::RParen),
|
||||
],
|
||||
docs: Default::default(),
|
||||
}));
|
||||
defines.insert(
|
||||
"EXCEPTION".to_owned(),
|
||||
(
|
||||
location,
|
||||
Define::Function {
|
||||
params: vec!["value".to_owned()],
|
||||
variadic: false,
|
||||
subst: vec![
|
||||
Ident("new".to_owned(), true),
|
||||
Punct(Punctuation::Slash),
|
||||
Ident("exception".to_owned(), false),
|
||||
Punct(Punctuation::LParen),
|
||||
Ident("value".to_owned(), false),
|
||||
Punct(Punctuation::RParen),
|
||||
],
|
||||
docs: Default::default(),
|
||||
},
|
||||
),
|
||||
);
|
||||
|
||||
// #define ASSERT(expression) if (!(expression)) { CRASH("[__FILE__]:[__LINE__]:Assertion Failed: [#X]") }
|
||||
defines.insert("ASSERT".to_owned(), (location, Define::Function {
|
||||
params: vec!["expression".to_owned()],
|
||||
variadic: false,
|
||||
subst: vec![
|
||||
Ident("if".to_owned(), true),
|
||||
Punct(Punctuation::LParen),
|
||||
Punct(Punctuation::Not),
|
||||
Punct(Punctuation::LParen),
|
||||
Ident("expression".to_owned(), false),
|
||||
Punct(Punctuation::RParen),
|
||||
Punct(Punctuation::RParen),
|
||||
Punct(Punctuation::LBrace),
|
||||
Ident("CRASH".to_owned(), false),
|
||||
Punct(Punctuation::LParen),
|
||||
InterpStringBegin("".to_owned()),
|
||||
Ident("__FILE__".to_owned(), false),
|
||||
InterpStringPart(":".to_owned()),
|
||||
Ident("__LINE__".to_owned(), false),
|
||||
InterpStringPart(":Assertion Failed: ".to_owned()),
|
||||
Punct(Punctuation::Hash),
|
||||
Ident("expression".to_owned(), false),
|
||||
InterpStringEnd("".to_owned()),
|
||||
Punct(Punctuation::RParen),
|
||||
Punct(Punctuation::RBrace),
|
||||
],
|
||||
docs: Default::default(),
|
||||
}));
|
||||
defines.insert(
|
||||
"ASSERT".to_owned(),
|
||||
(
|
||||
location,
|
||||
Define::Function {
|
||||
params: vec!["expression".to_owned()],
|
||||
variadic: false,
|
||||
subst: vec![
|
||||
Ident("if".to_owned(), true),
|
||||
Punct(Punctuation::LParen),
|
||||
Punct(Punctuation::Not),
|
||||
Punct(Punctuation::LParen),
|
||||
Ident("expression".to_owned(), false),
|
||||
Punct(Punctuation::RParen),
|
||||
Punct(Punctuation::RParen),
|
||||
Punct(Punctuation::LBrace),
|
||||
Ident("CRASH".to_owned(), false),
|
||||
Punct(Punctuation::LParen),
|
||||
InterpStringBegin("".to_owned()),
|
||||
Ident("__FILE__".to_owned(), false),
|
||||
InterpStringPart(":".to_owned()),
|
||||
Ident("__LINE__".to_owned(), false),
|
||||
InterpStringPart(":Assertion Failed: ".to_owned()),
|
||||
Punct(Punctuation::Hash),
|
||||
Ident("expression".to_owned(), false),
|
||||
InterpStringEnd("".to_owned()),
|
||||
Punct(Punctuation::RParen),
|
||||
Punct(Punctuation::RBrace),
|
||||
],
|
||||
docs: Default::default(),
|
||||
},
|
||||
),
|
||||
);
|
||||
|
||||
// constants
|
||||
macro_rules! c {
|
||||
|
|
@ -148,7 +160,10 @@ pub fn default_defines(defines: &mut DefineMap) {
|
|||
ANIMATION_END_NOW = Int(1);
|
||||
ANIMATION_LINEAR_TRANSFORM = Int(2);
|
||||
ANIMATION_PARALLEL = Int(4);
|
||||
ANIMATION_SLICE = Int(8); // 515
|
||||
ANIMATION_END_LOOP = Int(16); // 516
|
||||
ANIMATION_RELATIVE = Int(256);
|
||||
ANIMATION_CONTINUE = Int(512); // 515
|
||||
|
||||
// database
|
||||
DATABASE_OPEN = Int(0);
|
||||
|
|
@ -195,6 +210,14 @@ pub fn default_defines(defines: &mut DefineMap) {
|
|||
NORMAL_RAND = Int(1);
|
||||
LINEAR_RAND = Int(2);
|
||||
SQUARE_RAND = Int(3);
|
||||
|
||||
|
||||
// json encode flags (515)
|
||||
JSON_PRETTY_PRINT = Int(1);
|
||||
|
||||
// json decode flags (515)
|
||||
JSON_STRICT = Int(1);
|
||||
JSON_ALLOW_COMMENTS = Int(2); // default
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -393,7 +416,7 @@ pub fn register_builtins(tree: &mut ObjectTreeBuilder) {
|
|||
proc/abs(A);
|
||||
proc/addtext(Arg1, Arg2/*, ...*/);
|
||||
proc/alert(Usr/*=usr*/,Message,Title,Button1/*="Ok"*/,Button2,Button3);
|
||||
proc/animate(Object, time, loop, easing, flags, // +2 forms
|
||||
proc/animate(Object, time, loop, easing, flags, delay, tag, command, // +2 forms
|
||||
// these kwargs
|
||||
alpha, color, infra_luminosity, layer, maptext_width, maptext_height,
|
||||
maptext_x, maptext_y, luminosity, pixel_x, pixel_y, pixel_w, pixel_z,
|
||||
|
|
@ -441,7 +464,8 @@ pub fn register_builtins(tree: &mut ObjectTreeBuilder) {
|
|||
repeat,
|
||||
radius,
|
||||
falloff,
|
||||
alpha
|
||||
alpha,
|
||||
name // 516
|
||||
);
|
||||
proc/findlasttext(Haystack,Needle,Start=0,End=1);
|
||||
proc/findlasttextEx(Haystack,Needle,Start=0,End=1);
|
||||
|
|
@ -457,14 +481,14 @@ pub fn register_builtins(tree: &mut ObjectTreeBuilder) {
|
|||
proc/get_step_rand(Ref);
|
||||
proc/get_step_to(Ref,Trg,Min=0);
|
||||
proc/get_step_towards(Ref,Trg);
|
||||
proc/gradient(Gradient, index); // unsure how to handle (Item1, Item2, ..., index) form
|
||||
proc/gradient(Gradient, index, space = COLORSPACE_RGB); // unsure how to handle (Item1, Item2, ..., index) form
|
||||
proc/hascall(Object,ProcName);
|
||||
proc/hearers(Depth=world.view,Center=usr);
|
||||
proc/html_decode(HtmlText);
|
||||
proc/html_encode(PlainText);
|
||||
proc/icon(icon,icon_state,dir,frame,moving); // SNA
|
||||
proc/icon_states(Icon, mode=0);
|
||||
proc/image(icon,loc,icon_state,layer,dir,pixel_x,pixel_y); // SNA
|
||||
proc/image(icon,loc,icon_state,layer,dir,pixel_x,pixel_y,pixel_w,pixel_z); // SNA
|
||||
proc/initial(Var); // special form
|
||||
proc/input(Usr=usr,Message,Title,Default)/*as Type in List*/; // special form
|
||||
proc/isarea(Loc1, Loc2/*,...*/);
|
||||
|
|
@ -566,6 +590,12 @@ pub fn register_builtins(tree: &mut ObjectTreeBuilder) {
|
|||
proc/winshow(player, window, show=1);
|
||||
proc/CRASH(message); // kind of special, but let's pretend
|
||||
|
||||
proc/values_cut_over(Alist, Max, inclusive=0);
|
||||
proc/values_cut_under(Alist, Max, inclusive=0);
|
||||
proc/values_dot(A, B);
|
||||
proc/values_product(Alist);
|
||||
proc/values_sum(Alist);
|
||||
|
||||
// database builtin procs
|
||||
proc/_dm_db_new_query();
|
||||
proc/_dm_db_execute(db_query, sql_query, db_connection, cursor_handler, unknown);
|
||||
|
|
@ -590,7 +620,7 @@ pub fn register_builtins(tree: &mut ObjectTreeBuilder) {
|
|||
savefile yes yes yes yes yes yes
|
||||
client yes yes yes yes yes yes yes
|
||||
|
||||
All other root types have an implicit `parent_type = /datum`.
|
||||
Most other root types have an implicit `parent_type = /datum`.
|
||||
*/
|
||||
datum;
|
||||
datum/var/const/type; // not editable
|
||||
|
|
@ -608,6 +638,7 @@ pub fn register_builtins(tree: &mut ObjectTreeBuilder) {
|
|||
list/var/const/parent_type;
|
||||
list/var/tag;
|
||||
list/var/const/list/vars;
|
||||
list/proc/operator[]();
|
||||
list/proc/Add(Item1, Item2/*,...*/);
|
||||
list/proc/Copy(Start=1, End=0);
|
||||
list/proc/Cut(Start=1, End=0);
|
||||
|
|
@ -619,6 +650,23 @@ pub fn register_builtins(tree: &mut ObjectTreeBuilder) {
|
|||
list/proc/Swap(Index1, Index2);
|
||||
list/var/len;
|
||||
|
||||
// 516
|
||||
alist;
|
||||
alist/var/const/type;
|
||||
alist/var/const/parent_type;
|
||||
alist/var/tag;
|
||||
alist/proc/operator[]();
|
||||
alist/proc/Add(Item1, Item2/*,...*/);
|
||||
alist/proc/Copy(Start=1, End=0);
|
||||
alist/proc/Cut(Start=1, End=0);
|
||||
alist/proc/Find(Elem, Start=1, End=0);
|
||||
alist/proc/Insert(Index, Item1, Item2/*,...*/);
|
||||
alist/proc/Join(Glue, Start=1, End=0);
|
||||
alist/proc/Remove(Item1, Item2/*,...*/);
|
||||
alist/proc/Splice(Start=1, End=0, Item1, Item2/*,...*/);
|
||||
alist/proc/Swap(Index1, Index2);
|
||||
alist/var/len;
|
||||
|
||||
atom/parent_type = path!(/datum);
|
||||
atom/var/alpha = int!(255);
|
||||
atom/var/tmp/appearance; // not editable
|
||||
|
|
@ -656,6 +704,12 @@ pub fn register_builtins(tree: &mut ObjectTreeBuilder) {
|
|||
atom/var/pixel_y = int!(0);
|
||||
atom/var/pixel_w = int!(0);
|
||||
atom/var/pixel_z = int!(0);
|
||||
|
||||
// 516
|
||||
atom/var/icon_w = int!(0);
|
||||
atom/var/icon_z = int!(0);
|
||||
atom/var/pixloc/pixloc;
|
||||
|
||||
atom/var/plane = int!(0);
|
||||
atom/var/suffix;
|
||||
atom/var/text;
|
||||
|
|
@ -903,6 +957,11 @@ pub fn register_builtins(tree: &mut ObjectTreeBuilder) {
|
|||
// only used by client.SoundQuery() for now:
|
||||
sound/var/offset = int!(0);
|
||||
sound/var/len = int!(0);
|
||||
|
||||
// 516
|
||||
sound/var/tmp/atom/atom;
|
||||
sound/var/transform;
|
||||
|
||||
sound/New(file, repeat, wait, channel, volume);
|
||||
|
||||
icon;
|
||||
|
|
@ -960,12 +1019,14 @@ pub fn register_builtins(tree: &mut ObjectTreeBuilder) {
|
|||
regex/proc/Replace(text, rep, start, end);
|
||||
|
||||
database;
|
||||
database/var/_binobj;
|
||||
database/proc/Close();
|
||||
database/proc/Error();
|
||||
database/proc/ErrorMsg();
|
||||
database/proc/Open(filename);
|
||||
database/proc/New(filename);
|
||||
|
||||
database/query/var/database/database;
|
||||
database/query/proc/Add(text, item1, item2 /*...*/);
|
||||
database/query/proc/Close();
|
||||
database/query/proc/Columns(column);
|
||||
|
|
@ -1003,6 +1064,11 @@ pub fn register_builtins(tree: &mut ObjectTreeBuilder) {
|
|||
image/var/pixel_y;
|
||||
image/var/pixel_w;
|
||||
image/var/pixel_z;
|
||||
|
||||
// 516
|
||||
image/var/icon_w;
|
||||
image/var/icon_z;
|
||||
|
||||
image/var/plane;
|
||||
image/var/render_source;
|
||||
image/var/render_target;
|
||||
|
|
@ -1058,12 +1124,41 @@ pub fn register_builtins(tree: &mut ObjectTreeBuilder) {
|
|||
savefile/var/list/dir;
|
||||
savefile/var/eof;
|
||||
savefile/var/name;
|
||||
savefile/proc/operator[]();
|
||||
savefile/proc/ExportText(/* path=cd, file */);
|
||||
savefile/proc/Flush();
|
||||
savefile/proc/ImportText(/* path=cd, file */);
|
||||
savefile/proc/Lock(timeout);
|
||||
savefile/proc/Unlock();
|
||||
|
||||
//512 stuff
|
||||
|
||||
// /dm_filter is a hidden type that can be used to manipulate filter
|
||||
// instances without using the runtime search operator (:). It does
|
||||
// not descend from datum, cannot be subtyped, and can only be created
|
||||
// successfully by a valid call to proc/filter(...). All filter types
|
||||
// create the same kind of /dm_filter, but with different properties.
|
||||
dm_filter;
|
||||
dm_filter/var/const/type;
|
||||
dm_filter/var/x;
|
||||
dm_filter/var/y;
|
||||
dm_filter/var/icon;
|
||||
dm_filter/var/render_source;
|
||||
dm_filter/var/flags;
|
||||
dm_filter/var/size;
|
||||
dm_filter/var/threshold;
|
||||
dm_filter/var/offset;
|
||||
dm_filter/var/alpha;
|
||||
dm_filter/var/color;
|
||||
dm_filter/var/space;
|
||||
dm_filter/var/transform;
|
||||
dm_filter/var/blend_mode;
|
||||
dm_filter/var/density;
|
||||
dm_filter/var/factor;
|
||||
dm_filter/var/repeat;
|
||||
dm_filter/var/radius;
|
||||
dm_filter/var/falloff;
|
||||
|
||||
// 513 stuff
|
||||
proc/arctan(A,B);
|
||||
proc/clamp(NumberOrList,Low,High);
|
||||
|
|
@ -1100,6 +1195,7 @@ pub fn register_builtins(tree: &mut ObjectTreeBuilder) {
|
|||
// 514 stuff
|
||||
|
||||
generator;
|
||||
generator/var/_binobj;
|
||||
generator/proc/Rand();
|
||||
generator/proc/Turn(a);
|
||||
|
||||
|
|
@ -1130,6 +1226,83 @@ pub fn register_builtins(tree: &mut ObjectTreeBuilder) {
|
|||
particles/var/rotation;
|
||||
particles/var/spin;
|
||||
particles/var/drift;
|
||||
|
||||
//515 stuff
|
||||
|
||||
proc/ceil(A);
|
||||
proc/floor(A);
|
||||
proc/fract(A);
|
||||
proc/ftime(File, IsCreationTime);
|
||||
proc/get_steps_to(Ref, Trg, Min=0);
|
||||
proc/isinf(A);
|
||||
proc/isnan(A);
|
||||
proc/ispointer(Value);
|
||||
proc/nameof(VarPathProcRef);
|
||||
proc/noise_hash(param1/*, ...*/);
|
||||
proc/refcount(Object);
|
||||
proc/trimtext(Text);
|
||||
proc/trunc(A);
|
||||
proc/bound_pixloc(Atom, Dir);
|
||||
|
||||
client/proc/RenderIcon(object);
|
||||
|
||||
savefile/var/byond_build = int!(0);
|
||||
savefile/var/byond_version = int!(0);
|
||||
|
||||
|
||||
sound/var/params;
|
||||
sound/var/pitch = int!(0);
|
||||
|
||||
list/proc/RemoveAll(Item1/*, ...*/);
|
||||
|
||||
world/proc/Tick();
|
||||
|
||||
// 516
|
||||
proc/lerp(A, B, factor);
|
||||
proc/sign(A);
|
||||
proc/astype(Val, Type);
|
||||
proc/alist(A/* =a */,B/* =b */,C/* =c */);
|
||||
|
||||
proc/load_ext(LibName, FuncName);
|
||||
|
||||
callee;
|
||||
callee/var/args;
|
||||
callee/var/callee/caller;
|
||||
callee/var/category;
|
||||
callee/var/desc;
|
||||
callee/var/file;
|
||||
callee/var/name;
|
||||
callee/var/line;
|
||||
callee/var/proc;
|
||||
callee/var/src;
|
||||
callee/var/type;
|
||||
callee/var/usr;
|
||||
|
||||
proc/pixloc(x, y, z);
|
||||
|
||||
pixloc;
|
||||
pixloc/var/atom/loc;
|
||||
pixloc/var/step_x;
|
||||
pixloc/var/step_y;
|
||||
pixloc/var/x;
|
||||
pixloc/var/y;
|
||||
pixloc/var/z;
|
||||
|
||||
proc/vector(x, y, z);
|
||||
|
||||
vector;
|
||||
vector/var/len;
|
||||
vector/var/size;
|
||||
vector/var/x;
|
||||
vector/var/y;
|
||||
vector/var/z;
|
||||
|
||||
vector/proc/operator[]();
|
||||
vector/proc/Cross(B);
|
||||
vector/proc/Dot(B);
|
||||
vector/proc/Interpolate(B, t);
|
||||
vector/proc/Normalize();
|
||||
vector/proc/Turn(B);
|
||||
};
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -1,11 +1,10 @@
|
|||
//! Configuration file for diagnostics.
|
||||
|
||||
use foldhash::HashMap;
|
||||
use std::fs::File;
|
||||
use std::io::Read;
|
||||
use std::path::{Path, PathBuf};
|
||||
use std::collections::HashMap;
|
||||
|
||||
use ahash::RandomState;
|
||||
use serde::Deserialize;
|
||||
|
||||
use crate::error::Severity;
|
||||
|
|
@ -19,7 +18,7 @@ pub struct Config {
|
|||
|
||||
// diagnostic configuration
|
||||
display: WarningDisplay,
|
||||
diagnostics: HashMap<String, WarningLevel, RandomState>,
|
||||
diagnostics: HashMap<String, WarningLevel>,
|
||||
pub code_standards: CodeStandards,
|
||||
|
||||
// tool-specific configuration
|
||||
|
|
@ -69,6 +68,7 @@ pub struct Debugger {
|
|||
/// Severity overrides from configuration
|
||||
#[derive(Debug, Deserialize, Clone, Copy, PartialEq)]
|
||||
#[serde(rename_all(deserialize = "lowercase"))]
|
||||
#[derive(Default)]
|
||||
pub enum WarningLevel {
|
||||
#[serde(alias = "errors")]
|
||||
Error = 1,
|
||||
|
|
@ -80,15 +80,17 @@ pub enum WarningLevel {
|
|||
Hint = 4,
|
||||
#[serde(alias = "false", alias = "off")]
|
||||
Disabled = 5,
|
||||
#[default]
|
||||
Unset = 6,
|
||||
}
|
||||
|
||||
/// Available debug engines.
|
||||
#[derive(Debug, Deserialize, Clone, Copy, PartialEq)]
|
||||
#[derive(Debug, Default, Deserialize, Clone, Copy, PartialEq)]
|
||||
pub enum DebugEngine {
|
||||
#[serde(alias = "extools")]
|
||||
Extools,
|
||||
#[serde(alias = "auxtools")]
|
||||
#[default]
|
||||
Auxtools,
|
||||
}
|
||||
|
||||
|
|
@ -99,10 +101,10 @@ pub struct MapRenderer {
|
|||
/// Map from render pass name to whether it should be enabled/disabled.
|
||||
///
|
||||
/// Priority is: CLI arguments > config > defaults.
|
||||
pub render_passes: HashMap<String, bool, RandomState>,
|
||||
pub render_passes: HashMap<String, bool>,
|
||||
|
||||
/// Map from typepath to layer number.
|
||||
pub fancy_layers: HashMap<String, f32, RandomState>,
|
||||
pub fancy_layers: HashMap<String, f32>,
|
||||
|
||||
/// List of typepath to just hide
|
||||
pub hide_invisible: Vec<String>,
|
||||
|
|
@ -121,7 +123,7 @@ impl Config {
|
|||
|
||||
fn config_warninglevel(&self, error: &DMError) -> Option<&WarningLevel> {
|
||||
if let Some(errortype) = error.errortype() {
|
||||
return self.diagnostics.get(errortype)
|
||||
return self.diagnostics.get(errortype);
|
||||
}
|
||||
None
|
||||
}
|
||||
|
|
@ -161,12 +163,6 @@ impl WarningLevel {
|
|||
}
|
||||
}
|
||||
|
||||
impl Default for WarningLevel {
|
||||
fn default() -> WarningLevel {
|
||||
WarningLevel::Unset
|
||||
}
|
||||
}
|
||||
|
||||
impl From<Severity> for WarningLevel {
|
||||
fn from(severity: Severity) -> Self {
|
||||
match severity {
|
||||
|
|
@ -180,19 +176,13 @@ impl From<Severity> for WarningLevel {
|
|||
|
||||
impl PartialEq<Severity> for WarningLevel {
|
||||
fn eq(&self, other: &Severity) -> bool {
|
||||
match (self, other) {
|
||||
(WarningLevel::Error, Severity::Error) => true,
|
||||
(WarningLevel::Warning, Severity::Warning) => true,
|
||||
(WarningLevel::Info, Severity::Info) => true,
|
||||
(WarningLevel::Hint, Severity::Hint) => true,
|
||||
_ => false,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for DebugEngine {
|
||||
fn default() -> Self {
|
||||
Self::Extools
|
||||
matches!(
|
||||
(self, other),
|
||||
(WarningLevel::Error, Severity::Error)
|
||||
| (WarningLevel::Warning, Severity::Warning)
|
||||
| (WarningLevel::Info, Severity::Info)
|
||||
| (WarningLevel::Hint, Severity::Hint)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
File diff suppressed because it is too large
Load diff
|
|
@ -1,17 +1,47 @@
|
|||
//! DMI metadata parsing and representation.
|
||||
|
||||
use foldhash::{HashMap, HashMapExt};
|
||||
use std::collections::BTreeMap;
|
||||
use std::fmt::Display;
|
||||
use std::io;
|
||||
use std::path::Path;
|
||||
use std::collections::BTreeMap;
|
||||
|
||||
use derivative::Derivative;
|
||||
use lodepng::Decoder;
|
||||
|
||||
const VERSION: &str = "4.0";
|
||||
const EXPECTED_VERSION_LINE: &str = "version = 4.0";
|
||||
|
||||
/// Index into the state name table
|
||||
#[derive(Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Clone)]
|
||||
pub struct StateIndex(String, u32);
|
||||
|
||||
impl Display for StateIndex {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
if self.1 == 0 {
|
||||
write!(f, "{}", self.0)
|
||||
} else {
|
||||
write!(f, "{} ({})", self.0, self.1)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<String> for StateIndex {
|
||||
fn from(s: String) -> Self {
|
||||
StateIndex(s, 0)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<&str> for StateIndex {
|
||||
fn from(s: &str) -> Self {
|
||||
StateIndex(s.to_owned(), 0)
|
||||
}
|
||||
}
|
||||
|
||||
/// The two-dimensional facing subset of BYOND's direction type.
|
||||
#[derive(Debug, Clone, Copy, Eq, PartialEq, Hash)]
|
||||
#[derive(Debug, Clone, Copy, Eq, PartialEq, Hash, Default)]
|
||||
pub enum Dir {
|
||||
North = 1,
|
||||
#[default]
|
||||
South = 2,
|
||||
East = 4,
|
||||
West = 8,
|
||||
|
|
@ -23,8 +53,22 @@ pub enum Dir {
|
|||
|
||||
impl Dir {
|
||||
pub const CARDINALS: &'static [Dir] = &[Dir::North, Dir::South, Dir::East, Dir::West];
|
||||
pub const DIAGONALS: &'static [Dir] = &[Dir::Northeast, Dir::Northwest, Dir::Southeast, Dir::Southwest];
|
||||
pub const ALL: &'static [Dir] = &[Dir::North, Dir::South, Dir::East, Dir::West, Dir::Northeast, Dir::Northwest, Dir::Southeast, Dir::Southwest];
|
||||
pub const DIAGONALS: &'static [Dir] = &[
|
||||
Dir::Northeast,
|
||||
Dir::Northwest,
|
||||
Dir::Southeast,
|
||||
Dir::Southwest,
|
||||
];
|
||||
pub const ALL: &'static [Dir] = &[
|
||||
Dir::North,
|
||||
Dir::South,
|
||||
Dir::East,
|
||||
Dir::West,
|
||||
Dir::Northeast,
|
||||
Dir::Northwest,
|
||||
Dir::Southeast,
|
||||
Dir::Southwest,
|
||||
];
|
||||
|
||||
/// Attempt to build a direction from its integer representation.
|
||||
pub fn from_int(int: i32) -> Option<Dir> {
|
||||
|
|
@ -51,11 +95,7 @@ impl Dir {
|
|||
}
|
||||
|
||||
pub fn is_diagonal(self) -> bool {
|
||||
!matches!(self,
|
||||
Dir::North
|
||||
| Dir::South
|
||||
| Dir::East
|
||||
| Dir::West)
|
||||
!matches!(self, Dir::North | Dir::South | Dir::East | Dir::West)
|
||||
}
|
||||
|
||||
pub fn flip(self) -> Dir {
|
||||
|
|
@ -164,14 +204,8 @@ impl Dir {
|
|||
}
|
||||
}
|
||||
|
||||
impl Default for Dir {
|
||||
fn default() -> Self {
|
||||
Dir::South
|
||||
}
|
||||
}
|
||||
|
||||
/// Embedded metadata describing a DMI spritesheet's layout.
|
||||
#[derive(Debug)]
|
||||
#[derive(Debug, Clone, PartialEq)]
|
||||
pub struct Metadata {
|
||||
/// The width of the icon in pixels.
|
||||
pub width: u32,
|
||||
|
|
@ -180,20 +214,24 @@ pub struct Metadata {
|
|||
/// The list of states in the order they appear in the spritesheet.
|
||||
pub states: Vec<State>,
|
||||
/// A lookup table from state name to its position in `states`.
|
||||
pub state_names: BTreeMap<String, usize>,
|
||||
pub state_names: BTreeMap<StateIndex, usize>,
|
||||
}
|
||||
|
||||
/// The metadata belonging to a single icon state.
|
||||
#[derive(Debug)]
|
||||
#[derive(Derivative, Debug, Clone)]
|
||||
#[derivative(PartialEq)]
|
||||
pub struct State {
|
||||
/// The state's name, corresponding to the `icon_state` var.
|
||||
pub name: String,
|
||||
/// Whether this is a movement state (shown during gliding).
|
||||
pub movement: bool,
|
||||
/// The number of frames in the spritesheet before this state's first frame.
|
||||
#[derivative(PartialEq = "ignore")]
|
||||
pub offset: usize,
|
||||
/// 0 for infinite, 1+ for finite.
|
||||
pub loop_: u32,
|
||||
/// The number of `State`s before this with the same name.
|
||||
pub duplicate_index: u32,
|
||||
pub rewind: bool,
|
||||
pub dirs: Dirs,
|
||||
pub frames: Frames,
|
||||
|
|
@ -208,7 +246,7 @@ pub enum Dirs {
|
|||
}
|
||||
|
||||
/// How many frames of animation a state has, and their durations.
|
||||
#[derive(Debug, PartialEq)]
|
||||
#[derive(Debug, Clone, PartialEq)]
|
||||
pub enum Frames {
|
||||
/// Without an explicit setting, only one frame.
|
||||
One,
|
||||
|
|
@ -223,21 +261,26 @@ impl Metadata {
|
|||
/// Read the bitmap and DMI metadata from a given file in a single pass.
|
||||
pub fn from_file(path: &Path) -> io::Result<(lodepng::Bitmap<lodepng::RGBA>, Metadata)> {
|
||||
let path = &crate::fix_case(path);
|
||||
Self::from_bytes(&std::fs::read(path)?)
|
||||
}
|
||||
|
||||
/// Read a u8 array (raw data of a file) as a DMI into a bitmap and metadata
|
||||
pub fn from_bytes(data: &[u8]) -> io::Result<(lodepng::Bitmap<lodepng::RGBA>, Metadata)> {
|
||||
let mut decoder = Decoder::new();
|
||||
decoder.info_raw_mut().colortype = lodepng::ColorType::RGBA;
|
||||
decoder.info_raw_mut().set_bitdepth(8);
|
||||
decoder.remember_unknown_chunks(false);
|
||||
let bitmap = match decoder.decode_file(path) {
|
||||
let bitmap = match decoder.decode(data) {
|
||||
Ok(::lodepng::Image::RGBA(bitmap)) => bitmap,
|
||||
Ok(_) => return Err(io::Error::new(io::ErrorKind::InvalidData, "not RGBA")),
|
||||
Err(e) => return Err(io::Error::new(io::ErrorKind::InvalidData, e)),
|
||||
};
|
||||
|
||||
let metadata = Metadata::from_decoder(bitmap.width as u32, bitmap.height as u32, &decoder);
|
||||
let metadata = Metadata::from_decoder(bitmap.width as u32, bitmap.height as u32, &decoder)?;
|
||||
Ok((bitmap, metadata))
|
||||
}
|
||||
|
||||
fn from_decoder(width: u32, height: u32, decoder: &Decoder) -> Metadata {
|
||||
fn from_decoder(width: u32, height: u32, decoder: &Decoder) -> io::Result<Metadata> {
|
||||
for (key, value) in decoder.info_png().text_keys() {
|
||||
if key == b"Description" {
|
||||
if let Ok(value) = std::str::from_utf8(value) {
|
||||
|
|
@ -246,30 +289,31 @@ impl Metadata {
|
|||
break;
|
||||
}
|
||||
}
|
||||
Metadata {
|
||||
Ok(Metadata {
|
||||
width,
|
||||
height,
|
||||
states: Default::default(),
|
||||
state_names: Default::default(),
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
/// Parse metadata from a `Description` string.
|
||||
#[inline]
|
||||
pub fn meta_from_str(data: &str) -> Metadata {
|
||||
pub fn meta_from_str(data: &str) -> io::Result<Metadata> {
|
||||
parse_metadata(data)
|
||||
}
|
||||
|
||||
pub fn rect_of(&self, bitmap_width: u32, icon_state: &str, dir: Dir, frame: u32) -> Option<(u32, u32, u32, u32)> {
|
||||
pub fn rect_of(
|
||||
&self,
|
||||
bitmap_width: u32,
|
||||
icon_state: &StateIndex,
|
||||
dir: Dir,
|
||||
frame: u32,
|
||||
) -> Option<(u32, u32, u32, u32)> {
|
||||
if self.states.is_empty() {
|
||||
return Some((0, 0, self.width, self.height));
|
||||
}
|
||||
let state_index = match self.state_names.get(icon_state) {
|
||||
Some(&i) => i,
|
||||
None if icon_state == "" => 0,
|
||||
None => return None,
|
||||
};
|
||||
let state = &self.states[state_index];
|
||||
let state = self.get_icon_state(icon_state)?;
|
||||
let icon_index = state.index_of_frame(dir, frame);
|
||||
|
||||
let icon_count = bitmap_width / self.width;
|
||||
|
|
@ -281,11 +325,27 @@ impl Metadata {
|
|||
self.height,
|
||||
))
|
||||
}
|
||||
|
||||
pub fn get_icon_state(&self, icon_state: &StateIndex) -> Option<&State> {
|
||||
let state_index = match self.state_names.get(icon_state) {
|
||||
Some(&i) => i,
|
||||
None => return None,
|
||||
};
|
||||
Some(&self.states[state_index])
|
||||
}
|
||||
}
|
||||
|
||||
impl State {
|
||||
pub fn is_animated(&self) -> bool {
|
||||
match self.frames {
|
||||
Frames::One | Frames::Count(1) => false,
|
||||
Frames::Count(_) => true,
|
||||
Frames::Delays(_) => true,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn num_sprites(&self) -> usize {
|
||||
self.dirs.len() * self.frames.len()
|
||||
self.dirs.count() * self.frames.count()
|
||||
}
|
||||
|
||||
pub fn index_of_dir(&self, dir: Dir) -> u32 {
|
||||
|
|
@ -306,12 +366,16 @@ impl State {
|
|||
|
||||
#[inline]
|
||||
pub fn index_of_frame(&self, dir: Dir, frame: u32) -> u32 {
|
||||
self.index_of_dir(dir) + frame * self.dirs.len() as u32
|
||||
self.index_of_dir(dir) + frame * self.dirs.count() as u32
|
||||
}
|
||||
|
||||
pub fn get_state_name_index(&self) -> StateIndex {
|
||||
StateIndex(self.name.clone(), self.duplicate_index)
|
||||
}
|
||||
}
|
||||
|
||||
impl Dirs {
|
||||
pub fn len(self) -> usize {
|
||||
pub fn count(self) -> usize {
|
||||
match self {
|
||||
Dirs::One => 1,
|
||||
Dirs::Four => 4,
|
||||
|
|
@ -321,7 +385,7 @@ impl Dirs {
|
|||
}
|
||||
|
||||
impl Frames {
|
||||
pub fn len(&self) -> usize {
|
||||
pub fn count(&self) -> usize {
|
||||
match *self {
|
||||
Frames::One => 1,
|
||||
Frames::Count(n) => n,
|
||||
|
|
@ -341,7 +405,7 @@ impl Frames {
|
|||
// ----------------------------------------------------------------------------
|
||||
// Metadata parser
|
||||
|
||||
fn parse_metadata(data: &str) -> Metadata {
|
||||
fn parse_metadata(data: &str) -> io::Result<Metadata> {
|
||||
let mut metadata = Metadata {
|
||||
width: 32,
|
||||
height: 32,
|
||||
|
|
@ -349,47 +413,65 @@ fn parse_metadata(data: &str) -> Metadata {
|
|||
state_names: BTreeMap::new(),
|
||||
};
|
||||
if data.is_empty() {
|
||||
return metadata;
|
||||
return Ok(metadata);
|
||||
}
|
||||
|
||||
let mut lines = data.lines();
|
||||
assert_eq!(lines.next().unwrap(), "# BEGIN DMI");
|
||||
assert_eq!(lines.next().unwrap(), &format!("version = {}", VERSION));
|
||||
let header = (lines.next(), lines.next());
|
||||
let expected_header = (Some("# BEGIN DMI"), Some(EXPECTED_VERSION_LINE));
|
||||
if header != expected_header {
|
||||
return Err(io::Error::new(
|
||||
io::ErrorKind::InvalidData,
|
||||
format!("Wrong dmi metadata header. Expected {expected_header:?}, got {header:?}"),
|
||||
));
|
||||
}
|
||||
|
||||
let mut state: Option<State> = None;
|
||||
let mut frames_so_far = 0;
|
||||
|
||||
let mut duplicate_map: HashMap<String, u32> = HashMap::new();
|
||||
|
||||
for line in lines {
|
||||
if line.starts_with("# END DMI") {
|
||||
break;
|
||||
}
|
||||
let mut split = line.trim().splitn(2, " = ");
|
||||
let key = split.next().unwrap();
|
||||
let value = split.next().unwrap();
|
||||
let (key, value) = line.trim().split_once(" = ").unwrap();
|
||||
match key {
|
||||
"width" => metadata.width = value.parse().unwrap(),
|
||||
"height" => metadata.height = value.parse().unwrap(),
|
||||
"state" => {
|
||||
if let Some(state) = state.take() {
|
||||
frames_so_far += state.frames.len() * state.dirs.len();
|
||||
frames_so_far += state.frames.count() * state.dirs.count();
|
||||
metadata.states.push(state);
|
||||
}
|
||||
let unquoted = value[1..value.len() - 1].to_owned(); // TODO: unquote
|
||||
assert!(!unquoted.contains('\\') && !unquoted.contains('"'));
|
||||
if !metadata.state_names.contains_key(&unquoted) {
|
||||
metadata.state_names.insert(unquoted.clone(), metadata.states.len());
|
||||
}
|
||||
|
||||
state = Some(State {
|
||||
let count = duplicate_map.entry(unquoted.clone()).or_insert(0);
|
||||
|
||||
let new_state = State {
|
||||
offset: frames_so_far,
|
||||
name: unquoted,
|
||||
loop_: 0,
|
||||
duplicate_index: *count,
|
||||
rewind: false,
|
||||
movement: false,
|
||||
dirs: Dirs::One,
|
||||
frames: Frames::One,
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
let key = new_state.get_state_name_index();
|
||||
|
||||
if let std::collections::btree_map::Entry::Vacant(e) =
|
||||
metadata.state_names.entry(key)
|
||||
{
|
||||
e.insert(metadata.states.len());
|
||||
}
|
||||
|
||||
state = Some(new_state);
|
||||
|
||||
*count += 1;
|
||||
},
|
||||
"dirs" => {
|
||||
let state = state.as_mut().unwrap();
|
||||
let n: u8 = value.parse().unwrap();
|
||||
|
|
@ -399,7 +481,7 @@ fn parse_metadata(data: &str) -> Metadata {
|
|||
8 => Dirs::Eight,
|
||||
_ => panic!(),
|
||||
};
|
||||
}
|
||||
},
|
||||
"frames" => {
|
||||
let state = state.as_mut().unwrap();
|
||||
match state.frames {
|
||||
|
|
@ -407,31 +489,125 @@ fn parse_metadata(data: &str) -> Metadata {
|
|||
_ => panic!(),
|
||||
}
|
||||
state.frames = Frames::Count(value.parse().unwrap());
|
||||
}
|
||||
},
|
||||
"delay" => {
|
||||
let state = state.as_mut().unwrap();
|
||||
let mut vector: Vec<f32> = value.split(',').map(str::parse).collect::<Result<Vec<_>, _>>().unwrap();
|
||||
let mut vector: Vec<f32> = value
|
||||
.split(',')
|
||||
.map(str::parse)
|
||||
.collect::<Result<Vec<_>, _>>()
|
||||
.unwrap();
|
||||
match state.frames {
|
||||
Frames::One => if vector.iter().all(|&n| n == 1.) {
|
||||
state.frames = Frames::Count(vector.len());
|
||||
} else {
|
||||
state.frames = Frames::Delays(vector);
|
||||
Frames::One => {
|
||||
if vector.iter().all(|&n| n == 1.) {
|
||||
state.frames = Frames::Count(vector.len());
|
||||
} else {
|
||||
state.frames = Frames::Delays(vector);
|
||||
}
|
||||
},
|
||||
Frames::Count(n) => if !vector.iter().all(|&n| n == 1.) {
|
||||
Frames::Count(n) => {
|
||||
vector.truncate(n);
|
||||
state.frames = Frames::Delays(vector);
|
||||
if !vector.iter().all(|&n| n == 1.) {
|
||||
state.frames = Frames::Delays(vector);
|
||||
}
|
||||
},
|
||||
Frames::Delays(_) => panic!(),
|
||||
}
|
||||
}
|
||||
},
|
||||
"loop" => state.as_mut().unwrap().loop_ = value.parse().unwrap(),
|
||||
"rewind" => state.as_mut().unwrap().rewind = value.parse::<u8>().unwrap() != 0,
|
||||
"hotspot" => { /* TODO */ }
|
||||
"hotspot" => { /* TODO */ },
|
||||
"movement" => state.as_mut().unwrap().movement = value.parse::<u8>().unwrap() != 0,
|
||||
_ => panic!(),
|
||||
}
|
||||
}
|
||||
metadata.states.extend(state);
|
||||
|
||||
metadata
|
||||
Ok(metadata)
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod test {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn duplicate_states() {
|
||||
let description = r#"
|
||||
# BEGIN DMI
|
||||
version = 4.0
|
||||
width = 32
|
||||
height = 32
|
||||
state = "duplicate"
|
||||
dirs = 1
|
||||
frames = 1
|
||||
state = "duplicate"
|
||||
dirs = 1
|
||||
frames = 1
|
||||
state = "duplicate"
|
||||
dirs = 1
|
||||
frames = 1
|
||||
# END DMI
|
||||
"#
|
||||
.trim();
|
||||
|
||||
let metadata = parse_metadata(description).expect("Metadata is valid");
|
||||
assert_eq!(metadata.state_names.len(), 3);
|
||||
assert_eq!(
|
||||
metadata.state_names,
|
||||
BTreeMap::from([
|
||||
(StateIndex("duplicate".to_owned(), 0), 0),
|
||||
(StateIndex("duplicate".to_owned(), 1), 1),
|
||||
(StateIndex("duplicate".to_owned(), 2), 2)
|
||||
])
|
||||
);
|
||||
assert_eq!(metadata.states.len(), 3);
|
||||
|
||||
for (no, state) in metadata.states.iter().enumerate() {
|
||||
if no == 0 {
|
||||
assert_eq!(state.duplicate_index, 0)
|
||||
} else {
|
||||
assert_eq!(state.duplicate_index, no as u32);
|
||||
}
|
||||
|
||||
// Note: using `no` here only works by virtue of the test data being only composed of duplicates
|
||||
assert_eq!(
|
||||
no,
|
||||
*metadata
|
||||
.state_names
|
||||
.get(&state.get_state_name_index())
|
||||
.unwrap()
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
/// Sometimes, Dream Maker just doesn't get rid of extra delay
|
||||
/// information when a state has the number of frames edited.
|
||||
///
|
||||
/// This means we need to truncate our delay list to the number of frames specified by the frames key.
|
||||
///
|
||||
/// This always worked fine- however, we also simplify `delays = 1,1,...` to `Frames::Count(delays.len())`.
|
||||
///
|
||||
/// The bug in our code was that we checked if our `delays = 1,1,...` *before* truncating the array
|
||||
/// in the truncation case, so we would output `Frames::Delays([1,1])` for this metadata.
|
||||
fn delay_overflow_edge_case() {
|
||||
let description = r#"
|
||||
# BEGIN DMI
|
||||
version = 4.0
|
||||
width = 32
|
||||
height = 32
|
||||
state = "one"
|
||||
dirs = 1
|
||||
frames = 2
|
||||
delay = 1,1,0.5,0.5
|
||||
# END DMI
|
||||
"#
|
||||
.trim();
|
||||
|
||||
let metadata = parse_metadata(description).expect("Metadata is valid");
|
||||
let state = metadata
|
||||
.get_icon_state(&StateIndex("one".to_owned(), 0))
|
||||
.expect("Only one state, named one, should be found");
|
||||
assert_eq!(state.frames, Frames::Count(2));
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -2,8 +2,11 @@
|
|||
|
||||
use std::fmt;
|
||||
|
||||
use get_size::GetSize;
|
||||
use get_size_derive::GetSize;
|
||||
|
||||
/// A collection of documentation comments targeting the same item.
|
||||
#[derive(Default, Clone, Debug, PartialEq)]
|
||||
#[derive(Default, Clone, Debug, PartialEq, GetSize)]
|
||||
pub struct DocCollection {
|
||||
elems: Vec<DocComment>,
|
||||
pub builtin_docs: BuiltinDocs,
|
||||
|
|
@ -16,8 +19,8 @@ impl DocCollection {
|
|||
}
|
||||
|
||||
/// Combine another collection into this one.
|
||||
pub fn extend(&mut self, collection: DocCollection) {
|
||||
self.elems.extend(collection.elems);
|
||||
pub fn extend(&mut self, collection: impl IntoIterator<Item = DocComment>) {
|
||||
self.elems.extend(collection);
|
||||
}
|
||||
|
||||
/// Check whether this collection is empty.
|
||||
|
|
@ -59,8 +62,18 @@ impl DocCollection {
|
|||
}
|
||||
}
|
||||
|
||||
impl IntoIterator for DocCollection {
|
||||
type Item = DocComment;
|
||||
|
||||
type IntoIter = <Vec<DocComment> as IntoIterator>::IntoIter;
|
||||
|
||||
fn into_iter(self) -> Self::IntoIter {
|
||||
self.elems.into_iter()
|
||||
}
|
||||
}
|
||||
|
||||
/// A documentation comment.
|
||||
#[derive(Clone, Debug, PartialEq)]
|
||||
#[derive(Clone, Debug, PartialEq, GetSize)]
|
||||
pub struct DocComment {
|
||||
pub kind: CommentKind,
|
||||
pub target: DocTarget,
|
||||
|
|
@ -81,6 +94,16 @@ impl DocComment {
|
|||
fn is_empty(&self) -> bool {
|
||||
is_empty(&self.text, self.kind.ignore_char())
|
||||
}
|
||||
|
||||
/// Return the 3-character sequence that started this comment.
|
||||
pub fn describe_type(&self) -> &'static str {
|
||||
match (self.kind, self.target) {
|
||||
(CommentKind::Block, DocTarget::FollowingItem) => "/**",
|
||||
(CommentKind::Block, DocTarget::EnclosingItem) => "/*!",
|
||||
(CommentKind::Line, DocTarget::FollowingItem) => "///",
|
||||
(CommentKind::Line, DocTarget::EnclosingItem) => "//!",
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl fmt::Display for DocComment {
|
||||
|
|
@ -88,14 +111,14 @@ impl fmt::Display for DocComment {
|
|||
match (self.kind, self.target) {
|
||||
(CommentKind::Block, DocTarget::FollowingItem) => write!(f, "/**{}*/", self.text),
|
||||
(CommentKind::Block, DocTarget::EnclosingItem) => write!(f, "/*!{}*/", self.text),
|
||||
(CommentKind::Line, DocTarget::FollowingItem) => write!(f, "///{}", self.text),
|
||||
(CommentKind::Line, DocTarget::EnclosingItem) => write!(f, "//!{}", self.text),
|
||||
(CommentKind::Line, DocTarget::FollowingItem) => write!(f, "///{}", self.text),
|
||||
(CommentKind::Line, DocTarget::EnclosingItem) => write!(f, "//!{}", self.text),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// The possible documentation comment kinds.
|
||||
#[derive(Copy, Clone, Debug, Eq, PartialEq)]
|
||||
#[derive(Copy, Clone, Debug, Eq, PartialEq, GetSize)]
|
||||
pub enum CommentKind {
|
||||
/// A block `/** */` comment.
|
||||
Block,
|
||||
|
|
@ -126,9 +149,10 @@ fn simplify(out: &mut String, text: &str, ignore_char: char) -> bool {
|
|||
continue;
|
||||
}
|
||||
|
||||
let this_prefix = &line[..line.len() - line
|
||||
.trim_start_matches(|c: char| c.is_whitespace() || c == ignore_char)
|
||||
.len()];
|
||||
let this_prefix = &line[..line.len()
|
||||
- line
|
||||
.trim_start_matches(|c: char| c.is_whitespace() || c == ignore_char)
|
||||
.len()];
|
||||
match prefix {
|
||||
None => prefix = Some(this_prefix),
|
||||
Some(ref mut prefix) => {
|
||||
|
|
@ -137,17 +161,21 @@ fn simplify(out: &mut String, text: &str, ignore_char: char) -> bool {
|
|||
loop {
|
||||
no_match = chars.as_str();
|
||||
match chars.next() {
|
||||
Some(ch) => if Some(ch) != this_chars.next() {
|
||||
break;
|
||||
Some(ch) => {
|
||||
if Some(ch) != this_chars.next() {
|
||||
break;
|
||||
}
|
||||
},
|
||||
None => break,
|
||||
}
|
||||
}
|
||||
*prefix = &prefix[..prefix.len() - no_match.len()];
|
||||
}
|
||||
},
|
||||
}
|
||||
|
||||
let this_suffix = &line[line.trim_end_matches(|c: char| c.is_whitespace() || c == ignore_char).len()..];
|
||||
let this_suffix = &line[line
|
||||
.trim_end_matches(|c: char| c.is_whitespace() || c == ignore_char)
|
||||
.len()..];
|
||||
match suffix {
|
||||
None => suffix = Some(this_suffix),
|
||||
Some(ref mut suffix) => {
|
||||
|
|
@ -156,14 +184,16 @@ fn simplify(out: &mut String, text: &str, ignore_char: char) -> bool {
|
|||
loop {
|
||||
no_match = chars.as_str();
|
||||
match chars.next_back() {
|
||||
Some(ch) => if Some(ch) != this_chars.next_back() {
|
||||
break;
|
||||
Some(ch) => {
|
||||
if Some(ch) != this_chars.next_back() {
|
||||
break;
|
||||
}
|
||||
},
|
||||
None => break,
|
||||
}
|
||||
}
|
||||
*suffix = &suffix[no_match.len()..];
|
||||
}
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -197,7 +227,7 @@ fn is_empty(text: &str, ignore_char: char) -> bool {
|
|||
}
|
||||
|
||||
/// The possible items that a documentation comment may target.
|
||||
#[derive(Copy, Clone, Debug, Eq, PartialEq)]
|
||||
#[derive(Copy, Clone, Debug, Eq, PartialEq, GetSize)]
|
||||
pub enum DocTarget {
|
||||
/// Starting with `*` or `/`, referring to the following item.
|
||||
FollowingItem,
|
||||
|
|
@ -206,15 +236,10 @@ pub enum DocTarget {
|
|||
}
|
||||
|
||||
/// Information about where builtin docs can be found.
|
||||
#[derive(Clone, Debug, PartialEq)]
|
||||
#[derive(Clone, Debug, PartialEq, GetSize, Default)]
|
||||
pub enum BuiltinDocs {
|
||||
#[default]
|
||||
None,
|
||||
/// A DM reference hash such as "/DM/vars".
|
||||
ReferenceHash(&'static str),
|
||||
}
|
||||
|
||||
impl Default for BuiltinDocs {
|
||||
fn default() -> Self {
|
||||
BuiltinDocs::None
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,13 +1,13 @@
|
|||
//! Error, warning, and other diagnostics handling.
|
||||
|
||||
use std::{fmt, error, io};
|
||||
use std::path::{PathBuf, Path};
|
||||
use std::cell::{RefCell, Ref, RefMut};
|
||||
use std::collections::HashMap;
|
||||
use foldhash::HashMap;
|
||||
use std::cell::{Ref, RefCell, RefMut};
|
||||
use std::path::{Path, PathBuf};
|
||||
use std::{error, fmt, io};
|
||||
|
||||
use ahash::RandomState;
|
||||
|
||||
use termcolor::{ColorSpec, Color};
|
||||
use get_size::GetSize;
|
||||
use get_size_derive::GetSize;
|
||||
use termcolor::{Color, ColorSpec};
|
||||
|
||||
use crate::config::Config;
|
||||
|
||||
|
|
@ -15,6 +15,8 @@ use crate::config::Config;
|
|||
#[derive(Copy, Clone, Debug, Eq, PartialEq, Ord, PartialOrd, Hash)]
|
||||
pub struct FileId(u16);
|
||||
|
||||
impl GetSize for FileId {}
|
||||
|
||||
const FILEID_BUILTINS: FileId = FileId(0x0000);
|
||||
const FILEID_MIN: FileId = FileId(0x0001);
|
||||
const FILEID_MAX: FileId = FileId(0xfffe);
|
||||
|
|
@ -32,7 +34,7 @@ pub struct FileList {
|
|||
/// The list of loaded files.
|
||||
files: RefCell<Vec<PathBuf>>,
|
||||
/// Reverse mapping from paths to file numbers.
|
||||
reverse_files: RefCell<HashMap<PathBuf, FileId, RandomState>>,
|
||||
reverse_files: RefCell<HashMap<PathBuf, FileId>>,
|
||||
}
|
||||
|
||||
/// A diagnostics context, tracking loaded files and any observed errors.
|
||||
|
|
@ -42,7 +44,7 @@ pub struct Context {
|
|||
/// A list of errors, warnings, and other diagnostics generated.
|
||||
errors: RefCell<Vec<DMError>>,
|
||||
/// Warning config
|
||||
config: RefCell<Config>,
|
||||
config: Config,
|
||||
print_severity: Option<Severity>,
|
||||
|
||||
io_time: std::cell::Cell<std::time::Duration>,
|
||||
|
|
@ -71,16 +73,16 @@ impl FileList {
|
|||
}
|
||||
|
||||
/// Look up a file path by its index returned from `register_file`.
|
||||
pub fn get_path(&self, file: FileId) -> PathBuf {
|
||||
pub fn get_path(&self, file: FileId) -> Ref<'_, Path> {
|
||||
let files = self.files.borrow();
|
||||
if file == FILEID_BUILTINS {
|
||||
return "(builtins)".into();
|
||||
return Ref::map(files, |_| Path::new("(builtins)"));
|
||||
}
|
||||
let idx = (file.0 - FILEID_MIN.0) as usize;
|
||||
let files = self.files.borrow();
|
||||
if idx > files.len() {
|
||||
"(unknown)".into()
|
||||
Ref::map(files, |_| Path::new("(unknown)"))
|
||||
} else {
|
||||
files[idx].to_owned()
|
||||
Ref::map(files, |files| files[idx].as_path())
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -106,7 +108,7 @@ impl Context {
|
|||
}
|
||||
|
||||
/// Look up a file path by its index returned from `register_file`.
|
||||
pub fn file_path(&self, file: FileId) -> PathBuf {
|
||||
pub fn file_path(&self, file: FileId) -> Ref<'_, Path> {
|
||||
self.files.get_path(file)
|
||||
}
|
||||
|
||||
|
|
@ -122,28 +124,31 @@ impl Context {
|
|||
// ------------------------------------------------------------------------
|
||||
// Configuration
|
||||
|
||||
pub fn force_config(&self, toml: &Path) {
|
||||
pub fn force_config(&mut self, toml: &Path) {
|
||||
match Config::read_toml(toml) {
|
||||
Ok(config) => *self.config.borrow_mut() = config,
|
||||
Ok(config) => self.config = config,
|
||||
Err(io_error) => {
|
||||
let file = self.register_file(toml);
|
||||
let (line, column) = io_error.line_col().unwrap_or((1, 1));
|
||||
DMError::new(Location { file, line, column }, "Error reading configuration file")
|
||||
.with_boxed_cause(io_error.into_boxed_error())
|
||||
.register(self);
|
||||
}
|
||||
DMError::new(
|
||||
Location { file, line, column },
|
||||
"Error reading configuration file",
|
||||
)
|
||||
.with_boxed_cause(io_error.into_boxed_error())
|
||||
.register(self);
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
pub fn autodetect_config(&self, dme: &Path) {
|
||||
pub fn autodetect_config(&mut self, dme: &Path) {
|
||||
let toml = dme.parent().unwrap().join("SpacemanDMM.toml");
|
||||
if toml.exists() {
|
||||
self.force_config(&toml);
|
||||
}
|
||||
}
|
||||
|
||||
pub fn config(&self) -> Ref<Config> {
|
||||
self.config.borrow()
|
||||
pub fn config(&self) -> &Config {
|
||||
&self.config
|
||||
}
|
||||
|
||||
/// Set a severity at and above which errors will be printed immediately.
|
||||
|
|
@ -171,12 +176,12 @@ impl Context {
|
|||
|
||||
/// Push an error or other diagnostic to the context.
|
||||
pub fn register_error(&self, error: DMError) {
|
||||
guard!(let Some(error) = self.config.borrow().set_configured_severity(error) else {
|
||||
return // errortype is disabled
|
||||
});
|
||||
let Some(error) = self.config.set_configured_severity(error) else {
|
||||
return; // errortype is disabled
|
||||
};
|
||||
// ignore errors with severity above configured level
|
||||
if !self.config.borrow().registerable_error(&error) {
|
||||
return
|
||||
if !self.config.registerable_error(&error) {
|
||||
return;
|
||||
}
|
||||
if let Some(print_severity) = self.print_severity {
|
||||
if error.severity() <= print_severity {
|
||||
|
|
@ -189,18 +194,22 @@ impl Context {
|
|||
}
|
||||
|
||||
/// Access the list of diagnostics generated so far.
|
||||
pub fn errors(&self) -> Ref<[DMError]> {
|
||||
pub fn errors(&self) -> Ref<'_, [DMError]> {
|
||||
Ref::map(self.errors.borrow(), |x| &**x)
|
||||
}
|
||||
|
||||
/// Mutably access the diagnostics list. Dangerous.
|
||||
#[doc(hidden)]
|
||||
pub fn errors_mut(&self) -> RefMut<Vec<DMError>> {
|
||||
pub fn errors_mut(&self) -> RefMut<'_, Vec<DMError>> {
|
||||
self.errors.borrow_mut()
|
||||
}
|
||||
|
||||
/// Pretty-print a `DMError` to the given output.
|
||||
pub fn pretty_print_error<W: termcolor::WriteColor>(&self, w: &mut W, error: &DMError) -> io::Result<()> {
|
||||
pub fn pretty_print_error<W: termcolor::WriteColor>(
|
||||
&self,
|
||||
w: &mut W,
|
||||
error: &DMError,
|
||||
) -> io::Result<()> {
|
||||
writeln!(
|
||||
w,
|
||||
"{}, line {}, column {}:",
|
||||
|
|
@ -216,14 +225,12 @@ impl Context {
|
|||
|
||||
for note in error.notes().iter() {
|
||||
if note.location == error.location {
|
||||
writeln!(w, "- {}", note.description, )?;
|
||||
writeln!(w, "- {}", note.description,)?;
|
||||
} else if note.location.file == error.location.file {
|
||||
writeln!(
|
||||
w,
|
||||
"- {}:{}: {}",
|
||||
note.location.line,
|
||||
note.location.column,
|
||||
note.description,
|
||||
note.location.line, note.location.column, note.description,
|
||||
)?;
|
||||
} else {
|
||||
writeln!(
|
||||
|
|
@ -239,7 +246,11 @@ impl Context {
|
|||
writeln!(w)
|
||||
}
|
||||
|
||||
pub fn pretty_print_error_nocolor<W: io::Write>(&self, w: &mut W, error: &DMError) -> io::Result<()> {
|
||||
pub fn pretty_print_error_nocolor<W: io::Write>(
|
||||
&self,
|
||||
w: &mut W,
|
||||
error: &DMError,
|
||||
) -> io::Result<()> {
|
||||
self.pretty_print_error(&mut termcolor::NoColor::new(w), error)
|
||||
}
|
||||
|
||||
|
|
@ -253,7 +264,8 @@ impl Context {
|
|||
let mut printed = false;
|
||||
for err in errors.iter() {
|
||||
if err.severity <= min_severity {
|
||||
self.pretty_print_error(stderr, &err).expect("error writing to stderr");
|
||||
self.pretty_print_error(stderr, err)
|
||||
.expect("error writing to stderr");
|
||||
printed = true;
|
||||
}
|
||||
}
|
||||
|
|
@ -273,7 +285,7 @@ impl Context {
|
|||
// Location handling
|
||||
|
||||
/// File, line, and column information for an error.
|
||||
#[derive(Debug, Copy, Clone, Eq, PartialEq, Ord, PartialOrd, Default)]
|
||||
#[derive(Debug, Copy, Clone, Eq, PartialEq, Ord, PartialOrd, Default, GetSize)]
|
||||
pub struct Location {
|
||||
/// The index into the file table.
|
||||
pub file: FileId,
|
||||
|
|
@ -285,7 +297,11 @@ pub struct Location {
|
|||
|
||||
impl Location {
|
||||
pub fn builtins() -> Location {
|
||||
Location { file: FILEID_BUILTINS, line: 1, column: 1 }
|
||||
Location {
|
||||
file: FILEID_BUILTINS,
|
||||
line: 1,
|
||||
column: 1,
|
||||
}
|
||||
}
|
||||
|
||||
/// Pack this Location for use in `u64`-keyed structures.
|
||||
|
|
@ -300,6 +316,10 @@ impl Location {
|
|||
} else if self.line != 0 {
|
||||
self.column = !0;
|
||||
self.line -= 1;
|
||||
} else if self.file == FILEID_BAD {
|
||||
// This file ID generally comes from using Location::default().
|
||||
// In that case hopefully it's a test or something, so just let it
|
||||
// stay 0:0.
|
||||
} else if self.file.0 != 0 {
|
||||
self.column = !0;
|
||||
self.line = !0;
|
||||
|
|
@ -336,8 +356,9 @@ pub(crate) trait HasLocation {
|
|||
// Error handling
|
||||
|
||||
/// The possible diagnostic severities available.
|
||||
#[derive(Debug, Copy, Clone, Eq, PartialEq, Ord, PartialOrd)]
|
||||
#[derive(Debug, Copy, Clone, Eq, PartialEq, Ord, PartialOrd, Default)]
|
||||
pub enum Severity {
|
||||
#[default]
|
||||
Error = 1,
|
||||
Warning = 2,
|
||||
Info = 3,
|
||||
|
|
@ -348,21 +369,21 @@ impl Severity {
|
|||
fn style(self) -> ColorSpec {
|
||||
let mut spec = ColorSpec::new();
|
||||
match self {
|
||||
Severity::Error => { spec.set_fg(Some(Color::Red)); }
|
||||
Severity::Warning => { spec.set_fg(Some(Color::Yellow)); }
|
||||
Severity::Info => { spec.set_fg(Some(Color::White)).set_intense(true); }
|
||||
Severity::Error => {
|
||||
spec.set_fg(Some(Color::Red));
|
||||
},
|
||||
Severity::Warning => {
|
||||
spec.set_fg(Some(Color::Yellow));
|
||||
},
|
||||
Severity::Info => {
|
||||
spec.set_fg(Some(Color::White)).set_intense(true);
|
||||
},
|
||||
Severity::Hint => {},
|
||||
}
|
||||
spec
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for Severity {
|
||||
fn default() -> Severity {
|
||||
Severity::Error
|
||||
}
|
||||
}
|
||||
|
||||
impl fmt::Display for Severity {
|
||||
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
|
||||
match *self {
|
||||
|
|
@ -375,8 +396,9 @@ impl fmt::Display for Severity {
|
|||
}
|
||||
|
||||
/// A component which generated a diagnostic, when separation is desired.
|
||||
#[derive(Debug, Copy, Clone, Eq, PartialEq)]
|
||||
#[derive(Debug, Copy, Clone, Eq, PartialEq, Default)]
|
||||
pub enum Component {
|
||||
#[default]
|
||||
Unspecified,
|
||||
DreamChecker,
|
||||
}
|
||||
|
|
@ -390,12 +412,6 @@ impl Component {
|
|||
}
|
||||
}
|
||||
|
||||
impl Default for Component {
|
||||
fn default() -> Component {
|
||||
Component::Unspecified
|
||||
}
|
||||
}
|
||||
|
||||
impl fmt::Display for Component {
|
||||
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
|
||||
match self.name() {
|
||||
|
|
@ -519,7 +535,24 @@ impl DMError {
|
|||
|
||||
impl fmt::Display for DMError {
|
||||
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
|
||||
write!(f, "{}:{}:{}", self.location.line, self.location.column, self.description)
|
||||
// Like `pretty_print_error` above, but without filename information.
|
||||
write!(
|
||||
f,
|
||||
"{}:{}: {}: {}",
|
||||
self.location.line, self.location.column, self.severity, self.description
|
||||
)?;
|
||||
for note in self.notes.iter() {
|
||||
if note.location == self.location {
|
||||
write!(f, "\n- {}", note.description,)?;
|
||||
} else {
|
||||
write!(
|
||||
f,
|
||||
"\n- {}:{}: {}",
|
||||
note.location.line, note.location.column, note.description,
|
||||
)?;
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -541,7 +574,7 @@ impl Clone for DMError {
|
|||
component: self.component,
|
||||
description: self.description.clone(),
|
||||
notes: self.notes.clone(),
|
||||
cause: None, // not trivially cloneable
|
||||
cause: None, // not trivially cloneable
|
||||
errortype: self.errortype,
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,8 +1,8 @@
|
|||
//! The indentation processor.
|
||||
use std::collections::VecDeque;
|
||||
|
||||
use crate::{Location, Context, DMError};
|
||||
use crate::lexer::{LocatedToken, Token, Punctuation};
|
||||
use crate::lexer::{LocatedToken, Punctuation, Token};
|
||||
use crate::{Context, DMError, Location};
|
||||
|
||||
/// Eliminates blank lines, parses and validates indentation, braces, and semicolons.
|
||||
///
|
||||
|
|
@ -23,10 +23,14 @@ pub struct IndentProcessor<'ctx, I> {
|
|||
eof_yielded: bool,
|
||||
}
|
||||
|
||||
impl<'ctx, I> IndentProcessor<'ctx, I> where
|
||||
I: Iterator<Item=LocatedToken>
|
||||
impl<'ctx, I> IndentProcessor<'ctx, I>
|
||||
where
|
||||
I: Iterator<Item = LocatedToken>,
|
||||
{
|
||||
pub fn new<J: IntoIterator<Item=LocatedToken, IntoIter=I>>(context: &'ctx Context, inner: J) -> Self {
|
||||
pub fn new<J: IntoIterator<Item = LocatedToken, IntoIter = I>>(
|
||||
context: &'ctx Context,
|
||||
inner: J,
|
||||
) -> Self {
|
||||
IndentProcessor {
|
||||
context,
|
||||
inner: inner.into_iter(),
|
||||
|
|
@ -47,12 +51,16 @@ impl<'ctx, I> IndentProcessor<'ctx, I> where
|
|||
|
||||
#[inline]
|
||||
fn push(&mut self, tok: Token) {
|
||||
self.output.push_back(LocatedToken::new(self.last_input_loc, tok));
|
||||
self.output
|
||||
.push_back(LocatedToken::new(self.last_input_loc, tok));
|
||||
}
|
||||
|
||||
#[inline]
|
||||
fn push_eol(&mut self, tok: Token) {
|
||||
self.output.push_back(LocatedToken::new(self.eol_location.unwrap_or(self.last_input_loc), tok));
|
||||
self.output.push_back(LocatedToken::new(
|
||||
self.eol_location.unwrap_or(self.last_input_loc),
|
||||
tok,
|
||||
));
|
||||
}
|
||||
|
||||
#[inline]
|
||||
|
|
@ -72,15 +80,14 @@ impl<'ctx, I> IndentProcessor<'ctx, I> where
|
|||
self.eol_location = Some(self.last_input_loc);
|
||||
}
|
||||
return;
|
||||
}
|
||||
Token::Punct(Punctuation::Tab) |
|
||||
Token::Punct(Punctuation::Space) => {
|
||||
},
|
||||
Token::Punct(Punctuation::Tab) | Token::Punct(Punctuation::Space) => {
|
||||
if let Some(spaces) = self.current_spaces.as_mut() {
|
||||
*spaces += 1;
|
||||
}
|
||||
return;
|
||||
}
|
||||
_ => {}
|
||||
},
|
||||
_ => {},
|
||||
}
|
||||
|
||||
// handle pre-existing braces
|
||||
|
|
@ -88,8 +95,8 @@ impl<'ctx, I> IndentProcessor<'ctx, I> where
|
|||
Token::Punct(Punctuation::LBrace) => self.current_spaces = None,
|
||||
Token::Punct(Punctuation::RBrace) => {
|
||||
self.current_spaces = None;
|
||||
}
|
||||
_ => {}
|
||||
},
|
||||
_ => {},
|
||||
}
|
||||
|
||||
// handle indentation
|
||||
|
|
@ -105,7 +112,7 @@ impl<'ctx, I> IndentProcessor<'ctx, I> where
|
|||
new_indents = 1;
|
||||
self.current = Some((spaces, 1));
|
||||
}
|
||||
}
|
||||
},
|
||||
Some((spaces_per_indent, indents_)) => {
|
||||
indents = indents_;
|
||||
if spaces == 0 {
|
||||
|
|
@ -116,15 +123,18 @@ impl<'ctx, I> IndentProcessor<'ctx, I> where
|
|||
// Register the error, but cross our fingers and
|
||||
// hope that truncating division will approximate
|
||||
// a sane situation.
|
||||
DMError::new(self.last_input_loc, format!(
|
||||
"inconsistent indentation: {} % {} != 0",
|
||||
spaces, spaces_per_indent,
|
||||
)).register(self.context)
|
||||
DMError::new(
|
||||
self.last_input_loc,
|
||||
format!(
|
||||
"inconsistent indentation: {spaces} % {spaces_per_indent} != 0",
|
||||
),
|
||||
)
|
||||
.register(self.context)
|
||||
}
|
||||
new_indents = spaces / spaces_per_indent;
|
||||
self.current = Some((spaces_per_indent, new_indents));
|
||||
}
|
||||
}
|
||||
},
|
||||
}
|
||||
|
||||
if indents + 1 == new_indents {
|
||||
|
|
@ -132,20 +142,24 @@ impl<'ctx, I> IndentProcessor<'ctx, I> where
|
|||
self.push_eol(Token::Punct(Punctuation::LBrace));
|
||||
} else if indents < new_indents {
|
||||
// multiple indent is an error, register it but let it work
|
||||
DMError::new(self.last_input_loc, format!(
|
||||
"inconsistent multiple indentation: {} > 1",
|
||||
new_indents - indents,
|
||||
)).register(self.context);
|
||||
DMError::new(
|
||||
self.last_input_loc,
|
||||
format!(
|
||||
"inconsistent multiple indentation: {} > 1",
|
||||
new_indents - indents,
|
||||
),
|
||||
)
|
||||
.register(self.context);
|
||||
for _ in indents..new_indents {
|
||||
self.push_eol(Token::Punct(Punctuation::LBrace));
|
||||
}
|
||||
} else if indents == new_indents + 1 {
|
||||
// single unindent
|
||||
self.push(Token::Punct(Punctuation::RBrace));
|
||||
self.push_eol(Token::Punct(Punctuation::RBrace));
|
||||
} else if indents > new_indents {
|
||||
// multiple unindent
|
||||
for _ in new_indents..indents {
|
||||
self.push(Token::Punct(Punctuation::RBrace));
|
||||
self.push_eol(Token::Punct(Punctuation::RBrace));
|
||||
}
|
||||
} else {
|
||||
// same indent as before
|
||||
|
|
@ -160,24 +174,25 @@ impl<'ctx, I> IndentProcessor<'ctx, I> where
|
|||
None => Some((1, 1)),
|
||||
Some((x, y)) => Some((x, y + 1)),
|
||||
};
|
||||
}
|
||||
},
|
||||
Token::Punct(Punctuation::RBrace) => {
|
||||
self.current = match self.current {
|
||||
None => {
|
||||
DMError::new(self.last_input_loc, "unmatched right brace").register(self.context);
|
||||
DMError::new(self.last_input_loc, "unmatched right brace")
|
||||
.register(self.context);
|
||||
None
|
||||
}
|
||||
},
|
||||
Some((_, 1)) => None,
|
||||
Some((x, y)) => Some((x, y - 1)),
|
||||
};
|
||||
}
|
||||
},
|
||||
Token::Punct(Punctuation::LParen) => {
|
||||
self.parentheses += 1;
|
||||
}
|
||||
},
|
||||
Token::Punct(Punctuation::RParen) => {
|
||||
self.parentheses = self.parentheses.saturating_sub(1);
|
||||
}
|
||||
_ => {}
|
||||
},
|
||||
_ => {},
|
||||
}
|
||||
|
||||
self.eol_location = None;
|
||||
|
|
@ -185,8 +200,9 @@ impl<'ctx, I> IndentProcessor<'ctx, I> where
|
|||
}
|
||||
}
|
||||
|
||||
impl<'ctx, I> Iterator for IndentProcessor<'ctx, I> where
|
||||
I: Iterator<Item=LocatedToken>
|
||||
impl<'ctx, I> Iterator for IndentProcessor<'ctx, I>
|
||||
where
|
||||
I: Iterator<Item = LocatedToken>,
|
||||
{
|
||||
type Item = LocatedToken;
|
||||
|
||||
|
|
|
|||
Some files were not shown because too many files have changed in this diff Show more
Loading…
Add table
Add a link
Reference in a new issue