Compare commits

...

197 commits

Author SHA1 Message Date
Tad Hardesty
213284aa51 Fix clippy::needless_late_init warning 2025-12-18 22:02:37 -08:00
Tad Hardesty
0db4298875 Add more #[rustfmt::skip]s 2025-12-18 21:22:18 -08:00
Tad Hardesty
1259ccd3a0 Apply most rustfmt suggestions 2025-12-18 21:22:18 -08:00
LemonInTheDark
70176ca27d
Implement compile time warning for datum[] (#397)
Lummox recently made this a RUNTIME error, but also irrespective of
that this tends to mask HELL vars[] security holes. We really should be
linting for it.

I had to implement operator overloading support for the builtin parser,
which was far more annoying then I expected. Bit scuffed, but it works.
(I am unsure of how to parse for "" though so I left that unimplemented)

This will likely throw errors on most codebases, I'm unsure of how to
handle that.
2025-12-18 16:32:08 -08:00
ZeWaka
660457818e
Fix parsing of alist for-k,v key type filtering (#447)
fix parsing of
```dm
var/alist/foo = list()
for (var/index as num, C in foo)
	world << index
```
currently `got ',', expected one of: '|', 'in', ')'`

also fixes two clippy warnings

ref: bottom of https://www.byond.com/docs/ref/#/proc/for/list
2025-12-18 16:04:08 -08:00
Krashly
ab4e94def1
Fix displace filter flags (#445)
The Displace filter has the ability to add an Overlay flag. It works
properly and as expected. Link to the Byond documentation:

https://www.byond.com/docs/ref/#/{notes}/filters/displace

> The optional FILTER_OVERLAY flag is supported for the flags argument,
> which will overlay the displaced image onto the original.
2025-12-17 22:57:10 -08:00
Tad Hardesty
b6125848e2 Add a missing integrity check for bundled DLLs 2025-10-30 00:36:13 -07:00
Tad Hardesty
b8bb72a543 Change default debug engine to Auxtools 2025-10-30 00:36:13 -07:00
Tad Hardesty
6087530e0d Move DAP and extools handler bodies outside the dispatch macro 2025-10-30 00:36:13 -07:00
Tad Hardesty
848352e671 Move LSP handler bodies outside the dispatch macro 2025-10-29 23:55:23 -07:00
Tad Hardesty
567e57b817 Move version, authors, edition into workspace Cargo.toml 2025-10-28 18:11:42 -07:00
Tad Hardesty
0290db5c75 Bump versions for suite 1.11 2025-08-30 14:47:12 -07:00
Tad Hardesty
1b930c0a80 Update foldhash and maud dependencies
Also run cargo update.
2025-08-30 13:22:23 -07:00
Tad Hardesty
38f570f04e Resolve 'hiding a lifetime' warnings from Rust 1.89 2025-08-18 23:43:52 -07:00
Tad Hardesty
873b75c9d5 Run cargo update 2025-08-18 23:35:14 -07:00
Tad Hardesty
63b34b6585 Typecheck fields on callee.proc 2025-08-18 23:35:00 -07:00
Tad Hardesty
98e69a4b4a Set /callee parent_type to none instead of /datum 2025-08-18 23:34:59 -07:00
Tad Hardesty
814f96a538 Fix missing completions for caller and callee vars 2025-08-18 23:34:59 -07:00
Zonespace
72ed2bce62
Fix alists not being marked as iterable (#442)
Trying to loop over values using `for(var/v in alist("a" = 2))` would
throw an "iterating over a /alist which cannot be iterated" error
despite working in DM.
2025-08-02 13:03:14 -07:00
ZeWaka
5fa2cbb4db
Fix missing pixloc vars (#441)
`undefined field: "x" on /pixloc`
`undefined field: "y" on /pixloc`
2025-07-31 22:04:18 -07:00
Tad Hardesty
8187b2e065 Identify as DM 516.1666 2025-07-31 19:53:03 -07:00
Tad Hardesty
b40d2176cd Use math instead of a loop when clipping sprites 2025-07-31 19:51:02 -07:00
Tad Hardesty
208c5bfac8 Run RenderPass::sprite_filter prior to sorting sprites 2025-07-31 19:51:02 -07:00
Tad Hardesty
3162794032 Rename subpath to ispath and allow no trailing slash 2025-07-31 19:50:39 -07:00
Tad Hardesty
7a1a990934 Remove unused category field on minimap::Sprite 2025-07-31 19:50:39 -07:00
Tad Hardesty
5c3ea44a99 Remove macro use-near-undef warning
This BYOND bug was fixed in 515.1607 in May 2023. It's no longer
necessary for SpacemanDMM to warn about it.

Fixes #423.
2025-07-22 23:13:34 -07:00
Joshua Kidder
442845ff48
Add a body_range field to ProcValue (#438)
I wasn't able to find a concise way to reference the range and/or
contents of proc bodies without also needing to reference the Annotation
tree. I haven't implemented any use case for the added information
outside of some experiments I'm doing with the deeper analysis
companion; I think it could be useful for determining return values and
types without the need for SpacemanDMM directives, at a later point.

It's an Option to account for built-ins not having any actual presence
in the syntax tree.
2025-07-22 23:11:21 -07:00
Anthony "Shifty Rail
936a5ee5a0
Add basic support for for (var/k, v in X) syntax (#431)
This adds a new arm to the parser for the `for (var/k, v in X)` 
dict-like syntax that was added in 516.

I have hijacked the arm that checks for this type of expressions : 

`for (init, test, inc)` or `for (init; test; inc)`

I have seperated it between `init; test; inc` (left untouched) and
`init, test, inc`.
If `inc` is not None, I then check if `test` in a `BinaryOp::In`
(something like `for(init, [x in y])`. If not I just assume it's a `for
(init, test)` which is valid DM syntax.

I then check if `init` is a `var/k` type of statement, using copypaste.
DM will not compile `for (k, v)` nor will it compile `for (var/k,
var/v)`. I check if v is an ident.

If we're all good, I create a new type of structure
`ForKeyValueStatement` and the lib.rs / findreferences.rs knows what to
do with it.

Both `for (i = 0, i < 10, i++)`, `for (i = 0, i < 10)` still pass.
2025-07-18 20:10:15 -07:00
Zonespace
e2c596a99b
Fix filter() not taking name as a kwarg (#436)
This fixes an issue where the "name" kwarg was erroring (see screenshot)
when filter() was called, despite being valid & correct DM.

![](https://github.com/user-attachments/assets/076f7048-d30f-4561-b768-9ac05f2d3b05)

Fixes #430.
2025-07-12 11:56:08 -07:00
Lucy
5a96f6bc22
Add support for call_ext(LoadedFunc) syntax (#433)
This adds support for 516's `call_ext(LoadedFunc)` syntax (allowing
either one or two args to `call_ext`).

The `call_ext_missing_arg` test is removed as a result, as `call_ext`
with a single arg is now valid syntax.
2025-06-26 21:32:53 -07:00
Tad Hardesty
8ef9761b8e Fix clippy lint about unnecessary boxing 2025-06-26 19:05:16 -07:00
Tad Hardesty
ed90d86e8f Run cargo clippy --fix 2025-06-26 19:05:16 -07:00
Tad Hardesty
56a699be34 Remove Dockerfile
It's slightly annoying to have to bump the Rust version here to keep it
working, and it rarely gets noticed when it's out of date anyways
because it isn't really used.
2025-06-26 19:04:34 -07:00
Drathek
7f2641466b
Lint for set SpacemanDMM_ statements that have no effect (#435)
Currently when a spaceman setting like `SHOULD_CALL_PARENT` comes after
some other statement other than possibly another setting, it is silently
ignored. This PR now errors in that situation. The checking is only
performed on spaceman settings and not built in settings (e.g.
`background`) because those can situationally be allowed in control
statements so long as they otherwise would be at the top of the proc,
but dreammaker will already warn for them if they would have no effect.

See below where red lined are the new lints, and yellow lined are the
existing dreammaker warnings.

![image](https://github.com/user-attachments/assets/4fe70a7f-819b-458a-9960-557b7c5bf6ab)
2025-06-23 00:14:56 -07:00
Lucy
68a0d9d5f8
Update git2 to 0.20.2 (#434)
This updates the `git2` crate to 0.20.2, as linking on Windows (except
win7) is broken in Rust 1.87+ due to
https://github.com/rust-lang/rust/pull/138233, which
https://github.com/rust-lang/git2-rs/pull/1143 fixed in `git2` 0.20.1.

I'd try to convert everything to gitoxide instead, but that's easier
said than done.
2025-06-18 21:52:55 -07:00
Tad Hardesty
c1101fb3ad Bump versions for suite 1.10 2025-05-11 22:48:05 -07:00
Lucy
a58420e0b7
Fix dreammaker.debugServerDll accepting empty strings (#428)
This makes it so blank strings are filtered out of
`dreammaker.debugServerDll` (and also `dreammaker.extoolsDLL`, but I
highly doubt that is still in use at all), so the config option being a
blank string will just be treated as if said config option were unset.
2025-05-01 15:06:02 -07:00
harry
d6bcdd50d1
Add <=>, update new(), stub bound_pixloc() (#425)
new() is updated for /alist, /vector, and /pixloc.

Co-authored-by: harryob <55142896+harryob@users.noreply.github.com>
2025-04-05 15:24:16 -07:00
harry
fcc814dfbd
Add 516 compatibility stubs + astype (#420)
we still need to do `for (k, v in list()/alist())` and `pragma syntax c`
but it'll do for now
2025-04-04 00:31:13 -07:00
Tad Hardesty
e447107f6c Update to auxtools debug server v2.3.5 2025-03-26 18:27:06 -07:00
Waterpig
4b77cd487d
Add pixel_w and pixel_z to image() (#424)
The image constructor accepts pixel_w and pixel_z as arguments. This is
not documented anywhere but seemingly works completely fine, as shown
below.

![image](https://github.com/user-attachments/assets/ea30e0bd-bf03-4092-86f6-77105818e332)

So I added it to lints
2025-03-17 20:06:30 -07:00
dependabot[bot]
039e8208c2
Bump ring from 0.17.8 to 0.17.13 (#422)
Bumps [ring](https://github.com/briansmith/ring) from 0.17.8 to 0.17.13.
- [Changelog](https://github.com/briansmith/ring/blob/main/RELEASES.md)
- [Commits](https://github.com/briansmith/ring/commits)

---
updated-dependencies:
- dependency-name: ring
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-03-07 17:24:09 -08:00
Tad Hardesty
18a5d84ba5 Update copyright years for 2025 2025-02-23 19:06:40 -08:00
Tad Hardesty
9fd698e2d6 Update to auxtools debug server v2.3.4 2025-02-23 14:36:22 -08:00
Tad Hardesty
fa715c9194 Allocate exact amount in read_ident 2025-01-11 15:11:58 -08:00
Tad Hardesty
76725c5681 Check for 'in' keyword directly, not by looping
Best case 4% speedup on lint example.
2025-01-11 15:08:18 -08:00
Tad Hardesty
c5e4b0b1e3 Simplify skipping of UTF-8 BOM 2025-01-11 15:04:19 -08:00
Tad Hardesty
db59a08003 Add LocationTracker::count_location helper 2025-01-11 14:01:20 -08:00
Tad Hardesty
b3eb12c128 Reduce allocation in FileList::get_path 2025-01-11 13:08:11 -08:00
Tad Hardesty
844ca6395a Run cargo update 2025-01-10 23:37:04 -08:00
Tad Hardesty
c7448ea917 Require &mut Context to modify config 2025-01-10 23:35:49 -08:00
dependabot[bot]
4c8b80ec36
Bump hashbrown from 0.15.0 to 0.15.2 (#417)
Bumps [hashbrown](https://github.com/rust-lang/hashbrown) from 0.15.0 to 0.15.2.
- [Changelog](https://github.com/rust-lang/hashbrown/blob/master/CHANGELOG.md)
- [Commits](https://github.com/rust-lang/hashbrown/commits)

---
updated-dependencies:
- dependency-name: hashbrown
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
2024-12-04 19:46:34 -08:00
dependabot[bot]
a026dc6c09
Bump rustls from 0.23.15 to 0.23.18 (#415)
Bumps [rustls](https://github.com/rustls/rustls) from 0.23.15 to 0.23.18.
- [Release notes](https://github.com/rustls/rustls/releases)
- [Changelog](https://github.com/rustls/rustls/blob/main/CHANGELOG.md)
- [Commits](rustls/rustls@v/0.23.15...v/0.23.18)

---
updated-dependencies:
- dependency-name: rustls
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
2024-11-25 20:50:11 -08:00
Tad Hardesty
e22ff4757f Add and use Token::single_quoted 2024-11-25 11:14:52 -08:00
ZeWaka
d19602a6f0
Change the hasher used for HashMap/Sets (#414)
I chose to use
[foldhash](https://docs.rs/foldhash/0.1.3/foldhash/index.html) since
we're not needing cryptographically secure hashing, nor are we concerned
with people reverse-engineering application state.

Basically just changing the RandomState or the HashMap/Set import
everywhere. The *Ext types provide the `::new()` interface.

Tested dm-langserver, all works well:

![image](https://github.com/user-attachments/assets/957e9db8-eb99-4970-82e0-a3ca1434e178)
2024-11-08 17:49:06 -08:00
ZeWaka
6ede21716e
Run cargo upgrade (#413)
It's free, easy, and keeps us up-to-date.

### Changes
* Upgrades packages via `cargo upgrade` and then `cargo upgrades` to
make the cargo.toml versions match.
* Manually upgrades the git2 versions and implements better error
handling (if you weren't on a tag, say you had uncommitted changes to
the spacemandmm repo, it would error. now it just continues searching
for the nearest tag).

### Testing
Tested using dm-langserver on the goonstation codebase in release.

![image](https://github.com/user-attachments/assets/7dd2e777-2814-49b9-b735-de58e7f85afb)
2024-10-26 22:31:05 -07:00
Tad Hardesty
7c3593538b Update Rust version in Dockerfile to actually work 2024-10-25 21:07:42 -07:00
Tad Hardesty
63f43a1afc Use new :pat to simplify relevant macros 2024-10-25 21:07:42 -07:00
ZeWaka
88a9fd048c
Upgrade packages to Rust 2021 (#412)
More modern edition of the compiler
2024-10-25 21:07:11 -07:00
Tad Hardesty
86172de965 Handle 'as' return types in dreamchecker
Fixes #406.
2024-08-21 21:22:29 -07:00
Tad Hardesty
a7649bdce1 Fix module git links in dmdoc 2024-08-09 20:50:33 -07:00
Tad Hardesty
cb9444dc9e Show var as types in dmdoc 2024-08-09 18:35:53 -07:00
Tad Hardesty
8c06700a4a Make 'as' italic instead of small in dmdoc 2024-08-09 16:15:37 -07:00
Tad Hardesty
4ca3cdf670 Remove leftover proc return debugging in dmdoc 2024-08-09 15:12:04 -07:00
Tad Hardesty
6cae59f0ef Bundle extools and auxtools in all builds, not just releases 2024-08-08 20:27:25 -07:00
Tad Hardesty
d423f00f78 Fix dm-langserver warnings about unknown #[cfg] names 2024-08-08 20:06:39 -07:00
Tad Hardesty
568f4f417f Permit 'operator' in type names again
Fixes #374.
2024-08-08 19:49:11 -07:00
Tad Hardesty
a24a15a0fe Fix dmdoc linkification of types like /list/datum 2024-08-08 18:53:52 -07:00
Tad Hardesty
20dabdca3a Show proc set SpacemanDMM_return_type in dmdoc 2024-08-08 18:47:27 -07:00
Tad Hardesty
8df23f7793 Show proc return as types in dmdoc 2024-08-08 18:47:24 -07:00
Tad Hardesty
0a003dfd46 Show parameter as types in dmdoc 2024-08-08 18:47:20 -07:00
Tad Hardesty
9d988a1101 Parse as list return type
Fixes #399.
2024-08-08 17:24:33 -07:00
Tad Hardesty
47643caae1 Fix Clippy lints 2024-07-21 19:52:53 -07:00
Tad Hardesty
58258e6013 Remove old .travis.yml 2024-07-21 19:45:18 -07:00
Tad Hardesty
afc817e668 Improve fexists() error handling
Treat the call as invalid if no context is set, and error instead of
evaluating the existence of the wrong path if the current file has no
known parent.
2024-07-15 23:21:33 -07:00
Tad Hardesty
e72dd60746 Fix extra blank line on #warn and #error messages 2024-07-15 23:03:47 -07:00
ZeWaka
5de66004a7
Fix preprocessor relative filepath evaluation for fexists (#401)
The way I implemented const-eval'd fexists is slightly incorrect. It's 
evaulating the existence of the path relative to the working directory, 
instead of relative to the current file. This is causing errors in 
StrongDMM, because it's checking paths relative to the application exe 
file.

I tested by building StrongDMM and using this fixed version, and it 
worked correctly.
2024-07-15 23:03:29 -07:00
Tad Hardesty
49a0f89624 Parse 'as atom'
See #399.
2024-07-15 18:35:12 -07:00
Tad Hardesty
b93a13f6a5 Handle 'as movable' in proc return types and elsewhere
See #399.
2024-07-15 18:22:51 -07:00
Tad Hardesty
7c188f1f34 Fix dreamchecker and dmdoc not printing config errors 2024-07-07 18:15:49 -07:00
Tad Hardesty
367bcdd75f Update error positions in tests 2024-06-28 17:24:52 -07:00
Tad Hardesty
ed98cd63d9 Fix outline panic on certain definitions in macros
Fixed #395.
2024-06-28 17:23:04 -07:00
Tad Hardesty
9d99263bec Improve detail notes in document outline 2024-06-27 18:08:15 -07:00
Tad Hardesty
239ff91947 Tweak var/proc spans to fix issues with outline view
Fixes #394.
2024-06-27 01:05:20 -07:00
Tad Hardesty
b62b10ce40 Fix some redundant TreePath->TypePath->TreePath conversions 2024-06-26 23:19:43 -07:00
Tad Hardesty
8d264957b8 Fix broken cross-reference in docs 2024-06-26 19:18:14 -07:00
Tad Hardesty
ffa13126f9 Remove unused try_iter! macro 2024-06-25 16:54:21 -07:00
Tad Hardesty
5919d61c27 Remove leftover ConstFn::FileExists 2024-06-24 20:23:55 -07:00
Tad Hardesty
181067efd1 Bump versions for suite 1.9 2024-06-23 16:41:24 -07:00
Tad Hardesty
b80cd15fa2 Run cargo update 2024-06-23 16:37:51 -07:00
jimmyl
c2ed4ed6e1
Change the typepath in the gravitygenerator render pass (#393)
/tg/ gravity generators no longer have a /station subtype so the gravity
generator pass doesn't do anything.
2024-06-23 14:35:32 -07:00
Tad Hardesty
2d00d0e2d7 Allow setting environment variables on DS/DD launch 2024-06-23 12:30:06 -07:00
Tad Hardesty
3206ddb261 Fix proc return type tests 2024-06-22 12:59:23 -07:00
Tad Hardesty
d75085266b Fix parsing of union proc return types
Fixes #385.
2024-06-20 21:16:10 -07:00
Tad Hardesty
17373dc1d9 Add sizeof guards for main AST nodes 2024-06-20 21:13:12 -07:00
LemonInTheDark
aec2fc9173
Const-evaluate filter() properly (#390)
We have a constant for it but nothing actually generates that constant. 
Should fix that.
2024-06-16 20:29:05 -07:00
LemonInTheDark
ca9e5a2c8c
Add colorspace as an arg to the gradient proc (#391)
We need to allow colorspace setting to make HSV gradients possible,
which is real powerful. It is technically a bit fucked because byond is 
ok with passing a value for index after the assoc space value, but we 
can just pretend that isn't a problem.
2024-06-16 18:29:17 -07:00
Tad Hardesty
ef9485246d Improve unexpected-EOF error message 2024-06-01 14:56:01 -07:00
Tad Hardesty
d704d63485 Fix clippy lints 2024-06-01 14:40:24 -07:00
Tad Hardesty
fbf6838291 Expand 'term' in errors to 'literal, variable, proc call' 2024-06-01 14:10:14 -07:00
Tad Hardesty
e47fb7c91c Rewrite parser from next/put_back to peek/take
Fixes recent bugs in error message generation.

Removes the need to track whether next() has been called multiple times
in a row for the purpose of ignoring out-of-place doc comments.
2024-06-01 13:49:26 -07:00
Tad Hardesty
5f5236f379 Improve formatting of dreamchecker error tests 2024-06-01 11:52:43 -07:00
Tad Hardesty
a97f9b47bb rustfmt: Add some #[rustfmt::skip]s for later 2024-05-28 23:50:12 -07:00
Tad Hardesty
f030e316b9 rustfmt: Blind-format crates/interval-tree 2024-05-28 23:26:07 -07:00
Tad Hardesty
e1b51dfee9 rustfmt: Add spaces around = in attributes 2024-05-28 23:09:39 -07:00
Tad Hardesty
b4a6857928 rustfmt: Sort imports 2024-05-28 23:09:35 -07:00
Tad Hardesty
8bbd2e1b19 Overhaul doc comment parsing
Fixes #332.
2024-05-28 22:45:16 -07:00
Tad Hardesty
a4afc52612 Allow extending DocCollection from Vec<DocComment> 2024-05-28 00:39:04 -07:00
Tad Hardesty
1a03fe0d27 Fix Unicode being mangled in doc comments 2024-05-27 23:55:26 -07:00
Tad Hardesty
551c3c921f Fix unused field warning in constant folder 2024-05-27 23:43:07 -07:00
Waterpig
30055a5b9f
Add delay argument to animate (#387)
515.1590 added the delay argument to animate() and I happen to be using it
2024-05-24 00:16:33 -07:00
ZeWaka
c6d85c798f
fexists can be called in the preprocessor (#388)
515 feature https://www.byond.com/forum/post/2857912

currently, 
```dm
#if fexists("secret/secret.dme")   // non-constant function call: fexists
#warn wow you have a secret.dme file
#endif
```

can't actually const eval since it can have diff. behavior at runtime obv

---------

Co-authored-by: Tad Hardesty <tad@platymuus.com>
2024-05-23 22:30:58 -07:00
Tad Hardesty
6c5a751516 Update to auxtools debug server v2.3.3 2024-03-19 13:13:31 -07:00
Tad Hardesty
1f331f5db1 Improve update-auxtools in case of staged changes 2024-03-19 13:13:27 -07:00
Tad Hardesty
2dac6eba15 Update copyright years 2024-03-14 13:03:22 -07:00
Tad Hardesty
4883ef0e15 Enable secret debugServerDll setting in release builds too 2024-03-13 00:02:51 -07:00
Tad Hardesty
707c7b4fb8 Update git2 dependency 2024-03-09 22:03:54 -08:00
Tad Hardesty
671f484b40 Run cargo update 2024-03-09 21:12:43 -08:00
Tad Hardesty
e5fa81d78e Properly swallow input types on bare variables
ex: var/foo as text|null
2024-03-04 14:32:45 -08:00
Tad Hardesty
59cfdbf826 Revert "Update to auxtools debug server v2.3.1"
This reverts commit 74cc3b870b.
2024-01-01 11:49:52 -08:00
Tad Hardesty
74cc3b870b Update to auxtools debug server v2.3.1 2023-12-30 15:42:48 -08:00
dependabot[bot]
02b7d9c10d
Bump zerocopy from 0.7.19 to 0.7.31 (#380)
Bumps [zerocopy](https://github.com/google/zerocopy) from 0.7.19 to 0.7.31.
- [Release notes](https://github.com/google/zerocopy/releases)
- [Changelog](https://github.com/google/zerocopy/blob/main/CHANGELOG.md)
- [Commits](https://github.com/google/zerocopy/compare/v0.7.19...v0.7.31)

---
updated-dependencies:
- dependency-name: zerocopy
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-12-14 22:10:53 -08:00
Tad Hardesty
219269b9c4 Update to auxtools debug server v2.3.0 2023-12-14 17:35:31 -08:00
DreamySkrell
af80187a7f
Fix Map::adjust_key_length() (#379)
Currently, there is `Map::adjust_key_length()`, but it sets key length 
based on the amount of items in the dictionary. Which is fine if keys 
in order like 1-2-3-4-...-N, but it falls apart if the keys are not in 
order - which is valid for a dmm map.

If a map has keys like [1, 2, 3000], then, currently, 
`Map::adjust_key_length()` will set key length to 1, but the map will 
panic on save cause the 3000 key is bigger than max key for this key 
length (52).

This PR fixes `adjust_key_length` to work properly in that case, where 
it sets key length based on the biggest key in the map dictionary.
2023-11-18 17:41:40 -08:00
DreamySkrell
5311ff5f02
Let TGM writer take &mut impl Write (#378)
Also `dmm_tools::dmm::Map::to_writer()`, so it can be used to save to a 
string and not just a file, for use in external tooling that depends on 
this, and may want to, for example, pass the string back to byond from 
rust-g.

`dmm_tools::dmm::Map::to_file()` is kept for convenience (and 
backwards compatibility I guess to not break code), but it just uses 
the `::to_writer()` function.
2023-11-16 20:38:22 -08:00
DreamySkrell
356eeacab0
Derive Ord/PartialOrd for Coord2/Coord3 (#377)
So they can be used as keys in maps or the like, in external tooling 
that depends on this.
2023-11-16 20:33:11 -08:00
Tad Hardesty
cef9528642 Implement /proc/operator:=()
Fixes #376.
2023-11-07 19:16:15 -08:00
Tad Hardesty
5b4ba580ab Remove Tera template files
See 53f9447fa0
2023-11-05 23:02:06 -08:00
Tad Hardesty
6d0e149cc3 Include /final in var documentation
This was excluded back when it was a SpacemanDMM-specific feature, but
with BYOND 515 adding a real /final, it should be represented here.
2023-11-05 22:54:48 -08:00
Tad Hardesty
53f9447fa0 Speed up dmdoc massively by replacing Tera with Maud
A ton of time was spent constructing/destructing the Tera contexts, so
skipping that step by using a compile-time templater helps a lot. Takes
dmdoc time on /tg/station13 down from 10+ minutes to <5 seconds.
2023-11-05 22:54:12 -08:00
Tad Hardesty
0213b66f10 Fix Markdown headings rendering as <hh2> instead of <h2> 2023-11-05 21:57:57 -08:00
Tad Hardesty
f6b4c8f6fe Bump versions for suite 1.8 2023-10-29 12:56:45 -07:00
Tad Hardesty
ec65eb1cf9 Run cargo update 2023-10-29 12:56:22 -07:00
Tad Hardesty
7dc22d522b Bump declared BYOND version to 515.1619 2023-10-19 23:26:09 -07:00
Tad Hardesty
587361db60 Handle type::foo in dreamchecker 2023-10-19 23:25:20 -07:00
Tad Hardesty
311e43472a Fix additional clippy lints 2023-10-19 19:12:35 -07:00
Tad Hardesty
6207188963 Run cargo clippy --fix
Plus undo a formatting botch near `for candidate in` in preprocessor.rs.
2023-10-19 18:53:49 -07:00
Tad Hardesty
e5291ce927 Add consteval for nameof() 2023-10-19 18:44:44 -07:00
LemonInTheDark
2b3d30b9ac
Implement the scope operator (#367)
Adds support for :: to the parser, in all its forms (global proc/var, 
off type). Also implements behavior for it. We'll properly read the 
type of what  we attach to, and double check that everything matches. 
Works for the proc refs too.

I've added unit tests for all this to double check my work. I DIDN'T 
mirror the static var detection byond does. problem for another day.
2023-10-19 18:27:48 -07:00
LemonInTheDark
cdbb02897c
Parse 'as' return type syntax, but don't check it (#370)
I'm not hooking it into the existing return type system because I'm
pretty sure it doesn't already support primitives, and I am not prepared
to deal with that bullshit.

It will parse without erroring tho, so that's based.
2023-10-19 17:55:12 -07:00
Tad Hardesty
970e9d0cfd Fix operator/ and operator/=
Neither was handled in try_read_operator_name, and `operator/` needed
special handling in tree_path so that the `/` isn't taken as a tree path
separator and the method is ultimately named just `operator`.

Fixes #362.
2023-10-17 17:58:25 -07:00
LemonInTheDark
61165ec220
Implement /proc/final, piped into should not override (#369)
As a part of this we need to make a struct when building proc defs, to 
keep track of their flags AND their kind at the same time.

We then pass that down into a new flags var on the proc decl struct, 
and we're golden.

Only thing of note is I removed the is_private and is_protected vars 
from proc declarations because they were totally unused. Spooky added 
em a long time ago and I think it was for not much reason.
2023-10-17 17:26:38 -07:00
LemonInTheDark
e5dbc57757
Implement __IMPLIED_TYPE__ (#368)
It turns out this kinda sucks. it doesn't work with ::, so we don't even
really need to care about its value. I threw in support for constant
eval but that's inconsistent on our end cause type_hint doesn't always
play. I figure it's good to at least have something, and issues can get
sorted out as we go.

It is also seemingly massively annoying to eval in like, an istype(), 
since it has special case behavior there. I just kinda left it sit 
since I'm pretty sure it'd be a massive change to support and it like 
does not matter.
2023-10-16 23:54:22 -07:00
LemonInTheDark
a50249e6f8
Handle #pragma multiple, ignore other pragmas (#372)
I'm using a second hashmap to track multiple'd files. Not sure that 
this is the best way of going about it, but it's a living.
2023-10-16 22:32:57 -07:00
Tad Hardesty
39ef2d9aac Run cargo update 2023-10-14 23:07:07 -07:00
LemonInTheDark
d3dbed0fb2
Implements base functionality for __TYPE__ and __PROC__ (#366)
We basically just capture them early, and then treat them as unique
symbols. We don't replace inline or anything because the required
context isn't quite there.
2023-10-14 23:05:18 -07:00
LemonInTheDark
47ef0c58ab
Implement %%, %%=, and operator"" (#371)
The float stuff was easy. Operator was too, just took a bit to realize 
how it worked

Nothin much else to say, outside that I needed to roll my own operation 
for this one, since float remainder is kinda hard to find (and I 
couldn't after cursory reading)
2023-10-08 14:22:36 -07:00
Jordan Dominion
90992c7e6e
Allow measuring the memory footprint of the Objtree with GetSize trait (#359)
Useful for diagnostics.

Co-authored-by: Tad Hardesty <tad@platymuus.com>
2023-10-07 17:36:12 -07:00
Tad Hardesty
6df1be0be8 Add :: stubs to find_references 2023-09-03 22:06:49 -07:00
Tad Hardesty
6fc78ac43b Add AST variants for :: operator 2023-09-03 21:53:27 -07:00
Tad Hardesty
9467bd3ab3 Clean up after clippy --fix, including one real error 2023-07-20 21:57:25 -07:00
Tad Hardesty
fb57ec3c25 Replace unmaintained structopt with newer clap version 2023-07-20 20:11:36 -07:00
Tad Hardesty
269ae0ebb2 Run cargo update
Also explicitly declare our dependency on syn 1's "full" feature, as
otherwise the phf_macros upgrade means the feature will go missing and
cause failures in some situations.
2023-07-20 18:42:22 -07:00
Tad Hardesty
5eb5358fd9 Fix or quiet remaining clippy warnings 2023-07-20 17:47:51 -07:00
Tad Hardesty
3797c66c2f Run cargo clippy --fix 2023-07-20 17:30:38 -07:00
Tad Hardesty
1f9b4416b3 Remove dependency on guard crate
- `guard` 0.5.1 was released on 2021-04-11
- `let else` was stabilized in Rust 1.65.0 on 2022-11-03
- `box_syntax` became a hard error in Rust 1.70.0 on 2023-06-01
- `guard` has tests that use box_syntax so it no longer compiles
2023-06-01 19:33:19 -07:00
Tad Hardesty
fa608c5c9f Loosely parse proc return types
This is enough to get the parser past them but not enough to make them
useful.
2023-06-01 19:33:19 -07:00
Tad Hardesty
44a9a2ab3b Update to auxtools debug server v2.2.4 2023-06-01 19:33:19 -07:00
Jordan Dominion
34a656baf6
Optimize as much as possible in the release build (#357) 2023-05-08 20:27:46 -07:00
Spookerton
3eddd3204b
Add proc and constant builtins as of 515.1606 (#354)
Does not cover:

__PROC__
__TYPE__
proc/foo() as hint
#pragma
%%
%%=
A.operator%%(B)
A.operator%%=(B)
A.operator""()
2023-05-02 20:15:24 -07:00
Tad Hardesty
32bf1b3b98 Parse unary reference and dereference operators 2023-04-21 22:20:33 -07:00
Tad Hardesty
41297597fb Add final keyword as alias for SpacemanDMM_final 2023-04-21 19:19:04 -07:00
Tad Hardesty
240d8e02c4 Add Scope token '::' to lexer 2023-04-20 22:44:29 -07:00
willox
e5c7b9fcb5
Add call_ext()() support for BYOND 515 (#353) 2023-04-20 22:42:11 -07:00
Tad Hardesty
037f28d766 Update year in --version messages 2023-04-20 17:21:03 -07:00
Tad Hardesty
3be92a95ae Fix deprecation warning for chrono::Utc::today 2023-04-20 17:20:20 -07:00
Tad Hardesty
7a23489a3b Run cargo update 2023-04-20 17:11:21 -07:00
Tad Hardesty
20dec28f21 Fix a missed secure.byond.com reference 2023-04-20 17:10:59 -07:00
Tad Hardesty
56cd51d04c Swap secure.byond.com URLs to www.byond.com 2023-04-07 23:12:25 -07:00
Penelope Haze
d37df4c7c2
Update dreammaker crate readme to use proper subdomain (#352)
Lummox says you shouldn't use the `secure` subdomain for the BYOND 
forums.
2023-04-07 23:10:55 -07:00
Tad Hardesty
00df33a093 Bump versions for suite 1.7.3 2023-02-01 19:05:09 -08:00
Tad Hardesty
b02afe7786 Indent match_annotation! bodies 2023-01-30 19:07:22 -08:00
Tad Hardesty
75c989bf14 Add hover for macro documentation 2023-01-30 19:07:22 -08:00
dependabot[bot]
7a2f3b00da
Bump git2 from 0.15.0 to 0.16.1 (#348)
Bumps [git2](https://github.com/rust-lang/git2-rs) from 0.15.0 to 0.16.1.
- [Release notes](https://github.com/rust-lang/git2-rs/releases)
- [Commits](https://github.com/rust-lang/git2-rs/compare/git2-curl-0.15.0...0.16.1)

---
updated-dependencies:
- dependency-name: git2
  dependency-type: direct:production
...

Signed-off-by: dependabot[bot] <support@github.com>

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-01-20 17:51:24 -08:00
dependabot[bot]
c40fa46644
Bump bumpalo from 3.11.0 to 3.11.1 (#346)
Bumps [bumpalo](https://github.com/fitzgen/bumpalo) from 3.11.0 to 3.11.1.
- [Release notes](https://github.com/fitzgen/bumpalo/releases)
- [Changelog](https://github.com/fitzgen/bumpalo/blob/main/CHANGELOG.md)
- [Commits](https://github.com/fitzgen/bumpalo/compare/3.11.0...3.11.1)

---
updated-dependencies:
- dependency-name: bumpalo
  dependency-type: direct:production
...

Signed-off-by: dependabot[bot] <support@github.com>

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-01-20 15:38:07 -08:00
Tad Hardesty
9d278dcd8e Fix parsing heredocs with multiple quotes in a row
Previously misparsed {"foo""bar"} as String("foo\"\"\"bar").
2023-01-15 22:21:20 -08:00
TiviPlus
10d9575f9e
Add missing _binobj var for generators (#342)
Similar to the database binobj, it's a internal object whose string representation is useful for reading the generator's arguments.
2022-10-30 01:45:33 -07:00
LatteKat
5c07083706
Bump up all dependencies (#336) 2022-09-14 17:43:23 -07:00
tigercat2000
8641274b25
Fix edge case where delay has non-1 overflow (#331)
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.

Caused a PartialEq failure in IDB here:
https://github.com/tigercat2000/IconDiffBot2-test/pull/20
https://github.com/ParadiseSS13/Paradise/pull/18326
2022-07-11 22:57:15 -07:00
tigercat2000
4420bb5d60
Add an IconFile to gif/png renderer (#318)
This is finally something I'm comfortable with having added upstream, the API is stabilized and functions well for IDB2's use-case, but should be able to be used by other projects as well.

New dependencies:
- Added the `either` crate to get around an annoying type issue with chained iterators.
- Added the `derivative` crate to make `State` realistically comparable across different icon files (important for diffing in IDB2)
  - `offset` is ignored for PartialEq.

Notes on public API changes:
- Most of the new stuff is in the inline module `dmm_tools::dmi::render`.
- `IconFile`, `Rgba8`, and `Image` are now `Debug`
- `from_bytes` on `Image`, `IconFile`, and `State` no longer takes an explicit &[u8] array, instead it takes the trait bound `<B: AsRef<[u8]>>` for ease of "eat my ownership because I don't care"
- `IconFile` has a new function `pub fn get_icon_state<S: AsRef<str>>(&self, icon_state: S) -> io::Result<&State>` for looking up states by name.
- `Image` has a `clear()` method which fills the data array with defaults.
- `State` has a new member, `pub duplicate: Option<u32>`, which is used for correctly supporting duplicate icon states by enumerating how many times the state name has been seen before this particular `State` was read.
- `State` has a `get_icon_state` method which backs `IconFile`'s `get_icon_state`.
- `State` has a `is_animated(&self) -> bool` method which is used in IDB2 for precalculating the file extension
- `State` also has `get_state_name_index`, which is used by `parse_metadata` to give duplicate states a unique entry into the `state_names` `BTreeMap`.
- `parse_metadata` now keeps track of duplicate states and puts them in `state_names` according to the above.
2022-07-09 15:30:19 -07:00
Tad Hardesty
4a7d19401e Update to auxtools debug server v2.2.3 2022-06-27 17:10:00 -07:00
Mothblocks
f80d6373e7
Check for empty docs_in in flush_docs (#327)
Might speed things up a little.
2022-06-17 16:54:02 -07:00
Jordan Dominion
2be4bc87e3
Add some missing vars to database builtins (#326)
See stddef.dm https://www.byond.com/forum/post/2371109

Fixes #325
2022-06-15 18:07:15 -07:00
moxian
6f26e074de
Add a few simple derives to dmi-related structs (#320)
Specifically, add `#[derive(Clone, PartialEq)]` to `dmi::{Metadata, State, Frames}`

As per (very weakly authoritative) <https://rust-lang.github.io/api-guidelines/interoperability.html> one should derive all the std's traits whenever feasible. Currently for my own project I only need `State::Clone`. The `PartialEq` is being added for consistency with it being present on other structs already, and the rest of the `std`'s traits feel a bit like an overkill presently (despite what the guidelines might say)
2022-05-17 19:28:10 -07:00
moxian
860d6c48a6
Change panics when parsing a malformed dmi to return a Result instead (#319)
Currently parsing a malformed - but also well-formed, yet ancient - .dmi files results in a panic deep within the guts of SpacemanDMM. That's undesirable, since `.dmi`'s which SpacemanDMM does not understand (yet BYOND dreammaker handles fine) do exist in the wild (see https://github.com/ParadiseSS13/Paradise/pull/17800 for a couple of examples).

I don't think teaching SpacemanDMM of the legacy file formats is worth it, but making it return a Result instead of outright panicking definitely is (and it's much less work than the former).

This is technically a breaking change since it changes the signature of the public `dreammaker::dmi::Metadata::meta_from_str()`. (But it's probably rarely used, and definitely easy to fix at the call site?..)

Co-authored-by: Tad Hardesty <tad@platymuus.com>
2022-05-17 19:27:40 -07:00
Tad Hardesty
e979b2e69a Include values of Prefab and Pop in their Hash and Eq impls 2022-05-11 19:52:54 -07:00
Tad Hardesty
a2eaf42f9e Fix panic in langserver in case of disk I/O time rounding errors 2022-05-11 19:52:54 -07:00
Spookerton
59e161b02f
Add /dm_filter hidden type to builtins (#312)
reference to this type -
<https://www.byond.com/forum/post/2544111#comment25180960>

After some cursory poking, /dm_filter is a hidden type that can be used to manipulate filter instances without using the runtime search operator (:). There is no official reference material for it. 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, of which I believe type is the only const.

A contributor on Bay tried to use it but checker doesn't know about it. I'm not familiar with the guts of suite, but I think this is all that's necessary?
2022-05-11 19:52:50 -07:00
tigercat2000
4302562211
Add [u8] read/write functions to Image/IconFile/Metadata (#316)
This is so IconDiffBot2 can operate purely in memory.

Metadata::from_bytes and IconFile::from_bytes take an &[u8] as the 
sequence of bytes of a valid PNG+DMI file to produce their types.

Image::to_bytes gives a Vec<u8> which is just the output from 
png::Encoder.

There is no Image::from_bytes because there's no reason not to use 
IconFile::from_bytes, and there is no to_bytes for Metadata or IconFile 
because they don't support to_file either. IconFile could, but it's 
unnecessary.

Co-authored-by: Tad Hardesty <tad@platymuus.com>
2022-05-11 19:51:34 -07:00
LatteKat
8d57ddf399
Update all dependencies (#314)
notable changes:
- dmdoc: changes in brokenlink callback, no longer takes &str directly, borrows instead, so the closure must not outlive the doc its parsing
- langserver: some structs's member names are changed to uppercase, doc-id version is not an option<> anymore, some u64s are changed to be u32 instead, oneof enum uses in some cases

Co-authored-by: Tad Hardesty <tad@platymuus.com>
2022-05-11 19:42:55 -07:00
tigercat2000
a0430d44e3
Fix Clippy lints (#315)
This resolves every single error raised by [Clippy](https://github.com/rust-lang/rust-clippy) for every active crate. Most are formatting or redundant code, there's only a few things that are functionally better.

* Implement PartialEq manually on Prefab and Pop to ensure consistency with Hash
* Disable clippy::if_same_then_else for simple elif chains
* Add a standard path for the unit test codebase so I don't have to worry about git add
* Collapse nested if within smart cables loop
* mem::replace(x, X::default()) -> mem::take(x)
* Single character strings -> chars
* Use a const fn for Constant::null, unwrap_or -> unwrap_or_else calls to it
* writeln! always uses \n alone
* Address complex type signatures with type aliases
* PickArgs shouldn't have Box in it's type alias
* Random unnecessary variable access
* Add and remove closures where appropriate
* No more redundant field names in struct initialization
* Remove unnecessary references
* Use strip_prefix instead of starts_with -> [..]
* Use Range.contains instead of >= && <= comparisons
* Group hex literals by 4
* Elide lifetimes wherever possible
* Replace match with matches! wherever possible
* Remove needless borrows
* Control flow cleanup
* Misc signature & redundancy fixes
* Rename functions or move them to the correct std Trait
* Just ignore the too many arguments lint on the big ugly functions
* require! wasn't doing anything in tree_block.
  tree_entries returns a Status<()>, thus passing () to self.require.
  Macro expansion:
    ```rust
    fn tree_block(
        &mut self,
        current: NodeIndex,
        proc_kind: Option<ProcDeclKind>,
        var_type: Option<VarTypeBuilder>,
    ) -> Status<()> {
        match self.exact(Token::Punct(Punctuation::LBrace))? {
            Some(x) => x,
            None => return Ok(None),
        };
        Ok(Some({
            let v = self.tree_entries(
                current,
                proc_kind,
                var_type,
                Token::Punct(Punctuation::RBrace),
            );
            self.require(v)?
        }))
    }
    fn tree_entries(
        &mut self,
        current: NodeIndex,
        proc_kind: Option<ProcDeclKind>,
        var_type: Option<VarTypeBuilder>,
        terminator: Token,
    ) -> Status<()> {
    ```
* Fix large enum variants

Co-authored-by: Tad Hardesty <tad@platymuus.com>

* Clarify intent on some of the linted code
2022-04-30 11:02:50 -07:00
fira
7e5335e78d
Fix Sleep/Purity checks being skipped under certain overrides (#311)
* bugfix: fix part of the proc tree not being traversed for sleeps/purity when overrides are present

* add unit test

* copypasting whitespace be like

Co-authored-by: Fira <mekkiti@gmail.com>
2022-04-15 19:05:11 +08:00
KIBORG04
963c2c0792
Update dockerfile (#303)
I made it work
2022-02-09 16:38:23 -08:00
pali
0a121f6ae5
Add lints for the switch(rand(L, H)) pattern (#302)
A pattern such as

```dm
switch(rand(1, 3))
  if(1)
    foo()
  if(2)
    bar()
  if(3)
    baz()
```

is not uncommon in SS13 code. Sadly, it is also not uncommon for people to add new cases to a longer switch of this form and forget to change the `rand` arguments at the top. This PR adds a linter warning for when the `rand` range is not fully covered by the cases and for when a case lies completely outside of the `rand` range.

This currently generates 7 warnings on tgstation master. Sadly, it is (at least to me) not quite clear if the omissions of a part of the `rand` range are intentional there or not. However, I believe that even if they are intentional it is worth adding the missing part of the range as a case with empty body explicitly to make the intent clear to future developers.

As a less related (but perhaps more important) addition this PR now also adds a warning for switch branches of the form `if(X || Y)` which is pretty much always a mistake and should instead be `if(X, Y)`. This lint generates 5 warnings on current tg code, seemingly all being actual mistakes.
2022-02-07 17:09:44 -08:00
Tad Hardesty
4c24fcb883 Fix tests failing due to subtraction followed by overflow 2022-01-04 17:37:47 -08:00
Tad Hardesty
4756f31219 Improve error locations in DMM parser
Fixes a case where parse errors could refer to LocatedTokens with
locations starting at 0,0 instead of at where the input string was
actually taken from.
2022-01-03 17:59:25 -08:00
quardbreak
0bc598a459
Fix homepage links for dmdoc and interval-tree (#301) 2021-12-27 12:34:40 -08:00
130 changed files with 13869 additions and 6903 deletions

1
.gitignore vendored
View file

@ -1,2 +1,3 @@
target/
.vscode/
test_codebase/

View file

@ -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

View file

@ -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

File diff suppressed because it is too large Load diff

View file

@ -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

View file

@ -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

View file

@ -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"

View file

@ -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()
}

View file

@ -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"

View file

@ -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>,
/**

View file

@ -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)'] }

View file

@ -12,7 +12,7 @@ client support.
compatible.
[language server]: https://langserver.org/
[BYOND]: https://secure.byond.com/
[BYOND]: https://www.byond.com/
## Code completion

View file

@ -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());
}

View file

@ -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

View file

@ -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: &regex::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,

View file

@ -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(&param.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();

View file

@ -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:?}"
))
}
}

View file

@ -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);
}

View file

@ -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")))

View file

@ -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");
}
}

View file

@ -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);
}

View file

@ -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";

View file

@ -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

View file

@ -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

View file

@ -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 {

View file

@ -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) = &param.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..];
}

View file

@ -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

View file

@ -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

View file

@ -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())
}

View file

@ -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 }

View file

@ -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

View file

@ -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

View file

@ -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
}

View 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}" // &uarr;
}
}
}
(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} " // &ndash;
@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}" // &uarr;
}
}
}
(name)
aside {
"("
@for (i, param) in proc.params.iter().enumerate() {
@if i > 0 { ", " }
@if !param.type_path.is_empty() {
(env.linkify_type_str(&param.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(())
}

View file

@ -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 %}
&rightarrow; {{ env.git.remote_branch }}
{%- endif -%}
){% endif %}
{%- endif %} &mdash; {% 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>

View file

@ -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 %}

View file

@ -1,55 +0,0 @@
{% extends "base.html" %}
{% import "macros.html" as macros %}
{% block title %}{{ details.orig_filename }}{% endblock %}
{% block header -%}
{{ super() }}
{%- if details.defines %} &mdash; <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 %}

View file

@ -1,81 +0,0 @@
{% extends "base.html" %}
{% import "macros.html" as macros %}
{% block title %}{{ path }}{% endblock %}
{% block header -%}
{{ super() }}
{%- if details.vars %} &mdash; <a href="{{ details.htmlname | safe }}.html#var">Var Details</a>{% endif %}
{%- if details.procs %}{% if details.vars %} - {% else %} &mdash; {% 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 }}">&uarr;</a></aside>
{%- endif -%}
{{ name }}
{%- if var.type %}
<aside>&ndash; {% 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 }}">&uarr;</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 %}

View file

@ -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 {

View file

@ -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 %}

View file

@ -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(())
}

View file

@ -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 }

View file

@ -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> {

View file

@ -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,

View file

@ -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"

View file

@ -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();

View file

@ -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;
}

View 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(())
}
}

View file

@ -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'"
})
}

View file

@ -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),
}
},
}
}

View file

@ -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, "\"}}")?;
}
}

View file

@ -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
}
},
}
}

View file

@ -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;

View file

@ -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],
}

View file

@ -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;

View file

@ -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;

View file

@ -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

View file

@ -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;
}

View file

@ -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/") {

View file

@ -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

View file

@ -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);

View file

@ -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();

View file

@ -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 }

View file

@ -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

View file

@ -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

View file

@ -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! {{

View 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))
}

View file

@ -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,

View file

@ -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:?}"),
)),
}
}
}

View file

@ -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);
}

View 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);
}

View file

@ -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);
}

View file

@ -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);
}

View file

@ -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);
}

View file

@ -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);
}

View file

@ -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);
}

View file

@ -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);
}

View file

@ -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);
}

View file

@ -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);
}

View file

@ -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);
}

View 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, &[]);
}

View file

@ -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);
}

View file

@ -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"

View file

@ -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

View file

@ -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
);
}

View file

@ -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
);
}
});
}

View file

@ -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();

View file

@ -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)
}
}

View file

@ -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] = [];

View file

@ -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);
};
}

View file

@ -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

View file

@ -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));
}
}

View file

@ -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
}
}

View file

@ -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,
}
}

View file

@ -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