Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
188870dbe6 | ||
|
|
e2c90dced3 | ||
|
|
9fe64c0ec3 | ||
|
|
75873d2b92 | ||
|
|
cd2cc9b6a2 | ||
|
|
3bf345247e | ||
|
|
16f3e038d8 | ||
|
|
2cd7be00af | ||
|
|
92df70c864 | ||
|
|
10816add64 | ||
|
|
cd58eebf96 | ||
|
|
b04ad86537 | ||
|
|
6d2caca39c | ||
|
|
9472fac99d | ||
|
|
0c5b5900a5 | ||
|
|
c63935a34a | ||
|
|
f4f94eafce | ||
|
|
c546e8a709 | ||
|
|
bd533da585 | ||
|
|
609d56613a | ||
|
|
3675df76f5 | ||
|
|
26dd6eac3f | ||
|
|
eb5eb6b1eb | ||
|
|
bc79de3c4d | ||
|
|
2a57c57c11 | ||
|
|
9e0b51efc6 | ||
|
|
43abb25114 | ||
|
|
8feac720ac | ||
|
|
8ac9209c3b | ||
|
|
18a1f51397 | ||
|
|
a12ea5d020 | ||
|
|
413b9503a5 | ||
|
|
8fe9297a37 | ||
|
|
bfb4ad5743 | ||
|
|
f400ad99b8 | ||
|
|
029c3614a6 | ||
|
|
5c5b8f5e0c | ||
|
|
9354895f73 | ||
|
|
b502d5dcc6 | ||
|
|
dffd6eee25 | ||
|
|
f2bf74b4d1 | ||
|
|
38e7874b42 | ||
|
|
bc200f3677 | ||
|
|
f7cc0ffd27 | ||
|
|
91429de35d | ||
|
|
7bf3830c62 | ||
|
|
0bdc79715e | ||
|
|
cef369110b | ||
|
|
63f42de828 | ||
|
|
f862274a41 | ||
|
|
7197c28f62 | ||
|
|
8640a2a18b | ||
|
|
e248562592 | ||
|
|
1d2a47aea0 | ||
|
|
5c338f8f5c | ||
|
|
dafd087783 | ||
|
|
1163ae5a41 | ||
|
|
81d85711fc | ||
|
|
5567e76ce6 | ||
|
|
e4ab06cd73 | ||
|
|
622dd4b6fa | ||
|
|
3b5e5893c0 | ||
|
|
7fd8a46e94 | ||
|
|
1f310a1068 | ||
|
|
342c3cd7d6 | ||
|
|
31b8faefca | ||
|
|
9ddd3b1ee8 | ||
|
|
a7b1a4546e | ||
|
|
110200f6b2 | ||
|
|
b776e6e200 | ||
|
|
580d6da37f | ||
|
|
fd6ae9cf6c | ||
|
|
200f798437 | ||
|
|
00bc6c3d19 | ||
|
|
54a0234c6b | ||
|
|
e0fcb1a3ed | ||
|
|
696a1755b3 | ||
|
|
4631950db8 | ||
|
|
3bb9449c60 | ||
|
|
a966299926 | ||
|
|
c2bf9da77d | ||
|
|
2ac6471cff | ||
|
|
c79d27eca8 | ||
|
|
99c67e963b | ||
|
|
a7b253ce9f | ||
|
|
c25c784feb | ||
|
|
b2d7e0a681 | ||
|
|
da40dfb824 | ||
|
|
2177534a5b | ||
|
|
77ac4d8d00 | ||
|
|
d56fbf0318 | ||
|
|
e9ac655835 | ||
|
|
203e4cce81 | ||
|
|
4d842ec1c9 | ||
|
|
66d6e14edb | ||
|
|
25879b234c | ||
|
|
fcc4df0342 | ||
|
|
70e5ad8874 | ||
|
|
612b5d8009 | ||
|
|
d2fdda09d5 | ||
|
|
fc4bdb04b9 | ||
|
|
de4d1370c7 | ||
|
|
a06500d4ed | ||
|
|
8d3b99fbf7 | ||
|
|
c90b7832df | ||
|
|
9f59231d58 | ||
|
|
592bf1a922 | ||
|
|
6e326e8e18 | ||
|
|
e888708b0a | ||
|
|
3f668fa5c8 | ||
|
|
6e08aaeb34 | ||
|
|
c32f794a6f | ||
|
|
d2680209eb | ||
|
|
253bb2d7f6 | ||
|
|
8d4ced40a6 | ||
|
|
2cc04ee79d | ||
|
|
e123e7ba8f | ||
|
|
8920d9f992 | ||
|
|
6c905a59c8 | ||
|
|
5464394dff | ||
|
|
90f8242dc0 | ||
|
|
3fc00b02dd | ||
|
|
2b55a4c5d6 | ||
|
|
a8799fd60c | ||
|
|
8fb4a985f6 | ||
|
|
79d09e75fc | ||
|
|
5589b5a4d5 | ||
|
|
1e661c597a | ||
|
|
4e7755865c | ||
|
|
960d2f3cf1 | ||
|
|
7debbc155f | ||
|
|
fbdd68e9d3 | ||
|
|
12b2222819 | ||
|
|
da3b1ec2f2 | ||
|
|
66b49a6369 | ||
|
|
9d3ae3c974 | ||
|
|
5f80db48de | ||
|
|
8481785b3b | ||
|
|
1a6f6aee96 | ||
|
|
e1e49bb19b | ||
|
|
ca7891e5cb | ||
|
|
dbf0776dbf | ||
|
|
040605e2a9 | ||
|
|
16f047f643 | ||
|
|
f99eeae7a6 | ||
|
|
8cb5ed91ba | ||
|
|
3fe7644f72 | ||
|
|
9f6879ecc9 | ||
|
|
5c44707bc9 | ||
|
|
ee2a77d570 | ||
|
|
770bad3a6d | ||
|
|
e8ad0e605f | ||
|
|
3267ebf0d9 | ||
|
|
5a2c364c50 | ||
|
|
d3c16b45c6 | ||
|
|
dff2a1e487 | ||
|
|
5dce8af395 | ||
|
|
a844618ca2 | ||
|
|
8c43f6f67a | ||
|
|
09e1b17905 | ||
|
|
2294ef80d5 | ||
|
|
4dd87e7308 | ||
|
|
47e462e836 | ||
|
|
00a1a7d1e2 | ||
|
|
eeba677da9 | ||
|
|
9f0a4988a3 | ||
|
|
ede100c9da | ||
|
|
3b65050ef3 | ||
|
|
63fedc046b | ||
|
|
741c8aa5fc | ||
|
|
bdddf364a0 | ||
|
|
109fa509fa | ||
|
|
90f79be163 | ||
|
|
2d0a1cdabb | ||
|
|
3bc8b0317e | ||
|
|
d361a323a2 | ||
|
|
ceb364472e | ||
|
|
41f04a11b3 | ||
|
|
1a24ed42e1 | ||
|
|
75bc9300fb | ||
|
|
e76d3c3d26 | ||
|
|
6f3264676c | ||
|
|
67846d6c4b | ||
|
|
2c588b6a93 | ||
|
|
f102b0d681 | ||
|
|
86a0c8a04e | ||
|
|
75b13d0052 | ||
|
|
9654d5ad98 | ||
|
|
07fb33be62 | ||
|
|
9d8a653ab8 | ||
|
|
4a7115b103 | ||
|
|
b78d989394 | ||
|
|
88baab88e9 | ||
|
|
3f0082d6e7 | ||
|
|
0df50ecf5d | ||
|
|
55144f0a9e | ||
|
|
a8d5651ab0 | ||
|
|
535aecdc01 | ||
|
|
f7727c0584 | ||
|
|
1e52320014 | ||
|
|
3cd12d4298 | ||
|
|
f94ec82c96 | ||
|
|
3a0e32d24f | ||
|
|
6eba4bbeda | ||
|
|
724286f497 | ||
|
|
7c8b7eee57 | ||
|
|
c23472903a | ||
|
|
846dbd6259 | ||
|
|
05447c7894 | ||
|
|
c4c7e17361 | ||
|
|
f3057dd9c3 | ||
|
|
0e195cfb9e | ||
|
|
611280b069 | ||
|
|
74e2ff7b85 | ||
|
|
326b81c225 | ||
|
|
3d4a281da6 | ||
|
|
ff4a08541c | ||
|
|
7f5acf3b2d | ||
|
|
eed0665e42 | ||
|
|
85c549c15c | ||
|
|
5763ae5b72 | ||
|
|
55cad46640 | ||
|
|
65985173fa | ||
|
|
7dfd6ab8cd | ||
|
|
ba3481356c | ||
|
|
58467843d5 | ||
|
|
3219c40629 | ||
|
|
d35fe9726b | ||
|
|
d311a3ad5b | ||
|
|
358d91251e | ||
|
|
25256fad3c | ||
|
|
3f4056c9f9 | ||
|
|
055f62026e | ||
|
|
593456e53e | ||
|
|
ef9f77c665 | ||
|
|
dc88d62f43 | ||
|
|
6909b5eca1 | ||
|
|
949195fb9c | ||
|
|
23aa7156dc | ||
|
|
52694d0999 | ||
|
|
3e4c15e019 | ||
|
|
9e645db007 | ||
|
|
3040692852 | ||
|
|
193c443bc4 | ||
|
|
cb184f89ec | ||
|
|
c2d89243b6 | ||
|
|
75d085d733 | ||
|
|
e550440407 | ||
|
|
f1eb67bf52 | ||
|
|
efafb1acea | ||
|
|
56c6c392b3 | ||
|
|
d5afb340a2 | ||
|
|
f95fbf57b6 | ||
|
|
6b5db32522 | ||
|
|
300ef8fef1 | ||
|
|
e09c256c80 | ||
|
|
7174e776ac | ||
|
|
f10ea83530 | ||
|
|
e19f2155b1 | ||
|
|
86a18067dd | ||
|
|
15caa7a6f7 | ||
|
|
b2897bc86c | ||
|
|
cc486ad94b | ||
|
|
c5c22fe1a5 | ||
|
|
302beb2d6e | ||
|
|
a702040dfb | ||
|
|
1510e5922e | ||
|
|
92344d2e30 | ||
|
|
5ce3315967 | ||
|
|
858199e298 | ||
|
|
0739a40abf | ||
|
|
215bbdb9da | ||
|
|
c9d4d07dc7 | ||
|
|
7e1442e969 | ||
|
|
fcfde61b24 | ||
|
|
fe6f5eb821 | ||
|
|
512a3fc927 | ||
|
|
3bf06a1cc9 | ||
|
|
2c3d61e629 | ||
|
|
20051109dd | ||
|
|
267cc55e40 | ||
|
|
766937d5fe | ||
|
|
18d8fe7248 | ||
|
|
f7cb738803 | ||
|
|
400ef63c5a | ||
|
|
1ff6a1656c | ||
|
|
20da3f85f1 | ||
|
|
d5e14a7101 | ||
|
|
ffa8622903 | ||
|
|
8b559639e8 | ||
|
|
628c0e8975 | ||
|
|
1734ec1dbf | ||
|
|
b10ae8dfa5 | ||
|
|
dc634fa35c | ||
|
|
06246aa3d7 | ||
|
|
7c1eb4f88c | ||
|
|
38634ef5ca | ||
|
|
5d76d23d33 | ||
|
|
da422c913f | ||
|
|
ba52b932ee | ||
|
|
1b90b7bebf | ||
|
|
23ca9c14c1 | ||
|
|
ab300be709 | ||
|
|
e6aaca19f6 | ||
|
|
0fe226132d | ||
|
|
452e3a58d8 | ||
|
|
e8f60a23ef | ||
|
|
958a6f49ce | ||
|
|
2005628358 | ||
|
|
439901b42e | ||
|
|
7bbe356009 | ||
|
|
9d710b0490 | ||
|
|
d140caf4ba | ||
|
|
15b691ad7b | ||
|
|
02b53e4900 | ||
|
|
5b5d76aa59 | ||
|
|
ae861214cb | ||
|
|
4d5558f3d8 | ||
|
|
0af6f61fb9 | ||
|
|
72a2e9c895 | ||
|
|
4f1bbd3f9b | ||
|
|
b99089dd88 | ||
|
|
380eb5cbaf | ||
|
|
a398eb17c4 | ||
|
|
9765f5efe5 | ||
|
|
d6e1f0c90d | ||
|
|
f56fa1955e | ||
|
|
277abfab2b | ||
|
|
5c1b0ae184 | ||
|
|
ee952c0789 | ||
|
|
eb1d54e750 | ||
|
|
0d8fd214a1 | ||
|
|
07b0af53f1 | ||
|
|
7ffa3d5bf2 | ||
|
|
615d52aff2 | ||
|
|
4961d4ee99 | ||
|
|
33a020a34b | ||
|
|
6ba44d6cb0 | ||
|
|
277d16895b | ||
|
|
dbd19159b6 | ||
|
|
9f5c88ffa1 | ||
|
|
a528a11b8c | ||
|
|
a52f4c5e95 | ||
|
|
0be42bf047 | ||
|
|
ba31f3611a | ||
|
|
3119a0cb6e | ||
|
|
5326c40347 | ||
|
|
bbcb19d476 | ||
|
|
e6d6972ab0 | ||
|
|
4f8da59577 | ||
|
|
6686812504 | ||
|
|
80993df87f | ||
|
|
30361954f3 | ||
|
|
04965f38c6 | ||
|
|
5d8360333a | ||
|
|
2a88adf6b3 | ||
|
|
38e70cb678 | ||
|
|
878427edb5 | ||
|
|
4dcf83f909 | ||
|
|
c14bf35144 | ||
|
|
3ae09b6d5a | ||
|
|
b778195dd1 | ||
|
|
5f8bb643b8 | ||
|
|
af35608953 | ||
|
|
647f158788 | ||
|
|
e42b8c7b9f | ||
|
|
d9387f6a09 | ||
|
|
1804cdb183 | ||
|
|
2ad593341f | ||
|
|
1d18a14356 | ||
|
|
f1a1db0834 | ||
|
|
1ebce748d4 | ||
|
|
836129ec8a | ||
|
|
97a5e2ba14 | ||
|
|
f75f83f653 | ||
|
|
7a16a7465a | ||
|
|
62a3f73de9 | ||
|
|
113fa19bed | ||
|
|
7b8d6577ed | ||
|
|
36f9701b5b | ||
|
|
ec4ebf5a15 | ||
|
|
c1988f1858 | ||
|
|
be85dd06e5 | ||
|
|
4366fbe512 | ||
|
|
987b3846ef | ||
|
|
f40c74b153 | ||
|
|
23ca7357d4 | ||
|
|
95af46cbf7 | ||
|
|
56af0d6a11 | ||
|
|
c39046d999 | ||
|
|
476336be68 | ||
|
|
e0bd5b8038 | ||
|
|
0691207413 | ||
|
|
16ea626c28 | ||
|
|
81788550bf | ||
|
|
ed2bd14a71 | ||
|
|
7b66ff2304 | ||
|
|
54f2e75139 | ||
|
|
279b0755ea | ||
|
|
b44deb393d | ||
|
|
714af8ebe0 | ||
|
|
b4a1730e37 | ||
|
|
a85b3be037 | ||
|
|
90b8946429 | ||
|
|
09192474d7 | ||
|
|
6061d5f438 | ||
|
|
7aa9a0c9a8 | ||
|
|
b1a37a7674 | ||
|
|
ee413f06dd | ||
|
|
fbbc0b2dcc | ||
|
|
5c07c41e62 | ||
|
|
9dd4791bb8 | ||
|
|
9a842207ac | ||
|
|
e139ae252f | ||
|
|
fb8fd8b7d4 | ||
|
|
6bab076329 | ||
|
|
ec65cb3d27 | ||
|
|
96720e235f | ||
|
|
c2b68dcd90 | ||
|
|
7a09f0c02c | ||
|
|
1b0a0a476d | ||
|
|
28883fb9ed | ||
|
|
1b38dab9aa | ||
|
|
ab99cc6f74 | ||
|
|
8ba14b0e7b | ||
|
|
a7fc70a5c9 | ||
|
|
204dae213c | ||
|
|
fdaab2b229 | ||
|
|
e67643e0b1 | ||
|
|
d1d77dd85c | ||
|
|
766bdbcd62 | ||
|
|
7245553588 | ||
|
|
fddf0e92cb | ||
|
|
83c2ea2881 | ||
|
|
c170c25693 | ||
|
|
03f6f90636 | ||
|
|
bcc0a44979 | ||
|
|
6c20b70a14 | ||
|
|
c6bdd753df | ||
|
|
99ad396fa3 | ||
|
|
cdb3acc691 | ||
|
|
71b4a58cab | ||
|
|
934bce7bdc | ||
|
|
dd5ea7748b | ||
|
|
4fef6dc3b6 | ||
|
|
cb669335a3 | ||
|
|
68970b6716 | ||
|
|
d80682333a | ||
|
|
a8dec0cd70 | ||
|
|
6d0279fc37 | ||
|
|
f5fb39538c | ||
|
|
1b693b0752 | ||
|
|
4ca0e0deb5 | ||
|
|
26dbf918cc | ||
|
|
82adf357d2 | ||
|
|
8022643b8e | ||
|
|
3a44168d8c | ||
|
|
2818539686 | ||
|
|
6ad0502cce | ||
|
|
dccafc5e27 | ||
|
|
1653839753 | ||
|
|
7e09c25f5b | ||
|
|
f7fef85353 | ||
|
|
83667e6232 | ||
|
|
fa86e53b7d | ||
|
|
e43ec564e0 | ||
|
|
a25233ccdc | ||
|
|
a3a75b89b9 | ||
|
|
8733afe512 | ||
|
|
6059f21f2d | ||
|
|
02d9a507b4 | ||
|
|
d0a9a59de7 | ||
|
|
6dc5ac826e | ||
|
|
807b9ab09f | ||
|
|
45fc68f08a | ||
|
|
18f2334ddd | ||
|
|
6607645786 | ||
|
|
9497378600 | ||
|
|
4215b1c72e | ||
|
|
4300aff991 | ||
|
|
ca88b00d95 | ||
|
|
e3a5f329ad | ||
|
|
42916a8b6a | ||
|
|
12ed0107ab | ||
|
|
6016ee966e | ||
|
|
555ec09ef5 | ||
|
|
b30ea36fcc | ||
|
|
4c0d92507a | ||
|
|
8421e4c150 | ||
|
|
be3a6dee93 | ||
|
|
a76a868cb6 | ||
|
|
707d519110 | ||
|
|
a93e8a9f10 | ||
|
|
65da3049ba | ||
|
|
4a00429a57 | ||
|
|
da6303a0b0 | ||
|
|
96f8ee85e0 | ||
|
|
9f9a6924a7 | ||
|
|
8e6fa51c27 | ||
|
|
047187257b | ||
|
|
2f77ccfc6e | ||
|
|
d19245c23d | ||
|
|
52703536a6 | ||
|
|
c5dc625550 | ||
|
|
6405fe6bb3 | ||
|
|
c7367ae33d | ||
|
|
1df9632e31 | ||
|
|
94a1ec3902 | ||
|
|
c05c0160c3 | ||
|
|
6193aa2faa | ||
|
|
ca79c71851 | ||
|
|
d83149dc70 | ||
|
|
2c4167bc11 | ||
|
|
117f3be390 | ||
|
|
b81035f9bf | ||
|
|
8b38bcfbdd | ||
|
|
f47de1b71c | ||
|
|
61f2ea3511 | ||
|
|
9169beaa12 | ||
|
|
9f0041b196 | ||
|
|
6cc35815ba | ||
|
|
c88dfb4947 | ||
|
|
a27149aa70 | ||
|
|
8e9dea898f | ||
|
|
73d3c27c25 | ||
|
|
b9eeebdbea | ||
|
|
1f33aec825 | ||
|
|
2d891a4248 | ||
|
|
e533c87d08 | ||
|
|
c5f5fdb089 | ||
|
|
87fdb84202 | ||
|
|
d78ead2927 | ||
|
|
a4148102eb | ||
|
|
69affd43bb | ||
|
|
1dce731aa4 | ||
|
|
f3c0541ade | ||
|
|
eb40d3bda5 | ||
|
|
533c8f7ede | ||
|
|
b29339cd1a | ||
|
|
814bc4a3bf | ||
|
|
b05a5c7f54 | ||
|
|
eba71982c6 | ||
|
|
4a6c81b136 | ||
|
|
4f671f1e5b | ||
|
|
01a076b6ed | ||
|
|
f0f3e65bb0 | ||
|
|
2398fabffb | ||
|
|
a03c93b006 | ||
|
|
5c78c5ba2c | ||
|
|
c8d0ae6a9d | ||
|
|
3867e4bdca | ||
|
|
bdad46e96d | ||
|
|
d03199ccda | ||
|
|
0c4f22173e | ||
|
|
17bafc2b5e | ||
|
|
666aaf4428 | ||
|
|
618e093e1c | ||
|
|
2f865a5c7f | ||
|
|
aaf5084fd9 | ||
|
|
44c14ec4ce | ||
|
|
66882d9677 | ||
|
|
bc1d36ddea | ||
|
|
2d5ef19b3b | ||
|
|
a61016d2df | ||
|
|
f2f6b4c859 | ||
|
|
9f92d64641 | ||
|
|
bb71dcba91 | ||
|
|
23a38f76f9 | ||
|
|
9a8bda8b11 | ||
|
|
3c10cf5181 | ||
|
|
c5095e0a8d | ||
|
|
3486fe3dde | ||
|
|
56fe047e3a | ||
|
|
26b253d8ca | ||
|
|
3f85f1dd70 | ||
|
|
dad829dd91 | ||
|
|
e62c5a223c | ||
|
|
5fc7491121 | ||
|
|
9598455118 | ||
|
|
0e4b9753a0 | ||
|
|
2bbe3d788d | ||
|
|
cd731d7e40 | ||
|
|
e11e3d51c2 | ||
|
|
b19b78df5d | ||
|
|
1a061cbbcc | ||
|
|
9908c8be32 | ||
|
|
8522deab64 | ||
|
|
7e93521666 | ||
|
|
5aeb3a28fb | ||
|
|
b5db93726a | ||
|
|
20de8db41b | ||
|
|
5d73d65fdc | ||
|
|
8c750d368d | ||
|
|
8d328ace84 | ||
|
|
9d825f00aa | ||
|
|
c7b68507a8 | ||
|
|
970ccf2f4a | ||
|
|
479036a400 | ||
|
|
80d7ace714 | ||
|
|
a7d5736e14 | ||
|
|
b31be1043c | ||
|
|
6c4d27a842 | ||
|
|
fdab26ce56 | ||
|
|
8ead5eeb08 | ||
|
|
c4ebd3175d | ||
|
|
e593bc8a61 | ||
|
|
cb554bd5f7 | ||
|
|
dd54acfbd1 | ||
|
|
6783f04139 | ||
|
|
11eb4060d0 | ||
|
|
b5357acf98 | ||
|
|
f85f2f03d9 | ||
|
|
146a6cde68 | ||
|
|
65105df9ac | ||
|
|
9106ef215a | ||
|
|
38426bc5d7 | ||
|
|
79e8b5d71e | ||
|
|
6e954cfac0 | ||
|
|
041d33b2d3 | ||
|
|
1b5b0925ed | ||
|
|
cd0bc85b76 | ||
|
|
d4929164b6 | ||
|
|
7bc06861b5 | ||
|
|
3ccbed2cbb | ||
|
|
29c0b0f8eb | ||
|
|
7ff0753472 | ||
|
|
19328abb9e | ||
|
|
feb0863358 | ||
|
|
43e51e9a4a | ||
|
|
3b610d8910 | ||
|
|
63ec749c7a | ||
|
|
c8e6a78154 | ||
|
|
89b2b7fcc3 | ||
|
|
1b1c781426 | ||
|
|
5c8a888b77 | ||
|
|
257576b4d3 | ||
|
|
56f0a7780d | ||
|
|
faa47e8f3e | ||
|
|
b99d599bce | ||
|
|
92bc856f5b | ||
|
|
0490ca8145 | ||
|
|
c9b8f21f61 | ||
|
|
60caa80a84 | ||
|
|
477f85a71e | ||
|
|
b5aed5fb4f | ||
|
|
7d714ecfeb | ||
|
|
f3ff132e90 | ||
|
|
1912155b32 | ||
|
|
daa87c8c58 | ||
|
|
ad87832662 | ||
|
|
b8b0a5bf73 | ||
|
|
5a8b681e2f | ||
|
|
5a6a3f5599 | ||
|
|
bab422029b | ||
|
|
88fc5f9ed6 | ||
|
|
f3c7cfd799 | ||
|
|
231439d808 | ||
|
|
d6f80730a5 | ||
|
|
9cfee0398a | ||
|
|
aa5956a30b | ||
|
|
b9d68957e0 | ||
|
|
1b9f5c62fb | ||
|
|
a0a57ca94d | ||
|
|
271451b9df | ||
|
|
21458f26f2 | ||
|
|
c1b4d4d53e | ||
|
|
28d71dc2fe | ||
|
|
a10bbf60ac | ||
|
|
4e572b171f | ||
|
|
cc768c83aa | ||
|
|
035f3770d5 | ||
|
|
2d95754d49 | ||
|
|
47b1cee6d5 | ||
|
|
4ebd00bc06 | ||
|
|
66554ec4b6 | ||
|
|
bfc798c629 | ||
|
|
6b334b941a | ||
|
|
b06090a477 | ||
|
|
ca5fbb65e5 | ||
|
|
c2305f32f0 | ||
|
|
747446e2e4 | ||
|
|
250d2188b9 | ||
|
|
61c710fe20 | ||
|
|
0fd6f5b3b7 | ||
|
|
8a204c606a | ||
|
|
4401ea0c9a | ||
|
|
12ace54b2b | ||
|
|
f15cd73dff | ||
|
|
b39d7bb5a0 | ||
|
|
f1066a2b0d | ||
|
|
b04b555b38 | ||
|
|
f349eedc72 | ||
|
|
ee2f3c32eb | ||
|
|
0f99a1c7c2 | ||
|
|
95aba09cd6 | ||
|
|
811b9e3343 | ||
|
|
37b9823b36 | ||
|
|
555684afda | ||
|
|
0d5c6ac963 | ||
|
|
db67a6928c | ||
|
|
9c34ab5f9c | ||
|
|
f09074b79d | ||
|
|
a5842fbe3f | ||
|
|
8db654e63d | ||
|
|
60144c0dc7 | ||
|
|
830b5dfd71 | ||
|
|
12d81f0f3b | ||
|
|
8c835447c8 |
@@ -48,19 +48,19 @@ body:
|
||||
attributes:
|
||||
label: Mobile Device Model
|
||||
description: (Mobile users only) What mobile device are you playing on?
|
||||
placeholder: ex. iPhone 16, Galaxy S25, iPad 11th Gen
|
||||
placeholder: ex. iPhone 17, Galaxy S25, iPad 11th Gen
|
||||
|
||||
- type: input
|
||||
attributes:
|
||||
label: Mobile OS Version
|
||||
description: (Mobile users only) What version is your Operating System?
|
||||
placeholder: ex. iOS 18.5, Android 15, iPadOS 18.5
|
||||
placeholder: ex. iOS 26.2, Android 16, iPadOS 26.2
|
||||
|
||||
- type: input
|
||||
attributes:
|
||||
label: Version
|
||||
description: Which version are you playing on? The game version is in the bottom left corner of the main menu.
|
||||
placeholder: ex. 0.7.4
|
||||
placeholder: ex. 0.8.1
|
||||
validations:
|
||||
required: true
|
||||
|
||||
|
||||
@@ -36,7 +36,7 @@ body:
|
||||
attributes:
|
||||
label: Version
|
||||
description: Which version are you playing on? The game version is in the bottom left corner of the main menu.
|
||||
placeholder: ex. 0.7.4
|
||||
placeholder: ex. 0.8.1
|
||||
validations:
|
||||
required: true
|
||||
|
||||
|
||||
@@ -36,7 +36,7 @@ body:
|
||||
attributes:
|
||||
label: Version
|
||||
description: Which version are you compiling? The game version is in the bottom left corner of the main menu or in the project.hxp file.
|
||||
placeholder: ex. 0.7.4
|
||||
placeholder: ex. 0.8.1
|
||||
validations:
|
||||
required: true
|
||||
|
||||
|
||||
@@ -15,6 +15,7 @@ body:
|
||||
- label: I have read the [Contributing Guide](https://github.com/FunkinCrew/Funkin/blob/main/docs/CONTRIBUTING.md)
|
||||
- label: I have checked the Issues/Discussions pages to see if my issue has already been reported
|
||||
- label: I have properly titled my issue
|
||||
- label: I did not have any mods installed when I encountered this issue
|
||||
|
||||
- type: dropdown
|
||||
attributes:
|
||||
@@ -48,19 +49,19 @@ body:
|
||||
attributes:
|
||||
label: Mobile Device Model
|
||||
description: (Mobile users only) What mobile device are you playing on?
|
||||
placeholder: ex. iPhone 16, Galaxy S25, iPad 11th Gen
|
||||
placeholder: ex. iPhone 17, Galaxy S25, iPad 11th Gen
|
||||
|
||||
- type: input
|
||||
attributes:
|
||||
label: Mobile OS Version
|
||||
description: (Mobile users only) What version is your Operating System?
|
||||
placeholder: ex. iOS 18.5, Android 15, iPadOS 18.5
|
||||
placeholder: ex. iOS 26.2, Android 16, iPadOS 26.2
|
||||
|
||||
- type: input
|
||||
attributes:
|
||||
label: Version
|
||||
description: Which version are you playing on? The game version is in the bottom left corner of the main menu.
|
||||
placeholder: ex. 0.7.4
|
||||
placeholder: ex. 0.8.1
|
||||
validations:
|
||||
required: true
|
||||
|
||||
|
||||
@@ -35,14 +35,14 @@ jobs:
|
||||
docker:
|
||||
- '.github/workflows/build-game.yml'
|
||||
- '**/Dockerfile'
|
||||
- uses: actions/github-script@v7
|
||||
- uses: actions/github-script@v8
|
||||
id: should-trigger
|
||||
with:
|
||||
result-encoding: string
|
||||
script: |
|
||||
const { payload } = context
|
||||
const changes = ${{ steps.filter.outputs.changes }}
|
||||
const manual = payload.commits
|
||||
const manual = payload?.commits
|
||||
.some(c => c.message.toLowerCase().replace(/\W/g, " ").includes("docker rebuild"))
|
||||
|
||||
console.log({ payload, changes, manual, commits: payload.commits })
|
||||
@@ -167,6 +167,11 @@ jobs:
|
||||
cd .haxelib/hxcpp/git/tools/hxcpp
|
||||
haxe compile.hxml
|
||||
cd ../../../../..
|
||||
- name: Populate .env
|
||||
run: |
|
||||
touch .env
|
||||
grep API_NG_APP_ID .env || echo API_NG_APP_ID=${{ secrets.API_NG_APP_ID }} >> .env
|
||||
grep API_NG_ENC_KEY .env || echo API_NG_ENC_KEY=${{ secrets.API_NG_ENC_KEY }} >> .env
|
||||
- name: Build game (windows)
|
||||
if: ${{ matrix.target == 'windows' }}
|
||||
run: |
|
||||
@@ -246,6 +251,11 @@ jobs:
|
||||
path: /usr/share/hxcpp
|
||||
key: haxe-hxcpp-${{ runner.os }}-${{ github.ref_name }}-${{ github.sha }}
|
||||
restore-keys: haxe-hxcpp-${{ runner.os }}-${{ github.ref_name }}
|
||||
- name: Populate .env
|
||||
run: |
|
||||
touch .env
|
||||
grep API_NG_APP_ID .env || echo API_NG_APP_ID=${{ secrets.API_NG_APP_ID }} >> .env
|
||||
grep API_NG_ENC_KEY .env || echo API_NG_ENC_KEY=${{ secrets.API_NG_ENC_KEY }} >> .env
|
||||
- name: Build game
|
||||
run: |
|
||||
haxelib run lime build ${{ matrix.target }} -v -release --times $BUILD_DEFINES
|
||||
|
||||
+5
-1
@@ -13,10 +13,14 @@ shitAudio/
|
||||
.swp
|
||||
NewgroundsCredentials.hx
|
||||
|
||||
# Exclude IntelliJ IDEA stuff
|
||||
.idea/
|
||||
*.iml
|
||||
|
||||
# Mobile signing stuff
|
||||
.apple/
|
||||
.env
|
||||
key.keystore
|
||||
*.keystore
|
||||
|
||||
# Exclude JS stuff
|
||||
node_modules/
|
||||
|
||||
@@ -4,3 +4,4 @@
|
||||
[submodule "art"]
|
||||
path = art
|
||||
url = https://github.com/FunkinCrew/Funkin.art
|
||||
shallow = true
|
||||
|
||||
Vendored
+25
-87
@@ -1,31 +1,22 @@
|
||||
{
|
||||
"[haxe]": {
|
||||
// Automatically keep Haxe files formatted.
|
||||
"editor.formatOnSave": true,
|
||||
"editor.formatOnPaste": true,
|
||||
"editor.codeActionsOnSave": {
|
||||
"source.organizeImports": "never"
|
||||
},
|
||||
"editor.codeActionsOnSave": { "source.organizeImports": "never" },
|
||||
"editor.defaultFormatter": "nadako.vshaxe",
|
||||
"editor.tabSize": 2
|
||||
},
|
||||
|
||||
"[json]": {
|
||||
// Automatically keep JSON files formatted.
|
||||
"editor.formatOnSave": true,
|
||||
"editor.formatOnPaste": true,
|
||||
"editor.defaultFormatter": "esbenp.prettier-vscode"
|
||||
},
|
||||
|
||||
"[jsonc]": {
|
||||
// Automatically keep JSONC files formatted.
|
||||
"editor.formatOnSave": true,
|
||||
"editor.formatOnPaste": true,
|
||||
"editor.defaultFormatter": "esbenp.prettier-vscode"
|
||||
},
|
||||
"prettier.tabWidth": 2,
|
||||
|
||||
// XML formatting style configuration
|
||||
// XML Formatting
|
||||
"xml.format.enabled": true,
|
||||
"xml.format.legacy": false,
|
||||
"xml.format.emptyElements": "collapse",
|
||||
@@ -33,7 +24,7 @@
|
||||
"xml.format.enforceQuoteStyle": "preferred",
|
||||
"xml.format.preserveAttributeLineBreaks": false,
|
||||
"xml.format.preservedNewlines": 0,
|
||||
"xml.format.splitAttributes": false,
|
||||
"xml.format.splitAttributes": "preserve",
|
||||
"xml.format.joinCDATALines": true,
|
||||
"xml.format.preserveEmptyContent": false,
|
||||
"xml.format.joinCommentLines": false,
|
||||
@@ -56,26 +47,19 @@
|
||||
"xml.format.maxLineWidth": 0,
|
||||
"xml.format.grammarAwareFormatting": true,
|
||||
|
||||
// Generic file formatting style configuration
|
||||
// General formatting
|
||||
"files.insertFinalNewline": true,
|
||||
"files.trimFinalNewlines": false,
|
||||
"files.trimTrailingWhitespace": true,
|
||||
|
||||
// Automatically detect indentation.
|
||||
"editor.detectIndentation": true,
|
||||
"editor.insertSpaces": true,
|
||||
"editor.tabSize": 2,
|
||||
|
||||
// Automatically enforce Linux style line endings.
|
||||
"prettier.tabWidth": 2,
|
||||
"files.eol": "\n",
|
||||
|
||||
"haxe.displayPort": "auto",
|
||||
"haxe.enableCompilationServer": true,
|
||||
"haxe.enableServerView": true,
|
||||
"haxe.displayServer": {
|
||||
"arguments": ["-v"]
|
||||
},
|
||||
// Fix file associations for HScript.
|
||||
"haxe.displayServer": { "arguments": ["-v"] },
|
||||
"files.associations": {
|
||||
"*.hxp": "haxe",
|
||||
"*.hscript": "haxe",
|
||||
@@ -84,14 +68,27 @@
|
||||
"*.hxc": "haxe"
|
||||
},
|
||||
"projectManager.git.baseFolders": ["./"],
|
||||
|
||||
"haxecheckstyle.sourceFolders": ["src", "source"],
|
||||
"haxecheckstyle.externalSourceRoots": [],
|
||||
"haxecheckstyle.configurationFile": "checkstyle.json",
|
||||
"haxecheckstyle.codeSimilarityBufferSize": 100,
|
||||
|
||||
"lime.projectFile": "project.hxp",
|
||||
|
||||
"lime.targets": [
|
||||
{ "name": "windows", "enabled": true, "label": "Windows" },
|
||||
{ "name": "mac", "enabled": true, "label": "macOS" },
|
||||
{ "name": "linux", "enabled": true, "label": "Linux" },
|
||||
{ "name": "html5", "enabled": true, "label": "HTML5" },
|
||||
{ "name": "android", "enabled": true, "label": "Android" },
|
||||
{ "name": "ios", "enabled": true, "label": "iOS" },
|
||||
// Disabled targets
|
||||
{ "name": "hl", "enabled": false, "label": "HashLink" },
|
||||
{ "name": "air", "enabled": false },
|
||||
{ "name": "electron", "enabled": false },
|
||||
{ "name": "flash", "enabled": false },
|
||||
{ "name": "neko", "enabled": false },
|
||||
{ "name": "tvos", "enabled": false }
|
||||
],
|
||||
"lime.defaultTargetConfiguration": "Windows / Debug",
|
||||
"lime.targetConfigurations": [
|
||||
{
|
||||
"label": "Windows / Debug (Discord)",
|
||||
@@ -103,21 +100,11 @@
|
||||
"target": "windows",
|
||||
"args": ["-debug", "-DANIMATE", "-DFEATURE_DEBUG_FUNCTIONS"]
|
||||
},
|
||||
{
|
||||
"label": "HashLink / Debug (FlxAnimate Test)",
|
||||
"target": "hl",
|
||||
"args": ["-debug", "-DANIMATE"]
|
||||
},
|
||||
{
|
||||
"label": "Windows / Debug (Straight to Freeplay)",
|
||||
"target": "windows",
|
||||
"args": ["-debug", "-DFREEPLAY", "-DFEATURE_DEBUG_FUNCTIONS"]
|
||||
},
|
||||
{
|
||||
"label": "HashLink / Debug (Straight to Freeplay)",
|
||||
"target": "hl",
|
||||
"args": ["-debug", "-DFREEPLAY"]
|
||||
},
|
||||
{
|
||||
"label": "Windows / Debug (Straight to Play - Bopeebo Normal)",
|
||||
"target": "windows",
|
||||
@@ -132,26 +119,6 @@
|
||||
"target": "windows",
|
||||
"args": ["-debug", "-DSONG=2hot", "-DFEATURE_DEBUG_FUNCTIONS"]
|
||||
},
|
||||
{
|
||||
"label": "HashLink / Debug (Straight to Play - Bopeebo Normal)",
|
||||
"target": "hl",
|
||||
"args": ["-debug", "-DSONG=bopeebo -DDIFFICULTY=normal"]
|
||||
},
|
||||
{
|
||||
"label": "Windows / Debug (Conversation Test)",
|
||||
"target": "windows",
|
||||
"args": ["-debug", "-DDIALOGUE", "-DFEATURE_DEBUG_FUNCTIONS"]
|
||||
},
|
||||
{
|
||||
"label": "HashLink / Debug (Conversation Test)",
|
||||
"target": "hl",
|
||||
"args": ["-debug", "-DDIALOGUE"]
|
||||
},
|
||||
{
|
||||
"label": "Windows / Debug (Results Screen Test)",
|
||||
"target": "windows",
|
||||
"args": ["-debug", "-DRESULTS"]
|
||||
},
|
||||
{
|
||||
"label": "Windows / Debug (Straight to Stage Editor)",
|
||||
"target": "windows",
|
||||
@@ -172,36 +139,16 @@
|
||||
"target": "windows",
|
||||
"args": ["-debug", "-DHXVLC_LOGGING", "-DFEATURE_DEBUG_FUNCTIONS"]
|
||||
},
|
||||
{
|
||||
"label": "HashLink / Debug (Straight to Animation Editor)",
|
||||
"target": "hl",
|
||||
"args": ["-debug", "-DANIMDEBUG"]
|
||||
},
|
||||
{
|
||||
"label": "Windows / Debug (Latency Test)",
|
||||
"target": "windows",
|
||||
"args": ["-debug", "-DLATENCY", "-DFEATURE_DEBUG_FUNCTIONS"]
|
||||
},
|
||||
{
|
||||
"label": "HashLink / Debug (Latency Test)",
|
||||
"target": "hl",
|
||||
"args": ["-debug", "-DLATENCY"]
|
||||
},
|
||||
{
|
||||
"label": "Windows / Debug (Waveform Test)",
|
||||
"target": "windows",
|
||||
"args": ["-debug", "-DWAVEFORM", "-DFEATURE_DEBUG_FUNCTIONS"]
|
||||
},
|
||||
{
|
||||
"label": "Windows / Release (GitHub Actions)",
|
||||
"target": "windows",
|
||||
"args": ["-release", "-DGITHUB_BUILD"]
|
||||
},
|
||||
{
|
||||
"label": "HashLink / Debug (Waveform Test)",
|
||||
"target": "hl",
|
||||
"args": ["-debug", "-DWAVEFORM"]
|
||||
},
|
||||
{
|
||||
"label": "HTML5 / Debug (Watch)",
|
||||
"target": "html5",
|
||||
@@ -209,10 +156,7 @@
|
||||
}
|
||||
],
|
||||
"lime.buildTypes": [
|
||||
{
|
||||
"label": "Debug",
|
||||
"args": ["-debug", "-DFEATURE_DEBUG_FUNCTIONS"]
|
||||
},
|
||||
{ "label": "Debug", "args": ["-debug", "-DFEATURE_DEBUG_FUNCTIONS"] },
|
||||
{
|
||||
"label": "Debug (Unlock Everything)",
|
||||
"args": ["-debug", "-DUNLOCK_EVERYTHING"]
|
||||
@@ -225,10 +169,7 @@
|
||||
"label": "Debug (Straight to Chart Editor)",
|
||||
"args": ["-debug", "-DCHARTING", "-DFEATURE_DEBUG_FUNCTIONS"]
|
||||
},
|
||||
{
|
||||
"label": "Release",
|
||||
"args": ["-release"]
|
||||
},
|
||||
{ "label": "Release", "args": ["-release"] },
|
||||
{
|
||||
"label": "Release (GitHub Actions)",
|
||||
"args": ["-release", "-DGITHUB_BUILD"]
|
||||
@@ -244,9 +185,6 @@
|
||||
],
|
||||
"vscord.app.privacyMode.enable": true,
|
||||
"json.schemas": [
|
||||
{
|
||||
"fileMatch": ["/hmm.json"],
|
||||
"url": "./.vscode/schema/hmm.json"
|
||||
}
|
||||
{ "fileMatch": ["/hmm.json"], "url": "./.vscode/schema/hmm.json" }
|
||||
]
|
||||
}
|
||||
|
||||
+281
-1
@@ -4,6 +4,286 @@ All notable changes will be documented in this file.
|
||||
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
|
||||
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
||||
|
||||
## [0.8.1] - 2026-01-04
|
||||
|
||||
### Added
|
||||
|
||||
- Added a new "Non-scoreable" notekind to the Chart Editor that doesn't affect scores and ranks, or play miss animations.
|
||||
|
||||
### Changed
|
||||
|
||||
- Changed the order in which events are dispatched in PlayState. ([16f3e03](https://github.com/FunkinCrew/Funkin/commit/16f3e038d80c37d016c9d2b6cdd1012c94ef88b9)) - by @ComedyLost in [#6606](https://github.com/FunkinCrew/Funkin/pull/6606)
|
||||
|
||||
### Fixed
|
||||
|
||||
- The last note of SPAGHETTI no longer counts toward scoring, allowing players to earn Perfect ranks.
|
||||
|
||||
|
||||
|
||||
## [0.8.0] - 2025-12-31
|
||||
|
||||
The LE SSERAFIM collab update!
|
||||
|
||||
### Added
|
||||
|
||||
- Added a new playable song: SPAGHETTI (feat. j-hope of BTS) (Clean ver.) by LE SSERAFIM (feat. j-hope)!
|
||||
- Try it out from either Story Mode or the Freeplay Menu!
|
||||
- Added subtitles for various cutscenes and songs throughout the game.
|
||||
- This can be toggled in the Preferences menu.
|
||||
- [MOBILE] Added support for Newgrounds logins and saving/loading from the cloud.
|
||||
- [MOBILE] Added support for opening FNFC chart files through the filesystem.
|
||||
- [MOBILE] Added haptics for tapping to begin on the Title Screen.
|
||||
- [MOBILE] Added the back button to the Character Select menu.
|
||||
- Added new function callbacks for Module scripts in Freeplay and Character Select.
|
||||
- Added `getOtherNotes()` function for counting notes that are not of a certain notekind.
|
||||
- Implemented stacked patches to JSON merging.
|
||||
- This allows mods to add new variations to existing songs without any extra fuss. See the [Code Cookbook](https://thekade.net/funkin-cookbook/) for more information.
|
||||
- Added the ability to import Osu!Mania and StepMania charts in the Chart Editor.
|
||||
- Added Move Difficulty and Clone Difficulty buttons to the Chart Editor Difficulty window.
|
||||
- Added a checkbox to hide vocal waveforms in the Chart Editor.
|
||||
- Added support for SRT subtitles that can display in game. ([a7fc70a](https://github.com/FunkinCrew/Funkin/commit/a7fc70a5c9cbd811b48cf2eb2f6c221814c2a3cd)) - by @PurSnake in [#6206](https://github.com/FunkinCrew/Funkin/pull/6206)
|
||||
- Added support for various render types and scripting for the Freeplay DJ. ([8b38bcf](https://github.com/FunkinCrew/Funkin/commit/8b38bcfbdd9ef1b135046022b1b93eec5dedbd28)) - by @PurSnake and @AbnormalPoof in [#5698](https://github.com/FunkinCrew/Funkin/pull/5698)
|
||||
- Level props can now have a flipX field. ([4961d4e](https://github.com/FunkinCrew/Funkin/commit/4961d4ee992445a487587dd3a7f3ab965eb4f5cf)) - by @Starexify in [#6218](https://github.com/FunkinCrew/Funkin/pull/6218)
|
||||
- [LINUX] Added support for Feral Gamemode. ([555ec09](https://github.com/FunkinCrew/Funkin/commit/555ec09ef5be3c1d1966eeaa0275835e59aa5709)) - by @Noobz4Life in [#2473](https://github.com/FunkinCrew/Funkin/pull/2473)
|
||||
- Added support for importing charts from Osu!Mania. ([0be42bf](https://github.com/FunkinCrew/Funkin/commit/0be42bf0475975d3195d4da85e653899df72a28c)) - by @FuroYT in [#6155](https://github.com/FunkinCrew/Funkin/pull/6155)
|
||||
- Added a "No Animation" notekind to the Chart Editor. ([618e093](https://github.com/FunkinCrew/Funkin/commit/618e093e1ce2d4f45fa4da5d34ed84e58394126e)) - by @Eviethecoder in [#4036](https://github.com/FunkinCrew/Funkin/pull/4036)
|
||||
- Added a way to adjust time signature/BPM changes through the Metadata window in the Chart Editor. ([b05a5c7](https://github.com/FunkinCrew/Funkin/commit/b05a5c7f54f9b41d63081837dd2c7392203b4530)) - by @Keoiki and @Lasercar in [#4770](https://github.com/FunkinCrew/Funkin/pull/4770)
|
||||
- Added character preview windows for the player and opponent in the Chart Editor. ([9f59231](https://github.com/FunkinCrew/Funkin/commit/9f59231d58a889e2c9a0f8e7e30b2e1c370e0d9a)) - by @PurSnake and @ComedyLost in [#6221](https://github.com/FunkinCrew/Funkin/pull/6221)
|
||||
- Added the option to view the Results Screen after Chart Editor playtests. ([eeba677](https://github.com/FunkinCrew/Funkin/commit/eeba677da9fac75cd34481ead01dc05dfa074bce)) - by @Lasercar in [#4087](https://github.com/FunkinCrew/Funkin/pull/4087)
|
||||
- Added the option to carry over Chart Editor volume levels into a playtest. ([4dd87e7](https://github.com/FunkinCrew/Funkin/commit/4dd87e7308789bae9a9e903bb15737e8149012f7)) - by @KoloInDaCrib in [#6302](https://github.com/FunkinCrew/Funkin/pull/6302)
|
||||
- Added the ability to time-travel during a Chart Editor playtest in release builds with PgUp and PgDown. ([4a00429](https://github.com/FunkinCrew/Funkin/commit/4a00429a57f03f60159a4e2e4f531bd6fa118658)) - by @AbnormalPoof in [#4209](https://github.com/FunkinCrew/Funkin/pull/4209)
|
||||
- Added the Rift of the Necrodancer collab trailer to the pool of videos in Attract Mode.
|
||||
|
||||
### Changed
|
||||
|
||||
- The mod API version is now 0.8.0. Please update your mods to ensure they work.
|
||||
- Made some improvements to Polymod:
|
||||
- Scripts can no longer accidentally redefine variables defined in their super class.
|
||||
- `Math` and `Std` can now properly be used in static functions. (Thanks NotHyper-474!)
|
||||
- Significantly improved the speed of retrieving modded assets, improving the performance of the game with a lot of mods installed. (Thanks PurSnake!)
|
||||
- Replaced `FlxAnimate` with `flixel-animate` to overhaul texture atlas handling:
|
||||
- Significantly improved performance in the Character Select Menu.
|
||||
- Pico (holding Nene)’s game over confirm animation now plays properly.
|
||||
- The camera now properly focuses on atlas characters after restarting the song.
|
||||
- Atlas characters now properly display the rimlight shader.
|
||||
- Implemented multiple new settings for atlas sprites.
|
||||
- Fixed a ton of bugs across various menus.
|
||||
- Re-exported many assets to improve memory usage and performance throughout the game.
|
||||
- [MOBILE] Music from external sources now pauses when opening the game.
|
||||
- [MOBILE] The game is now named “FNF” on the Home Screen.
|
||||
- [DESKTOP] Reworked window resizing behavior to behave more consistently.
|
||||
- Made it easier to implement custom Pause Menu substates.
|
||||
- Pressing F6 (rebindable) now cycles through debug display modes.
|
||||
- Boyfriend and Girlfriend’s scared animations in Week 2 now last longer.
|
||||
- Added “Change difficulty” to the list of undo-able actions in the Chart Editor.
|
||||
- Changed the default easing type for the Chart Editor Focus Camera event from Linear to Classic.
|
||||
- The game now displays a user-friendly crash message when attempting to play without a graphics card. ([5270353](https://github.com/FunkinCrew/Funkin/commit/52703536a62fc004fb7be0457da89705f812364f)) - by @ACrazyTown in [#6160](https://github.com/FunkinCrew/Funkin/pull/6160)
|
||||
- Adjusted strumline confirm animations when hitting notes to feel more responsive. ([62a3f73](https://github.com/FunkinCrew/Funkin/commit/62a3f73de9d653d4ca5a7d7f4c895fc9fd1a4be7)) - by @gamerbross in [#6261](https://github.com/FunkinCrew/Funkin/pull/6261)
|
||||
- The Lag Adjustment menu now displays a note splash when hitting perfectly. ([342c3cd](https://github.com/FunkinCrew/Funkin/commit/342c3cd7d6f33b3babb70f3d1fb604b0bdb90464)) - by @ExtraCode75 in [#6602](https://github.com/FunkinCrew/Funkin/pull/6602)
|
||||
- Freeplay song previews now fade out before restarting. ([aaf5084](https://github.com/FunkinCrew/Funkin/commit/aaf5084fd9e9c2d3b7e823767a93db82ce908d86)) - by @JVNpixels in [#6094](https://github.com/FunkinCrew/Funkin/pull/6094)
|
||||
- The game's volume is now reduced when unfocused without pausing. ([3040692](https://github.com/FunkinCrew/Funkin/commit/30406928522f88762eab4bbf571fedf1424f7569)) - by @PurSnake in [#6250](https://github.com/FunkinCrew/Funkin/pull/6250)
|
||||
- Optimized the way the windows in Week 3 are handled. ([70d433d](https://github.com/FunkinCrew/funkin.assets/commit/70d433de3027badb39027d295d1b18d20a130741)) - by @CrusherNotDrip in [funkin.assets#291](https://github.com/FunkinCrew/funkin.assets/pull/291)
|
||||
- Script create events now also run after hot-reloading with F5, rather than only during initialization. ([2f865a5](https://github.com/FunkinCrew/Funkin/commit/2f865a5c7f4d8712f1b1e0bc70cb910f4fed08dc)) - by @VirtuGuy in [#6084](https://github.com/FunkinCrew/Funkin/pull/6084)
|
||||
- Hot-reloading with F5 during a Chart Editor playtest no longer returns to the Chart Editor. ([c1988f1](https://github.com/FunkinCrew/Funkin/commit/c1988f1858344a78a82bf335774d986703cac942)) - by @KoloInDaCrib in [#6275](https://github.com/FunkinCrew/Funkin/pull/6275)
|
||||
- Opening the Chart Editor during a song now places the playhead at the current song position. ([71b4a58](https://github.com/FunkinCrew/Funkin/commit/71b4a58cab821c3b5a530e3a2be6d73966faa2a6)) - by @KoloInDaCrib in [#6210](https://github.com/FunkinCrew/Funkin/pull/6210)
|
||||
- The Easing property for Chart Editor events is now split into two separate dropdowns: type and direction. ([d1d77dd](https://github.com/FunkinCrew/Funkin/commit/d1d77dd85c3daed22ee954353fa41e81279ced3c)) - by @PurSnake in [#5612](https://github.com/FunkinCrew/Funkin/pull/5612)
|
||||
- The Chart Editor Events window now supports collapsible groups. ([9f6879e](https://github.com/FunkinCrew/Funkin/commit/9f6879ecc98e725afa3de8f9b02755047b353cd7)) - by @KoloInDaCrib in [#6554](https://github.com/FunkinCrew/Funkin/pull/6554)
|
||||
- Chart Editor BPM fields now support up to 3 decimal places. ([c863ef3](https://github.com/FunkinCrew/funkin.assets/commit/c863ef378569ce0d6b0804029e5d395b1c321cbd)) - by @roma-perec-bp in [funkin.assets#273](https://github.com/FunkinCrew/funkin.assets/pull/273)
|
||||
- Long difficulty and variation names are now shortened in the Chart Editor playbar. ([2c4167b](https://github.com/FunkinCrew/Funkin/commit/2c4167bc11aec92693494816735063c381279b3e)) - by @Lasercar in [#6150](https://github.com/FunkinCrew/Funkin/pull/6150)
|
||||
- The remaining time in the playbar now also shows a millisecond value in the Chart Editor. ([2d5ef19](https://github.com/FunkinCrew/Funkin/commit/2d5ef19b3b3c55db1eb70506b00d4f820e7ccb20)) - by @JVNpixels in [#6089](https://github.com/FunkinCrew/Funkin/pull/6089)
|
||||
- Added more hotkeys to the Chart Editor user guide. ([7d0d9eb](https://github.com/FunkinCrew/funkin.assets/commit/7d0d9ebf7ea60e04d09c9b140772c36493dc6291)) - by @afreetoplaynoob in [funkin.assets#294](https://github.com/FunkinCrew/funkin.assets/pull/294)
|
||||
- The Stage Editor can now be opened from Freeplay to the selected capsule's stage. ([1ff6a16](https://github.com/FunkinCrew/Funkin/commit/1ff6a1656cebdfe415768ffe38bd43b37c663eda)) - by @Lasercar in [#5264](https://github.com/FunkinCrew/Funkin/pull/5264)
|
||||
- The Stage Editor now saves backups upon quitting or crashing. ([5326c40](https://github.com/FunkinCrew/Funkin/commit/5326c40347b1548a682ef1f79eeb4a70618256ff)) - by @Lasercar in [#6190](https://github.com/FunkinCrew/Funkin/pull/6190)
|
||||
- Stage Editor backups are now saved in the same folder as Chart Editor backups. ([4f8da59](https://github.com/FunkinCrew/Funkin/commit/4f8da595777c4b7fe6ddf6536ffa29a50c23dcb3)) - by @CrusherNotDrip in [#6297](https://github.com/FunkinCrew/Funkin/pull/6297)
|
||||
|
||||
### Fixed
|
||||
|
||||
- Fixed some issues with Polymod:
|
||||
- Modded assets now properly go in their respective asset libraries. (Thanks NotHyper-474!)
|
||||
- Static functions and fields from imported scripted classes can now be used.
|
||||
- Properties can now be used in a static context.
|
||||
- Local variables in a function no longer get dropped if said function calls its own `scriptCall` function. (Thanks KoloInDaCrib!)
|
||||
- Local functions now properly report the class name.
|
||||
- [DESKTOP] Switching audio devices no longer breaks audio processing.
|
||||
- [HTML5] The Character Select Menu no longer crashes on Firefox.
|
||||
- Pressing Escape and an arrow key with the Freeplay alternate instrumental selector open no longer crashes the game.
|
||||
- Hot reloading with F5 on the Game Over screen no longer crashes the game.
|
||||
- [HTML5] Restarting a video cutscene now properly plays the video again.
|
||||
- [iOS] The navigation bar is no longer unlocked after watching an ad.
|
||||
- [iOS] The “larger text” accessibility setting no longer causes the game to not fill the whole screen.
|
||||
- [iOS] The game now displays error popups.
|
||||
- [ANDROID] Keyboard detection and strumline positioning now work properly.
|
||||
- [ANDROID] Taking a screenshot no longer pauses the game for a moment.
|
||||
- The Title Screen no longer switches to Attract Mode while the Girlfriend’s Ringtone easter egg is playing.
|
||||
- Holding Escape in the Main Menu no longer quickly closes the game.
|
||||
- [MOBILE] Scrolling to another Week is no longer possible after entering a Story Week.
|
||||
- [MOBILE] Long Freeplay capsule song names now properly scroll sideways.
|
||||
- Exiting Character Select no longer allows mashing inputs in Freeplay.
|
||||
- Controller inputs now register in the Character Select Menu.
|
||||
- The player character in Character Select no longer slides in upon entering.
|
||||
- The Character Select cursor no longer flies in from the top left corner.
|
||||
- The Character Select cursor no longer continues to move without input after unfocusing the game.
|
||||
- Video cutscenes no longer start at maximum volume for a split second.
|
||||
- The input system now properly handles alternating between two keybinds bound to the same note.
|
||||
- The camera now bops independently from the framerate during songs.
|
||||
- The Pause Menu music now properly pauses when unfocusing with Pause on Unfocus enabled.
|
||||
- Changing to the same difficulty as the current one during a Story Week no longer resets the Week score.
|
||||
- More characters including parentheses now display properly in the Results Screen.
|
||||
- [MOBILE] The rimlight for Girlfriend (Tankman Stickup) now renders properly.
|
||||
- Character sprites with high global offsets no longer disappear near the edge of the screen.
|
||||
- Fixed positions for Pico (Pixel) and Nene (Pixel), and removed the School (Pico) stage.
|
||||
- Tweaked a few charts.
|
||||
- [macOS] Fixed a crash related to notifications in the Chart Editor.
|
||||
- [Windows] Opening the File Explorer no longer sometimes hangs in the Chart Editor.
|
||||
- Pressing F4 to exit a debug editor before a tooltip appears no longer crashes the game.
|
||||
- Changing the theme in the Chart Editor more than once no longer crashes the game.
|
||||
- Unplugging a controller no longer crashes the Chart Editor.
|
||||
- The Chart Editor measure ticks no longer cause a memory leak.
|
||||
- Selecting multiple events by Ctrl-clicking in the Chart Editor no longer converts one into the other.
|
||||
- Event tooltips now display all values including default ones.
|
||||
- Pause on Unfocus now properly applies to Chart Editor playtests.
|
||||
- Unfocusing while the Chart Editor is playing audio now pauses the playback.
|
||||
- Fixed a memory leak with the Freeplay backing card scrolling text. ([67846d6](https://github.com/FunkinCrew/Funkin/commit/67846d6c4bfd2f42e002fa129a51772f0717329d)) - by @FuroYT in [#5963](https://github.com/FunkinCrew/Funkin/pull/5963)
|
||||
- Using a static pixel icon in Character Select no longer crashes the game. ([96f8ee8](https://github.com/FunkinCrew/Funkin/commit/96f8ee85e0c2281834b7334b87d1862003e5fa05)) - by @VirtuGuy in [#6171](https://github.com/FunkinCrew/Funkin/pull/6171)
|
||||
- A-Bot's visualizer no longer causes a memory leak during gameplay or in Character Select. ([6607645](https://github.com/FunkinCrew/Funkin/commit/6607645786ed8611436fa5f92a35027c2a64f1d0)) - by @FuroYT in [#5908](https://github.com/FunkinCrew/Funkin/pull/5908)
|
||||
- The Credits menu no longer crashes when not using hardcoded credits. ([4366fbe](https://github.com/FunkinCrew/Funkin/commit/4366fbe51274438ad2bc190afcca42e37987386d)) - by @sphis-sinco in [#5982](https://github.com/FunkinCrew/Funkin/pull/5982)
|
||||
- The game no longer lag spikes when Darnell throws his can in 2hot. ([a0bc6d6](https://github.com/FunkinCrew/funkin.assets/commit/a0bc6d61fde5799a7fc619b7fdea691b0d8bae05)) - by @KoloInDaCrib in [funkin.assets#313](https://github.com/FunkinCrew/funkin.assets/pull/313)
|
||||
- Hold note covers no longer get permanently stuck playing after a lag spike. ([66882d9](https://github.com/FunkinCrew/Funkin/commit/66882d9677268396320ee003478a3e2627655755)) - by @KoloInDaCrib in [#6066](https://github.com/FunkinCrew/Funkin/pull/6066)
|
||||
- Scrolling through Freeplay songs for the first time no longer stutters. ([cc486ad](https://github.com/FunkinCrew/Funkin/commit/cc486ad94b5e9806d18fb65165663fea0ce0d7b4)) - by @mikolka9144 in [#4851](https://github.com/FunkinCrew/Funkin/pull/4851)
|
||||
- Re-implemented an error that appears when a video file does not exist. ([2ad5933](https://github.com/FunkinCrew/Funkin/commit/2ad593341f8bea8384646f4803dc8795a9efe32a)) - by @TechnikTil in [#6253](https://github.com/FunkinCrew/Funkin/pull/6253)
|
||||
- The game now properly parses .json files beginning with a bracket. ([c7367ae](https://github.com/FunkinCrew/Funkin/commit/c7367ae33d7e11f57b821f7d5fbff39a3edc1604)) - by @NotHyper-474 in [#6164](https://github.com/FunkinCrew/Funkin/pull/6164)
|
||||
- The Newgrounds logo no longer covers the intro text. ([28883fb](https://github.com/FunkinCrew/Funkin/commit/28883fb9ed025abf9f9aee2621f365cd6f4d8b6c)) - by @hucks5 in [#6196](https://github.com/FunkinCrew/Funkin/pull/6196)
|
||||
- Exiting from the Save Data Options menu no longer exits the Options Menu as well. ([26b253d](https://github.com/FunkinCrew/Funkin/commit/26b253d8cac97f0dcd09a5ef233f7058cd26008e)) - by @VirtuGuy in [#6036](https://github.com/FunkinCrew/Funkin/pull/6036)
|
||||
- Numeric values in the Options Menu are now rounded to prevent near-zero precision errors. ([934bce7](https://github.com/FunkinCrew/Funkin/commit/934bce7bdc5780e09623d845eda97a27c0ad0ecf)) - by @Starexify in [#6225](https://github.com/FunkinCrew/Funkin/pull/6225)
|
||||
- The Test/Offset Calibration menu no longer starts playing after exiting the Lag Adjustment menu. ([23a38f7](https://github.com/FunkinCrew/Funkin/commit/23a38f76f9c79c5fab5b9af5e2d98e51597e05a8)) - by @VioletSnowLeopard in [#6088](https://github.com/FunkinCrew/Funkin/pull/6088)
|
||||
- [MOBILE] The Back button now fades out when entering a Week in Story Mode. ([1ebce74](https://github.com/FunkinCrew/Funkin/commit/1ebce748d4e3306b4464a7bf82fab56bcb427a62)) - by @VirtuGuy in [#6255](https://github.com/FunkinCrew/Funkin/pull/6255)
|
||||
- The Freeplay difficulty can now only be changed in one direction at a time. ([95af46c](https://github.com/FunkinCrew/Funkin/commit/95af46cbf72291d9d3226df91ed17f1aa3d3839f)) - by @Starexify in [#6262](https://github.com/FunkinCrew/Funkin/pull/6262)
|
||||
- Pico's backing card now renders properly in widescreen aspect ratios. ([aa29b3d](https://github.com/FunkinCrew/funkin.assets/commit/aa29b3dcc1ee2f8e89d2622c480b2e997410d57d)) - by @VirtuGuy in [funkin.assets#297](https://github.com/FunkinCrew/funkin.assets/pull/297)
|
||||
- The album titles in Freeplay are now positioned consistently. ([ed8cca4](https://github.com/FunkinCrew/funkin.assets/commit/ed8cca4694a098ae6aa56c4d2a231587edb9688c)) - by @Donothan73 in [funkin.assets#257](https://github.com/FunkinCrew/funkin.assets/pull/257)
|
||||
- Health icons now bop independently from the framerate. ([8e6fa51](https://github.com/FunkinCrew/Funkin/commit/8e6fa51c27ceaa830efec06bb8a52e281a008255)) - by @PurSnake in [#6035](https://github.com/FunkinCrew/Funkin/pull/6035)
|
||||
- The note resetting animation on song restart is now consistent between Downscroll and Upscroll. ([7b8d657](https://github.com/FunkinCrew/Funkin/commit/7b8d6577eddba568f20c3d23663dded054699090)) - by @FuroYT in [#6220](https://github.com/FunkinCrew/Funkin/pull/6220)
|
||||
- Offsets are now properly applied to stage props. ([33a020a](https://github.com/FunkinCrew/Funkin/commit/33a020a34bb220f56c0ff9fbad8688e10ca5b44f)) - by @Starexify in [#6224](https://github.com/FunkinCrew/Funkin/pull/6224)
|
||||
- Notestyles now accept offset values. ([193c443](https://github.com/FunkinCrew/Funkin/commit/193c443bc47c1a7d1481d280a1d0674b920a323c)) - by @NebulaStellaNova in [#6326](https://github.com/FunkinCrew/Funkin/pull/6326)
|
||||
- The path for a notestyle's countdown sound is now properly loaded as a sound instead of an image. ([dd5ea77](https://github.com/FunkinCrew/Funkin/commit/dd5ea7748b3b1878139998c209aa06db0fa05e0b)) - by @gamerbross in [#6232](https://github.com/FunkinCrew/Funkin/pull/6232)
|
||||
- The Pause Menu theme no longer plays at maximum volume for a split second when pausing. ([6ba44d6](https://github.com/FunkinCrew/Funkin/commit/6ba44d6cb0c71deb29eb23b82b61f58b2e95193e)) - by @ComedyLost in [#6334](https://github.com/FunkinCrew/Funkin/pull/6334)
|
||||
- The scrolling text in the Results Screen is now positioned consistently. ([200f798](https://github.com/FunkinCrew/Funkin/commit/200f7984379cb1bd911e49de92be312750893420)) - by @NotHyper-474 in [#6595](https://github.com/FunkinCrew/Funkin/pull/6595)
|
||||
- The scrolling text in the Results Screen is now FPS-independent. ([8c43f6f](https://github.com/FunkinCrew/Funkin/commit/8c43f6f67a362bde4de4ad8dbd72966d7c4a9f60)) - by @VirtuGuy in [#6476](https://github.com/FunkinCrew/Funkin/pull/6476)
|
||||
- The Results Screen theme now loops more smoothly. ([592bf1a](https://github.com/FunkinCrew/Funkin/commit/592bf1a9221d4dcef53ae421571eddf78ed631e4)) - by @FuroYT in [#6477](https://github.com/FunkinCrew/Funkin/pull/6477)
|
||||
- Transition stickers now render properly with the window resized to any aspect ratio. ([4215b1c](https://github.com/FunkinCrew/Funkin/commit/4215b1c72e4391b2176014825885799b81dde2ba)) - by @PurSnake in [#6156](https://github.com/FunkinCrew/Funkin/pull/6156)
|
||||
- Boyfriend now stands in the same position in the Week 3 and Week 3 Erect stages. ([d2d15f1](https://github.com/FunkinCrew/funkin.assets/commit/d2d15f114256125d53c6331b46415701ae291c1f)) - by @CEliuxJV in [funkin.assets#261](https://github.com/FunkinCrew/funkin.assets/pull/261)
|
||||
- Pico's game over music now properly loops after his explosion death in 2hot. ([f1a1db0](https://github.com/FunkinCrew/Funkin/commit/f1a1db08347c3f9248fd16909cff06027297ad13)) - by @KoloInDaCrib in [#6216](https://github.com/FunkinCrew/Funkin/pull/6216)
|
||||
- 2hot now smoothly transitions into the cutscene. ([462b1b6](https://github.com/FunkinCrew/funkin.assets/commit/462b1b6e6e9b622ad8fd15f15a1f39c7dffa4764)) - by @hucks5 in [funkin.assets#286](https://github.com/FunkinCrew/funkin.assets/pull/286)
|
||||
- Nene now swings her legs during in-game cutscenes. ([6a5d9b2](https://github.com/FunkinCrew/funkin.assets/commit/6a5d9b265732d60da28083fb1c3d7b7b854edf63)) - by @hucks5 in [funkin.assets#289](https://github.com/FunkinCrew/funkin.assets/pull/289)
|
||||
- Nene's 50 combo animation now renders its heart properly. ([bcd96ee](https://github.com/FunkinCrew/funkin.assets/commit/bcd96eeb78b9b03cf7b24dc956ac3bc36438193e)) - by @VirtuGuy in [funkin.assets#296](https://github.com/FunkinCrew/funkin.assets/pull/296)
|
||||
- A-Bot (Christmas) now properly looks at the opponent. ([f81cc16](https://github.com/FunkinCrew/funkin.assets/commit/f81cc16c8f99655f76b7e27ce221819798c9c94e)) - by @ComedyLost in [funkin.assets#279](https://github.com/FunkinCrew/funkin.assets/pull/279)
|
||||
- Nene (Pixel) now raises and lowers her knife properly. ([1d9f50d](https://github.com/FunkinCrew/funkin.assets/commit/1d9f50d405f202e07efd883aeef5d6c1e9eaaa90)) - by @VirtuGuy in [funkin.assets#274](https://github.com/FunkinCrew/funkin.assets/pull/274)
|
||||
- Pico (Pixel)'s game over theme now plays at the right time. ([e8a4d24](https://github.com/FunkinCrew/funkin.assets/commit/e8a4d2472ca2dc28b03be05953dff3d7b5ec7433)) - by @JackXson-Real and @ComedyLost in [funkin.assets#288](https://github.com/FunkinCrew/funkin.assets/pull/288)
|
||||
- Pico holding Nene's idle animation no longer lasts too long. ([3483345](https://github.com/FunkinCrew/funkin.assets/commit/348334583746ec45f1c6ab2c5e1715a67038cc5c)) - by @JackXson-Real in [funkin.assets#271](https://github.com/FunkinCrew/funkin.assets/pull/271)
|
||||
- The tankmen in Stress no longer briefly appear behind the speakers when restarting. ([7f79ad8](https://github.com/FunkinCrew/funkin.assets/commit/7f79ad8c08d66fb909a9382de2c09f0430b756f1)) - by @VirtuGuy in [funkin.assets#298](https://github.com/FunkinCrew/funkin.assets/pull/298)
|
||||
- Encountering a Set Health Icon event during Minimal playtesting no longer crashes the Chart Editor. ([9f0a498](https://github.com/FunkinCrew/Funkin/commit/9f0a4988a3cc47581916a9750731bb7e63f168f3)) - by @charlesisfeline in [#6481](https://github.com/FunkinCrew/Funkin/pull/6481)
|
||||
- Typing in Chart Editor text fields no longer triggers bound key functions. ([3b5e589](https://github.com/FunkinCrew/Funkin/commit/3b5e5893c01c2b56036e53626eaea8d41e70f640)) - by @MightyTheArmiddilo in [#6601](https://github.com/FunkinCrew/Funkin/pull/6601)
|
||||
- Clicking between Chart Editor events no longer converts the second event's dropdown values into those of the first. ([c170c25](https://github.com/FunkinCrew/Funkin/commit/c170c256934ae316a735d38620ad5e24c2f15f4d)) - by @NotHyper-474 in [#6211](https://github.com/FunkinCrew/Funkin/pull/6211)
|
||||
- Chart Editor note kinds no longer lose their data when selecting multiple notes with different data. ([5a2c364](https://github.com/FunkinCrew/Funkin/commit/5a2c364c507fb4b86a740c3b2476c336496cd729)) - by @Lasercar in [#4248](https://github.com/FunkinCrew/Funkin/pull/4248)
|
||||
- Placing a note and removing it no longer breaks the cursor in the Chart Editor. ([8cb5ed9](https://github.com/FunkinCrew/Funkin/commit/8cb5ed91baf50b6f78eefc2e50612a01195a2742)) - by @Lasercar in [#4237](https://github.com/FunkinCrew/Funkin/pull/4237)
|
||||
- Playtesting in the Chart Editor now properly loads song variation scripts. ([055f620](https://github.com/FunkinCrew/Funkin/commit/055f62026ef44a928876d9065cc388da0905e9f1)) - by @PurSnake in [#6405](https://github.com/FunkinCrew/Funkin/pull/6405)
|
||||
- Playtesting in the Chart Editor no longer sometimes moves on to another song like Story Mode. ([9497378](https://github.com/FunkinCrew/Funkin/commit/9497378600bad44639ff8b3fc92cb329052fd08a)) - by @KoloInDaCrib in [#6192](https://github.com/FunkinCrew/Funkin/pull/6192)
|
||||
- The cursor is now hidden when playtesting through the Chart Editor. ([0471872](https://github.com/FunkinCrew/Funkin/commit/047187257b6712e61bf05fdebab33c7b3e9ac0f6)) - by @VirtuGuy in [#6137](https://github.com/FunkinCrew/Funkin/pull/6137)
|
||||
- The Chart Editor note moving sound no longer plays when dragging outside the grid. ([593456e](https://github.com/FunkinCrew/Funkin/commit/593456e53e02c53e71189fdb154e4a3e4f2b3092)) - by @amyspark-ng in [#4157](https://github.com/FunkinCrew/Funkin/pull/4157)
|
||||
- The Chart Editor music now fades in after a period of silence when enabled. ([960d2f3](https://github.com/FunkinCrew/Funkin/commit/960d2f3cf166400c6fecee212b87961f3df10c2e)) - by @Lasercar in [#4148](https://github.com/FunkinCrew/Funkin/pull/4148)
|
||||
- The Chart Editor Difficulty toolbox no longer displays the incorrect song name. ([dff2a1e](https://github.com/FunkinCrew/Funkin/commit/dff2a1e487dc20f86ef4f526a259083a7726ea6a)) - by @VirtuGuy in [#6547](https://github.com/FunkinCrew/Funkin/pull/6547)
|
||||
- Chart Editor notifications now display a bit higher. ([9e645db](https://github.com/FunkinCrew/Funkin/commit/9e645db0074424ffb2de42b27afb3adaff55de62)) - by @MightyTheArmiddilo in [#6445](https://github.com/FunkinCrew/Funkin/pull/6445)
|
||||
- The Yes and No text in the Chart Editor exit confirmation prompt buttons now always render properly. ([4c0d925](https://github.com/FunkinCrew/Funkin/commit/4c0d92507ae0b81a744d223e5292bf8d27013abd)) - by @NotHyper-474 in [#6173](https://github.com/FunkinCrew/Funkin/pull/6173)
|
||||
- Exiting the Chart Editor now consistently saves your audio preferences. ([2f77ccf](https://github.com/FunkinCrew/Funkin/commit/2f77ccfc6e93fabef27d3e06447f9f6edd8e840d)) - by @VioletSnowLeopard in [#6127](https://github.com/FunkinCrew/Funkin/pull/6127)
|
||||
- The Chart Editor and Stage Editor backup window now properly retrieves the most recently edited backup. ([a8dec0c](https://github.com/FunkinCrew/Funkin/commit/a8dec0cd7053f88865757f30a7d9f0ca5eb0fb02)) - by @Lasercar in [#6119](https://github.com/FunkinCrew/Funkin/pull/6119)
|
||||
- The Chart Editor backup dialog no longer appears if the latest backup is deleted. ([bc1d36d](https://github.com/FunkinCrew/Funkin/commit/bc1d36ddeab81a790e3604598248a9b2fe869cac)) - by @KoloInDaCrib in [#6027](https://github.com/FunkinCrew/Funkin/pull/6027)
|
||||
- Switching between Animation and Spritesheet mode no longer crashes the Animation Editor. ([a61016d](https://github.com/FunkinCrew/Funkin/commit/a61016d2dfc2ac6a50aae230969c036f0753cfaa)) - by @VirtuGuy in [#6114](https://github.com/FunkinCrew/Funkin/pull/6114)
|
||||
- The Animation Editor no longer displays a missing object icon next to the dropdown menus. ([b30ea36](https://github.com/FunkinCrew/Funkin/commit/b30ea36fcc982ce4ac0be9143fd4701e180fc684)) - by @KoloInDaCrib in [#6177](https://github.com/FunkinCrew/Funkin/pull/6177)
|
||||
- The Animation Editor onion skin now properly accounts for character scale and offsets. ([a76a868](https://github.com/FunkinCrew/Funkin/commit/a76a868cb6a27c1c76d09177b90cf6df3df29d17)) - by @VirtuGuy in [#6123](https://github.com/FunkinCrew/Funkin/pull/6123)
|
||||
- The debug cursor is now properly hidden when exiting the Animation Editor. ([f2f6b4c](https://github.com/FunkinCrew/Funkin/commit/f2f6b4c8591e1ab4a8c6f0ed998d6d25d7cc9f43)) - by @VirtuGuy in [#6112](https://github.com/FunkinCrew/Funkin/pull/6112)
|
||||
- Plenty more tiny fixes.
|
||||
|
||||
## New Contributors for 0.8.0
|
||||
|
||||
* @Eviethecoder made their first contribution in [#4036](https://github.com/FunkinCrew/Funkin/pull/4036)
|
||||
* @FuroYT made their first contribution in [#5908](https://github.com/FunkinCrew/Funkin/pull/5908)
|
||||
* @HeroEyad made their first contribution in [#6049](https://github.com/FunkinCrew/Funkin/pull/6049)
|
||||
* @hucks5 made their first contribution in [#6196](https://github.com/FunkinCrew/Funkin/pull/6196)
|
||||
* @Starexify made their first contribution in [#6225](https://github.com/FunkinCrew/Funkin/pull/6225)
|
||||
* @NebulaStellaNova made their first contribution in [#6326](https://github.com/FunkinCrew/Funkin/pull/6326)
|
||||
* @MightyTheArmiddilo made their first contribution in [#6445](https://github.com/FunkinCrew/Funkin/pull/6445)
|
||||
* @Donothan73 made their first contribution in [funkin.assets#257](https://github.com/FunkinCrew/funkin.assets/pull/257)
|
||||
* @CEliuxJV made their first contribution in [funkin.assets#261](https://github.com/FunkinCrew/funkin.assets/pull/261)
|
||||
* @roma-perec-bp made their first contribution in [funkin.assets#273](https://github.com/FunkinCrew/funkin.assets/pull/273)
|
||||
* @ComedyLost made their first contribution in [funkin.assets#279](https://github.com/FunkinCrew/funkin.assets/pull/279)
|
||||
|
||||
|
||||
|
||||
## [0.7.5] - 2025-09-19
|
||||
|
||||
### Added
|
||||
|
||||
- Added an `onStateCreate` event to the Module class.
|
||||
|
||||
### Changed
|
||||
|
||||
- Gave the debug display a huge upgrade!
|
||||
- Choose from three settings in the Preferences Menu: Off/Simple/Advanced
|
||||
- New “Task memory” counter shows how much RAM the game is using on your computer.
|
||||
- Advanced mode displays graphs for FPS and RAM usage.
|
||||
- Hot reloading now works in scripted states.
|
||||
- Made a few more classes scriptable.
|
||||
- FunkinVideoSprite
|
||||
- FlxStrip
|
||||
- FlxTypedGroup
|
||||
- Strumline
|
||||
- StageProp
|
||||
- The Random capsule now brings up the option to select a random instrumental. ([0d5c6ac](https://github.com/FunkinCrew/Funkin/commit/0d5c6ac9632ee3bf3977b9bd369daab9f2d95fea)) - by @VioletSnowLeopard in [#5891](https://github.com/FunkinCrew/Funkin/pull/5891)
|
||||
- The Character Select menu now returns to Freeplay after pressing a back button. ([250d218](https://github.com/FunkinCrew/Funkin/commit/250d2188b9cb2c92d283d8b12ed7de0286324592)) - by @MrMadera in [#5887](https://github.com/FunkinCrew/Funkin/pull/5887)
|
||||
- Pressing the Shift key in the Credits menu now pauses scrolling. ([479036a](https://github.com/FunkinCrew/Funkin/commit/479036a4008fe1c54adc970c0a476fae6b1b3e94)) - by @JVNpixels in [#2924](https://github.com/FunkinCrew/Funkin/pull/2924)
|
||||
- Chart Editor and Stage Editor backup files are now given relevant filenames. ([b31be10](https://github.com/FunkinCrew/Funkin/commit/b31be1043ccd1e906a656913b947bae160b451fe)) - by @Lasercar in [#4118](https://github.com/FunkinCrew/Funkin/pull/4118)
|
||||
- The Animation Editor's onion skin now properly accounts for offsets and displays the idle animation. ([0fd6f5b](https://github.com/FunkinCrew/Funkin/commit/0fd6f5b3b7bd115a3b6821096daa929c220bd235)) - by @KoloInDaCrib in [#5810](https://github.com/FunkinCrew/Funkin/pull/5810)
|
||||
|
||||
### Fixed
|
||||
|
||||
- The Freeplay new rank animation now shows the new rank slamming onto the old one.
|
||||
- Freeplay capsules with a Gold Perfect rank no longer become randomly offset when scrolling.
|
||||
- The Pico Freeplay DJ’s new rank animation now properly renders its lighting.
|
||||
- Freeplay song previews no longer play replaced or deleted audio files.
|
||||
- The strumline no longer disappears after re-entering Lag Adjustment.
|
||||
- Fixed various issues with Week 6 dialogue boxes, portraits, and audio.
|
||||
- [MOBILE] The strumlines are no longer offset to the left in Blazin’.
|
||||
- The preloader no longer disables fullscreen mode after completion.
|
||||
- Attempting to log in to Newgrounds during an ongoing login attempt no longer crashes the game. ([4e572b1](https://github.com/FunkinCrew/Funkin/commit/4e572b171fe901a84f12b4566963fd3c1f4ae6fb)) - by @KoloInDaCrib in [#5922](https://github.com/FunkinCrew/Funkin/pull/5922)
|
||||
- Added null-safety to Leaderboards and Medals to prevent a rare crash. ([88fc5f9](https://github.com/FunkinCrew/Funkin/commit/88fc5f9ed684e287d7411a59fb4fdf170acfed5a)) - by @NotHyper-474 in [#5926](https://github.com/FunkinCrew/Funkin/pull/5926)
|
||||
- Freeplay ranks no longer pop out when scrolling through song capsules. ([747446e](https://github.com/FunkinCrew/Funkin/commit/747446e2e420449f422fbc69aafda6128ac8d16e)) - by @VioletSnowLeopard in [#5822](https://github.com/FunkinCrew/Funkin/pull/5822)
|
||||
- The Freeplay intro no longer plays when returning from a sticker transition. ([63ec749](https://github.com/FunkinCrew/Funkin/commit/63ec749c7a3c90d529e9e8f2f3ed1c8913e8f9a1)) - by @PurSnake in [#5942](https://github.com/FunkinCrew/Funkin/pull/5942)
|
||||
- Hot-reloading with F5 after earning a rank no longer crashes Freeplay. ([79e8b5d](https://github.com/FunkinCrew/Funkin/commit/79e8b5d71e052953b77b81b66e0a83a7bbeeeeed)) - by @KoloInDaCrib in [#5971](https://github.com/FunkinCrew/Funkin/pull/5971)
|
||||
- The Main Menu music now fades in consistently when exiting from Freeplay to the Title Screen. ([a10bbf6](https://github.com/FunkinCrew/Funkin/commit/a10bbf60acbbeadfb5a91930745ef4fd338b1a24)) - by @VirtuGuy in [#5855](https://github.com/FunkinCrew/Funkin/pull/5855)
|
||||
- The game now falls back to default audio files when files specified by the metadata don't exist. ([b5357ac](https://github.com/FunkinCrew/Funkin/commit/b5357acf98bde81a4121a3723061f7c559a0640f)) - by @VioletSnowLeopard in [#5954](https://github.com/FunkinCrew/Funkin/pull/5954)
|
||||
- The cutscene pause menu no longer persists across songs after exiting a video cutscene. ([c2305f3](https://github.com/FunkinCrew/Funkin/commit/c2305f32f0147f6326e8665d21a6b8a793f45033)) - by @KoloInDaCrib in [#5880](https://github.com/FunkinCrew/Funkin/pull/5880)
|
||||
- Mashing to skip the Eggnog Erect cutscene no longer crashes the game. ([5ca3c6a](https://github.com/FunkinCrew/funkin.assets/commit/5ca3c6a5745b61a457db149c289784c08e560ee9)) - by @VioletSnowLeopard in [funkin.assets#258](https://github.com/FunkinCrew/funkin.assets/pull/258)
|
||||
- Stress (Pico Mix) no longer lag spikes when loading in the background tankmen. ([6701405](https://github.com/FunkinCrew/funkin.assets/commit/670140571239ca08c1ae1c5286773d99c2875c2d)) - by @NotHyper-474 and @KoloInDaCrib in [funkin.assets#242](https://github.com/FunkinCrew/funkin.assets/pull/242)
|
||||
- Stress (Pico Mix) no longer lag spikes when Tankman changes sprites. ([aa5956a](https://github.com/FunkinCrew/Funkin/commit/aa5956a30bdc47d53ebebb4b61a7ebb3f04c7dc6)) - by @NotHyper-474 in [#5939](https://github.com/FunkinCrew/Funkin/pull/5939)
|
||||
- The bricks in Stress (Pico Mix) no longer move after restarting the song. ([9cfee03](https://github.com/FunkinCrew/Funkin/commit/9cfee0398a057567f80ad68ff6052964da63af09)) - by @VirtuGuy in [#5875](https://github.com/FunkinCrew/Funkin/pull/5875)
|
||||
- Pico now properly shoots after restarting Stress. ([b0ef0d3](https://github.com/FunkinCrew/funkin.assets/commit/b0ef0d352c199e93903652a95ffdf16a3dc2d569)) - by @7oltan in [funkin.assets#254](https://github.com/FunkinCrew/funkin.assets/pull/254)
|
||||
- A-Bot's visualizer now zeroes out when restarting the song. ([ba3a8b0](https://github.com/FunkinCrew/funkin.assets/commit/ba3a8b0cbbf785a0729dcf85e25b6e59c0c12657)) - by @KoloInDaCrib in [funkin.assets#246](https://github.com/FunkinCrew/funkin.assets/pull/246)
|
||||
- Notekinds now properly receive dispatched events last in PlayState. ([89b2b7f](https://github.com/FunkinCrew/Funkin/commit/89b2b7fcc3130db0af1f782594af72ae1f4ce320)) - by @Az989YT in [#5936](https://github.com/FunkinCrew/Funkin/pull/5936)
|
||||
- Text from .json files is now properly sanitized before it is parsed. ([80d7ace](https://github.com/FunkinCrew/Funkin/commit/80d7ace714efb729cb8f1b23b61fdab4ac655275)) - by @KoloInDaCrib in [#5108](https://github.com/FunkinCrew/Funkin/pull/5108)
|
||||
- Fixed a crash when exiting the Chart Editor after interacting with a text field. ([f85f2f0](https://github.com/FunkinCrew/Funkin/commit/f85f2f03d97fc1332275f088ddce2b4056f0ded2)) - by @NotHyper-474 in [#5992](https://github.com/FunkinCrew/Funkin/pull/5992)
|
||||
- Fixed some issues with variation handling in the Chart Editor. ([970ccf2](https://github.com/FunkinCrew/Funkin/commit/970ccf2f4a9b5802b70d0b3c269c8b9e541d53f2)) - by @NotHyper-474 in [#4396](https://github.com/FunkinCrew/Funkin/pull/4396)
|
||||
- Audio no longer plays when dragging a playhead in the Chart Editor. ([2d95754](https://github.com/FunkinCrew/Funkin/commit/2d95754d492da06920d56de2b2bcbb9b81182af0)) - by @Lasercar in [#4140](https://github.com/FunkinCrew/Funkin/pull/4140)
|
||||
- The Chart Editor Offsets window song preview no longer continues playing during playtesting. ([11eb406](https://github.com/FunkinCrew/Funkin/commit/11eb4060d03b18a2f3604297eb9a24e54077fe3c)) - by @PurSnake and @Lasercar in [#5973](https://github.com/FunkinCrew/Funkin/pull/5973)
|
||||
- The fullscreen keybind no longer triggers when typing in a field. ([4ebd00b](https://github.com/FunkinCrew/Funkin/commit/4ebd00bc06a5c94e935f557ba14ba9729faec104)) - by @lemz1 in [#4131](https://github.com/FunkinCrew/Funkin/pull/4131)
|
||||
- The Stage Editor new stage menu can no longer be opened multiple times at once. ([47b1cee](https://github.com/FunkinCrew/Funkin/commit/47b1cee6d5ee37cd18146ca8098da85610c6b544)) - by @VioletSnowLeopard in [#5873](https://github.com/FunkinCrew/Funkin/pull/5873)
|
||||
- Adjusted the save hotkeys in the Animation Editor to be usable on Windows. ([8ead5ee](https://github.com/FunkinCrew/Funkin/commit/8ead5eeb0840756a843fdcf0f3f347b087e7182d)) - by @VioletSnowLeopard in [#5974](https://github.com/FunkinCrew/Funkin/pull/5974)
|
||||
- A few more tiny fixes.
|
||||
|
||||
## New Contributors for 0.7.5
|
||||
|
||||
* @Az989YT made their first contribution in [#5936](https://github.com/FunkinCrew/Funkin/pull/5936)
|
||||
|
||||
|
||||
|
||||
## [0.7.4] - 2025-09-01
|
||||
|
||||
### Added
|
||||
@@ -47,7 +327,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
||||
- Improved properties support, behaving more like regular Haxe. (Thanks NotHyper-474!)
|
||||
- Fixed a null object reference when evaluating static variables with no explicit values. (Thanks NotHyper-474!)
|
||||
- Fixed a null object reference when iterating over a null variable. (Thanks NotHyper-474!)
|
||||
- Scripts are now automatically cleared when Polymod is initialized. (Thanks KoloInDaKrib!)
|
||||
- Scripts are now automatically cleared when Polymod is initialized. (Thanks KoloInDaCrib!)
|
||||
- Polished every chart in the game!
|
||||
- The lengths of hold notes have been adjusted to match the vocals as closely as possible, across all variations and difficulties.
|
||||
- Easy and Normal charts from Week 1 to Week 3 have been adjusted for more consistency between the player and opponent.
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
<div align='center'><img src="https://upload.wikimedia.org/wikipedia/commons/thumb/d/d2/Friday_Night_Funkin%27_logo.svg/2560px-Friday_Night_Funkin%27_logo.svg.png" width="800">
|
||||
<div align='center'><img src="docs/readme_images/FNF_logo.png" width="800">
|
||||
|
||||
<h2>Friday Night Funkin' is a rhythm game. Built using HaxeFlixel for <a href="https://ldjam.com/events/ludum-dare/47">Ludum Dare 47.</a></h2>
|
||||
|
||||
@@ -14,8 +14,8 @@ This game was made with love to Newgrounds and its community. Extra love to Tom
|
||||
<div align='center'>
|
||||
<table>
|
||||
<tr>
|
||||
<td><img src="https://fridaynightfunkin.wiki.gg/images/d/d7/Title_Card.gif" alt="Title Screen" width="350"/></td>
|
||||
<td><img src="https://fridaynightfunkin.wiki.gg/images/9/99/Menu.png" alt="Main Menu" width="350"/></td>
|
||||
<td><img src="docs/readme_images/Title_Card.gif" alt="Title Screen" width="350"/></td>
|
||||
<td><img src="docs/readme_images/Menu.png" alt="Main Menu" width="350"/></td>
|
||||
</tr>
|
||||
</table>
|
||||
</div>
|
||||
@@ -63,4 +63,4 @@ Full credits can be found in-game, or in the `credits.json` file which is locate
|
||||
## Special Thanks
|
||||
- [Tom Fulp](https://twitter.com/tomfulp) - For being a great guy and for Newgrounds
|
||||
- [JohnnyUtah](https://twitter.com/JohnnyUtahNG/) - Voice of Tankman
|
||||
- [L0Litsmonica](https://twitter.com/L0Litsmonica) - Voice of Mommy Mearest
|
||||
- [L0Litsmonica](https://twitter.com/L0Litsmonica) - Voice of Mommy Mearest
|
||||
|
||||
+1
-1
Submodule art updated: 094ff10919...2e68e371f8
+1
-1
Submodule assets updated: e6f463fb64...39d4b3fc95
+67
-16
@@ -5,22 +5,6 @@
|
||||
"blocksize": "10x10",
|
||||
"colorprofile": "cl",
|
||||
"custom": [
|
||||
{
|
||||
"asset": "asset/shared/images/resultScreen/results-pico/resultsPERFECT/spritemap1.png",
|
||||
"blocksize": "4x4"
|
||||
},
|
||||
{
|
||||
"asset": "asset/shared/images/resultScreen/results-pico/resultsGREAT/spritemap1.png",
|
||||
"blocksize": "4x4"
|
||||
},
|
||||
{
|
||||
"asset": "asset/shared/images/resultScreen/results-pico/resultsGOOD/spritemap1.png",
|
||||
"blocksize": "4x4"
|
||||
},
|
||||
{
|
||||
"asset": "asset/shared/images/resultScreen/results-pico/resultsSHIT/spritemap1.png",
|
||||
"blocksize": "4x4"
|
||||
},
|
||||
{
|
||||
"asset": "assets/preload/images/NOTE_assets.png",
|
||||
"blocksize": "4x4"
|
||||
@@ -37,10 +21,26 @@
|
||||
"asset": "assets/preload/images/alphabet.png",
|
||||
"blocksize": "4x4"
|
||||
},
|
||||
{
|
||||
"asset": "assets/preload/images/charSelect/gfChill/spritemap1.png",
|
||||
"blocksize": "6x6"
|
||||
},
|
||||
{
|
||||
"asset": "assets/shared/images/NOTE_hold_assets.png",
|
||||
"blocksize": "4x4"
|
||||
},
|
||||
{
|
||||
"asset": "assets/shared/images/characters/nene-dark/spritemap1.png",
|
||||
"blocksize": "6x6"
|
||||
},
|
||||
{
|
||||
"asset": "assets/shared/images/characters/otis/muzzle-flashes/otis_flashes.png",
|
||||
"blocksize": "4x4"
|
||||
},
|
||||
{
|
||||
"asset": "assets/shared/images/characters/sserafim/chaewon/spritemap1.png",
|
||||
"blocksize": "4x4"
|
||||
},
|
||||
{
|
||||
"asset": "assets/shared/images/holdCoverBlue.png",
|
||||
"blocksize": "4x4"
|
||||
@@ -77,6 +77,30 @@
|
||||
"asset": "assets/shared/images/notes.png",
|
||||
"blocksize": "4x4"
|
||||
},
|
||||
{
|
||||
"asset": "assets/shared/images/resultScreen/results-pico/resultsGOOD/spritemap1.png",
|
||||
"blocksize": "4x4"
|
||||
},
|
||||
{
|
||||
"asset": "assets/shared/images/resultScreen/results-pico/resultsGREAT/spritemap1.png",
|
||||
"blocksize": "4x4"
|
||||
},
|
||||
{
|
||||
"asset": "assets/shared/images/resultScreen/results-pico/resultsPERFECT/spritemap1.png",
|
||||
"blocksize": "4x4"
|
||||
},
|
||||
{
|
||||
"asset": "assets/shared/images/resultScreen/results-pico/resultsSHIT/spritemap1.png",
|
||||
"blocksize": "4x4"
|
||||
},
|
||||
{
|
||||
"asset": "assets/sserafim/images/sserafim-lipsync-yunjin/spritemap1.png",
|
||||
"blocksize": "4x4"
|
||||
},
|
||||
{
|
||||
"asset": "assets/sserafim/images/sserafim-lipsync/spritemap1.png",
|
||||
"blocksize": "4x4"
|
||||
},
|
||||
{
|
||||
"asset": "assets/week1/image/erect/bg.png",
|
||||
"blocksize": "8x8"
|
||||
@@ -92,6 +116,14 @@
|
||||
{
|
||||
"asset": "assets/week1/images/erect/server.png",
|
||||
"blocksize": "8x8"
|
||||
},
|
||||
{
|
||||
"asset": "assets/week7/images/erect/rando/spritemap1.png",
|
||||
"blocksize": "6x6"
|
||||
},
|
||||
{
|
||||
"asset": "assets/week7/images/erect/sniper/spritemap1.png",
|
||||
"blocksize": "6x6"
|
||||
}
|
||||
],
|
||||
"excludes": [
|
||||
@@ -101,23 +133,42 @@
|
||||
"assets/preload/images/icons/",
|
||||
"assets/preload/images/soundtray/",
|
||||
"assets/preload/images/stageBuild/",
|
||||
"assets/preload/images/storymenu/titles/",
|
||||
"assets/preload/images/titleEnter.png",
|
||||
"assets/preload/images/titleEnter_mobile.png",
|
||||
"assets/preload/images/ui/popup/pixel/*",
|
||||
"assets/shared/images/characters/abotPixel/*",
|
||||
"assets/shared/images/characters/bfPixel.png",
|
||||
"assets/shared/images/characters/bfPixelsDEAD.png",
|
||||
"assets/shared/images/characters/gf-christmas/spritemap1.png",
|
||||
"assets/shared/images/characters/gf-dark/spritemap1.png",
|
||||
"assets/shared/images/characters/gf/spritemap1.png",
|
||||
"assets/shared/images/characters/gfCar/spritemap1.png",
|
||||
"assets/shared/images/characters/gfPixel.png",
|
||||
"assets/shared/images/characters/nene/spritemap1.png",
|
||||
"assets/shared/images/characters/nenePixel/*",
|
||||
"assets/shared/images/characters/pico-holding-nene/spritemap1.png",
|
||||
"assets/shared/images/characters/picoPixel/*",
|
||||
"assets/shared/images/characters/senpai.png",
|
||||
"assets/shared/images/characters/spirit.png",
|
||||
"assets/shared/images/characters/sserafim/eunchae/spritemap1.png",
|
||||
"assets/shared/images/characters/sserafim/kazuha/spritemap1.png",
|
||||
"assets/shared/images/characters/sserafim/sserafim-gf/spritemap1.png",
|
||||
"assets/shared/images/characters/sserafim/yunjin/spritemap1.png",
|
||||
"assets/shared/images/characters/sserafim/sakura/spritemap1.png",
|
||||
"assets/shared/images/healthBar.png",
|
||||
"assets/shared/images/resultScreen/*",
|
||||
"assets/shared/images/resultScreen/clearPercent/",
|
||||
"assets/shared/images/resultScreen/rankText/",
|
||||
"assets/shared/images/resultScreen/results-bf/resultsEXCELLENT/spritemap1.png",
|
||||
"assets/shared/images/resultScreen/results-bf/resultsGOOD/resultGirlfriendGOOD.png",
|
||||
"assets/shared/images/resultScreen/results-bf/resultsGREAT/gf/spritemap1.png",
|
||||
"assets/shared/images/resultScreen/results-bf/resultsSHIT/gf/spritemap1.png",
|
||||
"assets/shared/images/ui/chart-editor/",
|
||||
"assets/shared/images/ui/countdown/pixel/*",
|
||||
"assets/sserafim/images/cutscene/cutsceneMain/spritemap1.png",
|
||||
"assets/sserafim/images/cutscene/ref.png",
|
||||
"assets/sserafim/images/ref.png",
|
||||
"assets/week1/*",
|
||||
"assets/week1/images/erect/brightLightSmall.png",
|
||||
"assets/week1/images/erect/lightAbove.png",
|
||||
|
||||
+33
-20
@@ -1,10 +1,9 @@
|
||||
{
|
||||
"extendsConfigPath": "",
|
||||
"defineCombinations": [],
|
||||
"defaultSeverity": "INFO",
|
||||
"extendsConfigPath": "",
|
||||
"numberOfCheckerThreads": 5,
|
||||
"baseDefines": [],
|
||||
"version": 1,
|
||||
"exclude": {},
|
||||
"defineCombinations": [],
|
||||
"checks": [
|
||||
{
|
||||
"props": {},
|
||||
@@ -40,12 +39,6 @@
|
||||
"props": {},
|
||||
"type": "AvoidStarImport"
|
||||
},
|
||||
{
|
||||
"props": {
|
||||
"severity": "IGNORE"
|
||||
},
|
||||
"type": "AvoidTernaryOperator"
|
||||
},
|
||||
{
|
||||
"props": {},
|
||||
"type": "BlockBreakingConditional"
|
||||
@@ -71,7 +64,6 @@
|
||||
{
|
||||
"props": {
|
||||
"severity": "IGNORE",
|
||||
|
||||
"policy": "aligned",
|
||||
"allowSingleline": true
|
||||
},
|
||||
@@ -108,13 +100,12 @@
|
||||
{
|
||||
"props": {
|
||||
"lineStyle": "onestar",
|
||||
"startStyle": "twostars",
|
||||
"severity": "IGNORE"
|
||||
"startStyle": "ignore"
|
||||
},
|
||||
"type": "DocCommentStyle"
|
||||
},
|
||||
{
|
||||
"props": {},
|
||||
"props": { "severity": "IGNORE" },
|
||||
"type": "Dynamic"
|
||||
},
|
||||
{
|
||||
@@ -193,6 +184,14 @@
|
||||
},
|
||||
"type": "FileLength"
|
||||
},
|
||||
{
|
||||
"type": "FileNameCase",
|
||||
"props": {}
|
||||
},
|
||||
{
|
||||
"type": "Final",
|
||||
"props": {}
|
||||
},
|
||||
{
|
||||
"props": {
|
||||
"option": "upperCase"
|
||||
@@ -245,7 +244,7 @@
|
||||
{
|
||||
"props": {
|
||||
"max": 160,
|
||||
"ignorePattern": "^$"
|
||||
"ignorePattern": "^(\\s*['\"][^'\"]*['\"].*)$"
|
||||
},
|
||||
"type": "LineLength"
|
||||
},
|
||||
@@ -348,7 +347,7 @@
|
||||
{
|
||||
"props": {
|
||||
"allowSingleLineStatement": true,
|
||||
"tokens": ["FOR", "ELSE_IF", "WHILE", "DO_WHILE"]
|
||||
"tokens": ["ELSE_IF", "WHILE", "DO_WHILE"]
|
||||
},
|
||||
"type": "NeedBraces"
|
||||
},
|
||||
@@ -580,6 +579,12 @@
|
||||
"props": {},
|
||||
"type": "TrailingWhitespace"
|
||||
},
|
||||
{
|
||||
"type": "Type",
|
||||
"props": {
|
||||
"ignoreEnumAbstractValues": true
|
||||
}
|
||||
},
|
||||
{
|
||||
"props": {
|
||||
"tokens": ["CLASS_DEF", "ENUM_DEF", "INTERFACE_DEF", "TYPEDEF_DEF"]
|
||||
@@ -612,11 +617,18 @@
|
||||
"type": "UnusedLocalVar"
|
||||
},
|
||||
{
|
||||
"type": "VarTypeHint",
|
||||
"props": {
|
||||
"typeHintPolicy": "enforce_all",
|
||||
"ignoreEnumAbstractValues": true
|
||||
},
|
||||
"type": "VarTypeHint"
|
||||
"ignoreEnumAbstractValues": true,
|
||||
"severity": "IGNORE"
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "VariableInitialisation",
|
||||
"props": {
|
||||
"allowFinal": false
|
||||
}
|
||||
},
|
||||
{
|
||||
"props": {
|
||||
@@ -665,5 +677,6 @@
|
||||
"type": "WhitespaceAround"
|
||||
}
|
||||
],
|
||||
"numberOfCheckerThreads": 5
|
||||
"exclude": {},
|
||||
"version": 1
|
||||
}
|
||||
|
||||
@@ -60,7 +60,7 @@ This section provides guidelines to follow when [opening an issue](https://githu
|
||||
|
||||
## Requirements
|
||||
Make sure you're playing:
|
||||
- the latest version of the game (currently v0.7.4)
|
||||
- the latest version of the game (currently v0.8.1)
|
||||
- without any mods
|
||||
- on [Newgrounds](https://www.newgrounds.com/portal/view/770371) or downloaded from [itch.io](https://ninja-muffin24.itch.io/funkin)
|
||||
|
||||
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 501 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 541 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 2.0 MiB |
@@ -0,0 +1,5 @@
|
||||
Images stored in this folder come from these links:
|
||||
|
||||
FNF_logo.png - https://fridaynightfunkin.wiki.gg/images/FNF_logo.png
|
||||
Menu.png - https://fridaynightfunkin.wiki.gg/images/Menu.png
|
||||
Title_Card.gif - https://fridaynightfunkin.wiki.gg/images/d/d7/Title_Card.gif
|
||||
@@ -0,0 +1,37 @@
|
||||
<key>UTExportedTypeDeclarations</key>
|
||||
<array>
|
||||
<dict>
|
||||
<key>UTTypeIdentifier</key>
|
||||
<string>me.funkin.fnfc</string>
|
||||
<key>UTTypeDescription</key>
|
||||
<string>FNFC Document</string>
|
||||
<key>UTTypeConformsTo</key>
|
||||
<array>
|
||||
<string>public.zip-archive</string>
|
||||
</array>
|
||||
<key>UTTypeTagSpecification</key>
|
||||
<dict>
|
||||
<key>public.filename-extension</key>
|
||||
<array>
|
||||
<string>fnfc</string>
|
||||
</array>
|
||||
<key>public.mime-type</key>
|
||||
<string>application/zip</string>
|
||||
</dict>
|
||||
</dict>
|
||||
</array>
|
||||
<key>CFBundleDocumentTypes</key>
|
||||
<array>
|
||||
<dict>
|
||||
<key>CFBundleTypeName</key>
|
||||
<string>FNFC Document</string>
|
||||
<key>LSHandlerRank</key>
|
||||
<string>Owner</string>
|
||||
<key>CFBundleTypeRole</key>
|
||||
<string>Viewer</string>
|
||||
<key>LSItemContentTypes</key>
|
||||
<array>
|
||||
<string>me.funkin.fnfc</string>
|
||||
</array>
|
||||
</dict>
|
||||
</array>
|
||||
@@ -4,8 +4,8 @@
|
||||
"name": "FlxPartialSound",
|
||||
"type": "git",
|
||||
"dir": null,
|
||||
"ref": "3c9f63e3501c20c0b60442089dc05306f5a87968",
|
||||
"url": "https://github.com/FunkinDroidTeam/FlxPartialSound.git"
|
||||
"ref": "26ed20b9c181a576f4cbe18ccdea38912c01476e",
|
||||
"url": "https://github.com/FunkinCrew/FlxPartialSound.git"
|
||||
},
|
||||
{
|
||||
"name": "astc-compressor",
|
||||
@@ -18,7 +18,7 @@
|
||||
"name": "extension-admob",
|
||||
"type": "git",
|
||||
"dir": null,
|
||||
"ref": "02334589ff9603a5f483077a44395009644f6274",
|
||||
"ref": "4c69bb43f1fb89fd6003dc98d2f3a9d75c5364b6",
|
||||
"url": "https://github.com/FunkinCrew/extension-admob"
|
||||
},
|
||||
{
|
||||
@@ -41,26 +41,33 @@
|
||||
"type": "haxelib",
|
||||
"version": "1.0.3"
|
||||
},
|
||||
{
|
||||
"name": "extension-webviewcore",
|
||||
"type": "git",
|
||||
"dir": null,
|
||||
"ref": "d13109450d65c40678244ca23b1aa88ac18d77e6",
|
||||
"url": "https://github.com/LimeExtensions/extension-webviewcore"
|
||||
},
|
||||
{
|
||||
"name": "flixel",
|
||||
"type": "git",
|
||||
"dir": null,
|
||||
"ref": "ac2a34fe4b3400bc04218f46320a65af0f337fc0",
|
||||
"ref": "c8e79d2a42f9c11c639519ef2a6fd7ef2379a8aa",
|
||||
"url": "https://github.com/FunkinCrew/flixel"
|
||||
},
|
||||
{
|
||||
"name": "flixel-addons",
|
||||
"type": "git",
|
||||
"dir": null,
|
||||
"ref": "b9118f47f43a66bc0e5fbfcfd9903f0425e918ee",
|
||||
"ref": "6fa30b3f5209146c852c25f3d1003e08898083e2",
|
||||
"url": "https://github.com/FunkinCrew/flixel-addons"
|
||||
},
|
||||
{
|
||||
"name": "flxanimate",
|
||||
"name": "flixel-animate",
|
||||
"type": "git",
|
||||
"dir": null,
|
||||
"ref": "49214278b9124823582cdcecd94f4a1de9a4b36b",
|
||||
"url": "https://github.com/FunkinCrew/flxanimate"
|
||||
"ref": "c61476f4b3a3d225631ab3065e4e925a4b63c076",
|
||||
"url": "https://github.com/MaybeMaru/flixel-animate"
|
||||
},
|
||||
{
|
||||
"name": "format",
|
||||
@@ -71,14 +78,14 @@
|
||||
"name": "funkin.vis",
|
||||
"type": "git",
|
||||
"dir": null,
|
||||
"ref": "1966f8fbbbc509ed90d4b520f3c49c084fc92fd6",
|
||||
"ref": "02bada154b474c2554709b9d12aef0cbf0da3ec9",
|
||||
"url": "https://github.com/FunkinCrew/funkVis"
|
||||
},
|
||||
{
|
||||
"name": "grig.audio",
|
||||
"type": "git",
|
||||
"dir": "src",
|
||||
"ref": "8567c4dad34cfeaf2ff23fe12c3796f5db80685e",
|
||||
"ref": "6409f3c6d1b4c52176813d3ede86c0d34e8af2c1",
|
||||
"url": "https://github.com/FunkinCrew/grig.audio"
|
||||
},
|
||||
{
|
||||
@@ -90,8 +97,8 @@
|
||||
"name": "haxeui-core",
|
||||
"type": "git",
|
||||
"dir": null,
|
||||
"ref": "47ee9f5fa02d422e186e844f87e68220cabfcc5b",
|
||||
"url": "https://github.com/haxeui/haxeui-core"
|
||||
"ref": "088b153b35985f8e4419ef710057ebd5b3749513",
|
||||
"url": "https://github.com/FunkinCrew/haxeui-core"
|
||||
},
|
||||
{
|
||||
"name": "haxeui-flixel",
|
||||
@@ -104,29 +111,27 @@
|
||||
"name": "hscript",
|
||||
"type": "git",
|
||||
"dir": null,
|
||||
"ref": "8c1d238b069ef97cec13f5121b29d2afe2b8985d",
|
||||
"ref": "dcc6f822e0866ca4fda300ed23e99559260f7bf5",
|
||||
"url": "https://github.com/FunkinCrew/hscript"
|
||||
},
|
||||
{
|
||||
"name": "hxcpp",
|
||||
"type": "git",
|
||||
"dir": null,
|
||||
"ref": "5a0dc3f644dc676a4a092b7e6c8edc8be941f024",
|
||||
"ref": "5932340d095a7eea8635fe4d1355f1c0efd0b3c2",
|
||||
"url": "https://github.com/FunkinCrew/hxcpp"
|
||||
},
|
||||
{
|
||||
"name": "hxcpp-debug-server",
|
||||
"type": "git",
|
||||
"dir": "hxcpp-debug-server",
|
||||
"ref": "147294123f983e35f50a966741474438069a7a8f",
|
||||
"ref": "7459934666a473a4cc4d066ba4a93ef92f1ce94c",
|
||||
"url": "https://github.com/FunkinCrew/hxcpp-debugger"
|
||||
},
|
||||
{
|
||||
"name": "hxdiscord_rpc",
|
||||
"type": "git",
|
||||
"dir": null,
|
||||
"ref": "82c47ecc1a454b7dd644e4fcac7e91155f176dec",
|
||||
"url": "https://github.com/FunkinCrew/hxdiscord_rpc"
|
||||
"type": "haxelib",
|
||||
"version": "1.3.0"
|
||||
},
|
||||
{
|
||||
"name": "hxjsonast",
|
||||
@@ -135,6 +140,11 @@
|
||||
"ref": "20e72cc68c823496359775ac1f06500e67f189d5",
|
||||
"url": "https://github.com/nadako/hxjsonast/"
|
||||
},
|
||||
{
|
||||
"name": "hxgamemode",
|
||||
"type": "haxelib",
|
||||
"version": "1.0.1"
|
||||
},
|
||||
{
|
||||
"name": "hxp",
|
||||
"type": "haxelib",
|
||||
@@ -143,55 +153,51 @@
|
||||
{
|
||||
"name": "hxvlc",
|
||||
"type": "haxelib",
|
||||
"version": "2.2.2"
|
||||
"version": "2.2.5"
|
||||
},
|
||||
{
|
||||
"name": "json2object",
|
||||
"type": "git",
|
||||
"dir": null,
|
||||
"ref": "a8c26f18463c98da32f744c214fe02273e1823fa",
|
||||
"ref": "a7e4cef3d444bea8d8244d6e59ab82f5a593f597",
|
||||
"url": "https://github.com/FunkinCrew/json2object"
|
||||
},
|
||||
{
|
||||
"name": "jsonpatch",
|
||||
"type": "git",
|
||||
"dir": null,
|
||||
"ref": "f9b83215acd586dc28754b4ae7f69d4c06c3b4d3",
|
||||
"url": "https://github.com/EliteMasterEric/jsonpatch"
|
||||
"type": "haxelib",
|
||||
"version": "1.1.0"
|
||||
},
|
||||
{
|
||||
"name": "jsonpath",
|
||||
"type": "git",
|
||||
"dir": null,
|
||||
"ref": "7a24193717b36393458c15c0435bb7c4470ecdda",
|
||||
"url": "https://github.com/EliteMasterEric/jsonpath"
|
||||
"type": "haxelib",
|
||||
"version": "1.1.0"
|
||||
},
|
||||
{
|
||||
"name": "lime",
|
||||
"type": "git",
|
||||
"dir": null,
|
||||
"ref": "e5f8c27124598505917a001588b560244731adfb",
|
||||
"ref": "318253ea3a28a3299a167748912c40444f403777",
|
||||
"url": "https://github.com/FunkinCrew/lime"
|
||||
},
|
||||
{
|
||||
"name": "newgrounds",
|
||||
"type": "git",
|
||||
"dir": null,
|
||||
"ref": "c8b30832027fa04f48fe6a45fa7903d5b265d56e",
|
||||
"url": "https://github.com/Geokureli/Newgrounds/"
|
||||
"ref": "b2b53cd0a5f030af74efd325732951cddbea8826",
|
||||
"url": "https://github.com/FunkinCrew/Newgrounds"
|
||||
},
|
||||
{
|
||||
"name": "openfl",
|
||||
"type": "git",
|
||||
"dir": null,
|
||||
"ref": "a0df7c3afe360c9af59a76e45007dbf4e53b5131",
|
||||
"ref": "a69a6e778816c6a57eba7a91bfb0b60295508e88",
|
||||
"url": "https://github.com/FunkinCrew/openfl"
|
||||
},
|
||||
{
|
||||
"name": "polymod",
|
||||
"type": "git",
|
||||
"dir": null,
|
||||
"ref": "55ee00236806330c89c3bff1e1563eb6425a9d96",
|
||||
"ref": "9824bf4f00863c152d5afe3ac4bbcac173d80684",
|
||||
"url": "https://github.com/larsiusprime/polymod"
|
||||
},
|
||||
{
|
||||
|
||||
+691
-88
File diff suppressed because it is too large
Load Diff
+83
-53
@@ -1,18 +1,19 @@
|
||||
package;
|
||||
|
||||
import lime.system.System;
|
||||
import flixel.FlxG;
|
||||
import flixel.FlxGame;
|
||||
import flixel.FlxState;
|
||||
import funkin.ui.FullScreenScaleMode;
|
||||
import funkin.Preferences;
|
||||
import funkin.PlayerSettings;
|
||||
import funkin.util.logging.CrashHandler;
|
||||
import funkin.ui.debug.MemoryCounter;
|
||||
import funkin.ui.debug.FunkinDebugDisplay;
|
||||
import funkin.ui.debug.FunkinDebugDisplay.DebugDisplayMode;
|
||||
import funkin.save.Save;
|
||||
import haxe.ui.Toolkit;
|
||||
#if hxvlc
|
||||
import hxvlc.util.Handle;
|
||||
#end
|
||||
import openfl.display.FPS;
|
||||
import openfl.display.Sprite;
|
||||
import openfl.events.Event;
|
||||
import openfl.Lib;
|
||||
@@ -20,6 +21,8 @@ import openfl.media.Video;
|
||||
import openfl.net.NetStream;
|
||||
import funkin.util.WindowUtil;
|
||||
|
||||
using funkin.util.AnsiUtil;
|
||||
|
||||
/**
|
||||
* The main class which initializes HaxeFlixel and starts the game in its initial state.
|
||||
*/
|
||||
@@ -30,7 +33,6 @@ class Main extends Sprite
|
||||
var initialState:Class<FlxState> = funkin.InitState; // The FlxState the game starts with.
|
||||
var zoom:Float = -1; // If -1, zoom is automatically calculated to fit the window dimensions.
|
||||
var skipSplash:Bool = true; // Whether to skip the flixel splash screen that appears in release mode.
|
||||
var startFullscreen:Bool = false; // Whether to start the game in fullscreen on desktop targets
|
||||
|
||||
// You can pretty much ignore everything from here on - your code should go in your states.
|
||||
|
||||
@@ -60,6 +62,10 @@ class Main extends Sprite
|
||||
haxe.Log.trace = funkin.util.logging.AnsiTrace.trace;
|
||||
funkin.util.logging.AnsiTrace.traceBF();
|
||||
|
||||
// Get OpenFL to stop complaining so much.
|
||||
// You can remove this line if you want to read debug messages.
|
||||
openfl.utils._internal.Log.level = openfl.utils._internal.Log.LogLevel.INFO;
|
||||
|
||||
// Load mods to override assets.
|
||||
// TODO: Replace with loadEnabledMods() once the user can configure the mod list.
|
||||
funkin.modding.PolymodHandler.loadAllMods();
|
||||
@@ -81,36 +87,45 @@ class Main extends Sprite
|
||||
removeEventListener(Event.ADDED_TO_STAGE, init);
|
||||
}
|
||||
|
||||
// Manually crash the game when using a software renderer in order to give a nicer error message.
|
||||
var context = stage.window.context.type;
|
||||
if (context != WEBGL && context != OPENGL && context != OPENGLES)
|
||||
{
|
||||
var tech:String = #if web "WebGL" #elseif desktop "OpenGL" #else "OpenGL ES" #end;
|
||||
var requiredVersion:String = #if web '$tech 1.0 or newer' #elseif desktop '$tech 3.0 or newer' #else '$tech 2.0 or newer' #end;
|
||||
var desc:String = 'Failed to initialize the $tech rendering context!\n\n';
|
||||
#if web
|
||||
desc += 'Make sure your graphics card supports $requiredVersion, your graphics drivers are up to date, and hardware acceleration is enabled on your browser.';
|
||||
#elseif desktop
|
||||
desc += 'Make sure your graphics card supports $requiredVersion, and your graphics drivers are up to date.';
|
||||
#else
|
||||
desc += 'Make sure your device supports $requiredVersion.';
|
||||
#end
|
||||
|
||||
WindowUtil.showError('Failed to initialize $tech', desc);
|
||||
System.exit(1);
|
||||
}
|
||||
|
||||
setupGame();
|
||||
}
|
||||
|
||||
var video:Video;
|
||||
var netStream:NetStream;
|
||||
var overlay:Sprite;
|
||||
|
||||
/**
|
||||
* A frame counter displayed at the top left.
|
||||
* The debug display at the top left.
|
||||
*/
|
||||
public static var fpsCounter:FPS;
|
||||
|
||||
/**
|
||||
* A RAM counter displayed at the top left.
|
||||
*/
|
||||
public static var memoryCounter:MemoryCounter;
|
||||
public static var debugDisplay:FunkinDebugDisplay;
|
||||
|
||||
function setupGame():Void
|
||||
{
|
||||
#if FEATURE_HAXEUI
|
||||
initHaxeUI();
|
||||
|
||||
// addChild gets called by the user settings code.
|
||||
fpsCounter = new FPS(10, 3, 0xFFFFFF);
|
||||
|
||||
#if !html5
|
||||
// addChild gets called by the user settings code.
|
||||
// TODO: disabled on HTML5 (todo: find another method that works?)
|
||||
memoryCounter = new MemoryCounter(10, 13, 0xFFFFFF);
|
||||
#end
|
||||
|
||||
// addChild gets called by the user settings code.
|
||||
debugDisplay = new FunkinDebugDisplay(10, 10, 0xFFFFFF);
|
||||
|
||||
// Add this signal so the player can toggle the debug display using a hotkey.
|
||||
FlxG.signals.postUpdate.add(handleDebugDisplayKeys);
|
||||
|
||||
#if mobile
|
||||
// Add this signal so we can reposition and resize the memory and fps counter.
|
||||
FlxG.signals.preUpdate.add(repositionCounters.bind(true));
|
||||
@@ -121,7 +136,16 @@ class Main extends Sprite
|
||||
|
||||
#if hxvlc
|
||||
// Initialize hxvlc's Handle here so the videos are loading faster.
|
||||
Handle.init();
|
||||
Handle.initAsync(function(success:Bool):Void {
|
||||
if (success)
|
||||
{
|
||||
trace(' HXVLC '.bold().bg_orange() + ' LibVLC instance initialized!');
|
||||
}
|
||||
else
|
||||
{
|
||||
trace(' HXVLC '.bold().bg_orange() + ' LibVLC instance failed to initialize!');
|
||||
}
|
||||
});
|
||||
#end
|
||||
|
||||
// Don't call anything from the preferences until the save is loaded!
|
||||
@@ -133,7 +157,8 @@ class Main extends Sprite
|
||||
|
||||
WindowUtil.setVSyncMode(funkin.Preferences.vsyncMode);
|
||||
|
||||
var game:FlxGame = new FlxGame(gameWidth, gameHeight, initialState, Preferences.framerate, Preferences.framerate, skipSplash, startFullscreen);
|
||||
var game:FlxGame = new FlxGame(gameWidth, gameHeight, initialState, Preferences.framerate, Preferences.framerate, skipSplash,
|
||||
(FlxG.stage.window.fullscreen || Preferences.autoFullscreen));
|
||||
|
||||
// FlxG.game._customSoundTray wants just the class, it calls new from
|
||||
// create() in there, which gets called when it's added to the stage
|
||||
@@ -164,20 +189,43 @@ class Main extends Sprite
|
||||
#end
|
||||
}
|
||||
|
||||
#if FEATURE_HAXEUI
|
||||
function initHaxeUI():Void
|
||||
{
|
||||
// This has to come before Toolkit.init since locales get initialized there
|
||||
haxe.ui.locale.LocaleManager.instance.autoSetLocale = false;
|
||||
// Calling this before any HaxeUI components get used is important:
|
||||
// - It initializes the theme styles.
|
||||
// - It scans the class path and registers any HaxeUI components.
|
||||
Toolkit.init();
|
||||
Toolkit.theme = 'dark'; // don't be cringe
|
||||
// Toolkit.theme = 'light'; // embrace cringe
|
||||
Toolkit.autoScale = false;
|
||||
haxe.ui.Toolkit.init();
|
||||
haxe.ui.Toolkit.theme = 'dark'; // don't be cringe
|
||||
// haxe.ui.Toolkit.theme = 'light'; // embrace cringe
|
||||
haxe.ui.Toolkit.autoScale = false;
|
||||
// Don't focus on UI elements when they first appear.
|
||||
haxe.ui.focus.FocusManager.instance.autoFocus = false;
|
||||
funkin.input.Cursor.registerHaxeUICursors();
|
||||
haxe.ui.tooltips.ToolTipManager.defaultDelay = 200;
|
||||
}
|
||||
#end
|
||||
|
||||
function handleDebugDisplayKeys():Void
|
||||
{
|
||||
if (PlayerSettings.player1.controls == null || !PlayerSettings.player1.controls.check(DEBUG_DISPLAY)) return;
|
||||
|
||||
var nextMode:DebugDisplayMode;
|
||||
|
||||
switch (Preferences.debugDisplay)
|
||||
{
|
||||
case DebugDisplayMode.Off:
|
||||
nextMode = DebugDisplayMode.Simple;
|
||||
case DebugDisplayMode.Simple:
|
||||
nextMode = DebugDisplayMode.Advanced;
|
||||
case DebugDisplayMode.Advanced:
|
||||
nextMode = DebugDisplayMode.Off;
|
||||
}
|
||||
|
||||
Preferences.debugDisplay = nextMode;
|
||||
}
|
||||
|
||||
#if mobile
|
||||
function repositionCounters(lerp:Bool):Void
|
||||
@@ -191,41 +239,23 @@ class Main extends Sprite
|
||||
scale = Math.min(scale, 1);
|
||||
#end
|
||||
final thypos:Float = Math.max(FullScreenScaleMode.notchSize.x, 10);
|
||||
if (fpsCounter != null)
|
||||
|
||||
if (debugDisplay != null)
|
||||
{
|
||||
fpsCounter.scaleX = fpsCounter.scaleY = scale;
|
||||
debugDisplay.scaleX = debugDisplay.scaleY = scale;
|
||||
|
||||
if (FlxG.game != null)
|
||||
{
|
||||
if (lerp)
|
||||
{
|
||||
fpsCounter.x = flixel.math.FlxMath.lerp(fpsCounter.x, FlxG.game.x + thypos, FlxG.elapsed * 3);
|
||||
debugDisplay.x = flixel.math.FlxMath.lerp(debugDisplay.x, FlxG.game.x + thypos, FlxG.elapsed * 3);
|
||||
}
|
||||
else
|
||||
{
|
||||
fpsCounter.x = FlxG.game.x + FullScreenScaleMode.notchSize.x + 10;
|
||||
debugDisplay.x = FlxG.game.x + FullScreenScaleMode.notchSize.x + 10;
|
||||
}
|
||||
|
||||
fpsCounter.y = FlxG.game.y + (3 * scale);
|
||||
}
|
||||
}
|
||||
|
||||
if (memoryCounter != null)
|
||||
{
|
||||
memoryCounter.scaleX = memoryCounter.scaleY = scale;
|
||||
|
||||
if (FlxG.game != null)
|
||||
{
|
||||
if (lerp)
|
||||
{
|
||||
memoryCounter.x = flixel.math.FlxMath.lerp(memoryCounter.x, FlxG.game.x + thypos, FlxG.elapsed * 3);
|
||||
}
|
||||
else
|
||||
{
|
||||
memoryCounter.x = FlxG.game.x + FullScreenScaleMode.notchSize.x + 10;
|
||||
}
|
||||
|
||||
memoryCounter.y = FlxG.game.y + (13 * scale);
|
||||
debugDisplay.y = FlxG.game.y + (3 * scale);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+26
-5
@@ -3,6 +3,9 @@ package source; // Yeah, I know...
|
||||
import sys.FileSystem;
|
||||
import sys.io.File;
|
||||
|
||||
using StringTools;
|
||||
using tools.AnsiUtil;
|
||||
|
||||
/**
|
||||
* A script which executes after the game is built.
|
||||
*/
|
||||
@@ -27,14 +30,32 @@ class Postbuild
|
||||
|
||||
sys.FileSystem.deleteFile(BUILD_TIME_FILE);
|
||||
|
||||
var buildTime:Float = roundToTwoDecimals(end - start);
|
||||
|
||||
Sys.println('[INFO] Build took: ${buildTime} seconds');
|
||||
Sys.println(' INFO '.info() + ' Build took: ${format(end - start)}');
|
||||
}
|
||||
}
|
||||
|
||||
static function roundToTwoDecimals(value:Float):Float
|
||||
static function format(time:Float, decimals:Int = 1):String
|
||||
{
|
||||
return Math.round(value * 100) / 100;
|
||||
var units = [
|
||||
{name: "day", secs: 86400},
|
||||
{name: "hour", secs: 3600},
|
||||
{name: "minute", secs: 60},
|
||||
{name: "second", secs: 1}
|
||||
];
|
||||
|
||||
var parts:Array<String> = [];
|
||||
var remaining:Float = time;
|
||||
var factor = Math.pow(10, decimals); // compute once because the old code was computing it twice.
|
||||
|
||||
for (u in units)
|
||||
{
|
||||
var value:Float = (u.name == "second") ? Math.round(remaining * factor) / factor : Math.floor(remaining / u.secs);
|
||||
|
||||
if (u.name != "second") remaining %= u.secs;
|
||||
|
||||
if (value > 0 || (u.name == "second" && parts.length == 0)) parts.push('${value} ${u.name}${value == 1 ? "" : "s"}');
|
||||
}
|
||||
|
||||
return parts.join(" ");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,13 +1,14 @@
|
||||
package funkin;
|
||||
|
||||
import openfl.utils.Future;
|
||||
import funkin.util.macro.ConsoleMacro;
|
||||
|
||||
/**
|
||||
* A wrapper around `openfl.utils.Assets` which disallows access to the harmful functions.
|
||||
* Later we'll add Funkin-specific caching to this.
|
||||
*/
|
||||
@:nullSafety
|
||||
class Assets
|
||||
class Assets implements ConsoleClass
|
||||
{
|
||||
/**
|
||||
* The assets cache.
|
||||
@@ -114,9 +115,9 @@ class Assets
|
||||
* @param path The asset path to load from
|
||||
* @return The loaded Bitmap image
|
||||
*/
|
||||
public static function getBitmapData(path:String):openfl.display.BitmapData
|
||||
public static function getBitmapData(path:String, useCache:Bool = true):openfl.display.BitmapData
|
||||
{
|
||||
return openfl.utils.Assets.getBitmapData(path);
|
||||
return openfl.utils.Assets.getBitmapData(path, useCache);
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
+222
-42
@@ -18,20 +18,32 @@ import flixel.sound.FlxSound;
|
||||
@:nullSafety
|
||||
class Conductor
|
||||
{
|
||||
// onBeatHit is called every quarter note
|
||||
// onStepHit is called every sixteenth note
|
||||
// onBeatHit is called on every note determined by the denominator (4 = quarter, 8 = eighth, etc.)
|
||||
// onStepHit is called on every note which is a quarter of a beat (4 = sixteenth, 8 = thirty-second, etc.)
|
||||
// 4/4 = 4 beats per measure = 16 steps per measure
|
||||
// 120 BPM = 120 quarter notes per minute = 2 onBeatHit per second
|
||||
// 120 BPM = 480 sixteenth notes per minute = 8 onStepHit per second
|
||||
// 60 BPM = 60 quarter notes per minute = 1 onBeatHit per second
|
||||
// 60 BPM = 240 sixteenth notes per minute = 4 onStepHit per second
|
||||
// 3/4 = 3 beats per measure = 12 steps per measure
|
||||
// (IDENTICAL TO 4/4 but shorter measure length)
|
||||
// (Identical to 4/4 but has a shorter measure length)
|
||||
// 120 BPM = 120 quarter notes per minute = 2 onBeatHit per second
|
||||
// 120 BPM = 480 sixteenth notes per minute = 8 onStepHit per second
|
||||
// 60 BPM = 60 quarter notes per minute = 1 onBeatHit per second
|
||||
// 60 BPM = 240 sixteenth notes per minute = 4 onStepHit per second
|
||||
// 7/8 = 3.5 beats per measure = 14 steps per measure
|
||||
// 7/8 = 7 beats per measure = 28 steps per measure
|
||||
// Beats are EIGHTH NOTES!!
|
||||
// (Identical to 7/4 but beats happen twice as fast)
|
||||
// 120 BPM = 240 eighth notes per minute = 4 onBeatHit per second
|
||||
// 120 BPM = 960 twenty-second notes per minute = 16 onStepHit per second
|
||||
// 15/16 = 15 beats per measure = 60 steps per measure
|
||||
// Beats are SIXTEENTH NOTES!!
|
||||
// 120 BPM = 480 sixteenth notes per minute = 8 onBeatHit per second
|
||||
// 120 BPM = 1920 sixty-fourth notes per minute = 32 onStepHit per second
|
||||
// 3/2 = 3 beats per measure = 12 steps per measure
|
||||
// Beats are HALF NOTES!!
|
||||
// 120 BPM = 60 half notes per minute = 1 onBeatHit per second
|
||||
// 120 BPM = 240 eighth notes per minute = 4 onStepHit per second
|
||||
|
||||
/**
|
||||
* The current instance of the Conductor.
|
||||
@@ -113,7 +125,6 @@ class Conductor
|
||||
if (bpmOverride != null) return bpmOverride;
|
||||
|
||||
if (currentTimeChange == null) return Constants.DEFAULT_BPM;
|
||||
|
||||
@:privateAccess
|
||||
if (PlayState.instance != null && PlayState.instance.startingSong)
|
||||
{
|
||||
@@ -160,24 +171,24 @@ class Conductor
|
||||
}
|
||||
|
||||
/**
|
||||
* Duration of a beat (quarter note) in milliseconds. Calculated based on bpm.
|
||||
* Duration of a beat in milliseconds. Calculated based on bpm.
|
||||
*/
|
||||
public var beatLengthMs(get, never):Float;
|
||||
|
||||
function get_beatLengthMs():Float
|
||||
{
|
||||
// Tied directly to BPM.
|
||||
return ((Constants.SECS_PER_MIN / bpm) * Constants.MS_PER_SEC);
|
||||
return ((Constants.SECS_PER_MIN / bpm) * Constants.MS_PER_SEC) * (4 / timeSignatureDenominator);
|
||||
}
|
||||
|
||||
/**
|
||||
* Duration of a step (sixtennth note) in milliseconds. Calculated based on bpm.
|
||||
* Duration of a step in milliseconds. Calculated based on bpm.
|
||||
*/
|
||||
public var stepLengthMs(get, never):Float;
|
||||
|
||||
function get_stepLengthMs():Float
|
||||
{
|
||||
return beatLengthMs / timeSignatureNumerator;
|
||||
return beatLengthMs / Constants.STEPS_PER_BEAT;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -246,7 +257,7 @@ class Conductor
|
||||
|
||||
function get_instrumentalOffsetSteps():Float
|
||||
{
|
||||
var startingStepLengthMs:Float = ((Constants.SECS_PER_MIN / startingBPM) * Constants.MS_PER_SEC) / timeSignatureNumerator;
|
||||
var startingStepLengthMs:Float = (((Constants.SECS_PER_MIN / startingBPM) * Constants.MS_PER_SEC) * (4 / timeSignatureDenominator)) / Constants.STEPS_PER_BEAT;
|
||||
|
||||
return instrumentalOffset / startingStepLengthMs;
|
||||
}
|
||||
@@ -288,26 +299,23 @@ class Conductor
|
||||
}
|
||||
|
||||
/**
|
||||
* The number of beats in a measure. May be fractional depending on the time signature.
|
||||
* The number of beats in a measure.
|
||||
*/
|
||||
public var beatsPerMeasure(get, never):Float;
|
||||
|
||||
function get_beatsPerMeasure():Float
|
||||
{
|
||||
// NOTE: Not always an integer, for example 7/8 is 3.5 beats per measure
|
||||
return stepsPerMeasure / Constants.STEPS_PER_BEAT;
|
||||
return timeSignatureNumerator;
|
||||
}
|
||||
|
||||
/**
|
||||
* The number of steps in a measure.
|
||||
* TODO: I don't think this can be fractional?
|
||||
*/
|
||||
public var stepsPerMeasure(get, never):Int;
|
||||
|
||||
function get_stepsPerMeasure():Int
|
||||
{
|
||||
// TODO: Is this always an integer?
|
||||
return Std.int(timeSignatureNumerator / timeSignatureDenominator * Constants.STEPS_PER_BEAT * Constants.STEPS_PER_BEAT);
|
||||
return Std.int(timeSignatureNumerator * Constants.STEPS_PER_BEAT);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -389,11 +397,11 @@ class Conductor
|
||||
{
|
||||
if (bpm != null)
|
||||
{
|
||||
trace('[CONDUCTOR] Forcing BPM to ${bpm}');
|
||||
log('Forcing BPM to ${bpm}');
|
||||
}
|
||||
else
|
||||
{
|
||||
trace('[CONDUCTOR] Resetting BPM to default');
|
||||
log('Resetting BPM to default');
|
||||
}
|
||||
|
||||
this.bpmOverride = bpm;
|
||||
@@ -452,7 +460,7 @@ class Conductor
|
||||
|
||||
if (currentTimeChange == null && bpmOverride == null && FlxG.sound.music != null)
|
||||
{
|
||||
trace('WARNING: Conductor is broken, timeChanges is empty.');
|
||||
log(' WARNING '.warning() + 'Conductor is broken, timeChanges is empty.');
|
||||
}
|
||||
else if (currentTimeChange != null && this.songPosition > 0.0)
|
||||
{
|
||||
@@ -460,7 +468,7 @@ class Conductor
|
||||
this.currentStepTime = FlxMath.roundDecimal((currentTimeChange.beatTime * Constants.STEPS_PER_BEAT)
|
||||
+ (this.songPosition - currentTimeChange.timeStamp) / stepLengthMs, 6);
|
||||
this.currentBeatTime = currentStepTime / Constants.STEPS_PER_BEAT;
|
||||
this.currentMeasureTime = currentStepTime / stepsPerMeasure;
|
||||
this.currentMeasureTime = getTimeInMeasures(this.songPosition);
|
||||
this.currentStep = Math.floor(currentStepTime);
|
||||
this.currentBeat = Math.floor(currentBeatTime);
|
||||
this.currentMeasure = Math.floor(currentMeasureTime);
|
||||
@@ -517,7 +525,7 @@ class Conductor
|
||||
|
||||
/**
|
||||
* Can be called in-between frames, usually for input related things
|
||||
* that can potentially get processed on exact milliseconds/timestmaps.
|
||||
* that can potentially get processed on exact milliseconds/timestamps.
|
||||
* If you need song position, use `Conductor.instance.songPosition` instead
|
||||
* for use in update() related functions.
|
||||
* @param soundToCheck Which FlxSound object to check, defaults to FlxG.sound.music if no input
|
||||
@@ -526,12 +534,9 @@ class Conductor
|
||||
public function getTimeWithDiff(?soundToCheck:FlxSound):Float
|
||||
{
|
||||
if (soundToCheck == null) soundToCheck = FlxG.sound.music;
|
||||
// trace(this.songPosition);
|
||||
|
||||
@:privateAccess
|
||||
this.songPosition = soundToCheck._channel.position;
|
||||
// return this.songPosition + (Std.int(Timer.stamp() * 1000) - prevTimestamp);
|
||||
// trace("\t--> " + this.songPosition);
|
||||
return this.songPosition;
|
||||
}
|
||||
|
||||
@@ -566,7 +571,8 @@ class Conductor
|
||||
{
|
||||
var prevTimeChange:SongTimeChange = timeChanges[timeChanges.length - 1];
|
||||
songTimeChange.beatTime = FlxMath.roundDecimal(prevTimeChange.beatTime
|
||||
+ ((songTimeChange.timeStamp - prevTimeChange.timeStamp) * prevTimeChange.bpm / Constants.SECS_PER_MIN / Constants.MS_PER_SEC),
|
||||
+
|
||||
((songTimeChange.timeStamp - prevTimeChange.timeStamp) * prevTimeChange.bpm / Constants.SECS_PER_MIN / Constants.MS_PER_SEC * (prevTimeChange.timeSignatureDen / 4)),
|
||||
4);
|
||||
}
|
||||
}
|
||||
@@ -574,15 +580,118 @@ class Conductor
|
||||
timeChanges.push(songTimeChange);
|
||||
}
|
||||
|
||||
if (timeChanges.length > 0)
|
||||
if (timeChanges.length == 1)
|
||||
{
|
||||
trace('Done mapping time changes: ${timeChanges}');
|
||||
log('Done mapping single time change to ${timeChanges[0].bpm} BPM');
|
||||
}
|
||||
else if (timeChanges.length > 1)
|
||||
{
|
||||
log('Done mapping ${timeChanges.length} time changes (starting at ${timeChanges[0].bpm} BPM)');
|
||||
}
|
||||
else
|
||||
{
|
||||
log(' WARNING '.warning() + ' Conductor mapped no time changes?');
|
||||
}
|
||||
|
||||
// Update currentStepTime
|
||||
this.update(this.songPosition, false);
|
||||
}
|
||||
|
||||
/**
|
||||
* Given a time in milliseconds, return a time in measures.
|
||||
* @param ms The time in milliseconds.
|
||||
* @return The time in measures.
|
||||
*/
|
||||
public function getTimeInMeasures(ms:Float):Float
|
||||
{
|
||||
if (timeChanges.length == 0)
|
||||
{
|
||||
// Assume a constant BPM equal to the forced value.
|
||||
return ms / stepLengthMs / stepsPerMeasure;
|
||||
}
|
||||
else
|
||||
{
|
||||
var resultMeasureTime:Float = 0;
|
||||
ms = ms < 0 ? 0 : ms;
|
||||
|
||||
var lastTimeChange:SongTimeChange = timeChanges[0];
|
||||
var i:Int = -1;
|
||||
for (timeChange in timeChanges)
|
||||
{
|
||||
if (ms >= timeChange.timeStamp)
|
||||
{
|
||||
// We do NOT want to add the current time change's MEASURE LENGTH to the result, same for if we're inside the song's last time change.
|
||||
if (ms < timeChange.timeStamp || i == timeChanges.length - 1)
|
||||
{
|
||||
// However, we still want this for the ending calculation.
|
||||
lastTimeChange = timeChange;
|
||||
break;
|
||||
}
|
||||
var currentStepLengthMs:Float = (((Constants.SECS_PER_MIN / lastTimeChange.bpm) * Constants.MS_PER_SEC) * (4 / lastTimeChange.timeSignatureDen)) / Constants.STEPS_PER_BEAT;
|
||||
var currentStepsPerMeasure:Int = lastTimeChange.timeSignatureNum * Constants.STEPS_PER_BEAT;
|
||||
resultMeasureTime += (timeChange.timeStamp - lastTimeChange.timeStamp) / currentStepLengthMs / currentStepsPerMeasure;
|
||||
lastTimeChange = timeChange;
|
||||
}
|
||||
i++;
|
||||
}
|
||||
|
||||
var remainingStepLengthMs:Float = (((Constants.SECS_PER_MIN / lastTimeChange.bpm) * Constants.MS_PER_SEC) * (4 / lastTimeChange.timeSignatureDen)) / Constants.STEPS_PER_BEAT;
|
||||
var remainingStepsPerMeasure:Int = lastTimeChange.timeSignatureNum * Constants.STEPS_PER_BEAT;
|
||||
var remainingFractionalMeasure:Float = (ms - lastTimeChange.timeStamp) / remainingStepLengthMs / remainingStepsPerMeasure;
|
||||
resultMeasureTime += remainingFractionalMeasure;
|
||||
|
||||
return resultMeasureTime;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Given a time in measures and fractional measures, return a time in milliseconds.
|
||||
* @param measureTime The time in measures.
|
||||
* @return The time in milliseconds.
|
||||
*/
|
||||
public function getMeasureTimeInMs(measureTime:Float):Float
|
||||
{
|
||||
if (timeChanges.length == 0)
|
||||
{
|
||||
// Assume a constant BPM equal to the forced value.
|
||||
return measureTime * stepLengthMs * stepsPerMeasure;
|
||||
}
|
||||
else
|
||||
{
|
||||
var resultMs:Float = 0;
|
||||
measureTime = measureTime < 0 ? 0 : measureTime;
|
||||
|
||||
var lastTimeChange:SongTimeChange = timeChanges[0];
|
||||
var i:Int = -1;
|
||||
for (timeChange in timeChanges)
|
||||
{
|
||||
var currentTimeChangeMeasureTime:Float = getTimeInMeasures(timeChange.timeStamp);
|
||||
if (measureTime >= currentTimeChangeMeasureTime)
|
||||
{
|
||||
// We do NOT want to add the current time change's MEASURE LENGTH to the result, same for if we're inside the song's last time change.
|
||||
if (measureTime < currentTimeChangeMeasureTime || i == timeChanges.length - 1)
|
||||
{
|
||||
// However, we still want this for the ending calculation.
|
||||
lastTimeChange = timeChange;
|
||||
break;
|
||||
}
|
||||
var currentStepLengthMs:Float = (((Constants.SECS_PER_MIN / lastTimeChange.bpm) * Constants.MS_PER_SEC) * (4 / lastTimeChange.timeSignatureDen)) / Constants.STEPS_PER_BEAT;
|
||||
var currentStepsPerMeasure:Int = lastTimeChange.timeSignatureNum * Constants.STEPS_PER_BEAT;
|
||||
resultMs += (currentTimeChangeMeasureTime - getTimeInMeasures(lastTimeChange.timeStamp)) * currentStepLengthMs * currentStepsPerMeasure;
|
||||
lastTimeChange = timeChange;
|
||||
}
|
||||
i++;
|
||||
}
|
||||
|
||||
var remainingStepLengthMs:Float = (((Constants.SECS_PER_MIN / lastTimeChange.bpm) * Constants.MS_PER_SEC) * (4 / lastTimeChange.timeSignatureDen)) / Constants.STEPS_PER_BEAT;
|
||||
var remainingStepsPerMeasure:Int = lastTimeChange.timeSignatureNum * Constants.STEPS_PER_BEAT;
|
||||
var remainingFractionalMeasure:Float = (measureTime - getTimeInMeasures(lastTimeChange.timeStamp)) * remainingStepLengthMs * remainingStepsPerMeasure;
|
||||
resultMs += remainingFractionalMeasure;
|
||||
|
||||
return resultMs;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Given a time in milliseconds, return a time in steps.
|
||||
* @param ms The time in milliseconds.
|
||||
@@ -598,23 +707,28 @@ class Conductor
|
||||
else
|
||||
{
|
||||
var resultStep:Float = 0;
|
||||
ms = ms < 0 ? 0 : ms;
|
||||
|
||||
var lastTimeChange:SongTimeChange = timeChanges[0];
|
||||
var i:Int = -1;
|
||||
for (timeChange in timeChanges)
|
||||
{
|
||||
if (ms >= timeChange.timeStamp)
|
||||
{
|
||||
// We do NOT want to add the current time change's STEP LENGTH to the result, same for if we're inside the song's last time change.
|
||||
if (ms < timeChange.timeStamp || i == timeChanges.length - 1)
|
||||
{
|
||||
// However, we still want this for the ending calculation.
|
||||
lastTimeChange = timeChange;
|
||||
break;
|
||||
}
|
||||
resultStep += (timeChange.beatTime - lastTimeChange.beatTime) * Constants.STEPS_PER_BEAT;
|
||||
lastTimeChange = timeChange;
|
||||
resultStep = lastTimeChange.beatTime * Constants.STEPS_PER_BEAT;
|
||||
}
|
||||
else
|
||||
{
|
||||
// This time change is after the requested time.
|
||||
break;
|
||||
}
|
||||
i++;
|
||||
}
|
||||
|
||||
var lastStepLengthMs:Float = ((Constants.SECS_PER_MIN / lastTimeChange.bpm) * Constants.MS_PER_SEC) / timeSignatureNumerator;
|
||||
var lastStepLengthMs:Float = (((Constants.SECS_PER_MIN / lastTimeChange.bpm) * Constants.MS_PER_SEC) * (4 / lastTimeChange.timeSignatureDen)) / Constants.STEPS_PER_BEAT;
|
||||
var resultFractionalStep:Float = (ms - lastTimeChange.timeStamp) / lastStepLengthMs;
|
||||
resultStep += resultFractionalStep;
|
||||
|
||||
@@ -637,23 +751,28 @@ class Conductor
|
||||
else
|
||||
{
|
||||
var resultMs:Float = 0;
|
||||
stepTime = stepTime < 0 ? 0 : stepTime;
|
||||
|
||||
var lastTimeChange:SongTimeChange = timeChanges[0];
|
||||
var i:Int = -1;
|
||||
for (timeChange in timeChanges)
|
||||
{
|
||||
if (stepTime >= timeChange.beatTime * Constants.STEPS_PER_BEAT)
|
||||
{
|
||||
// We do NOT want to add the current time change's TIME LENGTH to the result, same for if we're inside the song's last time change.
|
||||
if (stepTime < (timeChange.beatTime * Constants.STEPS_PER_BEAT) || i == timeChanges.length - 1)
|
||||
{
|
||||
// However, we still want this for the ending calculation.
|
||||
lastTimeChange = timeChange;
|
||||
break;
|
||||
}
|
||||
resultMs += timeChange.timeStamp - lastTimeChange.timeStamp;
|
||||
lastTimeChange = timeChange;
|
||||
resultMs = lastTimeChange.timeStamp;
|
||||
}
|
||||
else
|
||||
{
|
||||
// This time change is after the requested time.
|
||||
break;
|
||||
}
|
||||
i++;
|
||||
}
|
||||
|
||||
var lastStepLengthMs:Float = ((Constants.SECS_PER_MIN / lastTimeChange.bpm) * Constants.MS_PER_SEC) / timeSignatureNumerator;
|
||||
var lastStepLengthMs:Float = (((Constants.SECS_PER_MIN / lastTimeChange.bpm) * Constants.MS_PER_SEC) * (4 / lastTimeChange.timeSignatureDen)) / Constants.STEPS_PER_BEAT;
|
||||
resultMs += (stepTime - lastTimeChange.beatTime * Constants.STEPS_PER_BEAT) * lastStepLengthMs;
|
||||
|
||||
return resultMs;
|
||||
@@ -691,13 +810,69 @@ class Conductor
|
||||
}
|
||||
}
|
||||
|
||||
var lastStepLengthMs:Float = ((Constants.SECS_PER_MIN / lastTimeChange.bpm) * Constants.MS_PER_SEC) / timeSignatureNumerator;
|
||||
var lastStepLengthMs:Float = (((Constants.SECS_PER_MIN / lastTimeChange.bpm) * Constants.MS_PER_SEC) * (4 / lastTimeChange.timeSignatureDen)) / Constants.STEPS_PER_BEAT;
|
||||
resultMs += (beatTime - lastTimeChange.beatTime) * lastStepLengthMs * Constants.STEPS_PER_BEAT;
|
||||
|
||||
return resultMs;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Given a time in milliseconds, return the time change that time is inside of.
|
||||
* @param ms The time in milliseconds.
|
||||
* @return The resulting time change.
|
||||
*/
|
||||
public function getTimeChange(ms:Float):SongTimeChange
|
||||
{
|
||||
if (timeChanges.length == 0)
|
||||
{
|
||||
return new SongTimeChange(0, 100);
|
||||
}
|
||||
var i:Int = 0;
|
||||
ms = ms < 0 ? 0 : ms;
|
||||
for (timeChange in timeChanges)
|
||||
{
|
||||
i++;
|
||||
if ((i == timeChanges.length && ms >= timeChange.timeStamp) || (ms >= timeChange.timeStamp && ms < timeChanges[i].timeStamp))
|
||||
{
|
||||
return timeChange;
|
||||
}
|
||||
}
|
||||
return new SongTimeChange(0, 100);
|
||||
}
|
||||
|
||||
/**
|
||||
* An all-in-one function for getting either a step, beat, or measure's length in milliseconds from a given time change.
|
||||
* @param ms The time in milliseconds. The time change is determined by this.
|
||||
* @param type The type of length to return. Either "step", "beat", or "measure" works, along with their first character.
|
||||
* @return The length of a step/beat/measure in milliseconds.
|
||||
*/
|
||||
public function getTypeLengthAtMs(ms:Float, type:String = "beat"):Float
|
||||
{
|
||||
if (timeChanges.length == 0) return 0;
|
||||
var wantedTimeChange:SongTimeChange = timeChanges[0];
|
||||
for (timeChange in timeChanges)
|
||||
{
|
||||
if (ms >= timeChange.timeStamp)
|
||||
{
|
||||
wantedTimeChange = timeChange;
|
||||
}
|
||||
else
|
||||
{
|
||||
// This time change is after the requested time.
|
||||
break;
|
||||
}
|
||||
}
|
||||
var wantedBeatLengthMs:Float = ((Constants.SECS_PER_MIN / wantedTimeChange.bpm) * Constants.MS_PER_SEC) * (4 / wantedTimeChange.timeSignatureDen);
|
||||
return switch (type.toLowerCase())
|
||||
{
|
||||
case "measure", "m": wantedBeatLengthMs * wantedTimeChange.timeSignatureNum;
|
||||
case "beat", "b": wantedBeatLengthMs;
|
||||
case "step", "s": wantedBeatLengthMs / Constants.STEPS_PER_BEAT;
|
||||
default: wantedBeatLengthMs;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds Conductor fields to the Flixel debugger variable display.
|
||||
* @param conductorToUse The conductor to use. Defaults to `Conductor.instance`.
|
||||
@@ -712,4 +887,9 @@ class Conductor
|
||||
FlxG.watch.addQuick('currentBeatTime', target.currentBeatTime);
|
||||
FlxG.watch.addQuick('currentStepTime', target.currentStepTime);
|
||||
}
|
||||
|
||||
static function log(message:String):Void
|
||||
{
|
||||
trace(' CONDUCTOR '.bg_purple().bold() + ' ${message}');
|
||||
}
|
||||
}
|
||||
|
||||
@@ -111,6 +111,16 @@ class FunkinMemory
|
||||
|
||||
///// TEXTURES /////
|
||||
|
||||
/**
|
||||
* Determine whether the texture with the given key is cached.
|
||||
* @param key The key of the texture to check.
|
||||
* @return Whether the texture is cached.
|
||||
*/
|
||||
public static function isTextureCached(key:String):Bool
|
||||
{
|
||||
return FlxG.bitmap.get(key) != null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Ensures a texture with the given key is cached.
|
||||
* @param key The key of the texture to cache.
|
||||
@@ -135,7 +145,7 @@ class FunkinMemory
|
||||
return;
|
||||
}
|
||||
|
||||
trace('Successfully cached graphic: $key');
|
||||
log('Cached asset $key');
|
||||
graphic.persist = true;
|
||||
currentCachedTextures.set(key, graphic);
|
||||
forceRender(graphic);
|
||||
@@ -156,11 +166,11 @@ class FunkinMemory
|
||||
return;
|
||||
}
|
||||
|
||||
trace('Successfully cached graphic: $key');
|
||||
log('Cached graphic $key');
|
||||
graphic.persist = true;
|
||||
permanentCachedTextures.set(key, graphic);
|
||||
forceRender(graphic);
|
||||
currentCachedTextures = permanentCachedTextures;
|
||||
currentCachedTextures = permanentCachedTextures.copy();
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -177,6 +187,7 @@ class FunkinMemory
|
||||
var sprite = new flixel.FlxSprite();
|
||||
sprite.loadGraphic(graphic);
|
||||
sprite.draw(); // Draw sprite and load it into game's memory.
|
||||
graphic.bitmap?.getTexture(FlxG.stage.context3D); // Just in case that didn't work...
|
||||
sprite.destroy();
|
||||
}
|
||||
|
||||
@@ -200,7 +211,7 @@ class FunkinMemory
|
||||
*/
|
||||
public inline static function preparePurgeTextureCache():Void
|
||||
{
|
||||
previousCachedTextures = currentCachedTextures;
|
||||
previousCachedTextures = currentCachedTextures.copy();
|
||||
|
||||
for (graphicKey in previousCachedTextures.keys())
|
||||
{
|
||||
@@ -210,7 +221,7 @@ class FunkinMemory
|
||||
}
|
||||
}
|
||||
|
||||
currentCachedTextures = permanentCachedTextures;
|
||||
currentCachedTextures = permanentCachedTextures.copy();
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -232,6 +243,7 @@ class FunkinMemory
|
||||
if (graphic != null)
|
||||
{
|
||||
FlxG.bitmap.remove(graphic);
|
||||
graphic.persist = false;
|
||||
graphic.destroy();
|
||||
previousCachedTextures.remove(graphicKey);
|
||||
Assets.cache.clear(graphicKey);
|
||||
@@ -249,7 +261,7 @@ class FunkinMemory
|
||||
{
|
||||
var obj:Null<FlxGraphic> = FlxG.bitmap.get(key);
|
||||
|
||||
if (obj == null || obj.persist || permanentCachedTextures.exists(key) || key.contains("fonts"))
|
||||
if (obj == null || (obj.persist && permanentCachedTextures.exists(key)) || key.contains("fonts"))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
@@ -261,6 +273,7 @@ class FunkinMemory
|
||||
if (key.contains(purgeEntry))
|
||||
{
|
||||
FlxG.bitmap.removeKey(key);
|
||||
obj.persist = false;
|
||||
obj.destroy();
|
||||
}
|
||||
}
|
||||
@@ -345,7 +358,7 @@ class FunkinMemory
|
||||
|
||||
public static function preparePurgeSoundCache():Void
|
||||
{
|
||||
previousCachedSounds = currentCachedSounds;
|
||||
previousCachedSounds = currentCachedSounds.copy();
|
||||
|
||||
for (key in previousCachedSounds.keys())
|
||||
{
|
||||
@@ -355,7 +368,7 @@ class FunkinMemory
|
||||
}
|
||||
}
|
||||
|
||||
currentCachedSounds = permanentCachedSounds;
|
||||
currentCachedSounds = permanentCachedSounds.copy();
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -408,7 +421,7 @@ class FunkinMemory
|
||||
@:privateAccess
|
||||
for (key in keysToRemove)
|
||||
{
|
||||
trace('Cleaning up $key');
|
||||
log('Cleaning asset $key');
|
||||
var obj:Null<FlxGraphic> = FlxG.bitmap.get(key);
|
||||
if (obj != null)
|
||||
{
|
||||
@@ -439,7 +452,7 @@ class FunkinMemory
|
||||
@:privateAccess
|
||||
for (key in keysToRemove)
|
||||
{
|
||||
trace('Cleaning up $key');
|
||||
log('Cleaning asset $key');
|
||||
var obj:Null<FlxGraphic> = FlxG.bitmap.get(key);
|
||||
if (obj != null)
|
||||
{
|
||||
@@ -450,4 +463,9 @@ class FunkinMemory
|
||||
Assets.cache.clear(key);
|
||||
}
|
||||
}
|
||||
|
||||
static function log(message:String):Void
|
||||
{
|
||||
trace(' MEMORY '.bg_bright_lilac().bold() + ' ${message}');
|
||||
}
|
||||
}
|
||||
|
||||
+98
-16
@@ -24,7 +24,7 @@ import funkin.data.event.SongEventRegistry;
|
||||
import funkin.data.stage.StageRegistry;
|
||||
import funkin.data.story.level.LevelRegistry;
|
||||
import funkin.modding.module.ModuleHandler;
|
||||
import funkin.play.character.CharacterData.CharacterDataParser;
|
||||
import funkin.data.character.CharacterData.CharacterDataParser;
|
||||
import funkin.play.notes.notekind.NoteKindManager;
|
||||
import funkin.play.PlayStatePlaylist;
|
||||
import funkin.ui.debug.charting.ChartEditorState;
|
||||
@@ -37,6 +37,7 @@ import funkin.util.macro.MacroUtil;
|
||||
import funkin.util.TrackerUtil;
|
||||
import funkin.util.WindowUtil;
|
||||
import openfl.display.BitmapData;
|
||||
import funkin.ui.debug.playtest.ChartPlaytestMenu;
|
||||
#if FEATURE_DISCORD_RPC
|
||||
import funkin.api.discord.DiscordClient;
|
||||
#end
|
||||
@@ -118,6 +119,11 @@ class InitState extends FlxState
|
||||
funkin.mobile.util.InAppReviewUtil.init();
|
||||
#end
|
||||
|
||||
#if FEATURE_MOBILE_WEBVIEW
|
||||
// Setup WebView
|
||||
funkin.mobile.util.WebViewUtil.init();
|
||||
#end
|
||||
|
||||
#if android
|
||||
// Setup Callback util.
|
||||
funkin.external.android.CallbackUtil.init();
|
||||
@@ -125,7 +131,12 @@ class InitState extends FlxState
|
||||
|
||||
#if ios
|
||||
// Setup Audio session
|
||||
funkin.external.ios.AudioSession.initialize();
|
||||
funkin.external.apple.AudioSession.initialize();
|
||||
#end
|
||||
|
||||
#if mobile
|
||||
// Setup Mobile FNFC launcher.
|
||||
funkin.mobile.util.FNFCUtil.init();
|
||||
#end
|
||||
|
||||
// This ain't a pixel art game! (most of the time)
|
||||
@@ -147,6 +158,11 @@ class InitState extends FlxState
|
||||
// Set the game to a lower frame rate while it is in the background.
|
||||
FlxG.game.focusLostFramerate = 30;
|
||||
|
||||
// Persist controls inputs between states.
|
||||
// Without this, the game would release your keybinds when you switch states,
|
||||
// and then act like you released and re-pressed them the frame after.
|
||||
FlxG.inputs.resetOnStateSwitch = false;
|
||||
|
||||
// Makes Flixel use frame times instead of locked movements per frame for things like tweens
|
||||
FlxG.fixedTimestep = false;
|
||||
|
||||
@@ -206,6 +222,10 @@ class InitState extends FlxState
|
||||
});
|
||||
#end
|
||||
|
||||
#if FEATURE_LOST_FOCUS_VOLUME
|
||||
FlxG.signals.focusLost.add(onLostFocus);
|
||||
FlxG.signals.focusGained.add(onGainFocus);
|
||||
#end
|
||||
//
|
||||
// ANDROID SETUP
|
||||
//
|
||||
@@ -267,7 +287,7 @@ class InitState extends FlxState
|
||||
// Move it to use a BaseRegistry.
|
||||
CharacterDataParser.loadCharacterCache();
|
||||
|
||||
NoteKindManager.loadScripts();
|
||||
NoteKindManager.initialize();
|
||||
|
||||
ModuleHandler.buildModuleCallbacks();
|
||||
ModuleHandler.loadModuleCache();
|
||||
@@ -281,6 +301,23 @@ class InitState extends FlxState
|
||||
#end
|
||||
}
|
||||
|
||||
#if FEATURE_LOST_FOCUS_VOLUME
|
||||
@:noCompletion var _lastFocusVolume:Null<Float>;
|
||||
|
||||
function onLostFocus()
|
||||
{
|
||||
if (FlxG.sound.muted || FlxG.sound.volume == 0 || FlxG.autoPause) return;
|
||||
_lastFocusVolume = FlxG.sound.volume;
|
||||
FlxG.sound.volume *= Constants.LOST_FOCUS_VOLUME_MULTIPLIER;
|
||||
}
|
||||
|
||||
function onGainFocus()
|
||||
{
|
||||
if (FlxG.sound.muted || FlxG.autoPause) return;
|
||||
if (_lastFocusVolume != null) FlxG.sound.volume = _lastFocusVolume;
|
||||
}
|
||||
#end
|
||||
|
||||
/**
|
||||
* Start the game.
|
||||
*
|
||||
@@ -374,22 +411,64 @@ class InitState extends FlxState
|
||||
|
||||
if (params.chart.shouldLoadChart)
|
||||
{
|
||||
#if FEATURE_CHART_EDITOR
|
||||
FlxG.switchState(() -> new ChartEditorState(
|
||||
{
|
||||
fnfcTargetPath: params.chart.chartPath,
|
||||
}));
|
||||
#else
|
||||
FlxG.switchState(() -> new TitleState());
|
||||
#end
|
||||
}
|
||||
else if (params.stage.shouldLoadStage)
|
||||
{
|
||||
#if FEATURE_STAGE_EDITOR
|
||||
FlxG.switchState(() -> new StageEditorState(
|
||||
{
|
||||
fnfsTargetPath: params.stage.stagePath,
|
||||
}));
|
||||
#else
|
||||
FlxG.switchState(() -> new TitleState());
|
||||
#end
|
||||
}
|
||||
else if (params.song.shouldLoadSong && params.song.songPath != null)
|
||||
{
|
||||
#if sys
|
||||
FlxG.switchState(() -> new ChartPlaytestMenu(params.song.songPath));
|
||||
#else
|
||||
FlxG.switchState(() -> new TitleState());
|
||||
#end
|
||||
}
|
||||
else
|
||||
{
|
||||
// FlxG.sound.cache(Paths.music('freakyMenu/freakyMenu'));
|
||||
#if mobile
|
||||
funkin.mobile.util.FNFCUtil.onFNFCOpen.add(function(fnfcFile:String) {
|
||||
flixel.tweens.FlxTween.globalManager.clear();
|
||||
flixel.util.FlxTimer.globalManager.clear();
|
||||
@:nullSafety(Off)
|
||||
if (FlxG.sound.music != null)
|
||||
{
|
||||
FlxG.sound.music.destroy();
|
||||
FlxG.sound.music = null;
|
||||
}
|
||||
|
||||
FlxG.switchState(() -> new ChartPlaytestMenu(fnfcFile));
|
||||
});
|
||||
|
||||
final fnfcFile = funkin.mobile.util.FNFCUtil.queryFNFC();
|
||||
if (fnfcFile != null)
|
||||
{
|
||||
trace('launching FNFC from $fnfcFile');
|
||||
FlxG.switchState(() -> new ChartPlaytestMenu(fnfcFile));
|
||||
}
|
||||
else
|
||||
{
|
||||
FlxG.switchState(() -> new TitleState());
|
||||
}
|
||||
#else
|
||||
FlxG.switchState(() -> new TitleState());
|
||||
#end
|
||||
}
|
||||
}
|
||||
|
||||
@@ -510,19 +589,6 @@ class InitState extends FlxState
|
||||
// Disable using ~ to open the console (we use that for the Editor menu)
|
||||
FlxG.debugger.toggleKeys = [F2];
|
||||
TrackerUtil.initTrackers();
|
||||
// Adds an additional Close Debugger button.
|
||||
// This big obnoxious white button is for MOBILE, so that you can press it
|
||||
// easily with your finger when debug bullshit pops up during testing lol!
|
||||
FlxG.debugger.addButton(LEFT, new BitmapData(200, 200), function() {
|
||||
FlxG.debugger.visible = false;
|
||||
|
||||
// Make errors and warnings less annoying.
|
||||
// Forcing this always since I have never been happy to have the debugger to pop up
|
||||
LogStyle.ERROR.openConsole = false;
|
||||
LogStyle.ERROR.errorSound = null;
|
||||
LogStyle.WARNING.openConsole = false;
|
||||
LogStyle.WARNING.errorSound = null;
|
||||
});
|
||||
|
||||
// Adds a red button to the debugger.
|
||||
// This pauses the game AND the music! This ensures the Conductor stops.
|
||||
@@ -565,6 +631,22 @@ class InitState extends FlxState
|
||||
FlxG.sound.music.pause();
|
||||
FlxG.sound.music.time += FlxG.elapsed * 1000;
|
||||
});
|
||||
|
||||
// Adds an additional Close Debugger button.
|
||||
// This big obnoxious white button is for MOBILE, so that you can press it
|
||||
// easily with your finger when debug bullshit pops up during testing lol!
|
||||
#if mobile
|
||||
FlxG.debugger.addButton(LEFT, new BitmapData(200, 200), function() {
|
||||
FlxG.debugger.visible = false;
|
||||
|
||||
// Make errors and warnings less annoying.
|
||||
// Forcing this always since I have never been happy to have the debugger to pop up
|
||||
LogStyle.ERROR.openConsole = false;
|
||||
LogStyle.ERROR.errorSound = null;
|
||||
LogStyle.WARNING.openConsole = false;
|
||||
LogStyle.WARNING.errorSound = null;
|
||||
});
|
||||
#end // mobile big butotn crap
|
||||
#end
|
||||
}
|
||||
|
||||
|
||||
+60
-1
@@ -1,13 +1,17 @@
|
||||
package funkin;
|
||||
|
||||
import flixel.graphics.frames.FlxAtlasFrames;
|
||||
import animate.FlxAnimateFrames;
|
||||
import funkin.graphics.FunkinSprite.AtlasSpriteSettings;
|
||||
import openfl.utils.AssetType;
|
||||
import funkin.util.macro.ConsoleMacro;
|
||||
import haxe.io.Path;
|
||||
|
||||
/**
|
||||
* A core class which handles determining asset paths.
|
||||
*/
|
||||
@:nullSafety
|
||||
class Paths
|
||||
class Paths implements ConsoleClass
|
||||
{
|
||||
static var currentLevel:Null<String> = null;
|
||||
|
||||
@@ -103,6 +107,11 @@ class Paths
|
||||
return getPath('data/$key.json', TEXT, library);
|
||||
}
|
||||
|
||||
public static function srt(key:String, ?library:String, ?directory:String = "data/"):String
|
||||
{
|
||||
return getPath('$directory$key.srt', TEXT, library);
|
||||
}
|
||||
|
||||
public static function sound(key:String, ?library:String):String
|
||||
{
|
||||
return getPath('sounds/$key.${Constants.EXT_SOUND}', SOUND, library);
|
||||
@@ -120,6 +129,13 @@ class Paths
|
||||
|
||||
public static function videos(key:String, ?library:String):String
|
||||
{
|
||||
final path:Path = new Path(key);
|
||||
|
||||
if (path.ext != null)
|
||||
{
|
||||
return getPath('videos/${path.file}.${path.ext}', BINARY, library ?? 'videos');
|
||||
}
|
||||
|
||||
return getPath('videos/$key.${Constants.EXT_VIDEO}', BINARY, library ?? 'videos');
|
||||
}
|
||||
|
||||
@@ -163,6 +179,49 @@ class Paths
|
||||
return FlxAtlasFrames.fromSparrow(image(key, library), file('images/$key.xml', library));
|
||||
}
|
||||
|
||||
public static function getAnimateAtlas(key:String, ?library:String, settings:AtlasSpriteSettings):FlxAnimateFrames
|
||||
{
|
||||
var assetLibrary:String = library ?? "";
|
||||
var graphicKey:String = "";
|
||||
|
||||
if (assetLibrary != "")
|
||||
{
|
||||
graphicKey = Paths.animateAtlas(key, assetLibrary);
|
||||
}
|
||||
else
|
||||
{
|
||||
graphicKey = Paths.animateAtlas(key);
|
||||
}
|
||||
|
||||
var validatedSettings:AtlasSpriteSettings =
|
||||
{
|
||||
swfMode: settings?.swfMode ?? false,
|
||||
cacheOnLoad: settings?.cacheOnLoad ?? false,
|
||||
filterQuality: settings?.filterQuality ?? MEDIUM,
|
||||
spritemaps: settings?.spritemaps ?? null,
|
||||
metadataJson: settings?.metadataJson ?? null,
|
||||
cacheKey: settings?.cacheKey ?? null,
|
||||
uniqueInCache: settings?.uniqueInCache ?? false,
|
||||
onSymbolCreate: settings?.onSymbolCreate ?? null,
|
||||
applyStageMatrix: settings?.applyStageMatrix ?? false,
|
||||
useRenderTexture: settings?.useRenderTexture ?? false
|
||||
};
|
||||
|
||||
// Validate asset path.
|
||||
if (!Assets.exists('${graphicKey}/Animation.json'))
|
||||
{
|
||||
throw 'No Animation.json file exists at the specified path (${graphicKey})';
|
||||
}
|
||||
|
||||
return FlxAnimateFrames.fromAnimate(graphicKey, validatedSettings.spritemaps, validatedSettings.metadataJson, validatedSettings.cacheKey,
|
||||
validatedSettings.uniqueInCache, {
|
||||
swfMode: validatedSettings.swfMode,
|
||||
cacheOnLoad: validatedSettings.cacheOnLoad,
|
||||
filterQuality: validatedSettings.filterQuality,
|
||||
onSymbolCreate: validatedSettings.onSymbolCreate
|
||||
});
|
||||
}
|
||||
|
||||
public static function getPackerAtlas(key:String, ?library:String):FlxAtlasFrames
|
||||
{
|
||||
return FlxAtlasFrames.fromSpriteSheetPacker(image(key, library), file('images/$key.txt', library));
|
||||
|
||||
@@ -7,6 +7,7 @@ import funkin.mobile.util.InAppPurchasesUtil;
|
||||
import funkin.save.Save;
|
||||
import funkin.util.WindowUtil;
|
||||
import funkin.util.HapticUtil.HapticsMode;
|
||||
import funkin.ui.debug.FunkinDebugDisplay.DebugDisplayMode;
|
||||
|
||||
/**
|
||||
* A core class which provides a store of user-configurable, globally relevant values.
|
||||
@@ -43,7 +44,7 @@ class Preferences
|
||||
#else
|
||||
var save:Save = Save.instance;
|
||||
save.options.framerate = value;
|
||||
save.flush();
|
||||
Save.system.flush();
|
||||
FlxG.updateFramerate = value;
|
||||
FlxG.drawFramerate = value;
|
||||
return value;
|
||||
@@ -73,7 +74,7 @@ class Preferences
|
||||
|
||||
var save:Save = Save.instance;
|
||||
save.options.naughtyness = value;
|
||||
save.flush();
|
||||
Save.system.flush();
|
||||
return value;
|
||||
}
|
||||
|
||||
@@ -92,7 +93,7 @@ class Preferences
|
||||
{
|
||||
var save:Save = Save.instance;
|
||||
save.options.downscroll = value;
|
||||
save.flush();
|
||||
Save.system.flush();
|
||||
return value;
|
||||
}
|
||||
|
||||
@@ -111,7 +112,7 @@ class Preferences
|
||||
{
|
||||
var save:Save = Save.instance;
|
||||
save.options.flashingLights = value;
|
||||
save.flush();
|
||||
Save.system.flush();
|
||||
return value;
|
||||
}
|
||||
|
||||
@@ -130,35 +131,54 @@ class Preferences
|
||||
{
|
||||
var save:Save = Save.instance;
|
||||
save.options.zoomCamera = value;
|
||||
save.flush();
|
||||
Save.system.flush();
|
||||
return value;
|
||||
}
|
||||
|
||||
/**
|
||||
* If enabled, an FPS and memory counter will be displayed even if this is not a debug build.
|
||||
* Always disabled on mobile.
|
||||
* @default `false`
|
||||
* Always disabled on release mobile builds.
|
||||
* @default `Off`
|
||||
*/
|
||||
public static var debugDisplay(get, set):Bool;
|
||||
public static var debugDisplay(get, set):DebugDisplayMode;
|
||||
|
||||
static function get_debugDisplay():Bool
|
||||
static function get_debugDisplay():DebugDisplayMode
|
||||
{
|
||||
#if mobile
|
||||
return false;
|
||||
#if NO_FEATURE_DEBUG_DISPLAY
|
||||
return DebugDisplayMode.Off;
|
||||
#end
|
||||
return Save?.instance?.options?.debugDisplay ?? false;
|
||||
|
||||
return Save?.instance?.options?.debugDisplay ?? 'Off';
|
||||
}
|
||||
|
||||
static function set_debugDisplay(value:Bool):Bool
|
||||
static function set_debugDisplay(value:DebugDisplayMode):DebugDisplayMode
|
||||
{
|
||||
if (value != Save.instance.options.debugDisplay)
|
||||
{
|
||||
toggleDebugDisplay(value);
|
||||
}
|
||||
if (value != Save.instance.options.debugDisplay) setDebugDisplayMode(value);
|
||||
|
||||
var save = Save.instance;
|
||||
save.options.debugDisplay = value;
|
||||
save.flush();
|
||||
Save.system.flush();
|
||||
return value;
|
||||
}
|
||||
|
||||
/**
|
||||
* Opacity of the debug display's background.
|
||||
* @default `50`
|
||||
*/
|
||||
public static var debugDisplayBGOpacity(get, set):Int;
|
||||
|
||||
static function get_debugDisplayBGOpacity():Int
|
||||
{
|
||||
return Save?.instance?.options?.debugDisplayBGOpacity ?? 50;
|
||||
}
|
||||
|
||||
static function set_debugDisplayBGOpacity(value:Int):Int
|
||||
{
|
||||
setDebugDisplayBGOpacity(value / 100);
|
||||
|
||||
var save:Save = Save.instance;
|
||||
save.options.debugDisplayBGOpacity = value;
|
||||
Save.system.flush();
|
||||
return value;
|
||||
}
|
||||
|
||||
@@ -199,7 +219,7 @@ class Preferences
|
||||
|
||||
var save:Save = Save.instance;
|
||||
save.options.hapticsMode = string;
|
||||
save.flush();
|
||||
Save.system.flush();
|
||||
return value;
|
||||
}
|
||||
|
||||
@@ -218,7 +238,7 @@ class Preferences
|
||||
{
|
||||
var save:Save = Save.instance;
|
||||
save.options.hapticsIntensityMultiplier = value;
|
||||
save.flush();
|
||||
Save.system.flush();
|
||||
return value;
|
||||
}
|
||||
|
||||
@@ -232,7 +252,7 @@ class Preferences
|
||||
static function get_autoPause():Bool
|
||||
{
|
||||
#if mobile
|
||||
return true;
|
||||
return false;
|
||||
#end
|
||||
return Save?.instance?.options?.autoPause ?? true;
|
||||
}
|
||||
@@ -243,7 +263,7 @@ class Preferences
|
||||
|
||||
var save:Save = Save.instance;
|
||||
save.options.autoPause = value;
|
||||
save.flush();
|
||||
Save.system.flush();
|
||||
return value;
|
||||
}
|
||||
|
||||
@@ -262,7 +282,7 @@ class Preferences
|
||||
{
|
||||
var save:Save = Save.instance;
|
||||
save.options.autoFullscreen = value;
|
||||
save.flush();
|
||||
Save.system.flush();
|
||||
return value;
|
||||
}
|
||||
|
||||
@@ -282,7 +302,7 @@ class Preferences
|
||||
{
|
||||
var save:Save = Save.instance;
|
||||
save.options.globalOffset = value;
|
||||
save.flush();
|
||||
Save.system.flush();
|
||||
return value;
|
||||
}
|
||||
|
||||
@@ -329,7 +349,7 @@ class Preferences
|
||||
|
||||
var save:Save = Save.instance;
|
||||
save.options.vsyncMode = string;
|
||||
save.flush();
|
||||
Save.system.flush();
|
||||
return value;
|
||||
}
|
||||
|
||||
@@ -351,7 +371,7 @@ class Preferences
|
||||
|
||||
var save:Save = Save.instance;
|
||||
save.options.unlockedFramerate = value;
|
||||
save.flush();
|
||||
Save.system.flush();
|
||||
return value;
|
||||
}
|
||||
|
||||
@@ -391,7 +411,7 @@ class Preferences
|
||||
{
|
||||
var save:Save = Save.instance;
|
||||
save.options.strumlineBackgroundOpacity = value;
|
||||
save.flush();
|
||||
Save.system.flush();
|
||||
return value;
|
||||
}
|
||||
|
||||
@@ -410,7 +430,7 @@ class Preferences
|
||||
{
|
||||
var save:Save = Save.instance;
|
||||
save.options.screenshot.shouldHideMouse = value;
|
||||
save.flush();
|
||||
Save.system.flush();
|
||||
return value;
|
||||
}
|
||||
|
||||
@@ -429,7 +449,7 @@ class Preferences
|
||||
{
|
||||
var save:Save = Save.instance;
|
||||
save.options.screenshot.fancyPreview = value;
|
||||
save.flush();
|
||||
Save.system.flush();
|
||||
return value;
|
||||
}
|
||||
|
||||
@@ -448,7 +468,7 @@ class Preferences
|
||||
{
|
||||
var save:Save = Save.instance;
|
||||
save.options.screenshot.previewOnSave = value;
|
||||
save.flush();
|
||||
Save.system.flush();
|
||||
return value;
|
||||
}
|
||||
|
||||
@@ -459,18 +479,15 @@ class Preferences
|
||||
{
|
||||
// Apply the autoPause setting (enables automatic pausing on focus lost).
|
||||
FlxG.autoPause = Preferences.autoPause;
|
||||
// WindowUtil.setVSyncMode(Preferences.vsyncMode);
|
||||
|
||||
// Apply the debugDisplay setting (enables the FPS and RAM display).
|
||||
toggleDebugDisplay(Preferences.debugDisplay);
|
||||
setDebugDisplayMode(Preferences.debugDisplay);
|
||||
setDebugDisplayBGOpacity(Preferences.debugDisplayBGOpacity / 100);
|
||||
|
||||
#if web
|
||||
toggleFramerateCap(Preferences.unlockedFramerate);
|
||||
#end
|
||||
|
||||
#if desktop
|
||||
// Apply the autoFullscreen setting (launches the game in fullscreen automatically)
|
||||
FlxG.fullscreen = Preferences.autoFullscreen;
|
||||
#end
|
||||
|
||||
#if mobile
|
||||
// Apply the allowScreenTimeout setting.
|
||||
lime.system.System.allowScreenTimeout = Preferences.screenTimeout;
|
||||
@@ -485,26 +502,41 @@ class Preferences
|
||||
#end
|
||||
}
|
||||
|
||||
static function toggleDebugDisplay(show:Bool):Void
|
||||
public static function setDebugDisplayMode(mode:DebugDisplayMode):Void
|
||||
{
|
||||
if (show)
|
||||
{
|
||||
// Enable the debug display.
|
||||
FlxG.game.parent.addChild(Main.fpsCounter);
|
||||
if (FlxG.game.parent.contains(Main.debugDisplay)) FlxG.game.parent.removeChild(Main.debugDisplay);
|
||||
|
||||
#if !html5
|
||||
FlxG.game.parent.addChild(Main.memoryCounter);
|
||||
#end
|
||||
}
|
||||
else
|
||||
{
|
||||
// Disable the debug display.
|
||||
FlxG.game.parent.removeChild(Main.fpsCounter);
|
||||
if (mode == DebugDisplayMode.Off) return;
|
||||
|
||||
#if !html5
|
||||
FlxG.game.parent.removeChild(Main.memoryCounter);
|
||||
#end
|
||||
}
|
||||
Main.debugDisplay.isAdvanced = (mode == DebugDisplayMode.Advanced);
|
||||
|
||||
FlxG.game.parent.addChild(Main.debugDisplay);
|
||||
}
|
||||
|
||||
static function setDebugDisplayBGOpacity(value:Float):Void
|
||||
{
|
||||
if (Main.debugDisplay == null) return;
|
||||
|
||||
Main.debugDisplay.backgroundOpacity = value;
|
||||
}
|
||||
|
||||
/**
|
||||
* If enabled, subtitles will appear during some songs and cutscenes.
|
||||
* @default `true`
|
||||
*/
|
||||
public static var subtitles(get, set):Bool;
|
||||
|
||||
static function get_subtitles():Bool
|
||||
{
|
||||
return Save?.instance?.options?.subtitles ?? true;
|
||||
}
|
||||
|
||||
static function set_subtitles(value:Bool):Bool
|
||||
{
|
||||
var save:Save = Save.instance;
|
||||
save.options.subtitles = value;
|
||||
Save.system.flush();
|
||||
return value;
|
||||
}
|
||||
|
||||
#if mobile
|
||||
@@ -525,7 +557,7 @@ class Preferences
|
||||
|
||||
var save:Save = Save.instance;
|
||||
save.mobileOptions.screenTimeout = value;
|
||||
save.flush();
|
||||
Save.system.flush();
|
||||
return value;
|
||||
}
|
||||
|
||||
@@ -544,7 +576,7 @@ class Preferences
|
||||
{
|
||||
var save:Save = Save.instance;
|
||||
save.mobileOptions.controlsScheme = value;
|
||||
save.flush();
|
||||
Save.system.flush();
|
||||
return value;
|
||||
}
|
||||
|
||||
@@ -569,7 +601,7 @@ class Preferences
|
||||
{
|
||||
var save:Save = Save.instance;
|
||||
save.mobileOptions.noAds = value;
|
||||
save.flush();
|
||||
Save.system.flush();
|
||||
return value;
|
||||
}
|
||||
#end
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
package funkin.api.discord;
|
||||
|
||||
import funkin.util.macro.EnvironmentConfigMacro;
|
||||
#if FEATURE_DISCORD_RPC
|
||||
import hxdiscord_rpc.Discord;
|
||||
import hxdiscord_rpc.Types.DiscordButton;
|
||||
@@ -9,10 +8,12 @@ import hxdiscord_rpc.Types.DiscordRichPresence;
|
||||
import hxdiscord_rpc.Types.DiscordUser;
|
||||
import sys.thread.Thread;
|
||||
|
||||
@:build(funkin.util.macro.EnvironmentMacro.build())
|
||||
@:nullSafety
|
||||
class DiscordClient
|
||||
{
|
||||
static final CLIENT_ID:Null<String> = EnvironmentConfigMacro.environmentConfig?.get("DESKTOP_DISCORD_CLIENT_ID");
|
||||
@:envField
|
||||
static final DISCORD_CLIENT_ID:Null<String>;
|
||||
|
||||
public static var instance(get, never):DiscordClient;
|
||||
static var _instance:Null<DiscordClient> = null;
|
||||
@@ -28,9 +29,9 @@ class DiscordClient
|
||||
|
||||
private function new()
|
||||
{
|
||||
trace('[DISCORD] Initializing event handlers...');
|
||||
trace(' DISCORD '.bold().bg_blue() + ' Initializing event handlers...');
|
||||
|
||||
handlers = DiscordEventHandlers.create();
|
||||
handlers = new DiscordEventHandlers();
|
||||
|
||||
handlers.ready = cpp.Function.fromStaticFunction(onReady);
|
||||
handlers.disconnected = cpp.Function.fromStaticFunction(onDisconnected);
|
||||
@@ -39,7 +40,7 @@ class DiscordClient
|
||||
|
||||
public function init():Void
|
||||
{
|
||||
trace('[DISCORD] Initializing connection...');
|
||||
trace(' DISCORD '.bold().bg_blue() + ' Initializing connection...');
|
||||
|
||||
if (!hasValidCredentials())
|
||||
{
|
||||
@@ -49,7 +50,7 @@ class DiscordClient
|
||||
|
||||
@:nullSafety(Off)
|
||||
{
|
||||
Discord.Initialize(CLIENT_ID, cpp.RawPointer.addressOf(handlers), 1, "");
|
||||
Discord.Initialize(DISCORD_CLIENT_ID, cpp.RawPointer.addressOf(handlers), false, "");
|
||||
}
|
||||
|
||||
createDaemon();
|
||||
@@ -60,7 +61,7 @@ class DiscordClient
|
||||
*/
|
||||
static function hasValidCredentials():Bool
|
||||
{
|
||||
return !(CLIENT_ID == null || CLIENT_ID == "" || (CLIENT_ID != null && CLIENT_ID.contains(" ")));
|
||||
return !(DISCORD_CLIENT_ID == null || DISCORD_CLIENT_ID == "" || (DISCORD_CLIENT_ID != null && DISCORD_CLIENT_ID.contains(" ")));
|
||||
}
|
||||
|
||||
var daemon:Null<Thread> = null;
|
||||
@@ -78,26 +79,21 @@ class DiscordClient
|
||||
Discord.updateConnection();
|
||||
#end
|
||||
|
||||
Discord.runCallbacks();
|
||||
Discord.RunCallbacks();
|
||||
Sys.sleep(2);
|
||||
}
|
||||
}
|
||||
|
||||
public function shutdown():Void
|
||||
{
|
||||
trace('[DISCORD] Shutting down...');
|
||||
trace(' DISCORD '.bold().bg_blue() + ' Shutting down...');
|
||||
|
||||
Discord.shutdown();
|
||||
Discord.Shutdown();
|
||||
}
|
||||
|
||||
public function setPresence(params:DiscordClientPresenceParams):Void
|
||||
{
|
||||
Discord.updatePresence(buildPresence(params));
|
||||
}
|
||||
|
||||
function buildPresence(params:DiscordClientPresenceParams):DiscordRichPresence
|
||||
{
|
||||
var presence = DiscordRichPresence.create();
|
||||
var presence:DiscordRichPresence = new DiscordRichPresence();
|
||||
|
||||
// Presence should always be playing the game.
|
||||
presence.type = DiscordActivityType_Playing;
|
||||
@@ -128,23 +124,23 @@ class DiscordClient
|
||||
// presence.startTimestamp = time - 10;
|
||||
// presence.endTimestamp = time + 30;
|
||||
|
||||
final button1:DiscordButton = DiscordButton.create();
|
||||
final button1:DiscordButton = new DiscordButton();
|
||||
button1.label = "Play on Web";
|
||||
button1.url = Constants.URL_NEWGROUNDS;
|
||||
presence.buttons[0] = button1;
|
||||
|
||||
final button2:DiscordButton = DiscordButton.create();
|
||||
final button2:DiscordButton = new DiscordButton();
|
||||
button2.label = "Download";
|
||||
button2.url = Constants.URL_ITCH;
|
||||
presence.buttons[1] = button2;
|
||||
|
||||
return presence;
|
||||
Discord.UpdatePresence(cpp.RawConstPointer.addressOf(presence));
|
||||
}
|
||||
|
||||
// TODO: WHAT THE FUCK get this pointer bullfuckery out of here
|
||||
private static function onReady(request:cpp.RawConstPointer<DiscordUser>):Void
|
||||
{
|
||||
trace('[DISCORD] Client has connected!');
|
||||
trace(' DISCORD '.bold().bg_blue() + ' Client has connected!');
|
||||
|
||||
final username:String = request[0].username;
|
||||
final globalName:String = request[0].username;
|
||||
@@ -152,22 +148,22 @@ class DiscordClient
|
||||
|
||||
if (discriminator != null && discriminator != 0)
|
||||
{
|
||||
trace('[DISCORD] User: ${username}#${discriminator} (${globalName})');
|
||||
trace(' DISCORD '.bold().bg_blue() + ' User: ${username}#${discriminator} (${globalName})');
|
||||
}
|
||||
else
|
||||
{
|
||||
trace('[DISCORD] User: @${username} (${globalName})');
|
||||
trace(' DISCORD '.bold().bg_blue() + ' User: @${username} (${globalName})');
|
||||
}
|
||||
}
|
||||
|
||||
private static function onDisconnected(errorCode:Int, message:cpp.ConstCharStar):Void
|
||||
{
|
||||
trace('[DISCORD] Client has disconnected! ($errorCode) "${cast (message, String)}"');
|
||||
trace(' DISCORD '.bold().bg_blue() + ' Client has disconnected! ($errorCode) "${cast (message, String)}"');
|
||||
}
|
||||
|
||||
private static function onError(errorCode:Int, message:cpp.ConstCharStar):Void
|
||||
{
|
||||
trace('[DISCORD] Client has received an error! ($errorCode) "${cast (message, String)}"');
|
||||
trace(' DISCORD '.bold().bg_blue() + ' Client has received an error! ($errorCode) "${cast (message, String)}"');
|
||||
}
|
||||
|
||||
// public var type(get, set):DiscordActivityType;
|
||||
|
||||
@@ -39,21 +39,21 @@ class Events
|
||||
switch (outcome)
|
||||
{
|
||||
case SUCCESS(data):
|
||||
trace('[NEWGROUNDS] Logged event: ${data.eventName}');
|
||||
trace(' NEWGROUNDS '.bold().bg_orange() + ' Logged event: ${data.eventName}');
|
||||
case FAIL(outcome):
|
||||
switch (outcome)
|
||||
{
|
||||
case HTTP(error):
|
||||
trace('[NEWGROUNDS] HTTP error while logging event: ${error}');
|
||||
trace(' NEWGROUNDS '.bold().bg_orange() + ' HTTP error while logging event: ${error}');
|
||||
case RESPONSE(error):
|
||||
trace('[NEWGROUNDS] Response error (${error.code}) while logging event: ${error.message}');
|
||||
trace(' NEWGROUNDS '.bold().bg_orange() + ' Response error (${error.code}) while logging event: ${error.message}');
|
||||
case RESULT(error):
|
||||
switch (error.code)
|
||||
{
|
||||
case 103: // Invalid custom event name
|
||||
trace('[NEWGROUNDS] Invalid custom event name: ${eventName}');
|
||||
trace(' NEWGROUNDS '.bold().bg_orange() + ' Invalid custom event name: ${eventName}');
|
||||
default:
|
||||
trace('[NEWGROUNDS] Result error (${error.code}) while logging event: ${error.message}');
|
||||
trace(' NEWGROUNDS '.bold().bg_orange() + ' Result error (${error.code}) while logging event: ${error.message}');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,22 +2,29 @@ package funkin.api.newgrounds;
|
||||
|
||||
#if FEATURE_NEWGROUNDS
|
||||
import io.newgrounds.Call.CallError;
|
||||
import io.newgrounds.components.ScoreBoardComponent;
|
||||
import io.newgrounds.components.ScoreBoardComponent.Period;
|
||||
import io.newgrounds.objects.Score;
|
||||
import io.newgrounds.objects.ScoreBoard as LeaderboardData;
|
||||
import io.newgrounds.objects.User;
|
||||
import io.newgrounds.objects.events.Outcome;
|
||||
import io.newgrounds.utils.ScoreBoardList;
|
||||
|
||||
/**
|
||||
* Handles interactions with the leaderboards on the Newgrounds website.
|
||||
*/
|
||||
@:nullSafety
|
||||
class Leaderboards
|
||||
{
|
||||
/**
|
||||
* Retrieve the leaderboard data via the Newgrounds API.
|
||||
* @return The leaderboard data.
|
||||
*/
|
||||
public static function listLeaderboardData():Map<Leaderboard, LeaderboardData>
|
||||
{
|
||||
var leaderboardList:Null<ScoreBoardList> = NewgroundsClient.instance.leaderboards;
|
||||
if (leaderboardList == null)
|
||||
{
|
||||
trace('[NEWGROUNDS] Not logged in, cannot fetch medal data!');
|
||||
trace(' NEWGROUNDS '.bold().bg_orange() + ' Not logged in, cannot fetch medal data!');
|
||||
return [];
|
||||
}
|
||||
|
||||
@@ -33,24 +40,25 @@ class Leaderboards
|
||||
public static function submitScore(leaderboard:Leaderboard, score:Int, ?tag:String):Void
|
||||
{
|
||||
// Silently reject submissions for unknown leaderboards.
|
||||
if (leaderboard == Leaderboard.Unknown) return;
|
||||
if (leaderboard == Leaderboard.Unknown)
|
||||
{
|
||||
trace(' NEWGROUNDS '.bold().bg_orange() + ' Unknown leaderboard, skipping score submission...');
|
||||
return;
|
||||
}
|
||||
|
||||
if (NewgroundsClient.instance.isLoggedIn())
|
||||
{
|
||||
var leaderboardList = NewgroundsClient.instance.leaderboards;
|
||||
if (leaderboardList == null) return;
|
||||
|
||||
var leaderboardData:Null<LeaderboardData> = leaderboardList.get(leaderboard.getId());
|
||||
var leaderboardData:Null<LeaderboardData> = listLeaderboardData().get(leaderboard.getId());
|
||||
if (leaderboardData != null)
|
||||
{
|
||||
leaderboardData.postScore(score, function(outcome:Outcome<CallError>):Void {
|
||||
switch (outcome)
|
||||
{
|
||||
case SUCCESS:
|
||||
trace('[NEWGROUNDS] Submitted score!');
|
||||
trace(' NEWGROUNDS '.bold().bg_orange() + ' Submitted leaderboard score!');
|
||||
case FAIL(error):
|
||||
trace('[NEWGROUNDS] Failed to submit score!');
|
||||
trace(error);
|
||||
trace(' NEWGROUNDS '.bold().bg_orange() + ' ERROR '.error() + ' Failed to submit leaderboard score!');
|
||||
trace(' ERROR '.error() + error);
|
||||
}
|
||||
});
|
||||
}
|
||||
@@ -67,10 +75,7 @@ class Leaderboards
|
||||
// Silently reject retrieving scores from unknown leaderboards.
|
||||
if (leaderboard == Leaderboard.Unknown) return;
|
||||
|
||||
var leaderboardList = NewgroundsClient.instance.leaderboards;
|
||||
if (leaderboardList == null) return;
|
||||
|
||||
var leaderboardData:Null<LeaderboardData> = leaderboardList.get(leaderboard.getId());
|
||||
var leaderboardData:Null<LeaderboardData> = listLeaderboardData().get(leaderboard.getId());
|
||||
if (leaderboardData == null) return;
|
||||
|
||||
var user:Null<User> = null;
|
||||
@@ -81,12 +86,12 @@ class Leaderboards
|
||||
switch (outcome)
|
||||
{
|
||||
case SUCCESS:
|
||||
trace('[NEWGROUNDS] Fetched scores!');
|
||||
trace(' NEWGROUNDS '.bold().bg_orange() + ' Fetched scores!');
|
||||
if (params != null && params.onComplete != null) params.onComplete(leaderboardData.scores);
|
||||
|
||||
case FAIL(error):
|
||||
trace('[NEWGROUNDS] Failed to fetch scores!');
|
||||
trace(error);
|
||||
trace(' NEWGROUNDS '.bold().bg_orange() + ' ERROR '.error() + ' Failed to fetch scores!');
|
||||
trace(' ERROR '.error() + error);
|
||||
if (params != null && params.onFail != null) params.onFail();
|
||||
}
|
||||
});
|
||||
@@ -94,19 +99,29 @@ class Leaderboards
|
||||
|
||||
/**
|
||||
* Submit a score for a Story Level to Newgrounds.
|
||||
*
|
||||
* @param levelId The ID for the story level.
|
||||
* @param difficultyId The current difficulty.
|
||||
* @param score The score to submit.
|
||||
*/
|
||||
public static function submitLevelScore(levelId:String, difficultyId:String, score:Int):Void
|
||||
{
|
||||
var tag = '${difficultyId}';
|
||||
trace(' NEWGROUNDS '.bold().bg_orange() + 'Submitting score for level "${levelId}"...');
|
||||
Leaderboards.submitScore(Leaderboard.getLeaderboardByLevel(levelId), score, tag);
|
||||
}
|
||||
|
||||
/**
|
||||
* Submit a score for a song to Newgrounds.
|
||||
*
|
||||
* @param songId The ID for the song.
|
||||
* @param difficultyId The current difficulty.
|
||||
* @param score The score to submit.
|
||||
*/
|
||||
public static function submitSongScore(songId:String, difficultyId:String, score:Int):Void
|
||||
{
|
||||
var tag = '${difficultyId}';
|
||||
trace(' NEWGROUNDS '.bold().bg_orange() + 'Submitting score for song "${songId}" (${difficultyId})...');
|
||||
Leaderboards.submitScore(Leaderboard.getLeaderboardBySong(songId, difficultyId), score, tag);
|
||||
}
|
||||
}
|
||||
@@ -117,16 +132,33 @@ class Leaderboards
|
||||
@:nullSafety
|
||||
class LeaderboardsSandboxed
|
||||
{
|
||||
public static function getLeaderboardBySong(songId:String, difficultyId:String)
|
||||
/**
|
||||
* Get the leaderboard for a given song and difficulty.
|
||||
*
|
||||
* @param songId The ID for the song.
|
||||
* @param difficultyId The current difficulty, suffixed with the variation, like `easy-pico` or `nightmare`.
|
||||
* @return The Leaderboard ID for the given level and difficulty.
|
||||
*/
|
||||
public static function getLeaderboardBySong(songId:String, difficultyId:String):Leaderboard
|
||||
{
|
||||
return Leaderboard.getLeaderboardBySong(songId, difficultyId);
|
||||
}
|
||||
|
||||
public static function getLeaderboardByLevel(levelId:String)
|
||||
/**
|
||||
* Get the leaderboard for a given story level and difficulty.
|
||||
* @param levelId The ID for the story level.
|
||||
* @return The Leaderboard ID for the given level and difficulty.
|
||||
*/
|
||||
public static function getLeaderboardByLevel(levelId:String):Leaderboard
|
||||
{
|
||||
return Leaderboard.getLeaderboardByLevel(levelId);
|
||||
}
|
||||
|
||||
/**
|
||||
* Request to receive scores from Newgrounds.
|
||||
* @param leaderboard The leaderboard to fetch scores from.
|
||||
* @param params Additional parameters for fetching the score.
|
||||
*/
|
||||
public function requestScores(leaderboard:Leaderboard, params:RequestScoresParams)
|
||||
{
|
||||
Leaderboards.requestScores(leaderboard, params);
|
||||
@@ -180,106 +212,118 @@ typedef RequestScoresParams =
|
||||
}
|
||||
#end
|
||||
|
||||
/**
|
||||
* An enumeration of Newgrounds leaderboards for the game's levels and songs.
|
||||
*/
|
||||
enum abstract Leaderboard(Int) from Int to Int
|
||||
{
|
||||
/**
|
||||
* Represents an undefined or invalid leaderboard.
|
||||
*/
|
||||
var Unknown = -1;
|
||||
public var Unknown = -1;
|
||||
|
||||
//
|
||||
// STORY LEVELS
|
||||
//
|
||||
var StoryWeek1 = #if FEATURE_NEWGROUNDS_TESTING_MEDALS 14239 #else 9615 #end;
|
||||
var StoryWeek2 = #if FEATURE_NEWGROUNDS_TESTING_MEDALS 14240 #else 9616 #end;
|
||||
var StoryWeek3 = #if FEATURE_NEWGROUNDS_TESTING_MEDALS 14242 #else 9767 #end;
|
||||
var StoryWeek4 = #if FEATURE_NEWGROUNDS_TESTING_MEDALS 14241 #else 9866 #end;
|
||||
var StoryWeek5 = #if FEATURE_NEWGROUNDS_TESTING_MEDALS 14243 #else 9956 #end;
|
||||
var StoryWeek6 = #if FEATURE_NEWGROUNDS_TESTING_MEDALS 14244 #else 9957 #end;
|
||||
var StoryWeek7 = #if FEATURE_NEWGROUNDS_TESTING_MEDALS 14245 #else 14682 #end;
|
||||
var StoryWeekend1 = #if FEATURE_NEWGROUNDS_TESTING_MEDALS 14237 #else 14683 #end;
|
||||
// Tutorial only has one song.
|
||||
public var StoryWeek1 = #if FEATURE_NEWGROUNDS_TESTING_MEDALS 14239 #else 9615 #end;
|
||||
public var StoryWeek2 = #if FEATURE_NEWGROUNDS_TESTING_MEDALS 14240 #else 9616 #end;
|
||||
public var StoryWeek3 = #if FEATURE_NEWGROUNDS_TESTING_MEDALS 14242 #else 9767 #end;
|
||||
public var StoryWeek4 = #if FEATURE_NEWGROUNDS_TESTING_MEDALS 14241 #else 9866 #end;
|
||||
public var StoryWeek5 = #if FEATURE_NEWGROUNDS_TESTING_MEDALS 14243 #else 9956 #end;
|
||||
public var StoryWeek6 = #if FEATURE_NEWGROUNDS_TESTING_MEDALS 14244 #else 9957 #end;
|
||||
public var StoryWeek7 = #if FEATURE_NEWGROUNDS_TESTING_MEDALS 14245 #else 14682 #end;
|
||||
public var StoryWeekend1 = #if FEATURE_NEWGROUNDS_TESTING_MEDALS 14237 #else 14683 #end;
|
||||
|
||||
// Collab 1 only has one song.
|
||||
//
|
||||
// SONGS
|
||||
//
|
||||
// Tutorial
|
||||
var Tutorial = #if FEATURE_NEWGROUNDS_TESTING_MEDALS 14249 #else 14684 #end;
|
||||
public var Tutorial = #if FEATURE_NEWGROUNDS_TESTING_MEDALS 14249 #else 14684 #end;
|
||||
|
||||
// Week 1
|
||||
var Bopeebo = #if FEATURE_NEWGROUNDS_TESTING_MEDALS 14246 #else 9603 #end;
|
||||
var BopeeboErect = #if FEATURE_NEWGROUNDS_TESTING_MEDALS 1000000 #else 14685 #end;
|
||||
var BopeeboPicoMix = #if FEATURE_NEWGROUNDS_TESTING_MEDALS 1000000 #else 14686 #end;
|
||||
var Fresh = #if FEATURE_NEWGROUNDS_TESTING_MEDALS 14247 #else 9602 #end;
|
||||
var FreshErect = #if FEATURE_NEWGROUNDS_TESTING_MEDALS 1000000 #else 14687 #end;
|
||||
var FreshPicoMix = #if FEATURE_NEWGROUNDS_TESTING_MEDALS 1000000 #else 14688 #end;
|
||||
var DadBattle = #if FEATURE_NEWGROUNDS_TESTING_MEDALS 14248 #else 9605 #end;
|
||||
var DadBattleErect = #if FEATURE_NEWGROUNDS_TESTING_MEDALS 1000000 #else 14689 #end;
|
||||
var DadBattlePicoMix = #if FEATURE_NEWGROUNDS_TESTING_MEDALS 1000000 #else 14690 #end;
|
||||
public var Bopeebo = #if FEATURE_NEWGROUNDS_TESTING_MEDALS 14246 #else 9603 #end;
|
||||
public var BopeeboErect = #if FEATURE_NEWGROUNDS_TESTING_MEDALS 1000000 #else 14685 #end;
|
||||
public var BopeeboPicoMix = #if FEATURE_NEWGROUNDS_TESTING_MEDALS 1000000 #else 14686 #end;
|
||||
public var Fresh = #if FEATURE_NEWGROUNDS_TESTING_MEDALS 14247 #else 9602 #end;
|
||||
public var FreshErect = #if FEATURE_NEWGROUNDS_TESTING_MEDALS 1000000 #else 14687 #end;
|
||||
public var FreshPicoMix = #if FEATURE_NEWGROUNDS_TESTING_MEDALS 1000000 #else 14688 #end;
|
||||
public var DadBattle = #if FEATURE_NEWGROUNDS_TESTING_MEDALS 14248 #else 9605 #end;
|
||||
public var DadBattleErect = #if FEATURE_NEWGROUNDS_TESTING_MEDALS 1000000 #else 14689 #end;
|
||||
public var DadBattlePicoMix = #if FEATURE_NEWGROUNDS_TESTING_MEDALS 1000000 #else 14690 #end;
|
||||
|
||||
// Week 2
|
||||
var Spookeez = #if FEATURE_NEWGROUNDS_TESTING_MEDALS 1000000 #else 9604 #end;
|
||||
var SpookeezErect = #if FEATURE_NEWGROUNDS_TESTING_MEDALS 1000000 #else 14691 #end;
|
||||
var SpookeezPicoMix = #if FEATURE_NEWGROUNDS_TESTING_MEDALS 1000000 #else 14692 #end;
|
||||
var South = #if FEATURE_NEWGROUNDS_TESTING_MEDALS 1000000 #else 9606 #end;
|
||||
var SouthErect = #if FEATURE_NEWGROUNDS_TESTING_MEDALS 1000000 #else 14693 #end;
|
||||
var SouthPicoMix = #if FEATURE_NEWGROUNDS_TESTING_MEDALS 1000000 #else 14694 #end;
|
||||
var Monster = #if FEATURE_NEWGROUNDS_TESTING_MEDALS 1000000 #else 14703 #end;
|
||||
public var Spookeez = #if FEATURE_NEWGROUNDS_TESTING_MEDALS 1000000 #else 9604 #end;
|
||||
public var SpookeezErect = #if FEATURE_NEWGROUNDS_TESTING_MEDALS 1000000 #else 14691 #end;
|
||||
public var SpookeezPicoMix = #if FEATURE_NEWGROUNDS_TESTING_MEDALS 1000000 #else 14692 #end;
|
||||
public var South = #if FEATURE_NEWGROUNDS_TESTING_MEDALS 1000000 #else 9606 #end;
|
||||
public var SouthErect = #if FEATURE_NEWGROUNDS_TESTING_MEDALS 1000000 #else 14693 #end;
|
||||
public var SouthPicoMix = #if FEATURE_NEWGROUNDS_TESTING_MEDALS 1000000 #else 14694 #end;
|
||||
public var Monster = #if FEATURE_NEWGROUNDS_TESTING_MEDALS 1000000 #else 14703 #end;
|
||||
|
||||
// Week 3
|
||||
var Pico = #if FEATURE_NEWGROUNDS_TESTING_MEDALS 1000000 #else 9766 #end;
|
||||
var PicoErect = #if FEATURE_NEWGROUNDS_TESTING_MEDALS 1000000 #else 14695 #end;
|
||||
var PicoPicoMix = #if FEATURE_NEWGROUNDS_TESTING_MEDALS 1000000 #else 14696 #end;
|
||||
var PhillyNice = #if FEATURE_NEWGROUNDS_TESTING_MEDALS 1000000 #else 9769 #end;
|
||||
var PhillyNiceErect = #if FEATURE_NEWGROUNDS_TESTING_MEDALS 1000000 #else 14697 #end;
|
||||
var PhillyNicePicoMix = #if FEATURE_NEWGROUNDS_TESTING_MEDALS 1000000 #else 14698 #end;
|
||||
var Blammed = #if FEATURE_NEWGROUNDS_TESTING_MEDALS 1000000 #else 9768 #end;
|
||||
var BlammedErect = #if FEATURE_NEWGROUNDS_TESTING_MEDALS 1000000 #else 14704 #end;
|
||||
var BlammedPicoMix = #if FEATURE_NEWGROUNDS_TESTING_MEDALS 1000000 #else 14705 #end;
|
||||
public var Pico = #if FEATURE_NEWGROUNDS_TESTING_MEDALS 1000000 #else 9766 #end;
|
||||
public var PicoErect = #if FEATURE_NEWGROUNDS_TESTING_MEDALS 1000000 #else 14695 #end;
|
||||
public var PicoPicoMix = #if FEATURE_NEWGROUNDS_TESTING_MEDALS 1000000 #else 14696 #end;
|
||||
public var PhillyNice = #if FEATURE_NEWGROUNDS_TESTING_MEDALS 1000000 #else 9769 #end;
|
||||
public var PhillyNiceErect = #if FEATURE_NEWGROUNDS_TESTING_MEDALS 1000000 #else 14697 #end;
|
||||
public var PhillyNicePicoMix = #if FEATURE_NEWGROUNDS_TESTING_MEDALS 1000000 #else 14698 #end;
|
||||
public var Blammed = #if FEATURE_NEWGROUNDS_TESTING_MEDALS 1000000 #else 9768 #end;
|
||||
public var BlammedErect = #if FEATURE_NEWGROUNDS_TESTING_MEDALS 1000000 #else 14704 #end;
|
||||
public var BlammedPicoMix = #if FEATURE_NEWGROUNDS_TESTING_MEDALS 1000000 #else 14705 #end;
|
||||
|
||||
// Week 4
|
||||
var SatinPanties = #if FEATURE_NEWGROUNDS_TESTING_MEDALS 1000000 #else 9869 #end;
|
||||
var SatinPantiesErect = #if FEATURE_NEWGROUNDS_TESTING_MEDALS 1000000 #else 14701 #end;
|
||||
var High = #if FEATURE_NEWGROUNDS_TESTING_MEDALS 1000000 #else 9867 #end;
|
||||
var HighErect = #if FEATURE_NEWGROUNDS_TESTING_MEDALS 1000000 #else 14699 #end;
|
||||
var MILF = #if FEATURE_NEWGROUNDS_TESTING_MEDALS 1000000 #else 9868 #end;
|
||||
public var SatinPanties = #if FEATURE_NEWGROUNDS_TESTING_MEDALS 1000000 #else 9869 #end;
|
||||
public var SatinPantiesErect = #if FEATURE_NEWGROUNDS_TESTING_MEDALS 1000000 #else 14701 #end;
|
||||
public var High = #if FEATURE_NEWGROUNDS_TESTING_MEDALS 1000000 #else 9867 #end;
|
||||
public var HighErect = #if FEATURE_NEWGROUNDS_TESTING_MEDALS 1000000 #else 14699 #end;
|
||||
public var MILF = #if FEATURE_NEWGROUNDS_TESTING_MEDALS 1000000 #else 9868 #end;
|
||||
|
||||
// Week 5
|
||||
var Cocoa = #if FEATURE_NEWGROUNDS_TESTING_MEDALS 1000000 #else 14706 #end;
|
||||
var CocoaErect = #if FEATURE_NEWGROUNDS_TESTING_MEDALS 1000000 #else 14707 #end;
|
||||
var CocoaPicoMix = #if FEATURE_NEWGROUNDS_TESTING_MEDALS 1000000 #else 14708 #end;
|
||||
var Eggnog = #if FEATURE_NEWGROUNDS_TESTING_MEDALS 1000000 #else 14709 #end;
|
||||
var EggnogErect = #if FEATURE_NEWGROUNDS_TESTING_MEDALS 1000000 #else 14711 #end;
|
||||
var EggnogPicoMix = #if FEATURE_NEWGROUNDS_TESTING_MEDALS 1000000 #else 14710 #end;
|
||||
var WinterHorrorland = #if FEATURE_NEWGROUNDS_TESTING_MEDALS 1000000 #else 14712 #end;
|
||||
public var Cocoa = #if FEATURE_NEWGROUNDS_TESTING_MEDALS 1000000 #else 14706 #end;
|
||||
public var CocoaErect = #if FEATURE_NEWGROUNDS_TESTING_MEDALS 1000000 #else 14707 #end;
|
||||
public var CocoaPicoMix = #if FEATURE_NEWGROUNDS_TESTING_MEDALS 1000000 #else 14708 #end;
|
||||
public var Eggnog = #if FEATURE_NEWGROUNDS_TESTING_MEDALS 1000000 #else 14709 #end;
|
||||
public var EggnogErect = #if FEATURE_NEWGROUNDS_TESTING_MEDALS 1000000 #else 14711 #end;
|
||||
public var EggnogPicoMix = #if FEATURE_NEWGROUNDS_TESTING_MEDALS 1000000 #else 14710 #end;
|
||||
public var WinterHorrorland = #if FEATURE_NEWGROUNDS_TESTING_MEDALS 1000000 #else 14712 #end;
|
||||
|
||||
// Week 6
|
||||
var Senpai = #if FEATURE_NEWGROUNDS_TESTING_MEDALS 1000000 #else 9958 #end;
|
||||
var SenpaiErect = #if FEATURE_NEWGROUNDS_TESTING_MEDALS 1000000 #else 14713 #end;
|
||||
var SenpaiPicoMix = #if FEATURE_NEWGROUNDS_TESTING_MEDALS 1000000 #else 14716 #end;
|
||||
var Roses = #if FEATURE_NEWGROUNDS_TESTING_MEDALS 1000000 #else 9959 #end;
|
||||
var RosesErect = #if FEATURE_NEWGROUNDS_TESTING_MEDALS 1000000 #else 14714 #end;
|
||||
var RosesPicoMix = #if FEATURE_NEWGROUNDS_TESTING_MEDALS 1000000 #else 14717 #end;
|
||||
var Thorns = #if FEATURE_NEWGROUNDS_TESTING_MEDALS 1000000 #else 9960 #end;
|
||||
var ThornsErect = #if FEATURE_NEWGROUNDS_TESTING_MEDALS 1000000 #else 14715 #end;
|
||||
public var Senpai = #if FEATURE_NEWGROUNDS_TESTING_MEDALS 1000000 #else 9958 #end;
|
||||
public var SenpaiErect = #if FEATURE_NEWGROUNDS_TESTING_MEDALS 1000000 #else 14713 #end;
|
||||
public var SenpaiPicoMix = #if FEATURE_NEWGROUNDS_TESTING_MEDALS 1000000 #else 14716 #end;
|
||||
public var Roses = #if FEATURE_NEWGROUNDS_TESTING_MEDALS 1000000 #else 9959 #end;
|
||||
public var RosesErect = #if FEATURE_NEWGROUNDS_TESTING_MEDALS 1000000 #else 14714 #end;
|
||||
public var RosesPicoMix = #if FEATURE_NEWGROUNDS_TESTING_MEDALS 1000000 #else 14717 #end;
|
||||
public var Thorns = #if FEATURE_NEWGROUNDS_TESTING_MEDALS 1000000 #else 9960 #end;
|
||||
public var ThornsErect = #if FEATURE_NEWGROUNDS_TESTING_MEDALS 1000000 #else 14715 #end;
|
||||
|
||||
// Week 7
|
||||
var Ugh = #if FEATURE_NEWGROUNDS_TESTING_MEDALS 1000000 #else 14718 #end;
|
||||
var UghErect = #if FEATURE_NEWGROUNDS_TESTING_MEDALS 1000000 #else 14722 #end;
|
||||
var UghPicoMix = #if FEATURE_NEWGROUNDS_TESTING_MEDALS 1000000 #else 14721 #end;
|
||||
var Guns = #if FEATURE_NEWGROUNDS_TESTING_MEDALS 1000000 #else 14719 #end;
|
||||
var GunsPicoMix = #if FEATURE_NEWGROUNDS_TESTING_MEDALS 1000000 #else 14723 #end;
|
||||
var Stress = #if FEATURE_NEWGROUNDS_TESTING_MEDALS 1000000 #else 14720 #end;
|
||||
var StressPicoMix = #if FEATURE_NEWGROUNDS_TESTING_MEDALS 1000000 #else 14724 #end;
|
||||
public var Ugh = #if FEATURE_NEWGROUNDS_TESTING_MEDALS 1000000 #else 14718 #end;
|
||||
public var UghErect = #if FEATURE_NEWGROUNDS_TESTING_MEDALS 1000000 #else 14722 #end;
|
||||
public var UghPicoMix = #if FEATURE_NEWGROUNDS_TESTING_MEDALS 1000000 #else 14721 #end;
|
||||
public var Guns = #if FEATURE_NEWGROUNDS_TESTING_MEDALS 1000000 #else 14719 #end;
|
||||
public var GunsPicoMix = #if FEATURE_NEWGROUNDS_TESTING_MEDALS 1000000 #else 14723 #end;
|
||||
public var Stress = #if FEATURE_NEWGROUNDS_TESTING_MEDALS 1000000 #else 14720 #end;
|
||||
public var StressPicoMix = #if FEATURE_NEWGROUNDS_TESTING_MEDALS 1000000 #else 14724 #end;
|
||||
|
||||
// Weekend 1
|
||||
var Darnell = #if FEATURE_NEWGROUNDS_TESTING_MEDALS 1000000 #else 14725 #end;
|
||||
var DarnellErect = #if FEATURE_NEWGROUNDS_TESTING_MEDALS 1000000 #else 14727 #end;
|
||||
var DarnellBFMix = #if FEATURE_NEWGROUNDS_TESTING_MEDALS 1000000 #else 14726 #end;
|
||||
var LitUp = #if FEATURE_NEWGROUNDS_TESTING_MEDALS 1000000 #else 14728 #end;
|
||||
var LitUpBFMix = #if FEATURE_NEWGROUNDS_TESTING_MEDALS 1000000 #else 14729 #end;
|
||||
var TwoHot = #if FEATURE_NEWGROUNDS_TESTING_MEDALS 1000000 #else 14730 #end; // Variable names can't start with a number!
|
||||
var Blazin = #if FEATURE_NEWGROUNDS_TESTING_MEDALS 1000000 #else 14731 #end;
|
||||
public var Darnell = #if FEATURE_NEWGROUNDS_TESTING_MEDALS 1000000 #else 14725 #end;
|
||||
public var DarnellErect = #if FEATURE_NEWGROUNDS_TESTING_MEDALS 1000000 #else 14727 #end;
|
||||
public var DarnellBFMix = #if FEATURE_NEWGROUNDS_TESTING_MEDALS 1000000 #else 14726 #end;
|
||||
public var LitUp = #if FEATURE_NEWGROUNDS_TESTING_MEDALS 1000000 #else 14728 #end;
|
||||
public var LitUpBFMix = #if FEATURE_NEWGROUNDS_TESTING_MEDALS 1000000 #else 14729 #end;
|
||||
public var TwoHot = #if FEATURE_NEWGROUNDS_TESTING_MEDALS 1000000 #else 14730 #end; // Variable names can't start with a number!
|
||||
public var Blazin = #if FEATURE_NEWGROUNDS_TESTING_MEDALS 1000000 #else 14731 #end;
|
||||
|
||||
// Collab 1
|
||||
public var Spaghetti = #if FEATURE_NEWGROUNDS_TESTING_MEDALS 15448 #else 15451 #end;
|
||||
|
||||
/**
|
||||
* Get the numeric ID for the leaderboard on the Newgrounds site.
|
||||
* @return The numeric ID for the leaderboard.
|
||||
*/
|
||||
public function getId():Int
|
||||
{
|
||||
return this;
|
||||
@@ -288,7 +332,6 @@ enum abstract Leaderboard(Int) from Int to Int
|
||||
/**
|
||||
* Get the leaderboard for a given level and difficulty.
|
||||
* @param levelId The ID for the story level.
|
||||
* @param difficulty The current difficulty.
|
||||
* @return The Leaderboard ID for the given level and difficulty.
|
||||
*/
|
||||
public static function getLeaderboardByLevel(levelId:String):Leaderboard
|
||||
@@ -311,14 +354,16 @@ enum abstract Leaderboard(Int) from Int to Int
|
||||
return StoryWeek7;
|
||||
case "weekend1":
|
||||
return StoryWeekend1;
|
||||
// Collab 1 has only one song.
|
||||
default:
|
||||
return Unknown;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the leaderboard for a given level and difficulty.
|
||||
* @param levelId The ID for the story level.
|
||||
* Get the leaderboard for a given song and difficulty.
|
||||
*
|
||||
* @param songId The ID for the song.
|
||||
* @param difficulty The current difficulty, suffixed with the variation, like `easy-pico` or `nightmare`.
|
||||
* @return The Leaderboard ID for the given level and difficulty.
|
||||
*/
|
||||
@@ -479,6 +524,8 @@ enum abstract Leaderboard(Int) from Int to Int
|
||||
return TwoHot;
|
||||
case "blazin":
|
||||
return Blazin;
|
||||
case "spaghetti":
|
||||
return Spaghetti;
|
||||
default:
|
||||
return Unknown;
|
||||
}
|
||||
|
||||
@@ -5,45 +5,54 @@ import io.newgrounds.objects.Medal as MedalData;
|
||||
import funkin.util.plugins.NewgroundsMedalPlugin;
|
||||
import flixel.graphics.FlxGraphic;
|
||||
import openfl.display.BitmapData;
|
||||
import io.newgrounds.utils.MedalList;
|
||||
import haxe.Json;
|
||||
|
||||
/**
|
||||
* Handles interactions with the medals on the Newgrounds website.
|
||||
*/
|
||||
@:nullSafety
|
||||
class Medals
|
||||
{
|
||||
/**
|
||||
* A cache for the medal data fetched from the local `medals.json`.
|
||||
*/
|
||||
public static var medalJSON:Array<MedalJSON> = [];
|
||||
|
||||
/**
|
||||
* Retrieve the leaderboard data via the Newgrounds API.
|
||||
* @return The leaderboard data.
|
||||
*/
|
||||
public static function listMedalData():Map<Medal, MedalData>
|
||||
{
|
||||
var medalList = NewgroundsClient.instance.medals;
|
||||
|
||||
if (medalList == null)
|
||||
{
|
||||
trace('[NEWGROUNDS] Not logged in, cannot fetch medal data!');
|
||||
trace(' NEWGROUNDS '.bold().bg_orange() + ' Not logged in, cannot fetch medal data!');
|
||||
return [];
|
||||
}
|
||||
|
||||
return @:privateAccess medalList._map?.copy() ?? [];
|
||||
}
|
||||
|
||||
/**
|
||||
* Awards the player a specific medal, updating their Newgrounds account and
|
||||
* displaying the corresponding popup.
|
||||
* @param medal The medal to be awarded.
|
||||
*/
|
||||
public static function award(medal:Medal):Void
|
||||
{
|
||||
if (NewgroundsClient.instance.isLoggedIn())
|
||||
{
|
||||
var medalList = NewgroundsClient.instance.medals;
|
||||
@:privateAccess
|
||||
if (medalList == null || medalList._map == null) return;
|
||||
|
||||
var medalData:Null<MedalData> = medalList.get(medal.getId());
|
||||
var medalData:Null<MedalData> = listMedalData().get(medal.getId());
|
||||
@:privateAccess
|
||||
if (medalData == null || medalData._data == null)
|
||||
{
|
||||
trace('[NEWGROUNDS] Could not retrieve data for medal: ${medal}');
|
||||
trace(' NEWGROUNDS '.bold().bg_orange() + ' Could not retrieve data for medal: ${medal}');
|
||||
return;
|
||||
}
|
||||
else if (!medalData.unlocked)
|
||||
{
|
||||
trace('[NEWGROUNDS] Awarding medal (${medal}).');
|
||||
trace(' NEWGROUNDS '.bold().bg_orange() + ' Awarding medal (${medal}).');
|
||||
medalData.sendUnlock();
|
||||
|
||||
// Play the medal unlock animation, but only if the user has not already unlocked it.
|
||||
@@ -85,15 +94,18 @@ class Medals
|
||||
}
|
||||
else
|
||||
{
|
||||
trace('[NEWGROUNDS] User already has medal (${medal}).');
|
||||
trace(' NEWGROUNDS '.bold().bg_orange() + ' User already has medal (${medal}).');
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
trace('[NEWGROUNDS] Attempted to award medal (${medal}), but not logged into Newgrounds.');
|
||||
trace(' NEWGROUNDS '.bold().bg_orange() + ' Attempted to award medal (${medal}), but not logged into Newgrounds.');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Loads the full medal data from local JSON data.
|
||||
*/
|
||||
public static function loadMedalJSON():Void
|
||||
{
|
||||
var jsonPath = Paths.json('medals');
|
||||
@@ -102,12 +114,12 @@ class Medals
|
||||
|
||||
var parser = new json2object.JsonParser<Array<MedalJSON>>();
|
||||
parser.ignoreUnknownVariables = false;
|
||||
trace('[NEWGROUNDS] Parsing local medal data...');
|
||||
trace(' NEWGROUNDS '.bold().bg_orange() + ' Parsing local medal data...');
|
||||
parser.fromJson(jsonString, jsonPath);
|
||||
|
||||
if (parser.errors.length > 0)
|
||||
{
|
||||
trace('[NEWGROUNDS] Failed to parse local medal data!');
|
||||
trace(' NEWGROUNDS '.bold().bg_orange() + ' Failed to parse local medal data!');
|
||||
for (error in parser.errors)
|
||||
funkin.data.DataError.printError(error);
|
||||
medalJSON = [];
|
||||
@@ -118,17 +130,18 @@ class Medals
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetches the data for a medal from the Newgrounds API.
|
||||
* @param medal The ID of the medal to fetch.
|
||||
* @return The fetched medal data.
|
||||
*/
|
||||
public static function fetchMedalData(medal:Medal):Null<FetchedMedalData>
|
||||
{
|
||||
var medalList = NewgroundsClient.instance.medals;
|
||||
@:privateAccess
|
||||
if (medalList == null || medalList._map == null) return null;
|
||||
|
||||
var medalData:Null<MedalData> = medalList.get(medal.getId());
|
||||
var medalData:Null<MedalData> = listMedalData().get(medal.getId());
|
||||
@:privateAccess
|
||||
if (medalData == null || medalData._data == null)
|
||||
{
|
||||
trace('[NEWGROUNDS] Could not retrieve data for medal: ${medal}');
|
||||
trace(' NEWGROUNDS '.bold().bg_orange() + ' Could not retrieve data for medal: ${medal}');
|
||||
return null;
|
||||
}
|
||||
|
||||
@@ -144,12 +157,16 @@ class Medals
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Awards the player a medal for completing a specific story level.
|
||||
* @param id The level ID for the story level that was completed.
|
||||
*/
|
||||
public static function awardStoryLevel(id:String):Void
|
||||
{
|
||||
var medal:Medal = Medal.getMedalByStoryLevel(id);
|
||||
if (medal == Medal.Unknown)
|
||||
{
|
||||
trace('[NEWGROUNDS] Story level does not have a medal! (${id}).');
|
||||
trace(' NEWGROUNDS '.bold().bg_orange() + ' Story level does not have a medal! (${id}).');
|
||||
return;
|
||||
}
|
||||
Medals.award(medal);
|
||||
@@ -161,16 +178,30 @@ class Medals
|
||||
*/
|
||||
class MedalsSandboxed
|
||||
{
|
||||
/**
|
||||
* Fetches the data for a medal from the Newgrounds API.
|
||||
* @param medal The ID of the medal to fetch.
|
||||
* @return The fetched medal data.
|
||||
*/
|
||||
public static function fetchMedalData(medal:Medal):Null<FetchedMedalData>
|
||||
{
|
||||
return Medals.fetchMedalData(medal);
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves the medal for completing a specific story level.
|
||||
* @param id The ID for the story level that was completed.
|
||||
* @return The medal to award.
|
||||
*/
|
||||
public static function getMedalByStoryLevel(id:String):Medal
|
||||
{
|
||||
return Medal.getMedalByStoryLevel(id);
|
||||
}
|
||||
|
||||
/**
|
||||
* Lists all medals aside from the `Unknown` one.
|
||||
* @return An array of all medals.
|
||||
*/
|
||||
public static function getAllMedals():Array<Medal>
|
||||
{
|
||||
return Medal.getAllMedals();
|
||||
@@ -219,140 +250,157 @@ typedef MedalJSON =
|
||||
var icon:String;
|
||||
}
|
||||
|
||||
/**
|
||||
* An enumeration of all the game's medals.
|
||||
*/
|
||||
enum abstract Medal(Int) from Int to Int
|
||||
{
|
||||
/**
|
||||
* Represents an undefined or invalid medal.
|
||||
*/
|
||||
var Unknown = -1;
|
||||
public var Unknown = -1;
|
||||
|
||||
/**
|
||||
* I Said Funkin'!
|
||||
* Start the game for the first time.
|
||||
*/
|
||||
var StartGame = #if FEATURE_NEWGROUNDS_TESTING_MEDALS 80894 #else 60960 #end;
|
||||
public var StartGame = #if FEATURE_NEWGROUNDS_TESTING_MEDALS 80894 #else 60960 #end;
|
||||
|
||||
/**
|
||||
* That's How You Do It!
|
||||
* Beat Tutoria l in Story Mode (on any difficulty).
|
||||
* Beat Tutorial in Story Mode (on any difficulty).
|
||||
*/
|
||||
var StoryTutorial = #if FEATURE_NEWGROUNDS_TESTING_MEDALS 80906 #else 83647 #end;
|
||||
public var StoryTutorial = #if FEATURE_NEWGROUNDS_TESTING_MEDALS 80906 #else 83647 #end;
|
||||
|
||||
/**
|
||||
* More Like Daddy Queerest
|
||||
* Beat Week 1 in Story Mode (on any difficulty).
|
||||
*/
|
||||
var StoryWeek1 = #if FEATURE_NEWGROUNDS_TESTING_MEDALS 80899 #else 60961 #end;
|
||||
public var StoryWeek1 = #if FEATURE_NEWGROUNDS_TESTING_MEDALS 80899 #else 60961 #end;
|
||||
|
||||
/**
|
||||
* IT IS THE SPOOKY MONTH
|
||||
* Beat Week 2 in Story Mode (on any difficulty).
|
||||
*/
|
||||
var StoryWeek2 = #if FEATURE_NEWGROUNDS_TESTING_MEDALS 80900 #else 83648 #end;
|
||||
public var StoryWeek2 = #if FEATURE_NEWGROUNDS_TESTING_MEDALS 80900 #else 83648 #end;
|
||||
|
||||
/**
|
||||
* Pico Funny
|
||||
* Beat Week 3 in Story Mode (on any difficulty).
|
||||
*/
|
||||
var StoryWeek3 = #if FEATURE_NEWGROUNDS_TESTING_MEDALS 80901 #else 83649 #end;
|
||||
public var StoryWeek3 = #if FEATURE_NEWGROUNDS_TESTING_MEDALS 80901 #else 83649 #end;
|
||||
|
||||
/**
|
||||
* Mommy Must Murder
|
||||
* Beat Week 4 in Story Mode (on any difficulty).
|
||||
*/
|
||||
var StoryWeek4 = #if FEATURE_NEWGROUNDS_TESTING_MEDALS 80902 #else 83650 #end;
|
||||
public var StoryWeek4 = #if FEATURE_NEWGROUNDS_TESTING_MEDALS 80902 #else 83650 #end;
|
||||
|
||||
/**
|
||||
* Yule Tide Joy
|
||||
* Beat Week 5 in Story Mode (on any difficulty).
|
||||
*/
|
||||
var StoryWeek5 = #if FEATURE_NEWGROUNDS_TESTING_MEDALS 80903 #else 83651 #end;
|
||||
public var StoryWeek5 = #if FEATURE_NEWGROUNDS_TESTING_MEDALS 80903 #else 83651 #end;
|
||||
|
||||
/**
|
||||
* A Visual Novelty
|
||||
* Beat Week 6 in Story Mode (on any difficulty).
|
||||
*/
|
||||
var StoryWeek6 = #if FEATURE_NEWGROUNDS_TESTING_MEDALS 80904 #else 83652 #end;
|
||||
public var StoryWeek6 = #if FEATURE_NEWGROUNDS_TESTING_MEDALS 80904 #else 83652 #end;
|
||||
|
||||
/**
|
||||
* I <3 JohnnyUtah
|
||||
* Beat Week 7 in Story Mode (on any difficulty).
|
||||
*/
|
||||
var StoryWeek7 = #if FEATURE_NEWGROUNDS_TESTING_MEDALS 80905 #else 83653 #end;
|
||||
public var StoryWeek7 = #if FEATURE_NEWGROUNDS_TESTING_MEDALS 80905 #else 83653 #end;
|
||||
|
||||
/**
|
||||
* Yo, Really Think So?
|
||||
* Beat Weekend 1 in Story Mode (on any difficulty).
|
||||
*/
|
||||
var StoryWeekend1 = #if FEATURE_NEWGROUNDS_TESTING_MEDALS 80907 #else 83654 #end;
|
||||
public var StoryWeekend1 = #if FEATURE_NEWGROUNDS_TESTING_MEDALS 80907 #else 83654 #end;
|
||||
|
||||
/**
|
||||
* Eat It Up!
|
||||
* Beat Collab 1 in Story Mode (on any difficulty).
|
||||
*/
|
||||
public var StoryCollab1 = #if FEATURE_NEWGROUNDS_TESTING_MEDALS 87697 #else 87714 #end;
|
||||
|
||||
/**
|
||||
* Stay Funky
|
||||
* Press TAB in Freeplay and unlock your first character.
|
||||
*/
|
||||
var CharSelect = #if FEATURE_NEWGROUNDS_TESTING_MEDALS 83633 #else 83655 #end;
|
||||
public var CharSelect = #if FEATURE_NEWGROUNDS_TESTING_MEDALS 83633 #else 83655 #end;
|
||||
|
||||
/**
|
||||
* A Challenger Appears
|
||||
* Beat any Pico remix in Freeplay (on any difficulty).
|
||||
*/
|
||||
var FreeplayPicoMix = #if FEATURE_NEWGROUNDS_TESTING_MEDALS 80910 #else 83656 #end;
|
||||
public var FreeplayPicoMix = #if FEATURE_NEWGROUNDS_TESTING_MEDALS 80910 #else 83656 #end;
|
||||
|
||||
/**
|
||||
* De-Stressing
|
||||
* Beat Stress (Pico Mix) in Freeplay (on Normal difficulty or higher).
|
||||
*/
|
||||
var FreeplayStressPico = #if FEATURE_NEWGROUNDS_TESTING_MEDALS 83629 #else 83657 #end;
|
||||
public var FreeplayStressPico = #if FEATURE_NEWGROUNDS_TESTING_MEDALS 83629 #else 83657 #end;
|
||||
|
||||
/**
|
||||
* L
|
||||
* Earn a Loss rating on any song (on any difficulty).
|
||||
*/
|
||||
var LossRating = #if FEATURE_NEWGROUNDS_TESTING_MEDALS 80915 #else 83658 #end;
|
||||
public var LossRating = #if FEATURE_NEWGROUNDS_TESTING_MEDALS 80915 #else 83658 #end;
|
||||
|
||||
/**
|
||||
* Getting Freaky
|
||||
* Earn a Perfect rating on any song on Hard difficulty or higher.
|
||||
* NOTE: Should also be awarded for a Gold Perfect because otherwise that would be annoying.
|
||||
*/
|
||||
var PerfectRatingHard = #if FEATURE_NEWGROUNDS_TESTING_MEDALS 80908 #else 83659 #end;
|
||||
public var PerfectRatingHard = #if FEATURE_NEWGROUNDS_TESTING_MEDALS 80908 #else 83659 #end;
|
||||
|
||||
/**
|
||||
* You Should Drink More Water
|
||||
* Earn a Golden Perfect rating on any song on Hard difficulty or higher.
|
||||
*/
|
||||
var GoldPerfectRatingHard = #if FEATURE_NEWGROUNDS_TESTING_MEDALS 80909 #else 83660 #end;
|
||||
public var GoldPerfectRatingHard = #if FEATURE_NEWGROUNDS_TESTING_MEDALS 80909 #else 83660 #end;
|
||||
|
||||
/**
|
||||
* Harder Than Hard
|
||||
* Beat any Erect remix in Freeplay on Erect or Nightmare difficulty.
|
||||
*/
|
||||
var ErectDifficulty = #if FEATURE_NEWGROUNDS_TESTING_MEDALS 80911 #else 83661 #end;
|
||||
public var ErectDifficulty = #if FEATURE_NEWGROUNDS_TESTING_MEDALS 80911 #else 83661 #end;
|
||||
|
||||
/**
|
||||
* The Rap God
|
||||
* Earn a Gold Perfect rating on any song on Nightmare difficulty.
|
||||
*/
|
||||
var GoldPerfectRatingNightmare = #if FEATURE_NEWGROUNDS_TESTING_MEDALS 80912 #else 83662 #end;
|
||||
public var GoldPerfectRatingNightmare = #if FEATURE_NEWGROUNDS_TESTING_MEDALS 80912 #else 83662 #end;
|
||||
|
||||
/**
|
||||
* Just like the game!
|
||||
* Get freaky on a Friday.
|
||||
* NOTE: You must beat at least one song on any difficulty.
|
||||
*/
|
||||
var FridayNight = #if FEATURE_NEWGROUNDS_TESTING_MEDALS 80913 #else 61034 #end;
|
||||
public var FridayNight = #if FEATURE_NEWGROUNDS_TESTING_MEDALS 80913 #else 61034 #end;
|
||||
|
||||
/**
|
||||
* Nice
|
||||
* Earn a rating of EXACTLY 69% (good luck).
|
||||
*/
|
||||
var Nice = #if FEATURE_NEWGROUNDS_TESTING_MEDALS 80914 #else 83646 #end;
|
||||
public var Nice = #if FEATURE_NEWGROUNDS_TESTING_MEDALS 80914 #else 83646 #end;
|
||||
|
||||
/**
|
||||
* @return The internal ID for the medal.
|
||||
*/
|
||||
public function getId():Int
|
||||
{
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves the medal for completing a specific story level.
|
||||
* @param levelId The ID for the story level that was completed.
|
||||
* @return The medal to award.
|
||||
*/
|
||||
public static function getMedalByStoryLevel(levelId:String):Medal
|
||||
{
|
||||
switch (levelId)
|
||||
@@ -375,6 +423,8 @@ enum abstract Medal(Int) from Int to Int
|
||||
return StoryWeek7;
|
||||
case "weekend1":
|
||||
return StoryWeekend1;
|
||||
case "sserafim":
|
||||
return StoryCollab1;
|
||||
default:
|
||||
return Unknown;
|
||||
}
|
||||
@@ -382,8 +432,9 @@ enum abstract Medal(Int) from Int to Int
|
||||
|
||||
/**
|
||||
* Lists all medals aside from the `Unknown` one.
|
||||
* @return An array of all medals.
|
||||
*/
|
||||
public static function getAllMedals()
|
||||
public static function getAllMedals():Array<Medal>
|
||||
{
|
||||
return [
|
||||
StartGame,
|
||||
|
||||
@@ -25,7 +25,7 @@ class NGSaveSlot
|
||||
|
||||
public static function loadInstance():NGSaveSlot
|
||||
{
|
||||
var loadedSave:NGSaveSlot = loadSlot(Save.BASE_SAVE_SLOT);
|
||||
var loadedSave:NGSaveSlot = loadSlot(Constants.BASE_SAVE_SLOT);
|
||||
if (_instance == null) _instance = loadedSave;
|
||||
|
||||
return loadedSave;
|
||||
@@ -33,7 +33,7 @@ class NGSaveSlot
|
||||
|
||||
static function loadSlot(slot:Int):NGSaveSlot
|
||||
{
|
||||
trace('[NEWGROUNDS] Getting save slot from ID $slot');
|
||||
trace(' NEWGROUNDS '.bold().bg_orange() + ' Getting save slot from ID $slot');
|
||||
|
||||
var saveSlot:Null<SaveSlot> = NewgroundsClient.instance.saveSlots?.getById(slot);
|
||||
|
||||
@@ -46,11 +46,6 @@ class NGSaveSlot
|
||||
public function new(?ngSaveSlot:Null<SaveSlot>)
|
||||
{
|
||||
this.ngSaveSlot = ngSaveSlot;
|
||||
|
||||
#if FLX_DEBUG
|
||||
FlxG.console.registerClass(NGSaveSlot);
|
||||
FlxG.console.registerClass(Save);
|
||||
#end
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -67,16 +62,16 @@ class NGSaveSlot
|
||||
switch (outcome)
|
||||
{
|
||||
case SUCCESS:
|
||||
trace('[NEWGROUNDS] Successfully saved save data to save slot!');
|
||||
trace(' NEWGROUNDS '.bold().bg_orange() + ' Successfully saved save data to save slot!');
|
||||
case FAIL(error):
|
||||
trace('[NEWGROUNDS] Failed to save data to save slot!');
|
||||
trace(' NEWGROUNDS '.bold().bg_orange() + ' Failed to save data to save slot!');
|
||||
trace(error);
|
||||
}
|
||||
});
|
||||
}
|
||||
catch (error:String)
|
||||
{
|
||||
trace('[NEWGROUNDS] Failed to save data to save slot!');
|
||||
trace(' NEWGROUNDS '.bold().bg_orange() + ' Failed to save data to save slot!');
|
||||
trace(error);
|
||||
}
|
||||
}
|
||||
@@ -89,7 +84,7 @@ class NGSaveSlot
|
||||
switch (outcome)
|
||||
{
|
||||
case SUCCESS(value):
|
||||
trace('[NEWGROUNDS] Loaded save slot with the ID of ${ngSaveSlot?.id}!');
|
||||
trace(' NEWGROUNDS '.bold().bg_orange() + ' Loaded save slot with the ID of ${ngSaveSlot?.id}!');
|
||||
#if FEATURE_DEBUG_FUNCTIONS
|
||||
trace('Save Slot Data:');
|
||||
trace(value);
|
||||
@@ -101,7 +96,7 @@ class NGSaveSlot
|
||||
onComplete(decodedData);
|
||||
}
|
||||
case FAIL(error):
|
||||
trace('[NEWGROUNDS] Failed to load save slot with the ID of ${ngSaveSlot?.id}!');
|
||||
trace(' NEWGROUNDS '.bold().bg_orange() + ' Failed to load save slot with the ID of ${ngSaveSlot?.id}!');
|
||||
trace(error);
|
||||
|
||||
if (onError != null)
|
||||
@@ -113,7 +108,7 @@ class NGSaveSlot
|
||||
}
|
||||
catch (error:String)
|
||||
{
|
||||
trace('[NEWGROUNDS] Failed to load save slot with the ID of ${ngSaveSlot?.id}!');
|
||||
trace(' NEWGROUNDS '.bold().bg_orange() + ' Failed to load save slot with the ID of ${ngSaveSlot?.id}!');
|
||||
trace(error);
|
||||
|
||||
if (onError != null)
|
||||
@@ -131,26 +126,26 @@ class NGSaveSlot
|
||||
switch (outcome)
|
||||
{
|
||||
case SUCCESS:
|
||||
trace('[NEWGROUNDS] Successfully cleared save slot!');
|
||||
trace(' NEWGROUNDS '.bold().bg_orange() + ' Successfully cleared save slot!');
|
||||
case FAIL(error):
|
||||
trace('[NEWGROUNDS] Failed to clear save slot!');
|
||||
trace(' NEWGROUNDS '.bold().bg_orange() + ' Failed to clear save slot!');
|
||||
trace(error);
|
||||
}
|
||||
});
|
||||
}
|
||||
catch (error:String)
|
||||
{
|
||||
trace('[NEWGROUNDS] Failed to clear save slot!');
|
||||
trace(' NEWGROUNDS '.bold().bg_orange() + ' Failed to clear save slot!');
|
||||
trace(error);
|
||||
}
|
||||
}
|
||||
|
||||
public function checkSlot():Void
|
||||
{
|
||||
trace('[NEWGROUNDS] Checking save slot with the ID of ${ngSaveSlot?.id}...');
|
||||
trace(' NEWGROUNDS '.bold().bg_orange() + ' Checking save slot with the ID of ${ngSaveSlot?.id}...');
|
||||
|
||||
trace(' Is null? ${ngSaveSlot == null}');
|
||||
trace(' Is empty? ${ngSaveSlot?.isEmpty() ?? false}');
|
||||
trace(' Is null? ${ngSaveSlot == null}');
|
||||
trace(' Is empty? ${ngSaveSlot?.isEmpty() ?? false}');
|
||||
}
|
||||
}
|
||||
#end
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
package funkin.api.newgrounds;
|
||||
|
||||
import funkin.util.macro.EnvironmentConfigMacro;
|
||||
import funkin.save.Save;
|
||||
import funkin.api.newgrounds.Medals.Medal;
|
||||
#if FEATURE_NEWGROUNDS
|
||||
@@ -14,12 +13,19 @@ import io.newgrounds.utils.MedalList;
|
||||
import io.newgrounds.utils.SaveSlotList;
|
||||
import io.newgrounds.utils.ScoreBoardList;
|
||||
import io.newgrounds.objects.User;
|
||||
#if FEATURE_MOBILE_WEBVIEW
|
||||
import funkin.mobile.util.WebViewUtil;
|
||||
#end
|
||||
|
||||
@:build(funkin.util.macro.EnvironmentMacro.build())
|
||||
@:nullSafety
|
||||
class NewgroundsClient
|
||||
{
|
||||
static final APP_ID:Null<String> = EnvironmentConfigMacro.environmentConfig?.get("API_NG_APP_ID");
|
||||
static final ENCRYPTION_KEY:Null<String> = EnvironmentConfigMacro.environmentConfig?.get("API_NG_ENC_KEY");
|
||||
@:envField
|
||||
static final API_NG_APP_ID:Null<String>;
|
||||
|
||||
@:envField
|
||||
static final API_NG_ENC_KEY:Null<String>;
|
||||
|
||||
public static var instance(get, never):NewgroundsClient;
|
||||
|
||||
@@ -39,11 +45,11 @@ class NewgroundsClient
|
||||
|
||||
private function new()
|
||||
{
|
||||
trace('[NEWGROUNDS] Initializing client...');
|
||||
trace(' NEWGROUNDS '.bold().bg_orange() + ' Initializing client...');
|
||||
|
||||
#if FEATURE_NEWGROUNDS_DEBUG
|
||||
trace('[NEWGROUNDS] App ID: ${APP_ID}');
|
||||
trace('[NEWGROUNDS] Encryption Key: ${ENCRYPTION_KEY}');
|
||||
trace(' NEWGROUNDS '.bold().bg_orange() + ' App ID: ${API_NG_APP_ID}');
|
||||
trace(' NEWGROUNDS '.bold().bg_orange() + ' Encryption Key: ${API_NG_ENC_KEY}');
|
||||
#end
|
||||
|
||||
if (!hasValidCredentials())
|
||||
@@ -54,9 +60,9 @@ class NewgroundsClient
|
||||
|
||||
@:nullSafety(Off)
|
||||
{
|
||||
NG.create(APP_ID, getSessionId(), #if FEATURE_NEWGROUNDS_DEBUG true #else false #end, onLoginResolved);
|
||||
NG.create(API_NG_APP_ID, getSessionId(), #if FEATURE_NEWGROUNDS_DEBUG true #else false #end, onLoginResolved);
|
||||
|
||||
NG.core.setupEncryption(ENCRYPTION_KEY);
|
||||
NG.core.setupEncryption(API_NG_ENC_KEY);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -64,7 +70,7 @@ class NewgroundsClient
|
||||
{
|
||||
if (NG.core == null) return;
|
||||
|
||||
trace('[NEWGROUNDS] Setting up connection...');
|
||||
trace(' NEWGROUNDS '.bold().bg_orange() + ' Setting up connection...');
|
||||
|
||||
#if FEATURE_NEWGROUNDS_DEBUG
|
||||
NG.core.verbose = true;
|
||||
@@ -75,16 +81,16 @@ class NewgroundsClient
|
||||
if (NG.core.attemptingLogin)
|
||||
{
|
||||
// Session ID was valid and we should be logged in soon.
|
||||
trace('[NEWGROUNDS] Waiting for existing login!');
|
||||
trace(' NEWGROUNDS '.bold().bg_orange() + ' Waiting for existing login!');
|
||||
}
|
||||
else
|
||||
{
|
||||
#if FEATURE_NEWGROUNDS_AUTOLOGIN
|
||||
// Attempt an automatic login.
|
||||
trace('[NEWGROUNDS] Attempting new login immediately!');
|
||||
trace(' NEWGROUNDS '.bold().bg_orange() + ' Attempting new login immediately!');
|
||||
this.autoLogin();
|
||||
#else
|
||||
trace('[NEWGROUNDS] Not logged in, you have to login manually!');
|
||||
trace(' NEWGROUNDS '.bold().bg_orange() + ' Not logged in, you have to login manually!');
|
||||
#end
|
||||
}
|
||||
}
|
||||
@@ -101,14 +107,39 @@ class NewgroundsClient
|
||||
FlxG.log.warn("No Newgrounds client initialized! Are your credentials invalid?");
|
||||
return;
|
||||
}
|
||||
if (NG.core.attemptingLogin)
|
||||
{
|
||||
trace(" NEWGROUNDS '.bold().bg_orange() + ' Login attempt ongoing, will not login until finished.");
|
||||
return;
|
||||
}
|
||||
|
||||
var passportHandler:String->Void = function(passportUrl:String) {
|
||||
// This exists so we can create a popup on mobile but with a WebView instead.
|
||||
#if FEATURE_MOBILE_WEBVIEW
|
||||
if (passportUrl != null)
|
||||
{
|
||||
NG.core.logVerbose('Loading passport from WebView: ${passportUrl}');
|
||||
|
||||
WebViewUtil.openURL(passportUrl, function():Void {
|
||||
NG.core.cancelLoginRequest();
|
||||
});
|
||||
|
||||
NG.core.onPassportUrlOpen();
|
||||
}
|
||||
else
|
||||
NG.core.logError("Cannot open passport");
|
||||
#else
|
||||
NG.core.openPassportUrl();
|
||||
#end
|
||||
};
|
||||
|
||||
if (onSuccess != null && onError != null)
|
||||
{
|
||||
NG.core.requestLogin(onLoginResolvedWithCallbacks.bind(_, onSuccess, onError));
|
||||
NG.core.requestLogin(onLoginResolvedWithCallbacks.bind(_, onSuccess, onError), passportHandler);
|
||||
}
|
||||
else
|
||||
{
|
||||
NG.core.requestLogin(onLoginResolved);
|
||||
NG.core.requestLogin(onLoginResolved, passportHandler);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -156,7 +187,7 @@ class NewgroundsClient
|
||||
}
|
||||
}
|
||||
|
||||
Save.instance.ngSessionId = null;
|
||||
Save.instance.ngSessionId.value = null;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -176,16 +207,20 @@ class NewgroundsClient
|
||||
*/
|
||||
static function hasValidCredentials():Bool
|
||||
{
|
||||
return !(APP_ID == null
|
||||
|| APP_ID == ""
|
||||
|| (APP_ID != null && APP_ID.contains(" "))
|
||||
|| ENCRYPTION_KEY == null
|
||||
|| ENCRYPTION_KEY == ""
|
||||
|| (ENCRYPTION_KEY != null && ENCRYPTION_KEY.contains(" ")));
|
||||
return !(API_NG_APP_ID == null
|
||||
|| API_NG_APP_ID == ""
|
||||
|| (API_NG_APP_ID != null && API_NG_APP_ID.contains(" "))
|
||||
|| API_NG_ENC_KEY == null
|
||||
|| API_NG_ENC_KEY == ""
|
||||
|| (API_NG_ENC_KEY != null && API_NG_ENC_KEY.contains(" ")));
|
||||
}
|
||||
|
||||
function onLoginResolved(outcome:LoginOutcome):Void
|
||||
{
|
||||
#if FEATURE_MOBILE_WEBVIEW
|
||||
WebViewUtil.close();
|
||||
#end
|
||||
|
||||
switch (outcome)
|
||||
{
|
||||
case SUCCESS:
|
||||
@@ -236,17 +271,19 @@ class NewgroundsClient
|
||||
{
|
||||
if (NG.core == null) return;
|
||||
|
||||
trace('[NEWGROUNDS] Login successful!');
|
||||
trace(' NEWGROUNDS '.bold().bg_orange() + ' Login successful!');
|
||||
|
||||
// Persist the session ID.
|
||||
Save.instance.ngSessionId = NG.core.sessionId;
|
||||
Save.instance.ngSessionId.value = NG.core.sessionId;
|
||||
|
||||
trace('[NEWGROUNDS] Submitting medal request...');
|
||||
trace(NG.core.sessionId);
|
||||
|
||||
trace(' NEWGROUNDS '.bold().bg_orange() + ' Submitting medal request...');
|
||||
NG.core.requestMedals(onFetchedMedals);
|
||||
|
||||
trace('[NEWGROUNDS] Submitting leaderboard request...');
|
||||
trace(' NEWGROUNDS '.bold().bg_orange() + ' Submitting leaderboard request...');
|
||||
NG.core.scoreBoards.loadList(onFetchedLeaderboards);
|
||||
trace('[NEWGROUNDS] Submitting save slot request...');
|
||||
trace(' NEWGROUNDS '.bold().bg_orange() + ' Submitting save slot request...');
|
||||
NG.core.saveSlots.loadList(onFetchedSaveSlots);
|
||||
}
|
||||
|
||||
@@ -258,32 +295,32 @@ class NewgroundsClient
|
||||
switch (type)
|
||||
{
|
||||
case PASSPORT:
|
||||
trace('[NEWGROUNDS] Login cancelled by passport website.');
|
||||
trace(' NEWGROUNDS '.bold().bg_orange() + ' Login cancelled by passport website.');
|
||||
case MANUAL:
|
||||
trace('[NEWGROUNDS] Login cancelled by application.');
|
||||
trace(' NEWGROUNDS '.bold().bg_orange() + ' Login cancelled by application.');
|
||||
default:
|
||||
trace('[NEWGROUNDS] Login cancelled by unknown source.');
|
||||
trace(' NEWGROUNDS '.bold().bg_orange() + ' Login cancelled by unknown source.');
|
||||
}
|
||||
case ERROR(error):
|
||||
switch (error)
|
||||
{
|
||||
case HTTP(error):
|
||||
trace('[NEWGROUNDS] Login failed due to HTTP error: ${error}');
|
||||
trace(' NEWGROUNDS '.bold().bg_orange() + ' Login failed due to HTTP error: ${error}');
|
||||
case RESPONSE(error):
|
||||
trace('[NEWGROUNDS] Login failed due to response error: ${error.message} (${error.code})');
|
||||
trace(' NEWGROUNDS '.bold().bg_orange() + ' Login failed due to response error: ${error.message} (${error.code})');
|
||||
case RESULT(error):
|
||||
trace('[NEWGROUNDS] Login failed due to result error: ${error.message} (${error.code})');
|
||||
trace(' NEWGROUNDS '.bold().bg_orange() + ' Login failed due to result error: ${error.message} (${error.code})');
|
||||
default:
|
||||
trace('[NEWGROUNDS] Login failed due to unknown error: ${error}');
|
||||
trace(' NEWGROUNDS '.bold().bg_orange() + ' Login failed due to unknown error: ${error}');
|
||||
}
|
||||
default:
|
||||
trace('[NEWGROUNDS] Login failed due to unknown reason.');
|
||||
trace(' NEWGROUNDS '.bold().bg_orange() + ' Login failed due to unknown reason.');
|
||||
}
|
||||
}
|
||||
|
||||
function onLogoutSuccessful():Void
|
||||
{
|
||||
trace('[NEWGROUNDS] Logout successful!');
|
||||
trace(' NEWGROUNDS '.bold().bg_orange() + ' Logout successful!');
|
||||
}
|
||||
|
||||
function onLogoutFailed(result:CallError):Void
|
||||
@@ -291,31 +328,31 @@ class NewgroundsClient
|
||||
switch (result)
|
||||
{
|
||||
case HTTP(error):
|
||||
trace('[NEWGROUNDS] Logout failed due to HTTP error: ${error}');
|
||||
trace(' NEWGROUNDS '.bold().bg_orange() + ' Logout failed due to HTTP error: ${error}');
|
||||
case RESPONSE(error):
|
||||
trace('[NEWGROUNDS] Logout failed due to response error: ${error.message} (${error.code})');
|
||||
trace(' NEWGROUNDS '.bold().bg_orange() + ' Logout failed due to response error: ${error.message} (${error.code})');
|
||||
case RESULT(error):
|
||||
trace('[NEWGROUNDS] Logout failed due to result error: ${error.message} (${error.code})');
|
||||
trace(' NEWGROUNDS '.bold().bg_orange() + ' Logout failed due to result error: ${error.message} (${error.code})');
|
||||
default:
|
||||
trace('[NEWGROUNDS] Logout failed due to unknown error: ${result}');
|
||||
trace(' NEWGROUNDS '.bold().bg_orange() + ' Logout failed due to unknown error: ${result}');
|
||||
}
|
||||
}
|
||||
|
||||
function onFetchedMedals(outcome:Outcome<CallError>):Void
|
||||
{
|
||||
trace('[NEWGROUNDS] Fetched medals!');
|
||||
trace(' NEWGROUNDS '.bold().bg_orange() + ' Fetched medals!');
|
||||
}
|
||||
|
||||
function onFetchedLeaderboards(outcome:Outcome<CallError>):Void
|
||||
{
|
||||
trace('[NEWGROUNDS] Fetched leaderboards!');
|
||||
trace(' NEWGROUNDS '.bold().bg_orange() + ' Fetched leaderboards!');
|
||||
|
||||
// trace(funkin.api.newgrounds.Leaderboards.listLeaderboardData());
|
||||
}
|
||||
|
||||
function onFetchedSaveSlots(outcome:Outcome<CallError>):Void
|
||||
{
|
||||
trace('[NEWGROUNDS] Fetched save slots!');
|
||||
trace(' NEWGROUNDS '.bold().bg_orange() + ' Fetched save slots!');
|
||||
|
||||
NGSaveSlot.instance.checkSlot();
|
||||
}
|
||||
@@ -353,7 +390,7 @@ class NewgroundsClient
|
||||
#end
|
||||
|
||||
// We have to fetch the session ID from the save file.
|
||||
return Save.instance.ngSessionId;
|
||||
return Save.instance.ngSessionId.value;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -124,11 +124,6 @@ class FunkinSound extends FlxSound implements ICloneable<FunkinSound>
|
||||
*/
|
||||
var _label:String = "unknown";
|
||||
|
||||
/**
|
||||
* Whether we received a focus lost event.
|
||||
*/
|
||||
var _lostFocus:Bool = false;
|
||||
|
||||
public function new()
|
||||
{
|
||||
super();
|
||||
@@ -229,37 +224,6 @@ class FunkinSound extends FlxSound implements ICloneable<FunkinSound>
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Called when the user clicks to focus on the window.
|
||||
*/
|
||||
override function onFocus():Void
|
||||
{
|
||||
// Flixel can sometimes toss spurious `onFocus` events, e.g. if the Flixel debugger is toggled
|
||||
// on and off. We only want to resume the sound if we actually lost focus, and if we weren't
|
||||
// already paused before we lost focus.
|
||||
if (_lostFocus && !_alreadyPaused)
|
||||
{
|
||||
// trace('Resuming audio (${this._label}) on focus!');
|
||||
resume();
|
||||
}
|
||||
else
|
||||
{
|
||||
// trace('Not resuming audio (${this._label}) on focus!');
|
||||
}
|
||||
_lostFocus = false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Called when the user tabs away from the window.
|
||||
*/
|
||||
override function onFocusLost():Void
|
||||
{
|
||||
// trace('Focus lost, pausing audio!');
|
||||
_lostFocus = true;
|
||||
_alreadyPaused = _paused;
|
||||
pause();
|
||||
}
|
||||
|
||||
public override function resume():FunkinSound
|
||||
{
|
||||
if (this._time < 0)
|
||||
@@ -405,10 +369,7 @@ class FunkinSound extends FlxSound implements ICloneable<FunkinSound>
|
||||
var music = FunkinSound.load(pathToUse, params?.startingVolume ?? 1.0, params.loop ?? true, false, true, params.persist ?? false, params.onComplete);
|
||||
if (music != null)
|
||||
{
|
||||
FlxG.sound.music = music;
|
||||
|
||||
// Prevent repeat update() and onFocus() calls.
|
||||
FlxG.sound.list.remove(FlxG.sound.music);
|
||||
setMusic(music);
|
||||
|
||||
if (FlxG.sound.music != null && params.onLoad != null) params.onLoad();
|
||||
|
||||
@@ -421,6 +382,18 @@ class FunkinSound extends FlxSound implements ICloneable<FunkinSound>
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Replaces the Flixel current music object with the given `FunkinSound` object.
|
||||
* @param newMusic The new music to be set as the current music.
|
||||
*/
|
||||
public static function setMusic(newMusic:FunkinSound):Void
|
||||
{
|
||||
FlxG.sound.music = newMusic;
|
||||
|
||||
// Prevent repeat update() and onFocus() calls.
|
||||
FlxG.sound.list.remove(FlxG.sound.music);
|
||||
}
|
||||
|
||||
public static function emptyPartialQueue():Void
|
||||
{
|
||||
while (partialQueue.length > 0)
|
||||
|
||||
@@ -14,6 +14,7 @@ class ABotVis extends FlxTypedSpriteGroup<FlxSprite>
|
||||
{
|
||||
// public var vis:VisShit;
|
||||
var analyzer:Null<SpectralAnalyzer> = null;
|
||||
var analyzerLevelsCache:Array<Bar> = new Array<Bar>();
|
||||
|
||||
var volumes:Array<Float> = [];
|
||||
|
||||
@@ -112,11 +113,11 @@ class ABotVis extends FlxTypedSpriteGroup<FlxSprite>
|
||||
*/
|
||||
function drawFFT():Void
|
||||
{
|
||||
var levels = (analyzer != null) ? analyzer.getLevels() : getDefaultLevels();
|
||||
analyzerLevelsCache = (analyzer != null) ? analyzer.getLevels(analyzerLevelsCache) : getDefaultLevels();
|
||||
|
||||
for (i in 0...min(group.members.length, levels.length))
|
||||
for (i in 0...min(group.members.length, analyzerLevelsCache.length))
|
||||
{
|
||||
var animFrame:Int = (FlxG.sound.volume == 0 || FlxG.sound.muted) ? 0 : Math.round(levels[i].value * 6);
|
||||
var animFrame:Int = (FlxG.sound.volume == 0 || FlxG.sound.muted) ? 0 : Math.round(analyzerLevelsCache[i].value * 6);
|
||||
|
||||
// don't display if we're at 0 volume from the level
|
||||
group.members[i].visible = animFrame > 0;
|
||||
|
||||
@@ -176,7 +176,7 @@ class SpectogramSprite extends FlxTypedSpriteGroup<FlxSprite>
|
||||
for (i in 0...group.members.length)
|
||||
{
|
||||
// needs to be exponential growth / scaling
|
||||
// still need to optmize the FFT to run better, gets only samples needed?
|
||||
// still need to optimize the FFT to run better, gets only samples needed?
|
||||
// not every frequency is built the same!
|
||||
// 20hz to 40z is a LOT of subtle low ends, but somethin like 20,000hz to 20,020hz, the difference is NOT the same!
|
||||
|
||||
|
||||
@@ -166,7 +166,7 @@ class WaveformData
|
||||
|
||||
var ratio = newSamplesPerPoint / samplesPerPoint;
|
||||
if (ratio == 1) return result;
|
||||
if (ratio < 1) trace('[WARNING] Downsampling will result in a low precision.');
|
||||
if (ratio < 1) trace(' WARNING '.warning() + ' Downsampling will result in a low precision.');
|
||||
|
||||
var inputSampleCount = this.lenSamples();
|
||||
var outputSampleCount = Std.int(inputSampleCount * ratio);
|
||||
|
||||
@@ -3,6 +3,10 @@ package funkin.audio.waveform;
|
||||
import funkin.graphics.rendering.MeshRender;
|
||||
import flixel.util.FlxColor;
|
||||
|
||||
/**
|
||||
* A sprite which displays the waveform of audio data.
|
||||
* Generate a WaveformData and provide it to this sprite.
|
||||
*/
|
||||
class WaveformSprite extends MeshRender
|
||||
{
|
||||
static final DEFAULT_COLOR:FlxColor = FlxColor.WHITE;
|
||||
@@ -18,14 +22,17 @@ class WaveformSprite extends MeshRender
|
||||
* Do this any time the data or drawable area of the waveform changes.
|
||||
* This often (but not always) needs to be done every frame.
|
||||
*/
|
||||
var isWaveformDirty:Bool = true;
|
||||
var isWaveformDirty:Bool;
|
||||
|
||||
/**
|
||||
* If true, force the waveform to redraw every frame.
|
||||
* Useful if the waveform's clipRect is constantly changing.
|
||||
*/
|
||||
public var forceUpdate:Bool = false;
|
||||
public var forceUpdate:Bool;
|
||||
|
||||
/**
|
||||
* The data to render the waveform with.
|
||||
*/
|
||||
public var waveformData(default, set):Null<WaveformData>;
|
||||
|
||||
function set_waveformData(value:Null<WaveformData>):Null<WaveformData>
|
||||
@@ -52,6 +59,9 @@ class WaveformSprite extends MeshRender
|
||||
return waveformColor;
|
||||
}
|
||||
|
||||
/**
|
||||
* Whether the Waveform is horizontal or vertical.
|
||||
*/
|
||||
public var orientation(default, set):WaveformOrientation;
|
||||
|
||||
function set_orientation(value:WaveformOrientation):WaveformOrientation
|
||||
@@ -68,7 +78,7 @@ class WaveformSprite extends MeshRender
|
||||
*/
|
||||
public var time(default, set):Float;
|
||||
|
||||
function set_time(value:Float)
|
||||
function set_time(value:Float):Float
|
||||
{
|
||||
if (time == value) return value;
|
||||
|
||||
@@ -77,13 +87,22 @@ class WaveformSprite extends MeshRender
|
||||
return time;
|
||||
}
|
||||
|
||||
override function set_visible(value:Bool):Bool
|
||||
{
|
||||
if (visible == value) return value;
|
||||
|
||||
visible = value;
|
||||
isWaveformDirty = true;
|
||||
return visible;
|
||||
}
|
||||
|
||||
/**
|
||||
* The duration, in seconds, that the waveform represents.
|
||||
* The section of waveform from `time` to `time + duration` and `width` are used to determine how many samples each pixel represents.
|
||||
*/
|
||||
public var duration(default, set):Float;
|
||||
|
||||
function set_duration(value:Float)
|
||||
function set_duration(value:Float):Float
|
||||
{
|
||||
if (duration == value) return value;
|
||||
|
||||
@@ -120,13 +139,13 @@ class WaveformSprite extends MeshRender
|
||||
*
|
||||
* NOTE: This is technically doubled since it's applied above and below the center of the waveform.
|
||||
*/
|
||||
public var minWaveformSize:Int = 1;
|
||||
public var minWaveformSize:Int;
|
||||
|
||||
/**
|
||||
* A multiplier on the size of the waveform.
|
||||
* Still capped at the width and height set for the sprite.
|
||||
*/
|
||||
public var amplitude:Float = 1.0;
|
||||
public var amplitude:Float;
|
||||
|
||||
public function new(?waveformData:WaveformData, ?orientation:WaveformOrientation, ?color:FlxColor, ?duration:Float)
|
||||
{
|
||||
@@ -135,6 +154,11 @@ class WaveformSprite extends MeshRender
|
||||
this.width = DEFAULT_WIDTH;
|
||||
this.height = DEFAULT_HEIGHT;
|
||||
|
||||
this.minWaveformSize = 1;
|
||||
this.amplitude = 1.0;
|
||||
this.isWaveformDirty = true;
|
||||
this.forceUpdate = false;
|
||||
|
||||
this.waveformData = waveformData;
|
||||
this.orientation = orientation ?? DEFAULT_ORIENTATION;
|
||||
this.time = 0.0;
|
||||
@@ -151,7 +175,7 @@ class WaveformSprite extends MeshRender
|
||||
isWaveformDirty = true;
|
||||
}
|
||||
|
||||
public override function update(elapsed:Float)
|
||||
public override function update(elapsed:Float):Void
|
||||
{
|
||||
super.update(elapsed);
|
||||
|
||||
@@ -170,6 +194,11 @@ class WaveformSprite extends MeshRender
|
||||
makeGraphic(1, 1, this.waveformColor);
|
||||
}
|
||||
|
||||
public override function draw():Void
|
||||
{
|
||||
super.draw();
|
||||
}
|
||||
|
||||
/**
|
||||
* @param offsetX Horizontal offset to draw the waveform at, in samples.
|
||||
*/
|
||||
@@ -183,12 +212,12 @@ class WaveformSprite extends MeshRender
|
||||
|
||||
this.clear();
|
||||
|
||||
if (waveformData == null) return;
|
||||
if (waveformData == null || !this.visible) return;
|
||||
|
||||
// Center point of the waveform. When horizontal this is half the height, when vertical this is half the width.
|
||||
var waveformCenterPos:Int = orientation == HORIZONTAL ? Std.int(this.height / 2) : Std.int(this.width / 2);
|
||||
|
||||
var oneSecondInIndices:Int = waveformData.secondsToIndex(1);
|
||||
// var oneSecondInIndices:Int = waveformData.secondsToIndex(1)
|
||||
|
||||
var startTime:Float = time;
|
||||
var endTime:Float = time + duration;
|
||||
@@ -223,7 +252,10 @@ class WaveformSprite extends MeshRender
|
||||
|
||||
var isBeforeClipRect:Bool = (clipRect != null) && ((orientation == HORIZONTAL) ? pixelPos < clipRect.x : pixelPos < clipRect.y);
|
||||
|
||||
if (isBeforeClipRect) continue;
|
||||
if (isBeforeClipRect)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
var isAfterClipRect:Bool = (clipRect != null)
|
||||
&& ((orientation == HORIZONTAL) ? pixelPos > (clipRect.x + clipRect.width) : pixelPos > (clipRect.y + clipRect.height));
|
||||
@@ -426,20 +458,40 @@ class WaveformSprite extends MeshRender
|
||||
}
|
||||
}
|
||||
|
||||
public static function buildFromWaveformData(data:WaveformData, ?orientation:WaveformOrientation, ?color:FlxColor, ?duration:Float)
|
||||
/**
|
||||
* Build a WaveformSprite from waveform data.
|
||||
* @param data The data for the waveform to use.
|
||||
* @param orientation Whether the waveform should be horizontal or vertical.
|
||||
* @param color The color of the waveform.
|
||||
* @param duration The width of the waveform, in seconds.
|
||||
*
|
||||
* @return The resulting WaveformSprite.
|
||||
*/
|
||||
public static function buildFromWaveformData(data:WaveformData, ?orientation:WaveformOrientation, ?color:FlxColor, ?duration:Float):WaveformSprite
|
||||
{
|
||||
return new WaveformSprite(data, orientation, color, duration);
|
||||
}
|
||||
|
||||
public static function buildFromFunkinSound(sound:FunkinSound, ?orientation:WaveformOrientation, ?color:FlxColor, ?duration:Float)
|
||||
/**
|
||||
* Build a WaveformSprite from a FunkinSound's waveform data.
|
||||
* @param sound The audio for the waveform to use.
|
||||
* @param orientation Whether the waveform should be horizontal or vertical.
|
||||
* @param color The color of the waveform.
|
||||
* @param duration The width of the waveform, in seconds.
|
||||
*
|
||||
* @return The resulting WaveformSprite.
|
||||
*/
|
||||
public static function buildFromFunkinSound(sound:FunkinSound, ?orientation:WaveformOrientation, ?color:FlxColor, ?duration:Float):WaveformSprite
|
||||
{
|
||||
// TODO: Build waveform data from FunkinSound.
|
||||
var data = null;
|
||||
var data = WaveformDataParser.interpretFlxSound(sound);
|
||||
|
||||
return buildFromWaveformData(data, orientation, color, duration);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* The possible orientations of a waveform.
|
||||
*/
|
||||
enum WaveformOrientation
|
||||
{
|
||||
HORIZONTAL;
|
||||
|
||||
@@ -18,7 +18,7 @@ typedef EntryConstructorFunction = (String, ?Dynamic) -> Void;
|
||||
*/
|
||||
@:nullSafety
|
||||
@:generic
|
||||
@:autoBuild(funkin.util.macro.DataRegistryMacro.buildRegistry())
|
||||
@:autoBuild(funkin.util.macro.RegistryMacro.buildRegistry())
|
||||
abstract class BaseRegistry<T:(IRegistryEntry<J> & Constructible<EntryConstructorFunction>), J, P>
|
||||
{
|
||||
/**
|
||||
@@ -78,7 +78,7 @@ abstract class BaseRegistry<T:(IRegistryEntry<J> & Constructible<EntryConstructo
|
||||
// SCRIPTED ENTRIES
|
||||
//
|
||||
var scriptedEntryClassNames:Array<String> = getScriptedClassNames();
|
||||
log('Parsing ${scriptedEntryClassNames.length} scripted entries...');
|
||||
log(' INFO '.info() + 'Parsing ${scriptedEntryClassNames.length} scripted entries...');
|
||||
|
||||
for (entryCls in scriptedEntryClassNames)
|
||||
{
|
||||
@@ -112,7 +112,7 @@ abstract class BaseRegistry<T:(IRegistryEntry<J> & Constructible<EntryConstructo
|
||||
var unscriptedEntryIds:Array<String> = entryIdList.filter(function(entryId:String):Bool {
|
||||
return !entries.exists(entryId);
|
||||
});
|
||||
log('Parsing ${unscriptedEntryIds.length} unscripted entries...');
|
||||
log(' INFO '.info() + 'Parsing ${unscriptedEntryIds.length} unscripted entries...');
|
||||
for (entryId in unscriptedEntryIds)
|
||||
{
|
||||
try
|
||||
@@ -120,14 +120,14 @@ abstract class BaseRegistry<T:(IRegistryEntry<J> & Constructible<EntryConstructo
|
||||
var entry:Null<T> = createEntry(entryId);
|
||||
if (entry != null)
|
||||
{
|
||||
trace(' Loaded entry data: ${entry}');
|
||||
log('Loaded entry data: ${entry}');
|
||||
entries.set(entry.id, entry);
|
||||
}
|
||||
}
|
||||
catch (e)
|
||||
{
|
||||
// Print the error.
|
||||
trace(' Failed to load entry data: ${entryId}');
|
||||
log(' WARNING '.warning() + ' Failed to load entry data: ${entryId}');
|
||||
trace(e);
|
||||
continue;
|
||||
}
|
||||
@@ -157,7 +157,7 @@ abstract class BaseRegistry<T:(IRegistryEntry<J> & Constructible<EntryConstructo
|
||||
* @param id The ID of the entry.
|
||||
* @return `true` if the entry has an attached script, `false` otherwise.
|
||||
*/
|
||||
public function isScriptedEntry(id:String):Bool
|
||||
public function isScriptedEntry(id:String, ?params:Null<P>):Bool
|
||||
{
|
||||
return scriptedEntryIds.exists(id);
|
||||
}
|
||||
@@ -167,7 +167,7 @@ abstract class BaseRegistry<T:(IRegistryEntry<J> & Constructible<EntryConstructo
|
||||
* @param id The ID of the entry.
|
||||
* @return The class name, or `null` if it does not exist.
|
||||
*/
|
||||
public function getScriptedEntryClassName(id:String):Null<String>
|
||||
public function getScriptedEntryClassName(id:String, ?params:Null<P>):Null<String>
|
||||
{
|
||||
return scriptedEntryIds.get(id);
|
||||
}
|
||||
@@ -224,7 +224,7 @@ abstract class BaseRegistry<T:(IRegistryEntry<J> & Constructible<EntryConstructo
|
||||
|
||||
function log(message:String):Void
|
||||
{
|
||||
trace('[' + registryId + '] ' + message);
|
||||
trace(' $registryId '.bold().bg_note_down() + ' $message');
|
||||
}
|
||||
|
||||
function loadEntryFile(id:String):JsonFile
|
||||
@@ -333,7 +333,7 @@ abstract class BaseRegistry<T:(IRegistryEntry<J> & Constructible<EntryConstructo
|
||||
|
||||
function printErrors(errors:Array<json2object.Error>, id:String = ''):Void
|
||||
{
|
||||
trace('[${registryId}] Failed to parse entry data: ${id}');
|
||||
trace(' $registryId '.bold().bg_note_down() + ' ERROR '.error() + 'Failed to parse entry data: ${id}');
|
||||
|
||||
for (error in errors)
|
||||
{
|
||||
|
||||
@@ -12,27 +12,27 @@ class DataError
|
||||
switch (error)
|
||||
{
|
||||
case IncorrectType(vari, expected, pos):
|
||||
trace(' Expected field "$vari" to be of type "$expected".');
|
||||
trace(' ERROR '.error() + 'Expected field "$vari" to be of type "$expected".');
|
||||
printPos(pos);
|
||||
case IncorrectEnumValue(value, expected, pos):
|
||||
trace(' Invalid enum value (expected "$expected", got "$value")');
|
||||
trace(' ERROR '.error() + 'Invalid enum value (expected "$expected", got "$value")');
|
||||
printPos(pos);
|
||||
case InvalidEnumConstructor(value, expected, pos):
|
||||
trace(' Invalid enum constructor (epxected "$expected", got "$value")');
|
||||
trace(' ERROR '.error() + 'Invalid enum constructor (epxected "$expected", got "$value")');
|
||||
printPos(pos);
|
||||
case UninitializedVariable(vari, pos):
|
||||
trace(' Uninitialized variable "$vari"');
|
||||
trace(' ERROR '.error() + 'Uninitialized variable "$vari"');
|
||||
printPos(pos);
|
||||
case UnknownVariable(vari, pos):
|
||||
trace(' Unknown variable "$vari"');
|
||||
trace(' ERROR '.error() + 'Unknown variable "$vari"');
|
||||
printPos(pos);
|
||||
case ParserError(message, pos):
|
||||
trace(' Parsing error: ${message}');
|
||||
trace(' ERROR '.error() + 'Parsing error: ${message}');
|
||||
printPos(pos);
|
||||
case CustomFunctionException(e, pos):
|
||||
if (Std.isOfType(e, String))
|
||||
{
|
||||
trace(' ${e}');
|
||||
trace(' ERROR '.error() + '${e}');
|
||||
}
|
||||
else
|
||||
{
|
||||
@@ -49,11 +49,11 @@ class DataError
|
||||
switch (Type.typeof(e))
|
||||
{
|
||||
case TClass(c):
|
||||
trace(' [${Type.getClassName(c)}] ${e.toString()}');
|
||||
trace(' ERROR '.error() + '(${Type.getClassName(c)}) ${e.toString()}');
|
||||
case TEnum(c):
|
||||
trace(' [${Type.getEnumName(c)}] ${e.toString()}');
|
||||
trace(' ERROR '.error() + '(${Type.getEnumName(c)}) ${e.toString()}');
|
||||
default:
|
||||
trace(' [${Type.typeof(e)}] ${e.toString()}');
|
||||
trace(' ERROR '.error() + '(${Type.typeof(e)}) ${e.toString()}');
|
||||
}
|
||||
}
|
||||
|
||||
@@ -66,11 +66,11 @@ class DataError
|
||||
{
|
||||
if (pos.lines[0].number == pos.lines[pos.lines.length - 1].number)
|
||||
{
|
||||
trace(' at ${(pos.file == '') ? 'line ' : '${pos.file}:'}${pos.lines[0].number}');
|
||||
trace(' at ${(pos.file == '') ? 'line ' : '${pos.file}:'}${pos.lines[0].number}');
|
||||
}
|
||||
else
|
||||
{
|
||||
trace(' at ${(pos.file == '') ? 'line ' : '${pos.file}:'}${pos.lines[0].number}-${pos.lines[pos.lines.length - 1].number}');
|
||||
trace(' at ${(pos.file == '') ? 'line ' : '${pos.file}:'}${pos.lines[0].number}-${pos.lines[pos.lines.length - 1].number}');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -83,7 +83,7 @@ typedef UnnamedAnimationData =
|
||||
|
||||
/**
|
||||
* Optionally specify an asset path to use for this specific animation.
|
||||
* ONLY for use by MultiSparrow characters.
|
||||
* ONLY for use by MultiSparrow and MultiAnimateAtlas characters.
|
||||
* @default The assetPath of the parent sprite
|
||||
*/
|
||||
@:optional
|
||||
@@ -138,4 +138,13 @@ typedef UnnamedAnimationData =
|
||||
@:default([])
|
||||
@:optional
|
||||
var frameIndices:Null<Array<Int>>;
|
||||
|
||||
/**
|
||||
* The type of animation to use.
|
||||
* Only available for texture atlases.
|
||||
* Options: "framelabel", "symbol"
|
||||
*/
|
||||
@:default("framelabel")
|
||||
@:optional
|
||||
var animType:String;
|
||||
}
|
||||
|
||||
+118
-28
@@ -1,4 +1,4 @@
|
||||
package funkin.play.character;
|
||||
package funkin.data.character;
|
||||
|
||||
import funkin.data.animation.AnimationData;
|
||||
import funkin.modding.events.ScriptEvent;
|
||||
@@ -6,8 +6,15 @@ import funkin.modding.events.ScriptEventDispatcher;
|
||||
import funkin.play.character.ScriptedCharacter.ScriptedAnimateAtlasCharacter;
|
||||
import funkin.play.character.ScriptedCharacter.ScriptedBaseCharacter;
|
||||
import funkin.play.character.ScriptedCharacter.ScriptedMultiSparrowCharacter;
|
||||
import funkin.play.character.ScriptedCharacter.ScriptedMultiAnimateAtlasCharacter;
|
||||
import funkin.play.character.ScriptedCharacter.ScriptedPackerCharacter;
|
||||
import funkin.play.character.ScriptedCharacter.ScriptedSparrowCharacter;
|
||||
import funkin.play.character.AnimateAtlasCharacter;
|
||||
import funkin.play.character.BaseCharacter;
|
||||
import funkin.play.character.SparrowCharacter;
|
||||
import funkin.play.character.MultiSparrowCharacter;
|
||||
import funkin.play.character.MultiAnimateAtlasCharacter;
|
||||
import funkin.play.character.PackerCharacter;
|
||||
import funkin.util.assets.DataAssets;
|
||||
import funkin.util.VersionUtil;
|
||||
import haxe.Json;
|
||||
@@ -44,7 +51,7 @@ class CharacterDataParser
|
||||
{
|
||||
// Clear any stages that are cached if there were any.
|
||||
clearCharacterCache();
|
||||
trace('[CHARACTER] Parsing all entries...');
|
||||
log(' INFO '.info() + 'Parsing all entries...');
|
||||
|
||||
//
|
||||
// UNSCRIPTED CHARACTERS
|
||||
@@ -53,7 +60,7 @@ class CharacterDataParser
|
||||
var unscriptedCharIds:Array<String> = charIdList.filter(function(charId:String):Bool {
|
||||
return !characterCache.exists(charId);
|
||||
});
|
||||
trace(' Fetching data for ${unscriptedCharIds.length} characters...');
|
||||
log('Fetching data for ${unscriptedCharIds.length} characters...');
|
||||
for (charId in unscriptedCharIds)
|
||||
{
|
||||
try
|
||||
@@ -61,7 +68,7 @@ class CharacterDataParser
|
||||
var charData:Null<CharacterData> = parseCharacterData(charId);
|
||||
if (charData != null)
|
||||
{
|
||||
trace(' Loaded character data: ${charId}');
|
||||
log('Loaded character "${charId}"');
|
||||
characterCache.set(charId, charData);
|
||||
}
|
||||
}
|
||||
@@ -81,19 +88,19 @@ class CharacterDataParser
|
||||
var scriptedCharClassNames1:Array<String> = ScriptedSparrowCharacter.listScriptClasses();
|
||||
if (scriptedCharClassNames1.length > 0)
|
||||
{
|
||||
trace(' Instantiating ${scriptedCharClassNames1.length} (Sparrow) scripted characters...');
|
||||
log('Instantiating ${scriptedCharClassNames1.length} (Sparrow) scripted characters...');
|
||||
for (charCls in scriptedCharClassNames1)
|
||||
{
|
||||
try
|
||||
{
|
||||
var character:SparrowCharacter = ScriptedSparrowCharacter.init(charCls, DEFAULT_CHAR_ID);
|
||||
trace(' Initialized character ${character.characterName}');
|
||||
log('Loaded character ${character.characterName} (scripted: $charCls)');
|
||||
characterScriptedClass.set(character.characterId, charCls);
|
||||
}
|
||||
catch (e)
|
||||
{
|
||||
trace(' FAILED to instantiate scripted Sparrow character: ${charCls}');
|
||||
trace(e);
|
||||
log(' ERROR '.error() + 'Failed to initialize scripted Sparrow character: $charCls');
|
||||
log(' ERROR '.error() + '$e');
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -101,18 +108,19 @@ class CharacterDataParser
|
||||
var scriptedCharClassNames2:Array<String> = ScriptedPackerCharacter.listScriptClasses();
|
||||
if (scriptedCharClassNames2.length > 0)
|
||||
{
|
||||
trace(' Instantiating ${scriptedCharClassNames2.length} (Packer) scripted characters...');
|
||||
log('Instantiating ${scriptedCharClassNames2.length} (Packer) scripted characters...');
|
||||
for (charCls in scriptedCharClassNames2)
|
||||
{
|
||||
try
|
||||
{
|
||||
var character:PackerCharacter = ScriptedPackerCharacter.init(charCls, DEFAULT_CHAR_ID);
|
||||
log('Loaded character ${character.characterName} (scripted: $charCls)');
|
||||
characterScriptedClass.set(character.characterId, charCls);
|
||||
}
|
||||
catch (e)
|
||||
{
|
||||
trace(' FAILED to instantiate scripted Packer character: ${charCls}');
|
||||
trace(e);
|
||||
log(' ERROR '.error() + 'Failed to initialize scripted Packer character: $charCls');
|
||||
log(' ERROR '.error() + '$e');
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -120,18 +128,19 @@ class CharacterDataParser
|
||||
var scriptedCharClassNames3:Array<String> = ScriptedMultiSparrowCharacter.listScriptClasses();
|
||||
if (scriptedCharClassNames3.length > 0)
|
||||
{
|
||||
trace(' Instantiating ${scriptedCharClassNames3.length} (Multi-Sparrow) scripted characters...');
|
||||
log('Instantiating ${scriptedCharClassNames3.length} (Multi-Sparrow) scripted characters...');
|
||||
for (charCls in scriptedCharClassNames3)
|
||||
{
|
||||
try
|
||||
{
|
||||
var character:MultiSparrowCharacter = ScriptedMultiSparrowCharacter.init(charCls, DEFAULT_CHAR_ID);
|
||||
log('Loaded character ${character.characterName} (scripted: $charCls)');
|
||||
characterScriptedClass.set(character.characterId, charCls);
|
||||
}
|
||||
catch (e)
|
||||
{
|
||||
trace(' FAILED to instantiate scripted Multi-Sparrow character: ${charCls}');
|
||||
trace(e);
|
||||
log(' ERROR '.error() + 'Failed to initialize scripted Multi-Sparrow character: $charCls');
|
||||
log(' ERROR '.error() + '$e');
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -139,18 +148,39 @@ class CharacterDataParser
|
||||
var scriptedCharClassNames4:Array<String> = ScriptedAnimateAtlasCharacter.listScriptClasses();
|
||||
if (scriptedCharClassNames4.length > 0)
|
||||
{
|
||||
trace(' Instantiating ${scriptedCharClassNames4.length} (Animate Atlas) scripted characters...');
|
||||
log('Instantiating ${scriptedCharClassNames4.length} (Animate Atlas) scripted characters...');
|
||||
for (charCls in scriptedCharClassNames4)
|
||||
{
|
||||
try
|
||||
{
|
||||
var character:AnimateAtlasCharacter = ScriptedAnimateAtlasCharacter.init(charCls, DEFAULT_CHAR_ID);
|
||||
log('Loaded character ${character.characterName} (scripted: $charCls)');
|
||||
characterScriptedClass.set(character.characterId, charCls);
|
||||
}
|
||||
catch (e)
|
||||
{
|
||||
trace(' FAILED to instantiate scripted Animate Atlas character: ${charCls}');
|
||||
trace(e);
|
||||
log(' ERROR '.error() + 'Failed to initialize scripted Animate Atlas character: $charCls');
|
||||
log(' ERROR '.error() + '$e');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var scriptedCharClassNames5:Array<String> = ScriptedMultiAnimateAtlasCharacter.listScriptClasses();
|
||||
if (scriptedCharClassNames5.length > 0)
|
||||
{
|
||||
log('Instantiating ${scriptedCharClassNames5.length} (Multi-Animate Atlas) scripted characters...');
|
||||
for (charCls in scriptedCharClassNames5)
|
||||
{
|
||||
try
|
||||
{
|
||||
var character:MultiAnimateAtlasCharacter = ScriptedMultiAnimateAtlasCharacter.init(charCls, DEFAULT_CHAR_ID);
|
||||
log('Loaded character ${character.characterName} (scripted: $charCls)');
|
||||
characterScriptedClass.set(character.characterId, charCls);
|
||||
}
|
||||
catch (e)
|
||||
{
|
||||
log(' ERROR '.error() + 'Failed to initialize scripted Multi-Animate Atlas character: $charCls');
|
||||
log(' ERROR '.error() + '$e');
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -162,29 +192,30 @@ class CharacterDataParser
|
||||
return !(scriptedCharClassNames1.contains(charCls)
|
||||
|| scriptedCharClassNames2.contains(charCls)
|
||||
|| scriptedCharClassNames3.contains(charCls)
|
||||
|| scriptedCharClassNames4.contains(charCls));
|
||||
|| scriptedCharClassNames4.contains(charCls)
|
||||
|| scriptedCharClassNames5.contains(charCls));
|
||||
});
|
||||
|
||||
if (scriptedCharClassNames.length > 0)
|
||||
{
|
||||
trace(' Instantiating ${scriptedCharClassNames.length} (Base) scripted characters...');
|
||||
log('Instantiating ${scriptedCharClassNames.length} (Base) scripted characters...');
|
||||
for (charCls in scriptedCharClassNames)
|
||||
{
|
||||
var character:BaseCharacter = ScriptedBaseCharacter.init(charCls, DEFAULT_CHAR_ID, Custom);
|
||||
if (character == null)
|
||||
{
|
||||
trace(' Failed to instantiate scripted character: ${charCls}');
|
||||
log(' ERROR '.error() + 'Failed to initialize scripted character: $charCls');
|
||||
continue;
|
||||
}
|
||||
else
|
||||
{
|
||||
trace(' Successfully instantiated scripted character: ${charCls}');
|
||||
log('Loaded character ${character.characterName} (scripted: $charCls)');
|
||||
characterScriptedClass.set(character.characterId, charCls);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
trace(' Successfully loaded ${Lambda.count(characterCache)} stages.');
|
||||
log(' INFO '.info() + 'Successfully loaded ${characterCache.size()} stages.');
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -221,6 +252,8 @@ class CharacterDataParser
|
||||
char = ScriptedSparrowCharacter.init(charScriptClass, charId);
|
||||
case CharacterRenderType.Packer:
|
||||
char = ScriptedPackerCharacter.init(charScriptClass, charId);
|
||||
case CharacterRenderType.MultiAnimateAtlas:
|
||||
char = ScriptedMultiAnimateAtlasCharacter.init(charScriptClass, charId);
|
||||
default:
|
||||
// We're going to assume that the script class does the rendering.
|
||||
char = ScriptedBaseCharacter.init(charScriptClass, charId, CharacterRenderType.Custom);
|
||||
@@ -238,8 +271,10 @@ class CharacterDataParser
|
||||
char = new SparrowCharacter(charId);
|
||||
case CharacterRenderType.Packer:
|
||||
char = new PackerCharacter(charId);
|
||||
case CharacterRenderType.MultiAnimateAtlas:
|
||||
char = new MultiAnimateAtlasCharacter(charId);
|
||||
default:
|
||||
trace('[WARN] Creating character with undefined renderType ${charData.renderType}');
|
||||
trace(' WARNING '.warning() + ' Creating character with undefined renderType ${charData.renderType}');
|
||||
char = new BaseCharacter(charId, CharacterRenderType.Custom);
|
||||
}
|
||||
}
|
||||
@@ -307,7 +342,7 @@ class CharacterDataParser
|
||||
|
||||
if (!Assets.exists(Paths.image(charPath)))
|
||||
{
|
||||
trace('[WARN] Character ${char} has no freeplay icon.');
|
||||
trace(' WARNING '.warning() + ' Character ${char} has no freeplay icon.');
|
||||
return null;
|
||||
}
|
||||
|
||||
@@ -324,7 +359,7 @@ class CharacterDataParser
|
||||
|
||||
if (idleFrame == null)
|
||||
{
|
||||
trace('[WARN] Character ${char} has no idle in their freeplay icon.');
|
||||
trace(' WARNING '.warning() + ' Character ${char} has no idle in their freeplay icon.');
|
||||
return null;
|
||||
}
|
||||
|
||||
@@ -399,8 +434,8 @@ class CharacterDataParser
|
||||
}
|
||||
catch (e)
|
||||
{
|
||||
trace(' Error parsing data for character: ${charId}');
|
||||
trace(' ${e}');
|
||||
trace(' Error parsing data for character: ${charId}');
|
||||
trace(' ${e}');
|
||||
return null;
|
||||
}
|
||||
}
|
||||
@@ -426,6 +461,16 @@ class CharacterDataParser
|
||||
public static final DEFAULT_SCALE:Float = 1;
|
||||
public static final DEFAULT_SCROLL:Array<Float> = [0, 0];
|
||||
public static final DEFAULT_STARTINGANIM:String = 'idle';
|
||||
public static final DEFAULT_APPLYSTAGEMATRIX:Bool = false;
|
||||
public static final DEFAULT_ANIMTYPE:String = "framelabel";
|
||||
public static final DEFAULT_ATLASSETTINGS:funkin.data.stage.StageData.TextureAtlasData =
|
||||
{
|
||||
swfMode: true,
|
||||
cacheOnLoad: false,
|
||||
filterQuality: 1,
|
||||
applyStageMatrix: false,
|
||||
useRenderTexture: false
|
||||
};
|
||||
|
||||
/**
|
||||
* Set unspecified parameters to their defaults.
|
||||
@@ -554,6 +599,16 @@ class CharacterDataParser
|
||||
input.flipX = DEFAULT_FLIPX;
|
||||
}
|
||||
|
||||
if (input.applyStageMatrix == null)
|
||||
{
|
||||
input.applyStageMatrix = DEFAULT_APPLYSTAGEMATRIX;
|
||||
}
|
||||
|
||||
if (input.atlasSettings == null)
|
||||
{
|
||||
input.atlasSettings = DEFAULT_ATLASSETTINGS;
|
||||
}
|
||||
|
||||
if (input.animations.length == 0 && input.startingAnimation != null)
|
||||
{
|
||||
return null;
|
||||
@@ -591,11 +646,21 @@ class CharacterDataParser
|
||||
{
|
||||
inputAnimation.flipY = DEFAULT_FLIPY;
|
||||
}
|
||||
|
||||
if (inputAnimation.animType == null)
|
||||
{
|
||||
inputAnimation.animType = DEFAULT_ANIMTYPE;
|
||||
}
|
||||
}
|
||||
|
||||
// All good!
|
||||
return input;
|
||||
}
|
||||
|
||||
static function log(message:String):Void
|
||||
{
|
||||
trace(' CHARACTER '.bold().bg_note_down() + ' $message');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -619,10 +684,15 @@ enum abstract CharacterRenderType(String) from String to String
|
||||
public var MultiSparrow = 'multisparrow';
|
||||
|
||||
/**
|
||||
* Renders the character using a spritesheet of symbols and JSON data.
|
||||
* Renders the character using a single spritesheet of symbols and JSON data.
|
||||
*/
|
||||
public var AnimateAtlas = 'animateatlas';
|
||||
|
||||
/**
|
||||
* Renders the character using multiple spritesheets of symbols and JSON data.
|
||||
*/
|
||||
public var MultiAnimateAtlas = 'multianimateatlas';
|
||||
|
||||
/**
|
||||
* Renders the character using a custom method.
|
||||
*/
|
||||
@@ -669,6 +739,9 @@ typedef CharacterData =
|
||||
*/
|
||||
var healthIcon:Null<HealthIconData>;
|
||||
|
||||
/**
|
||||
* Optional data about the death animation for the character.
|
||||
*/
|
||||
var death:Null<DeathData>;
|
||||
|
||||
/**
|
||||
@@ -729,6 +802,23 @@ typedef CharacterData =
|
||||
* @default false
|
||||
*/
|
||||
var flipX:Null<Bool>;
|
||||
|
||||
/**
|
||||
* NOTE: This only applies to animate atlas characters.
|
||||
*
|
||||
* Whether to apply the stage matrix, if it was exported from a symbol instance.
|
||||
* Also positions the Texture Atlas as it displays in Animate.
|
||||
* Turning this on is only recommended if you prepositioned the character in Animate.
|
||||
* For other cases, it should be turned off to act similarly to a normal FlxSprite.
|
||||
*/
|
||||
var applyStageMatrix:Null<Bool>;
|
||||
|
||||
/**
|
||||
* Various settings for the prop.
|
||||
* Only available for texture atlases.
|
||||
*/
|
||||
@:optional
|
||||
var atlasSettings:funkin.data.stage.StageData.TextureAtlasData;
|
||||
};
|
||||
|
||||
/**
|
||||
@@ -13,7 +13,7 @@ class SongEventRegistry
|
||||
{
|
||||
/**
|
||||
* Every built-in event class must be added to this list.
|
||||
* Thankfully, with the power of `SongEventMacro`, this is done automatically.
|
||||
* Thankfully, with the power of `ClassMacro`, this is done automatically.
|
||||
*/
|
||||
static final BUILTIN_EVENTS:List<Class<SongEvent>> = ClassMacro.listSubclassesOf(SongEvent);
|
||||
|
||||
@@ -46,12 +46,12 @@ class SongEventRegistry
|
||||
|
||||
if (event != null)
|
||||
{
|
||||
trace(' Loaded built-in song event: ${event.id}');
|
||||
trace(' Loaded built-in song event: ${event.id}');
|
||||
eventCache.set(event.id, event);
|
||||
}
|
||||
else
|
||||
{
|
||||
trace(' Failed to load built-in song event: ${Type.getClassName(eventCls)}');
|
||||
trace(' Failed to load built-in song event: ${Type.getClassName(eventCls)}');
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -68,12 +68,12 @@ class SongEventRegistry
|
||||
|
||||
if (event != null)
|
||||
{
|
||||
trace(' Loaded scripted song event: ${event.id}');
|
||||
trace(' Loaded scripted song event: ${event.id}');
|
||||
eventCache.set(event.id, event);
|
||||
}
|
||||
else
|
||||
{
|
||||
trace(' Failed to instantiate scripted song event class: ${eventCls}');
|
||||
trace(' Failed to instantiate scripted song event class: ${eventCls}');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
package funkin.data.event;
|
||||
|
||||
@:nullSafety
|
||||
@:forward(name, title, type, keys, min, max, step, units, defaultValue, iterator)
|
||||
abstract SongEventSchema(SongEventSchemaRaw)
|
||||
{
|
||||
@@ -10,13 +11,22 @@ abstract SongEventSchema(SongEventSchemaRaw)
|
||||
|
||||
public function new(?fields:Array<SongEventSchemaField>)
|
||||
{
|
||||
this = fields;
|
||||
this = fields ?? [];
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve a SongEventSchemaField by name. This works even if the field is inside a Frame.
|
||||
* You can use array access to call this function; `schema["field_name"]`
|
||||
*
|
||||
* @param name The name of the field to retreive.
|
||||
* @return The retrieved field, or null if not found.
|
||||
*/
|
||||
@:arrayAccess
|
||||
public function getByName(name:String):SongEventSchemaField
|
||||
public function getByName(name:String):Null<SongEventSchemaField>
|
||||
{
|
||||
for (field in this)
|
||||
var allFields = listAllFields(this);
|
||||
|
||||
for (field in allFields)
|
||||
{
|
||||
if (field.name == name) return field;
|
||||
}
|
||||
@@ -24,26 +34,70 @@ abstract SongEventSchema(SongEventSchemaRaw)
|
||||
return null;
|
||||
}
|
||||
|
||||
public function getFirstField():SongEventSchemaField
|
||||
/**
|
||||
* Return whether the field with the given name exists.
|
||||
* @param name The name of the field to check.
|
||||
* @return Whether the field exists.
|
||||
*/
|
||||
public function hasField(name:String):Bool
|
||||
{
|
||||
return abstract.getByName(name) != null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve the first field in the schema.
|
||||
* @return The first field.
|
||||
*/
|
||||
public function getFirstField():Null<SongEventSchemaField>
|
||||
{
|
||||
return this[0];
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve a field from the schema by numeric index.
|
||||
* @param key The index of the field to retrieve.
|
||||
* @return The retrieved field.
|
||||
*/
|
||||
@:arrayAccess
|
||||
public inline function get(key:Int)
|
||||
public inline function get(key:Int):Null<SongEventSchemaField>
|
||||
{
|
||||
return this[key];
|
||||
}
|
||||
|
||||
/**
|
||||
* Write a field to the schema by numeric index.
|
||||
* @param k The index of the field to write.
|
||||
* @param v The new field value to write.
|
||||
* @return The assigned value.
|
||||
*/
|
||||
@:arrayAccess
|
||||
public inline function arrayWrite(k:Int, v:SongEventSchemaField):SongEventSchemaField
|
||||
{
|
||||
return this[k] = v;
|
||||
}
|
||||
|
||||
/**
|
||||
* For a given song event field, retrieve its default value.
|
||||
* @param name The name of the field to retrieve.
|
||||
* @return The default value of the field, or null if not found.
|
||||
*/
|
||||
public function getDefaultFieldValue(name:String):Null<Dynamic>
|
||||
{
|
||||
return getByName(name)?.defaultValue;
|
||||
}
|
||||
|
||||
/**
|
||||
* For a given song event field, convert the value into a string.
|
||||
* This is particularly useful for ENUM fields.
|
||||
*
|
||||
* @param name The name of the field to display.
|
||||
* @param value The value of the field to convert.
|
||||
* @param addUnits Whether to add the units specified by the schema to the resulting string.
|
||||
* @return The resulting string.
|
||||
*/
|
||||
public function stringifyFieldValue(name:String, value:Dynamic, addUnits:Bool = true):String
|
||||
{
|
||||
var field:SongEventSchemaField = getByName(name);
|
||||
var field:Null<SongEventSchemaField> = getByName(name);
|
||||
if (field == null) return 'Unknown';
|
||||
|
||||
switch (field.type)
|
||||
@@ -62,10 +116,12 @@ abstract SongEventSchema(SongEventSchemaRaw)
|
||||
return Std.string(value);
|
||||
case SongEventFieldType.ENUM:
|
||||
var valueString:String = Std.string(value);
|
||||
for (key in field.keys.keys())
|
||||
var fieldKeys:Array<String> = field.keys?.keyValues() ?? [];
|
||||
for (key in fieldKeys)
|
||||
{
|
||||
var value:Null<Dynamic> = field.keys?.get(key) ?? null;
|
||||
// Comparing these values as strings because comparing Dynamic variables is jank.
|
||||
if (Std.string(field.keys.get(key)) == valueString) return key;
|
||||
if (Std.string(value) == valueString) return key;
|
||||
}
|
||||
return valueString;
|
||||
default:
|
||||
@@ -73,7 +129,13 @@ abstract SongEventSchema(SongEventSchemaRaw)
|
||||
}
|
||||
}
|
||||
|
||||
function addUnitsToString(value:String, field:SongEventSchemaField)
|
||||
/**
|
||||
* Apply the song event field's specified units to the value.
|
||||
* @param value The value to add the units to.
|
||||
* @param field The field to get the units from.
|
||||
* @return The resulting string.
|
||||
*/
|
||||
function addUnitsToString(value:String, field:SongEventSchemaField):String
|
||||
{
|
||||
if (field.units == null || field.units == '') return value;
|
||||
|
||||
@@ -81,10 +143,50 @@ abstract SongEventSchema(SongEventSchemaRaw)
|
||||
|
||||
return value + (NO_SPACE_UNITS.contains(unit) ? '' : ' ') + '${unit}';
|
||||
}
|
||||
|
||||
/**
|
||||
* Build a flat list of all the fields in the schema. Frames containing children are parsed recursively.
|
||||
*
|
||||
* @param schema The song event schema schema to parse.
|
||||
* @return The array of fields, parsed recursively from the schema and its child frames.
|
||||
*/
|
||||
function listAllFields(schema:SongEventSchemaRaw):Array<SongEventSchemaField>
|
||||
{
|
||||
var result:Array<SongEventSchemaField> = [];
|
||||
|
||||
for (field in schema)
|
||||
{
|
||||
if (field.children == null)
|
||||
{
|
||||
result.push(field);
|
||||
}
|
||||
else
|
||||
{
|
||||
result = result.concat(field.children);
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a list of all the field names in the schema, so they can be iterated over and retrieved.
|
||||
* @return The list of field names.
|
||||
*/
|
||||
public function listAllFieldNames():Array<String>
|
||||
{
|
||||
return listAllFields(this).map((field:SongEventSchemaField) -> field.name);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* The raw underlying data for a song event schema is an array of fields.
|
||||
*/
|
||||
typedef SongEventSchemaRaw = Array<SongEventSchemaField>;
|
||||
|
||||
/**
|
||||
* The individual fields of a song event schema.
|
||||
*/
|
||||
typedef SongEventSchemaField =
|
||||
{
|
||||
/**
|
||||
@@ -135,37 +237,58 @@ typedef SongEventSchemaField =
|
||||
*/
|
||||
?units:String,
|
||||
|
||||
/**
|
||||
* Used for FRAME values.
|
||||
* The child components that this frame contains.
|
||||
*/
|
||||
?children:SongEventSchemaRaw,
|
||||
|
||||
/**
|
||||
* Used for FRAME values.
|
||||
* Whether to make the frame be collapsible.
|
||||
*/
|
||||
?collapsible:Bool,
|
||||
|
||||
/**
|
||||
* An optional default value for the field.
|
||||
*/
|
||||
?defaultValue:Dynamic,
|
||||
}
|
||||
|
||||
/**
|
||||
* The available field types for a song event schema.
|
||||
*/
|
||||
enum abstract SongEventFieldType(String) from String to String
|
||||
{
|
||||
/**
|
||||
* The STRING type will display as a text field.
|
||||
*/
|
||||
var STRING = "string";
|
||||
public var STRING = "string";
|
||||
|
||||
/**
|
||||
* The INTEGER type will display as a text field that only accepts numbers.
|
||||
*/
|
||||
var INTEGER = "integer";
|
||||
public var INTEGER = "integer";
|
||||
|
||||
/**
|
||||
* The FLOAT type will display as a text field that only accepts numbers.
|
||||
*/
|
||||
var FLOAT = "float";
|
||||
public var FLOAT = "float";
|
||||
|
||||
/**
|
||||
* The BOOL type will display as a checkbox.
|
||||
*/
|
||||
var BOOL = "bool";
|
||||
public var BOOL = "bool";
|
||||
|
||||
/**
|
||||
* The ENUM type will display as a dropdown.
|
||||
* Make sure to specify the `keys` field in the schema.
|
||||
*/
|
||||
var ENUM = "enum";
|
||||
public var ENUM = "enum";
|
||||
|
||||
/**
|
||||
* The FRAME type will display a frame with child components.
|
||||
* Make sure to specify the `children` field in the schema.
|
||||
*/
|
||||
public var FRAME = "frame";
|
||||
}
|
||||
|
||||
@@ -89,7 +89,7 @@ class PlayerData
|
||||
updateVersionToLatest();
|
||||
|
||||
var writer = new json2object.JsonWriter<PlayerData>();
|
||||
return writer.write(this, pretty ? ' ' : null);
|
||||
return writer.write(this, pretty ? ' ' : null);
|
||||
}
|
||||
|
||||
public function updateVersionToLatest():Void
|
||||
@@ -103,6 +103,10 @@ class PlayerFreeplayDJData
|
||||
var assetPath:String;
|
||||
var animations:Array<AnimationData>;
|
||||
|
||||
@:optional
|
||||
@:default(false)
|
||||
var applyStageMatrix:Bool;
|
||||
|
||||
@:optional
|
||||
@:default("BOYFRIEND")
|
||||
var text1:String;
|
||||
@@ -130,6 +134,14 @@ class PlayerFreeplayDJData
|
||||
@:optional
|
||||
var fistPump:Null<PlayerFreeplayDJFistPumpData>;
|
||||
|
||||
@:optional
|
||||
@:default("animateatlas")
|
||||
public var renderType:Null<String>;
|
||||
|
||||
@:optional
|
||||
@:default("")
|
||||
public var scriptClass:Null<String>;
|
||||
|
||||
public function new()
|
||||
{
|
||||
animationMap = new Map();
|
||||
@@ -149,9 +161,15 @@ class PlayerFreeplayDJData
|
||||
}
|
||||
}
|
||||
|
||||
public function getAtlasPath():String
|
||||
public inline function getAssetPath():String
|
||||
return assetPath; // return assetPath;
|
||||
|
||||
public inline function getAnimationsList():Array<AnimationData>
|
||||
return animations;
|
||||
|
||||
public function useApplyStageMatrix():Bool
|
||||
{
|
||||
return Paths.animateAtlas(assetPath);
|
||||
return applyStageMatrix;
|
||||
}
|
||||
|
||||
public function getFreeplayDJText(index:Int):String
|
||||
@@ -334,6 +352,10 @@ typedef PlayerResultsAnimationData =
|
||||
*/
|
||||
var renderType:String;
|
||||
|
||||
@:optional
|
||||
@:default(false)
|
||||
var applyStageMatrix:Bool;
|
||||
|
||||
@:optional
|
||||
var assetPath:Null<String>;
|
||||
|
||||
|
||||
@@ -70,7 +70,7 @@ class PlayerRegistry extends BaseRegistry<PlayableCharacter, PlayerData, PlayerE
|
||||
public function hasNewCharacter():Bool
|
||||
{
|
||||
#if (!UNLOCK_EVERYTHING)
|
||||
var charactersSeen = Save.instance.charactersSeen.clone();
|
||||
var charactersSeen = Save.instance.charactersSeen.value.clone();
|
||||
|
||||
for (charId in listEntryIds())
|
||||
{
|
||||
@@ -94,7 +94,7 @@ class PlayerRegistry extends BaseRegistry<PlayableCharacter, PlayerData, PlayerE
|
||||
var result = [];
|
||||
|
||||
#if (!UNLOCK_EVERYTHING)
|
||||
var charactersSeen = Save.instance.charactersSeen.clone();
|
||||
var charactersSeen = Save.instance.charactersSeen.value.clone();
|
||||
for (charId in listEntryIds())
|
||||
{
|
||||
var player = fetchEntry(charId);
|
||||
@@ -143,7 +143,7 @@ class PlayerRegistry extends BaseRegistry<PlayableCharacter, PlayerData, PlayerE
|
||||
#if UNLOCK_EVERYTHING
|
||||
return true;
|
||||
#else
|
||||
return Save.instance.charactersSeen.contains(characterId);
|
||||
return Save.instance.charactersSeen.value.contains(characterId);
|
||||
#end
|
||||
}
|
||||
}
|
||||
|
||||
@@ -126,7 +126,7 @@ class SongMetadata implements ICloneable<SongMetadata>
|
||||
// I believe @:jignored should be ignored by the writer?
|
||||
// var output = this.clone();
|
||||
// output.variation = null; // Not sure how to make a field optional on the reader and ignored on the writer.
|
||||
return writer.write(this, pretty ? ' ' : null);
|
||||
return writer.write(this, pretty ? ' ' : null);
|
||||
}
|
||||
|
||||
public function updateVersionToLatest():Void
|
||||
@@ -664,7 +664,7 @@ class SongChartData implements ICloneable<SongChartData>
|
||||
|
||||
var ignoreNullOptionals = true;
|
||||
var writer = new json2object.JsonWriter<SongChartData>(ignoreNullOptionals);
|
||||
return writer.write(this, pretty ? ' ' : null);
|
||||
return writer.write(this, pretty ? ' ' : null);
|
||||
}
|
||||
|
||||
public function updateVersionToLatest():Void
|
||||
@@ -700,6 +700,10 @@ class SongChartData implements ICloneable<SongChartData>
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* The raw underlying data for a song event.
|
||||
* This value gets wrapped in an `abstract` to allow operators to be overloaded.
|
||||
*/
|
||||
class SongEventDataRaw implements ICloneable<SongEventDataRaw>
|
||||
{
|
||||
/**
|
||||
@@ -735,7 +739,7 @@ class SongEventDataRaw implements ICloneable<SongEventDataRaw>
|
||||
|
||||
/**
|
||||
* Whether this event has been activated.
|
||||
* This is only used internally by the game. It should not be serialized.
|
||||
* This is only used internally by the game during gameplay. It should not be serialized.
|
||||
*/
|
||||
@:jignored
|
||||
public var activated:Bool = false;
|
||||
@@ -747,9 +751,19 @@ class SongEventDataRaw implements ICloneable<SongEventDataRaw>
|
||||
this.value = value;
|
||||
}
|
||||
|
||||
/**
|
||||
* The cached step time of the event. Should not be serialized in case the BPM of the song changes.
|
||||
*/
|
||||
@:jignored
|
||||
var _stepTime:Null<Float> = null;
|
||||
|
||||
/**
|
||||
* Get the position of the event in the song, in steps.
|
||||
* This value is cached for performance. Use `force` to require a recalculation.
|
||||
*
|
||||
* @param force Force the value to be recalculated.
|
||||
* @return The position of the event in the song, in steps.
|
||||
*/
|
||||
public function getStepTime(force:Bool = false):Float
|
||||
{
|
||||
if (_stepTime != null && !force) return _stepTime;
|
||||
@@ -757,6 +771,10 @@ class SongEventDataRaw implements ICloneable<SongEventDataRaw>
|
||||
return _stepTime = Conductor.instance.getTimeInSteps(this.time);
|
||||
}
|
||||
|
||||
/**
|
||||
* Clone this event data, creating an independent instance with identical data.
|
||||
* @return The newly created event data.
|
||||
*/
|
||||
public function clone():SongEventDataRaw
|
||||
{
|
||||
return new SongEventDataRaw(this.time, this.eventKind, this.value);
|
||||
@@ -784,26 +802,49 @@ class SongEventDataRaw implements ICloneable<SongEventDataRaw>
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve the SongEvent handler class for this event.
|
||||
* @return The handler class, or `null` if not found.
|
||||
*/
|
||||
public function getHandler():Null<SongEvent>
|
||||
{
|
||||
return SongEventRegistry.getEvent(this.eventKind);
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve the SongEventSchema for this event.
|
||||
* @return The schema, or `null` if not found.
|
||||
*/
|
||||
public function getSchema():Null<SongEventSchema>
|
||||
{
|
||||
return SongEventRegistry.getEventSchema(this.eventKind);
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve a field from this event's data, as a `Dynamic` value.
|
||||
* @param key The name of the field to retrieve.
|
||||
* @return The field value, or `null` if not provided.
|
||||
*/
|
||||
public function getDynamic(key:String):Null<Dynamic>
|
||||
{
|
||||
return this.value == null ? null : Reflect.field(this.value, key);
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve a field from this event's data, as a `Bool` value.
|
||||
* @param key The name of the field to retrieve.
|
||||
* @return The field value, or `null` if not provided.
|
||||
*/
|
||||
public function getBool(key:String):Null<Bool>
|
||||
{
|
||||
return this.value == null ? null : cast Reflect.field(this.value, key);
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve a field from this event's data, as an `Int` value.
|
||||
* @param key The name of the field to retrieve.
|
||||
* @return The field value, or `null` if not provided.
|
||||
*/
|
||||
public function getInt(key:String):Null<Int>
|
||||
{
|
||||
if (this.value == null) return null;
|
||||
@@ -814,6 +855,11 @@ class SongEventDataRaw implements ICloneable<SongEventDataRaw>
|
||||
return cast result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve a field from this event's data, as a `Float` value.
|
||||
* @param key The name of the field to retrieve.
|
||||
* @return The field value, or `null` if not provided.
|
||||
*/
|
||||
public function getFloat(key:String):Null<Float>
|
||||
{
|
||||
if (this.value == null) return null;
|
||||
@@ -824,21 +870,60 @@ class SongEventDataRaw implements ICloneable<SongEventDataRaw>
|
||||
return cast result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve a field from this event's data, as a `String` value.
|
||||
* @param key The name of the field to retrieve.
|
||||
* @return The field value, or `null` if not provided.
|
||||
*/
|
||||
public function getString(key:String):String
|
||||
{
|
||||
return this.value == null ? null : cast Reflect.field(this.value, key);
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve a field from this event's data, as an `Array<Dynamic>` value.
|
||||
* @param key The name of the field to retrieve.
|
||||
* @return The field value, or `null` if not provided.
|
||||
*/
|
||||
public function getArray(key:String):Array<Dynamic>
|
||||
{
|
||||
return this.value == null ? null : cast Reflect.field(this.value, key);
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve a field from this event's data, as an `Array<Bool>` value.
|
||||
* @param key The name of the field to retrieve.
|
||||
* @return The field value, or `null` if not provided.
|
||||
*/
|
||||
public function getBoolArray(key:String):Array<Bool>
|
||||
{
|
||||
return this.value == null ? null : cast Reflect.field(this.value, key);
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve a field from this event's data, as an `Array<Float>` value.
|
||||
* @param key The name of the field to retrieve.
|
||||
* @return The field value, or `null` if not provided.
|
||||
*/
|
||||
public function getFloatArray(key:String):Array<Float>
|
||||
{
|
||||
return this.value == null ? null : cast Reflect.field(this.value, key);
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve a field from this event's data, as an `Array<String>` value.
|
||||
* @param key The name of the field to retrieve.
|
||||
* @return The field value, or `null` if not provided.
|
||||
*/
|
||||
public function getStringArray(key:String):Array<Float>
|
||||
{
|
||||
return this.value == null ? null : cast Reflect.field(this.value, key);
|
||||
}
|
||||
|
||||
/**
|
||||
* Build the tooltip string for this particular instance of the song event.
|
||||
* @return A tooltip string, containing readable values for the event's data.
|
||||
*/
|
||||
public function buildTooltip():String
|
||||
{
|
||||
var eventHandler = getHandler();
|
||||
@@ -851,15 +936,26 @@ class SongEventDataRaw implements ICloneable<SongEventDataRaw>
|
||||
var defaultKey = eventSchema.getFirstField()?.name;
|
||||
var valueStruct:haxe.DynamicAccess<Dynamic> = valueAsStruct(defaultKey);
|
||||
|
||||
for (fieldName in eventSchema.listAllFieldNames())
|
||||
{
|
||||
var fieldValue:Dynamic = valueStruct.exists(fieldName) ? valueStruct.get(fieldName) : eventSchema.getDefaultFieldValue(fieldName);
|
||||
|
||||
var title:String = eventSchema.getByName(fieldName)?.title ?? 'UnknownField';
|
||||
var valueStr:String = eventSchema.stringifyFieldValue(fieldName, fieldValue) ?? 'UnknownValue';
|
||||
|
||||
result += '\n- ${title}: ${valueStr}';
|
||||
}
|
||||
|
||||
for (pair in valueStruct.keyValueIterator())
|
||||
{
|
||||
var key = pair.key;
|
||||
var value = pair.value;
|
||||
var fieldName:String = pair.key;
|
||||
var fieldValue:Dynamic = pair.value;
|
||||
|
||||
var title = eventSchema.getByName(key)?.title ?? 'UnknownField';
|
||||
// Already populated above.
|
||||
if (eventSchema.hasField(fieldName)) continue;
|
||||
|
||||
// if (eventSchema.stringifyFieldValue(key, value) != null) trace(eventSchema.stringifyFieldValue(key, value));
|
||||
var valueStr = eventSchema.stringifyFieldValue(key, value) ?? 'UnknownValue';
|
||||
var title:String = eventSchema.getByName(fieldName)?.title ?? 'Unknown Field ($fieldName)';
|
||||
var valueStr:String = eventSchema.stringifyFieldValue(fieldName, fieldValue) ?? 'Unknown Value ($fieldValue)';
|
||||
|
||||
result += '\n- ${title}: ${valueStr}';
|
||||
}
|
||||
@@ -880,41 +976,87 @@ abstract SongEventData(SongEventDataRaw) from SongEventDataRaw to SongEventDataR
|
||||
this = new SongEventDataRaw(time, eventKind, value);
|
||||
}
|
||||
|
||||
/**
|
||||
* Create an independent copy of this event with the same underlying data.
|
||||
* @return The newly created event.
|
||||
*/
|
||||
public function clone():SongEventData
|
||||
{
|
||||
return new SongEventData(this.time, this.eventKind, this.value);
|
||||
}
|
||||
|
||||
/**
|
||||
* Check whether this event is equal to another event.
|
||||
* Ex. `event1 == event2`
|
||||
*
|
||||
* @param other The other event to compare.
|
||||
* @return Whether the events have the same time, kind, and data.
|
||||
*/
|
||||
@:op(A == B)
|
||||
public function op_equals(other:SongEventData):Bool
|
||||
{
|
||||
return this.time == other.time && this.eventKind == other.eventKind && this.value == other.value;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check whether this event is not equal to another event.
|
||||
* Ex. `event1 != event2`
|
||||
*
|
||||
* @param other The other event to compare.
|
||||
* @return Whether the events have a different same time, kind, or data.
|
||||
*/
|
||||
@:op(A != B)
|
||||
public function op_notEquals(other:SongEventData):Bool
|
||||
{
|
||||
return this.time != other.time || this.eventKind != other.eventKind || this.value != other.value;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check whether this event is later than another event.
|
||||
* Ex. `event1 > event2`
|
||||
*
|
||||
* @param other The other event to compare.
|
||||
* @return Bool
|
||||
*/
|
||||
@:op(A > B)
|
||||
public function op_greaterThan(other:SongEventData):Bool
|
||||
{
|
||||
return this.time > other.time;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check whether this event is earlier than another event.
|
||||
* Ex. `event1 < event2`
|
||||
*
|
||||
* @param other The other event to compare.
|
||||
* @return Whether this event is earlier than the other event.
|
||||
*/
|
||||
@:op(A < B)
|
||||
public function op_lessThan(other:SongEventData):Bool
|
||||
{
|
||||
return this.time < other.time;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check whether this event is later than or at the same time as another event.
|
||||
* Ex. `event1 >= event2`
|
||||
*
|
||||
* @param other The other event to compare.
|
||||
* @return Whether this event is later than or at the same time as the other event.
|
||||
*/
|
||||
@:op(A >= B)
|
||||
public function op_greaterThanOrEquals(other:SongEventData):Bool
|
||||
{
|
||||
return this.time >= other.time;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check whether this event is earlier than or at the same time as another event.
|
||||
* Ex. `event1 <= event2`
|
||||
*
|
||||
* @param other The other event to compare.
|
||||
* @return Whether this event is earlier than or at the same time as the other event.
|
||||
*/
|
||||
@:op(A <= B)
|
||||
public function op_lessThanOrEquals(other:SongEventData):Bool
|
||||
{
|
||||
@@ -922,7 +1064,7 @@ abstract SongEventData(SongEventDataRaw) from SongEventDataRaw to SongEventDataR
|
||||
}
|
||||
|
||||
/**
|
||||
* Produces a string representation suitable for debugging.
|
||||
* Produces a string representation of this event, suitable for debugging.
|
||||
*/
|
||||
public function toString():String
|
||||
{
|
||||
@@ -930,6 +1072,9 @@ abstract SongEventData(SongEventDataRaw) from SongEventDataRaw to SongEventDataR
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* The underlying data for a single note.
|
||||
*/
|
||||
class SongNoteDataRaw implements ICloneable<SongNoteDataRaw>
|
||||
{
|
||||
/**
|
||||
@@ -991,6 +1136,10 @@ class SongNoteDataRaw implements ICloneable<SongNoteDataRaw>
|
||||
return this.kind = value;
|
||||
}
|
||||
|
||||
/**
|
||||
* The parameters for the note.
|
||||
* Used for custom behavior on custom note kinds. Defaults to an empty array.
|
||||
*/
|
||||
@:alias("p")
|
||||
@:default([])
|
||||
@:optional
|
||||
@@ -1010,12 +1159,22 @@ class SongNoteDataRaw implements ICloneable<SongNoteDataRaw>
|
||||
* Strips the strumline index from the data.
|
||||
*
|
||||
* 0 = left, 1 = down, 2 = up, 3 = right
|
||||
*
|
||||
* @param strumlineSize The size of the strumline for the current song. Defaults to 4.
|
||||
* @return The direction of the note as an index.
|
||||
*/
|
||||
public inline function getDirection(strumlineSize:Int = 4):Int
|
||||
{
|
||||
return this.data % strumlineSize;
|
||||
}
|
||||
|
||||
/**
|
||||
* The name of the direction of the note, if applicable.
|
||||
* Strips the strumline index from the data.
|
||||
*
|
||||
* @param strumlineSize The size of the strumline for the current song. Defaults to 4.
|
||||
* @return The direction of the note as a name.
|
||||
*/
|
||||
public function getDirectionName(strumlineSize:Int = 4):String
|
||||
{
|
||||
return SongNoteData.buildDirectionName(this.data, strumlineSize);
|
||||
@@ -1026,6 +1185,9 @@ class SongNoteDataRaw implements ICloneable<SongNoteDataRaw>
|
||||
* Strips the direction from the data.
|
||||
*
|
||||
* 0 = player, 1 = opponent, etc.
|
||||
*
|
||||
* @param strumlineSize The size of the strumline for the current song. Defaults to 4.
|
||||
* @return The strumline index of the note.
|
||||
*/
|
||||
public function getStrumlineIndex(strumlineSize:Int = 4):Int
|
||||
{
|
||||
@@ -1043,6 +1205,11 @@ class SongNoteDataRaw implements ICloneable<SongNoteDataRaw>
|
||||
return getStrumlineIndex(strumlineSize) == 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* The position of the note in the song, in steps.
|
||||
* Calculated from the time and the BPM.
|
||||
* Cached for performance. Set to `null` to recalculate.
|
||||
*/
|
||||
@:jignored
|
||||
var _stepTime:Null<Float> = null;
|
||||
|
||||
@@ -1078,6 +1245,10 @@ class SongNoteDataRaw implements ICloneable<SongNoteDataRaw>
|
||||
return _stepLength = Conductor.instance.getTimeInSteps(this.time + this.length) - getStepTime();
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the length of the hold note, in steps.
|
||||
* @param value The desired length of the hold note, in steps. Use `0` to make this a tap note.
|
||||
*/
|
||||
public function setStepLength(value:Float):Void
|
||||
{
|
||||
if (value <= 0)
|
||||
@@ -1097,16 +1268,24 @@ class SongNoteDataRaw implements ICloneable<SongNoteDataRaw>
|
||||
_stepLength = null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Clone the `params` data for this node, creating a new independent instance.
|
||||
* @return A new array of cloned `NoteParamData` instances.
|
||||
*/
|
||||
public function cloneParams():Array<NoteParamData>
|
||||
{
|
||||
var params:Array<NoteParamData> = [];
|
||||
var newParams:Array<NoteParamData> = [];
|
||||
for (param in this.params)
|
||||
{
|
||||
params.push(param.clone());
|
||||
newParams.push(param.clone());
|
||||
}
|
||||
return params;
|
||||
return newParams;
|
||||
}
|
||||
|
||||
/**
|
||||
* Clone this song note data, creating a new independent instance with identical data.
|
||||
* @return The newly created song note data.
|
||||
*/
|
||||
public function clone():SongNoteDataRaw
|
||||
{
|
||||
return new SongNoteDataRaw(this.time, this.data, this.length, this.kind, cloneParams());
|
||||
@@ -1118,6 +1297,10 @@ class SongNoteDataRaw implements ICloneable<SongNoteDataRaw>
|
||||
+ (this.kind != '' ? ' [kind: ${this.kind}])' : ')');
|
||||
}
|
||||
|
||||
/**
|
||||
* Build a tooltip string for this note, as seen when hovering in the Chart Editor.
|
||||
* @return The tooltip string to display.
|
||||
*/
|
||||
public function buildTooltip():String
|
||||
{
|
||||
if ((this.kind?.length ?? 0) == 0) return "";
|
||||
@@ -1164,14 +1347,24 @@ abstract SongNoteData(SongNoteDataRaw) from SongNoteDataRaw to SongNoteDataRaw
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Whether this note is a hold note.
|
||||
*/
|
||||
@:jignored
|
||||
public var isHoldNote(get, never):Bool;
|
||||
|
||||
public function get_isHoldNote():Bool
|
||||
function get_isHoldNote():Bool
|
||||
{
|
||||
return this.length > 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Determine if two notes are equal.
|
||||
* Ex. `note1 == note2`
|
||||
*
|
||||
* @param other The other note to compare.
|
||||
* @return Whether the two notes have equal data.
|
||||
*/
|
||||
@:op(A == B)
|
||||
public function op_equals(other:SongNoteData):Bool
|
||||
{
|
||||
|
||||
@@ -212,7 +212,7 @@ class SongDataUtils
|
||||
{
|
||||
var ignoreNullOptionals = true;
|
||||
var writer = new json2object.JsonWriter<SongClipboardItems>(ignoreNullOptionals);
|
||||
var dataString:String = writer.write(data, ' ');
|
||||
var dataString:String = writer.write(data, ' ');
|
||||
|
||||
ClipboardUtil.setClipboard(dataString);
|
||||
|
||||
|
||||
@@ -56,7 +56,7 @@ using funkin.data.song.migrator.SongDataMigrator;
|
||||
// SCRIPTED ENTRIES
|
||||
//
|
||||
var scriptedEntryClassNames:Array<String> = getScriptedClassNames();
|
||||
log('Parsing ${scriptedEntryClassNames.length} scripted entries...');
|
||||
log(' INFO '.info() + 'Parsing ${scriptedEntryClassNames.length} scripted entries...');
|
||||
|
||||
for (entryCls in scriptedEntryClassNames)
|
||||
{
|
||||
@@ -99,15 +99,15 @@ using funkin.data.song.migrator.SongDataMigrator;
|
||||
var entry:Null<Song> = createEntry(entryId);
|
||||
if (entry != null)
|
||||
{
|
||||
trace(' Loaded entry data: ${entry}');
|
||||
log('Loaded entry data: ${entry}');
|
||||
entries.set(entry.id, entry);
|
||||
}
|
||||
}
|
||||
catch (e:Dynamic)
|
||||
{
|
||||
// Print the error.
|
||||
trace(' Failed to load entry data: ${entryId}');
|
||||
trace(e);
|
||||
log(' ERROR '.error() + 'Failed to load entry data: ${entryId}');
|
||||
log(' ERROR '.error() + e);
|
||||
continue;
|
||||
}
|
||||
}
|
||||
@@ -129,6 +129,29 @@ using funkin.data.song.migrator.SongDataMigrator;
|
||||
return parseEntryMetadataRaw(contents);
|
||||
}
|
||||
|
||||
public override function isScriptedEntry(id:String, ?params:Null<SongEntryParams>)
|
||||
{
|
||||
var variation:String = params?.variation ?? Constants.DEFAULT_VARIATION;
|
||||
if (variation != Constants.DEFAULT_VARIATION)
|
||||
{
|
||||
return scriptedSongVariations.exists('${id}:${variation}');
|
||||
}
|
||||
return super.isScriptedEntry(id, params);
|
||||
}
|
||||
|
||||
public override function getScriptedEntryClassName(id:String, ?params:Null<SongEntryParams>):Null<String>
|
||||
{
|
||||
var variation:String = params?.variation ?? Constants.DEFAULT_VARIATION;
|
||||
if (variation != Constants.DEFAULT_VARIATION)
|
||||
{
|
||||
final variationSongId:ScriptedSong = cast scriptedSongVariations.get('${id}:${variation}');
|
||||
@:privateAccess
|
||||
var path:String = variationSongId._asc._c.name;
|
||||
return path;
|
||||
}
|
||||
return super.getScriptedEntryClassName(id, params);
|
||||
}
|
||||
|
||||
/**
|
||||
* We override `fetchEntry` to handle song variations!
|
||||
*/
|
||||
@@ -426,12 +449,13 @@ using funkin.data.song.migrator.SongDataMigrator;
|
||||
}
|
||||
}
|
||||
|
||||
public function parseEntryChartDataRawWithMigration(contents:String, ?fileName:String = 'raw', version:thx.semver.Version):Null<SongChartData>
|
||||
public function parseEntryChartDataRawWithMigration(contents:String, ?fileName:String = 'raw', version:thx.semver.Version,
|
||||
?variation:String):Null<SongChartData>
|
||||
{
|
||||
// If a version rule is not specified, do not check against it.
|
||||
if (SONG_CHART_DATA_VERSION_RULE == null || VersionUtil.validateVersion(version, SONG_CHART_DATA_VERSION_RULE))
|
||||
{
|
||||
return parseEntryChartDataRaw(contents, fileName);
|
||||
return parseEntryChartDataRaw(contents, fileName, variation);
|
||||
}
|
||||
else
|
||||
{
|
||||
@@ -445,7 +469,7 @@ using funkin.data.song.migrator.SongDataMigrator;
|
||||
var entryFilePath:String = Paths.json('$dataFilePath/$id/$id-metadata${variation == Constants.DEFAULT_VARIATION ? '' : '-$variation'}');
|
||||
if (!openfl.Assets.exists(entryFilePath))
|
||||
{
|
||||
trace(' [WARN] Could not locate file $entryFilePath');
|
||||
trace(' WARNING '.bold().bg_yellow() + ' Could not locate file $entryFilePath');
|
||||
return null;
|
||||
}
|
||||
var rawJson:Null<String> = openfl.Assets.getText(entryFilePath);
|
||||
@@ -523,7 +547,7 @@ using funkin.data.song.migrator.SongDataMigrator;
|
||||
|
||||
if (character == null)
|
||||
{
|
||||
trace(' [WARN] Could not locate character $characterId');
|
||||
trace(' WARNING '.bold().bg_yellow() + ' Could not locate character $characterId');
|
||||
return allDifficulties;
|
||||
}
|
||||
|
||||
@@ -541,7 +565,7 @@ using funkin.data.song.migrator.SongDataMigrator;
|
||||
|
||||
if (allDifficulties.length == 0)
|
||||
{
|
||||
trace(' [WARN] No difficulties found. Returning default difficulty list.');
|
||||
trace(' WARNING '.bold().bg_yellow() + ' No difficulties found. Returning default difficulty list.');
|
||||
allDifficulties = Constants.DEFAULT_DIFFICULTY_LIST.copy();
|
||||
}
|
||||
|
||||
|
||||
@@ -22,6 +22,7 @@ class ChartManifestData
|
||||
* The metadata and chart data file names are derived from this.
|
||||
*/
|
||||
public var songId(default, set):String;
|
||||
|
||||
public function set_songId(value:String):String
|
||||
{
|
||||
return songId = invalidIdRegex.replace(value.trim(), '');
|
||||
@@ -71,7 +72,7 @@ class ChartManifestData
|
||||
updateVersionToLatest();
|
||||
|
||||
var writer = new json2object.JsonWriter<ChartManifestData>();
|
||||
return writer.write(this, pretty ? ' ' : null);
|
||||
return writer.write(this, pretty ? ' ' : null);
|
||||
}
|
||||
|
||||
public function updateVersionToLatest():Void
|
||||
|
||||
@@ -0,0 +1,82 @@
|
||||
package funkin.data.song.importer;
|
||||
|
||||
/**
|
||||
* Structure of a parsed Osu!Mania .osu file
|
||||
* Stuctured like a INI file format by CSV for HitObjects and more
|
||||
*/
|
||||
typedef OsuManiaData =
|
||||
{
|
||||
var General:
|
||||
{
|
||||
var PreviewTime:Int;
|
||||
};
|
||||
var Editor:
|
||||
{
|
||||
var DistanceSpacing:Float;
|
||||
var BeatDivisor:Int;
|
||||
var GridSize:Int;
|
||||
};
|
||||
var Metadata:
|
||||
{
|
||||
var Title:String;
|
||||
var TitleUnicode:String;
|
||||
var Artist:String;
|
||||
var ArtistUnicode:String;
|
||||
var Creator:String;
|
||||
var Version:String;
|
||||
};
|
||||
var Difficulty:
|
||||
{
|
||||
var OverallDifficulty:Int;
|
||||
var SliderMultiplier:Float;
|
||||
var SliderTickRate:Float;
|
||||
var CircleSize:Int;
|
||||
};
|
||||
var HitObjects:Array<ManiaHitObject>;
|
||||
var TimingPoints:Array<TimingPoint>;
|
||||
var Events:Array<Any>;
|
||||
}
|
||||
|
||||
class TimingPoint
|
||||
{
|
||||
public var time:Float;
|
||||
public var beatLength:Float;
|
||||
public var meter:Int;
|
||||
public var sampleSet:Int;
|
||||
public var sampleIndex:Int;
|
||||
public var volume:Int;
|
||||
public var uninherited:Int;
|
||||
public var effects:Int;
|
||||
public var bpm:Null<Float>;
|
||||
public var sv:Null<Float>;
|
||||
|
||||
public function new(time:Float, beatLength:Float, meter:Int, sampleSet:Int, sampleIndex:Int, volume:Int, uninherited:Int, effects:Int)
|
||||
{
|
||||
this.time = time;
|
||||
this.beatLength = beatLength;
|
||||
this.meter = meter;
|
||||
this.sampleSet = sampleSet;
|
||||
this.sampleIndex = sampleIndex;
|
||||
this.volume = volume;
|
||||
this.uninherited = uninherited;
|
||||
this.effects = effects;
|
||||
|
||||
// Derived values
|
||||
this.bpm = (uninherited == 1) ? (Math.round((60000 / beatLength) * 10) / 10) : null;
|
||||
this.sv = (uninherited == 0) ? (beatLength / 100) : null; // Just incase someone wants to add Scroll Velocity Support
|
||||
}
|
||||
}
|
||||
|
||||
class ManiaHitObject
|
||||
{
|
||||
public var time:Int;
|
||||
public var column:Int;
|
||||
public var holdDuration:Int;
|
||||
|
||||
public function new(time:Int, column:Int, holdDuration:Int)
|
||||
{
|
||||
this.time = time;
|
||||
this.column = column;
|
||||
this.holdDuration = holdDuration;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,199 @@
|
||||
package funkin.data.song.importer;
|
||||
|
||||
import funkin.data.song.SongData.SongMetadata;
|
||||
import funkin.data.song.SongData.SongChartData;
|
||||
import funkin.data.song.SongData.SongCharacterData;
|
||||
import funkin.data.song.SongData.SongNoteData;
|
||||
import funkin.data.song.SongData.SongTimeChange;
|
||||
import funkin.data.song.importer.OsuManiaData;
|
||||
import funkin.data.song.importer.OsuManiaData.TimingPoint;
|
||||
import funkin.data.song.importer.OsuManiaData.ManiaHitObject;
|
||||
|
||||
class OsuManiaImporter
|
||||
{
|
||||
public static function parseOsuFile(osuContent:String):OsuManiaData
|
||||
{
|
||||
var lines:Array<String> = osuContent.split("\n");
|
||||
var result:Dynamic = {};
|
||||
var currentSection:String = null;
|
||||
|
||||
var nonCSVLikeSections = ["General", "Editor", "Metadata", "Difficulty"];
|
||||
|
||||
for (line in lines)
|
||||
{
|
||||
line = StringTools.trim(line);
|
||||
if (line == "" || StringTools.startsWith(line, "//")) continue;
|
||||
|
||||
// Section header like [General]
|
||||
var sectionRegex = ~/^\[(.+)\]$/;
|
||||
if (sectionRegex.match(line))
|
||||
{
|
||||
currentSection = sectionRegex.matched(1);
|
||||
if (nonCSVLikeSections.contains(currentSection))
|
||||
{
|
||||
Reflect.setField(result, currentSection, {});
|
||||
}
|
||||
else
|
||||
{
|
||||
Reflect.setField(result, currentSection, []);
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
// Key-value pairs (INI style)
|
||||
if (currentSection != null && nonCSVLikeSections.contains(currentSection))
|
||||
{
|
||||
var parts:Array<String> = line.split(":");
|
||||
var key:String = StringTools.trim(parts.shift());
|
||||
var value:String = StringTools.trim(parts.join(":"));
|
||||
if (Reflect.field(result, currentSection) == null) Reflect.setField(result, currentSection, {});
|
||||
Reflect.setField(Reflect.field(result, currentSection), key, value);
|
||||
}
|
||||
// For CSV-like sections
|
||||
else if (currentSection != null)
|
||||
{
|
||||
var theArray:Array<String> = cast Reflect.field(result, currentSection);
|
||||
theArray.push(line);
|
||||
Reflect.setField(result, currentSection, theArray);
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param songData The raw parsed JSON data to migrate, as a Dynamic.
|
||||
* @param difficulty The difficulty name to assign to the migrated chart.
|
||||
* @return SongMetadata
|
||||
*/
|
||||
public static function migrateMetadata(songData:OsuManiaData, difficulty:String = 'normal'):SongMetadata
|
||||
{
|
||||
trace('Migrating song metadata from Osu!Mania.');
|
||||
|
||||
var songMetadata:SongMetadata = new SongMetadata('Import', songData.Metadata.ArtistUnicode ?? songData.Metadata.Artist ?? Constants.DEFAULT_ARTIST,
|
||||
songData.Metadata.Creator ?? Constants.DEFAULT_CHARTER, Constants.DEFAULT_VARIATION);
|
||||
|
||||
// Set generatedBy string for debugging.
|
||||
songMetadata.generatedBy = 'Chart Editor Import (Osu!Mania)';
|
||||
|
||||
songMetadata.playData.stage = 'mainStage';
|
||||
songMetadata.songName = songData.Metadata.TitleUnicode ?? songData.Metadata.Title ?? 'Import';
|
||||
songMetadata.playData.difficulties = [difficulty];
|
||||
|
||||
songMetadata.playData.songVariations = [];
|
||||
|
||||
songMetadata.timeChanges = rebuildTimeChanges(songData);
|
||||
|
||||
songMetadata.playData.characters = new SongCharacterData('bf', 'gf', 'dad');
|
||||
songMetadata.playData.ratings.set(difficulty, songData.Difficulty.OverallDifficulty ?? 0);
|
||||
|
||||
return songMetadata;
|
||||
}
|
||||
|
||||
static function rebuildTimeChanges(songData:OsuManiaData):Array<SongTimeChange>
|
||||
{
|
||||
var timings:Array<TimingPoint> = parseTimingPoints(cast songData.TimingPoints);
|
||||
var bpmPoints:Array<TimingPoint> = timings.filter((tp) -> tp.uninherited == 1);
|
||||
|
||||
var result:Array<SongTimeChange> = [];
|
||||
if (bpmPoints.length >= 1)
|
||||
{
|
||||
result.push(new SongTimeChange(0, bpmPoints[0].bpm ?? Constants.DEFAULT_BPM));
|
||||
|
||||
for (i in 1...bpmPoints.length)
|
||||
{
|
||||
var bpmPoint:TimingPoint = bpmPoints[i];
|
||||
|
||||
result.push(new SongTimeChange(bpmPoint.time, bpmPoint.bpm ?? Constants.DEFAULT_BPM));
|
||||
}
|
||||
}
|
||||
|
||||
if (result.length == 0)
|
||||
{
|
||||
result.push(new SongTimeChange(0, Constants.DEFAULT_BPM));
|
||||
trace("[WARN] No BPM points found, resulting to default BPM...");
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param songData The raw parsed JSON data to migrate, as a Dynamic.
|
||||
* @param difficulty The difficulty name to assign to the migrated chart.
|
||||
* @return SongChartData
|
||||
*/
|
||||
public static function migrateChartData(songData:OsuManiaData, difficulty:String = 'normal'):SongChartData
|
||||
{
|
||||
trace('Migrating song chart data from Osu!Mania.');
|
||||
|
||||
// Osu!Mania doesn't have a scroll speed variable as its controlled by the player
|
||||
var songChartData:SongChartData = new SongChartData([difficulty => Constants.DEFAULT_SCROLLSPEED], [], [difficulty => []]);
|
||||
|
||||
// songData.HitObjects is a Array<String> here so im casting it so haxe stops yelling at me
|
||||
var osuNotes:Array<ManiaHitObject> = parseManiaHitObjects(cast songData.HitObjects, songData.Difficulty.CircleSize);
|
||||
songChartData.notes.set(difficulty, convertNotes(osuNotes, songData.Difficulty.CircleSize));
|
||||
|
||||
songChartData.events = [];
|
||||
|
||||
return songChartData;
|
||||
}
|
||||
|
||||
static final STRUMLINE_SIZE = 4;
|
||||
|
||||
static function convertNotes(hitObjects:Array<ManiaHitObject>, keyCount:Int):Array<SongNoteData>
|
||||
{
|
||||
var result:Array<SongNoteData> = [];
|
||||
|
||||
for (hitObject in hitObjects)
|
||||
{
|
||||
var wrappedColumn:Int = hitObject.column % (keyCount * 2); // wrap overflow for 9K+
|
||||
if (keyCount <= 4) // if its 4K or less, flip to BF side
|
||||
{
|
||||
wrappedColumn -= 4;
|
||||
var noteOffset:Int = Std.int(Math.abs(keyCount - STRUMLINE_SIZE));
|
||||
var flippedNoteData:Int = wrappedColumn + keyCount + noteOffset;
|
||||
result.push(new SongNoteData(hitObject.time, flippedNoteData, hitObject.holdDuration ?? 0, ''));
|
||||
}
|
||||
else
|
||||
result.push(new SongNoteData(hitObject.time, wrappedColumn, hitObject.holdDuration ?? 0, ''));
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
static function parseTimingPoints(timingLines:Array<String>):Array<TimingPoint>
|
||||
{
|
||||
return timingLines.map(function(line:String):TimingPoint {
|
||||
var parts = line.split(",");
|
||||
var time = Std.parseFloat(parts[0]);
|
||||
var beatLength = Std.parseFloat(parts[1]);
|
||||
var meter = Std.parseInt(parts[2]);
|
||||
var sampleSet = Std.parseInt(parts[3]);
|
||||
var sampleIndex = Std.parseInt(parts[4]);
|
||||
var volume = Std.parseInt(parts[5]);
|
||||
var uninherited = Std.parseInt(parts[6]);
|
||||
var effects = Std.parseInt(parts[7]);
|
||||
|
||||
return new TimingPoint(time, beatLength, meter, sampleSet, sampleIndex, volume, uninherited, effects);
|
||||
});
|
||||
}
|
||||
|
||||
static function parseManiaHitObjects(hitObjectsLines:Array<String>, ?columns:Int = 4):Array<ManiaHitObject>
|
||||
{
|
||||
return hitObjectsLines.map(function(line:String):ManiaHitObject {
|
||||
var parts = line.split(",");
|
||||
|
||||
var x:Int = Std.parseInt(parts[0]);
|
||||
var time:Int = Std.parseInt(parts[2]);
|
||||
var type:Int = Std.parseInt(parts[3]);
|
||||
var hasHold:Bool = (type & 128) == 128;
|
||||
|
||||
var noteD:Int = Std.int(x / (512 / columns));
|
||||
var holdEndTime:Null<Int> = hasHold ? Std.parseInt(parts[5].split(":")[0]) : null;
|
||||
|
||||
var holdDuration:Int = (holdEndTime != null) ? (holdEndTime - time) : 0;
|
||||
|
||||
return new ManiaHitObject(time, noteD, holdDuration);
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,123 @@
|
||||
package funkin.data.song.importer;
|
||||
|
||||
typedef StepManiaData =
|
||||
{
|
||||
var Metadata:
|
||||
{
|
||||
var Title:String;
|
||||
var Artist:String;
|
||||
var Genre:String;
|
||||
var Credit:String;
|
||||
var Banner:String;
|
||||
var Background:String;
|
||||
|
||||
var Offset:Float;
|
||||
var SampleStart:Float;
|
||||
};
|
||||
var TimingPoints:Array<StepTimingPoint>;
|
||||
var Stops:Array<StepStop>;
|
||||
var Difficulties:Array<StepDifficulty>;
|
||||
}
|
||||
|
||||
enum StepManiaChartType {
|
||||
DanceSingle;
|
||||
DanceDouble;
|
||||
Unknown;
|
||||
}
|
||||
|
||||
enum StepManiaNoteType {
|
||||
Tap;
|
||||
Head;
|
||||
Tail;
|
||||
Roll;
|
||||
Mine;
|
||||
Fake;
|
||||
}
|
||||
|
||||
class StepNote
|
||||
{
|
||||
public var beat:Float;
|
||||
public var column:Int;
|
||||
public var type:StepManiaNoteType;
|
||||
|
||||
public function new(t:String, beat:Float, column:Int)
|
||||
{
|
||||
this.beat = beat;
|
||||
this.column = column;
|
||||
switch (t) {
|
||||
case "2":
|
||||
this.type = StepManiaNoteType.Head;
|
||||
case "3":
|
||||
this.type = StepManiaNoteType.Tail;
|
||||
case "4":
|
||||
this.type = StepManiaNoteType.Roll;
|
||||
case "M":
|
||||
this.type = StepManiaNoteType.Mine;
|
||||
case "F":
|
||||
this.type = StepManiaNoteType.Fake;
|
||||
default:
|
||||
this.type = StepManiaNoteType.Tap;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class StepDifficulty
|
||||
{
|
||||
public var name:String;
|
||||
public var charter:String;
|
||||
public var difficultyRating:Int;
|
||||
public var type:StepManiaChartType;
|
||||
|
||||
public var notes:Array<StepNote>;
|
||||
|
||||
public function parseChartType(chartTypeStr:String):StepManiaChartType
|
||||
{
|
||||
switch (chartTypeStr) {
|
||||
case "dance-single":
|
||||
return StepManiaChartType.DanceSingle;
|
||||
case "dance-double":
|
||||
return StepManiaChartType.DanceDouble;
|
||||
default:
|
||||
return StepManiaChartType.Unknown;
|
||||
}
|
||||
}
|
||||
|
||||
public function new(name:String, charter:String, difficultyRating:Int, type:String)
|
||||
{
|
||||
this.name = name;
|
||||
this.charter = charter;
|
||||
this.difficultyRating = difficultyRating;
|
||||
this.type = parseChartType(type);
|
||||
}
|
||||
}
|
||||
|
||||
class StepTimingPoint
|
||||
{
|
||||
public var bpm:Float;
|
||||
public var startBeat:Float = Math.NEGATIVE_INFINITY;
|
||||
public var endBeat:Float = Math.POSITIVE_INFINITY;
|
||||
public var startTimestamp:Float = 0;
|
||||
public var endTimestamp:Float = 0;
|
||||
|
||||
public function new(bpm:Float, startBeat:Float)
|
||||
{
|
||||
this.bpm = bpm;
|
||||
this.startBeat = startBeat;
|
||||
}
|
||||
}
|
||||
|
||||
// Not implemented, but if any chart uses them then the chart will break.
|
||||
// IE this messes with the timing of the notes.
|
||||
class StepStop
|
||||
{
|
||||
public var startBeat:Float = Math.NEGATIVE_INFINITY;
|
||||
public var duration:Float = 0;
|
||||
|
||||
public var startTimestamp:Float = Math.NEGATIVE_INFINITY;
|
||||
|
||||
public function new(startBeat:Float, duration:Float)
|
||||
{
|
||||
this.startBeat = startBeat;
|
||||
this.duration = duration;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,613 @@
|
||||
package funkin.data.song.importer;
|
||||
|
||||
import funkin.data.song.SongData.SongMetadata;
|
||||
import funkin.data.song.SongData.SongChartData;
|
||||
import funkin.data.song.SongData.SongCharacterData;
|
||||
import funkin.data.song.SongData.SongNoteData;
|
||||
import funkin.data.song.SongData.SongTimeChange;
|
||||
|
||||
import funkin.data.song.importer.StepManiaData.StepTimingPoint;
|
||||
import funkin.data.song.importer.StepManiaData.StepDifficulty;
|
||||
import funkin.data.song.importer.StepManiaData.StepManiaChartType;
|
||||
import funkin.data.song.importer.StepManiaData.StepNote;
|
||||
import funkin.data.song.importer.StepManiaData.StepStop;
|
||||
import funkin.data.song.importer.StepManiaData.StepManiaNoteType;
|
||||
|
||||
enum StepStateEnum
|
||||
{
|
||||
Metadata;
|
||||
TimingPoints;
|
||||
Stops;
|
||||
Notes;
|
||||
}
|
||||
|
||||
class StepManiaImporter
|
||||
{
|
||||
private static var readDiffMetadata:Map<String, String> = new Map<String, String>();
|
||||
|
||||
static function parseMetadataLine(line:String, result:StepManiaData):StepManiaData
|
||||
{
|
||||
var parts:Array<String> = line.split(":");
|
||||
if (parts.length != 2) return result;
|
||||
|
||||
var key:String = StringTools.trim(parts[0]);
|
||||
var value:String = StringTools.trim(parts[1]);
|
||||
|
||||
// remove trailing ; on value
|
||||
if (StringTools.endsWith(value, ";")) value = value.substr(0, value.length - 1);
|
||||
|
||||
switch (key)
|
||||
{
|
||||
case "TITLE":
|
||||
result.Metadata.Title = value;
|
||||
case "ARTIST":
|
||||
result.Metadata.Artist = value;
|
||||
case "GENRE":
|
||||
result.Metadata.Genre = value;
|
||||
case "CREDIT":
|
||||
if (result.Metadata.Credit == "") result.Metadata.Credit = value;
|
||||
else // .ssc
|
||||
readDiffMetadata.set("CREDIT", value);
|
||||
case "BANNER":
|
||||
result.Metadata.Banner = value;
|
||||
case "BACKGROUND":
|
||||
result.Metadata.Background = value;
|
||||
case "OFFSET":
|
||||
result.Metadata.Offset = Std.parseFloat(value);
|
||||
case "SAMPLESTART":
|
||||
result.Metadata.SampleStart = Std.parseFloat(value);
|
||||
// .ssc
|
||||
case "STEPSTYPE":
|
||||
readDiffMetadata.set("STEPSTYPE", value);
|
||||
case "DESCRIPTION":
|
||||
readDiffMetadata.set("DESCRIPTION", value);
|
||||
case "DIFFICULTY":
|
||||
readDiffMetadata.set("DIFFICULTY", value);
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
static function parseTimingPointLine(line:String):Array<StepTimingPoint>
|
||||
{
|
||||
// Remove #BPMS: prefix if present
|
||||
if (StringTools.startsWith(line, "#BPMS:")) line = line.substr(6);
|
||||
|
||||
var parts:Array<String> = line.split(",");
|
||||
|
||||
var stepTimingPoints:Array<StepTimingPoint> = [];
|
||||
for (i in 0...parts.length)
|
||||
{
|
||||
// Split the =
|
||||
var split = parts[i].split("=");
|
||||
if (split.length != 2) continue;
|
||||
|
||||
var beat:Float = Std.parseFloat(StringTools.trim(split[0]));
|
||||
|
||||
// Check if split[i + 1] has ; and remove it
|
||||
var bpmSplit = split[1];
|
||||
if (StringTools.endsWith(bpmSplit, ";")) bpmSplit = bpmSplit.substr(0, bpmSplit.length - 1);
|
||||
|
||||
var tp:StepTimingPoint = new StepTimingPoint(Std.parseFloat(bpmSplit), beat);
|
||||
stepTimingPoints.push(tp);
|
||||
}
|
||||
|
||||
return stepTimingPoints;
|
||||
}
|
||||
|
||||
static function parseStopsLine(line:String):Array<StepStop>
|
||||
{
|
||||
// Remove #STOPS: prefix if present
|
||||
if (StringTools.startsWith(line, "#STOPS:")) line = line.substr(7);
|
||||
|
||||
// Same as timing points
|
||||
var parts:Array<String> = line.split(",");
|
||||
var stepStopPoints:Array<StepStop> = [];
|
||||
for (i in 0...parts.length)
|
||||
{
|
||||
// Split the =
|
||||
var split = parts[i].split("=");
|
||||
if (split.length != 2) continue;
|
||||
|
||||
var beat:Float = Std.parseFloat(StringTools.trim(split[0]));
|
||||
|
||||
// Check if split[i + 1] has ; and remove it
|
||||
var durSplit = split[1];
|
||||
if (StringTools.endsWith(durSplit, ";")) durSplit = durSplit.substr(0, durSplit.length - 1);
|
||||
|
||||
var tp:StepStop = new StepStop(beat, Std.parseFloat(durSplit));
|
||||
stepStopPoints.push(tp);
|
||||
}
|
||||
|
||||
return stepStopPoints;
|
||||
}
|
||||
|
||||
static function parseMeasure(lines:Array<String>, measureIndex:Int):Array<StepNote>
|
||||
{
|
||||
var lengthInRows:Float = 192.0 / lines.length;
|
||||
var rowIndex:Int = 0;
|
||||
var beat:Float = 0;
|
||||
|
||||
var stepNotes:Array<StepNote> = [];
|
||||
|
||||
for (i in 0...lines.length)
|
||||
{
|
||||
var stepNoteRow:Float = measureIndex * 192 + (lengthInRows * rowIndex);
|
||||
beat = stepNoteRow / 48.0; // 48 rows per beat
|
||||
|
||||
for (j in 0...lines[i].length)
|
||||
{
|
||||
var char:String = lines[i].charAt(j);
|
||||
if (char != "0")
|
||||
{
|
||||
var stepNote:StepNote = new StepNote(char, beat, j);
|
||||
stepNotes.push(stepNote);
|
||||
}
|
||||
}
|
||||
rowIndex++;
|
||||
}
|
||||
|
||||
return stepNotes;
|
||||
}
|
||||
|
||||
static function synchronizeStepTimingPoints(stepStopPoints:Array<StepStop>, stepTimingPoints:Array<StepTimingPoint>):Array<StepTimingPoint>
|
||||
{
|
||||
// Initialize startTimestamp/endTimestamp and endBeat for timing points
|
||||
for (tpIndex in 0...stepTimingPoints.length)
|
||||
{
|
||||
var tp = stepTimingPoints[tpIndex];
|
||||
// ensure fields exist
|
||||
if (tp.startTimestamp == Math.NEGATIVE_INFINITY) tp.startTimestamp = 0.0;
|
||||
}
|
||||
|
||||
var tpIndex:Int = 1;
|
||||
var spIndex:Int = 0;
|
||||
|
||||
// if we have more than one timing point or any StepStops, synchronize
|
||||
if (stepTimingPoints.length > 1 || stepStopPoints.length > 0)
|
||||
{
|
||||
if (stepStopPoints.length > 1)
|
||||
{
|
||||
while (tpIndex < stepTimingPoints.length || spIndex < stepStopPoints.length)
|
||||
{
|
||||
var prevTp:StepTimingPoint = stepTimingPoints[tpIndex - 1];
|
||||
|
||||
if (tpIndex == stepTimingPoints.length)
|
||||
{
|
||||
var cts:Float = 0;
|
||||
while (spIndex < stepStopPoints.length)
|
||||
{
|
||||
var sp = stepStopPoints[spIndex];
|
||||
sp.startTimestamp = (prevTp.startTimestamp + (sp.startBeat - prevTp.startBeat) / (prevTp.bpm / 60)) + cts;
|
||||
cts += sp.duration;
|
||||
spIndex++;
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
if (spIndex == stepStopPoints.length)
|
||||
{
|
||||
while (tpIndex < stepTimingPoints.length)
|
||||
{
|
||||
var tp = stepTimingPoints[tpIndex];
|
||||
prevTp = stepTimingPoints[tpIndex - 1];
|
||||
prevTp.endBeat = tp.startBeat;
|
||||
prevTp.endTimestamp += (prevTp.endBeat - prevTp.startBeat) / (prevTp.bpm / 60);
|
||||
tp.startTimestamp = prevTp.endTimestamp;
|
||||
tp.endTimestamp = tp.startTimestamp;
|
||||
tpIndex++;
|
||||
if (tpIndex == stepTimingPoints.length)
|
||||
{
|
||||
tp.endTimestamp = Math.POSITIVE_INFINITY;
|
||||
tp.endBeat = Math.POSITIVE_INFINITY;
|
||||
}
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
var tp:StepTimingPoint = stepTimingPoints[tpIndex];
|
||||
var sp:StepStop = stepStopPoints[spIndex];
|
||||
|
||||
prevTp.endBeat = tp.startBeat;
|
||||
|
||||
if (sp.startBeat < prevTp.endBeat)
|
||||
{
|
||||
sp.startTimestamp = prevTp.endTimestamp + (sp.startBeat - prevTp.startBeat) / (prevTp.bpm / 60);
|
||||
prevTp.endTimestamp += sp.duration;
|
||||
spIndex++;
|
||||
}
|
||||
else
|
||||
{
|
||||
prevTp.endTimestamp += (prevTp.endBeat - prevTp.startBeat) / (prevTp.bpm / 60);
|
||||
tp.startTimestamp = prevTp.endTimestamp;
|
||||
tp.endTimestamp = tp.startTimestamp;
|
||||
tpIndex++;
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
while (tpIndex < stepTimingPoints.length)
|
||||
{
|
||||
var prevTp:StepTimingPoint = stepTimingPoints[tpIndex - 1];
|
||||
var tp:StepTimingPoint = stepTimingPoints[tpIndex];
|
||||
prevTp.endBeat = tp.startBeat;
|
||||
prevTp.endTimestamp = prevTp.startTimestamp + (prevTp.endBeat - prevTp.startBeat) / (prevTp.bpm / 60);
|
||||
tp.startTimestamp = prevTp.endTimestamp;
|
||||
tpIndex++;
|
||||
if (tpIndex == stepTimingPoints.length)
|
||||
{
|
||||
tp.endTimestamp = Math.POSITIVE_INFINITY;
|
||||
tp.endBeat = Math.POSITIVE_INFINITY;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return stepTimingPoints;
|
||||
}
|
||||
|
||||
static function pushWorking(workingDiff:StepDifficulty, result:StepManiaData):StepManiaData
|
||||
{
|
||||
if (workingDiff != null && workingDiff.notes.length > 0)
|
||||
{
|
||||
// Apply .ssc metadata if any
|
||||
for (metaKey in readDiffMetadata.keys())
|
||||
{
|
||||
var metaValue = readDiffMetadata.get(metaKey);
|
||||
switch (metaKey)
|
||||
{
|
||||
case "STEPSTYPE":
|
||||
workingDiff.type = workingDiff.parseChartType(metaValue);
|
||||
case "DIFFICULTY":
|
||||
workingDiff.name = metaValue;
|
||||
case "CREDIT":
|
||||
workingDiff.charter = metaValue;
|
||||
// DESCRIPTION is ignored for now
|
||||
}
|
||||
}
|
||||
result.Difficulties.push(workingDiff);
|
||||
}
|
||||
readDiffMetadata.clear();
|
||||
return result;
|
||||
}
|
||||
|
||||
static function parseBPMS(line:String, result:StepManiaData):StepManiaData
|
||||
{
|
||||
var tps = parseTimingPointLine(line);
|
||||
if (tps != null)
|
||||
{
|
||||
for (tp in tps)
|
||||
result.TimingPoints.push(tp);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
static function parseStops(line:String, result:StepManiaData):StepManiaData
|
||||
{
|
||||
var sps = parseStopsLine(line);
|
||||
if (sps != null)
|
||||
{
|
||||
for (sp in sps)
|
||||
result.Stops.push(sp);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Parses a StepMania file content into StepManiaData structure.
|
||||
* @param stepContent The content of the StepMania file as a string.
|
||||
* @return StepManiaData The parsed StepMania data.
|
||||
*/
|
||||
public static function parseStepManiaFile(stepContent:String):StepManiaData
|
||||
{
|
||||
readDiffMetadata = new Map<String, String>();
|
||||
var lines:Array<String> = stepContent.split("\n");
|
||||
|
||||
var currentMeasure:Int = 0;
|
||||
var measure:Array<String> = [];
|
||||
var workingDiff:StepDifficulty = null;
|
||||
|
||||
var state:StepStateEnum = StepStateEnum.Metadata;
|
||||
var headerLines:Int = 0;
|
||||
|
||||
var result:StepManiaData = {
|
||||
Metadata: {
|
||||
Title: "",
|
||||
Artist: "",
|
||||
Genre: "",
|
||||
Credit: "",
|
||||
Banner: "",
|
||||
Background: "",
|
||||
Offset: 0,
|
||||
SampleStart: 0
|
||||
},
|
||||
TimingPoints: [],
|
||||
Stops: [],
|
||||
Difficulties: []
|
||||
};
|
||||
|
||||
// Parsing metadata
|
||||
|
||||
for (line in lines)
|
||||
{
|
||||
line = StringTools.trim(line);
|
||||
if (line == "") continue;
|
||||
if (StringTools.startsWith(line, "//")) continue; // Comment line
|
||||
|
||||
switch (state)
|
||||
{
|
||||
case StepStateEnum.Metadata:
|
||||
if (StringTools.startsWith(line, "#BPMS:"))
|
||||
{
|
||||
state = StepStateEnum.TimingPoints;
|
||||
result = parseBPMS(line, result);
|
||||
if (StringTools.endsWith(line, ";")) state = StepStateEnum.Metadata;
|
||||
}
|
||||
else if (StringTools.startsWith(line, "#STOPS:"))
|
||||
{
|
||||
state = StepStateEnum.Stops;
|
||||
result = parseStops(line, result);
|
||||
if (StringTools.endsWith(line, ";")) state = StepStateEnum.Metadata;
|
||||
}
|
||||
else if (StringTools.startsWith(line, "#NOTES:"))
|
||||
{
|
||||
if (workingDiff != null)
|
||||
{
|
||||
// Save previous difficulty
|
||||
result = pushWorking(workingDiff, result);
|
||||
}
|
||||
|
||||
// Start new difficulty
|
||||
workingDiff = new StepDifficulty("", "", 0, "");
|
||||
workingDiff.notes = [];
|
||||
|
||||
headerLines = 0;
|
||||
currentMeasure = 0;
|
||||
measure = [];
|
||||
state = StepStateEnum.Notes;
|
||||
}
|
||||
else if (StringTools.startsWith(line, "#")) result = parseMetadataLine(line.substr(1), result);
|
||||
case StepStateEnum.TimingPoints:
|
||||
if (line == ";") state = StepStateEnum.Metadata;
|
||||
else
|
||||
{
|
||||
result = parseBPMS(line, result);
|
||||
if (StringTools.endsWith(line, ";")) state = StepStateEnum.Metadata;
|
||||
}
|
||||
case StepStateEnum.Stops:
|
||||
if (line == ";") state = StepStateEnum.Metadata;
|
||||
else
|
||||
{
|
||||
result = parseStops(line, result);
|
||||
if (StringTools.endsWith(line, ";")) state = StepStateEnum.Metadata;
|
||||
}
|
||||
case StepStateEnum.Notes:
|
||||
if (line == "#NOTES:") continue;
|
||||
// remove comments in line (if any) ie, "0000 // some comment"
|
||||
var commentIndex = line.indexOf("//");
|
||||
if (commentIndex != -1) line = StringTools.trim(line.substr(0, commentIndex));
|
||||
|
||||
if (StringTools.contains(line, ":")) // header
|
||||
{
|
||||
switch (headerLines)
|
||||
{
|
||||
case 0:
|
||||
// First line is chart type
|
||||
var chartTypeStr = StringTools.trim(line).replace(":", "");
|
||||
workingDiff.type = workingDiff.parseChartType(chartTypeStr);
|
||||
case 1:
|
||||
// Second line is charter
|
||||
workingDiff.charter = StringTools.trim(line).replace(":", "");
|
||||
case 2:
|
||||
// Third line is difficulty name
|
||||
workingDiff.name = StringTools.trim(line).replace(":", "");
|
||||
case 3:
|
||||
// Fourth line is difficulty rating
|
||||
workingDiff.difficultyRating = Std.parseInt(StringTools.trim(line).replace(":", ""));
|
||||
// we dont care about the rest lol
|
||||
}
|
||||
headerLines++;
|
||||
continue;
|
||||
}
|
||||
|
||||
// we're reading StepNote data now
|
||||
// start gathering measures until we hit a ,
|
||||
if (line == "," || line == ";")
|
||||
{
|
||||
// end of measure
|
||||
var stepNotesInMeasure = parseMeasure(measure, currentMeasure);
|
||||
for (stepNote in stepNotesInMeasure)
|
||||
workingDiff.notes.push(stepNote);
|
||||
measure = [];
|
||||
currentMeasure++;
|
||||
// end of StepNotes section
|
||||
if (line == ";") state = StepStateEnum.Metadata;
|
||||
}
|
||||
else
|
||||
{
|
||||
measure.push(line);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
if (workingDiff != null && workingDiff.notes.length > 0)
|
||||
{
|
||||
// Save last difficulty
|
||||
result = pushWorking(workingDiff, result);
|
||||
}
|
||||
|
||||
// Syncronize timing points with StepStops
|
||||
result.TimingPoints = synchronizeStepTimingPoints(result.Stops, result.TimingPoints);
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
static function getStepStopAtBeat(beat:Float, stepStops:Array<StepStop>):StepStop
|
||||
{
|
||||
var stepStop:StepStop = null;
|
||||
|
||||
for (s in stepStops)
|
||||
{
|
||||
if (s.startBeat < beat) stepStop = s;
|
||||
}
|
||||
|
||||
return stepStop;
|
||||
}
|
||||
|
||||
static function beatToTime(beat:Float, offset:Float, stepTimingPoints:Array<StepTimingPoint>, stepStops:Array<StepStop>):Float
|
||||
{
|
||||
var time:Float = 0;
|
||||
for (tp in stepTimingPoints)
|
||||
{
|
||||
if (tp.startBeat <= beat && tp.endBeat > beat)
|
||||
{
|
||||
var stepStop:StepStop = getStepStopAtBeat(beat, stepStops);
|
||||
var b:Float = tp.startBeat;
|
||||
var startTime:Float = tp.startTimestamp;
|
||||
if (stepStop != null)
|
||||
{
|
||||
if (stepStop.startBeat > tp.startBeat)
|
||||
{
|
||||
b = stepStop.startBeat;
|
||||
startTime = stepStop.startTimestamp + stepStop.duration;
|
||||
}
|
||||
}
|
||||
|
||||
var nb:Float = (beat - b) / (tp.bpm / 60);
|
||||
time = startTime + nb;
|
||||
}
|
||||
}
|
||||
|
||||
return (time * 1000) - (offset * 1000); // convert to ms
|
||||
}
|
||||
|
||||
static function convertStepNotes(offset:Float, type:StepManiaChartType, stepNotes:Array<StepNote>,
|
||||
stepTimingPoints:Array<StepTimingPoint>, stepStops:Array<StepStop>):Array<SongNoteData>
|
||||
{
|
||||
var result:Array<SongNoteData> = [];
|
||||
var holdArray:Array<Float> = [];
|
||||
if (type == StepManiaChartType.DanceSingle)
|
||||
{
|
||||
holdArray = [-1, -1, -1, -1];
|
||||
}
|
||||
else if (type == StepManiaChartType.DanceDouble)
|
||||
{
|
||||
holdArray = [-1, -1, -1, -1, -1, -1, -1, -1];
|
||||
}
|
||||
else
|
||||
{
|
||||
trace("[WARN] Unknown StepMania chart type when converting notes.");
|
||||
return result;
|
||||
}
|
||||
|
||||
for (stepNote in stepNotes)
|
||||
{
|
||||
var time = beatToTime(stepNote.beat, offset, stepTimingPoints, stepStops);
|
||||
|
||||
if (stepNote.type == StepManiaNoteType.Head || stepNote.type == StepManiaNoteType.Roll)
|
||||
{
|
||||
holdArray[stepNote.column] = time;
|
||||
continue;
|
||||
}
|
||||
|
||||
var snd:SongNoteData = new SongNoteData(time, stepNote.column, 0);
|
||||
if (stepNote.type == StepManiaNoteType.Mine) snd.kind = "mine";
|
||||
else if (stepNote.type == StepManiaNoteType.Fake) snd.kind = "fake";
|
||||
if (stepNote.type == StepManiaNoteType.Tail)
|
||||
{
|
||||
var length:Float = 0;
|
||||
if (holdArray[stepNote.column] != -1) length = time - holdArray[stepNote.column];
|
||||
snd.time = holdArray[stepNote.column];
|
||||
snd.length = length;
|
||||
holdArray[stepNote.column] = -1;
|
||||
}
|
||||
|
||||
if (type != StepManiaChartType.DanceSingle)
|
||||
{
|
||||
if (stepNote.column < 4) snd.data = stepNote.column + 4;
|
||||
else
|
||||
snd.data = stepNote.column - 4;
|
||||
}
|
||||
else
|
||||
{
|
||||
if (snd.data >= 4) snd.data -= 4;
|
||||
}
|
||||
|
||||
result.push(snd);
|
||||
}
|
||||
return result;
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Migrates StepManiaData to SongMetadata.
|
||||
* @param songData The StepManiaData to migrate.
|
||||
* @return SongMetadata The migrated SongMetadata.
|
||||
*/
|
||||
public static function migrateChartMetadata(songData:StepManiaData):SongMetadata
|
||||
{
|
||||
var metadata:SongMetadata = new SongMetadata(songData.Metadata.Title, songData.Metadata.Artist);
|
||||
|
||||
metadata.playData.stage = 'mainStage';
|
||||
metadata.playData.characters = new SongCharacterData('bf', 'gf', 'dad');
|
||||
|
||||
metadata.generatedBy = 'Chart Editor Import (StepMania)';
|
||||
|
||||
metadata.playData.songVariations = [];
|
||||
|
||||
// Difficulties
|
||||
var difficulties:Array<String> = [];
|
||||
for (diff in songData.Difficulties)
|
||||
{
|
||||
if (diff.type == StepManiaChartType.Unknown)
|
||||
{
|
||||
trace("[WARN] Skipping unknown StepMania chart type. Name: " + diff.name);
|
||||
continue; // skip unknown chart types
|
||||
}
|
||||
difficulties.push(diff.name);
|
||||
metadata.playData.ratings.set(diff.name, diff.difficultyRating);
|
||||
}
|
||||
|
||||
metadata.playData.difficulties = difficulties;
|
||||
|
||||
metadata.charter = songData.Metadata.Credit != "" ? songData.Metadata.Credit : null;
|
||||
|
||||
// TimeChanges
|
||||
metadata.timeChanges = [];
|
||||
for (tp in songData.TimingPoints)
|
||||
{
|
||||
var timeChange = new SongTimeChange(tp.startTimestamp * 1000, tp.bpm);
|
||||
timeChange.beatTime = tp.startBeat;
|
||||
metadata.timeChanges.push(timeChange);
|
||||
}
|
||||
|
||||
return metadata;
|
||||
}
|
||||
|
||||
/**
|
||||
* Migrates StepManiaData to SongChartData.
|
||||
* @param songData The StepManiaData to migrate.
|
||||
* @return SongChartData The migrated SongChartData.
|
||||
*/
|
||||
public static function migrateChartData(songData:StepManiaData):SongChartData {
|
||||
var scrollsMap:Map<String, Float> = new Map<String, Float>();
|
||||
var stepNoteMap:Map<String, Array<SongNoteData>> = new Map<String, Array<SongNoteData>>();
|
||||
|
||||
for (diff in songData.Difficulties)
|
||||
{
|
||||
if (diff.type == StepManiaChartType.Unknown)
|
||||
{
|
||||
trace("[WARN] Skipping unknown StepMania chart type. Name: " + diff.name);
|
||||
continue; // skip unknown chart types
|
||||
}
|
||||
scrollsMap.set(diff.name, Constants.DEFAULT_SCROLLSPEED);
|
||||
stepNoteMap.set(diff.name, convertStepNotes(songData.Metadata.Offset, diff.type, diff.notes, songData.TimingPoints, songData.Stops));
|
||||
}
|
||||
|
||||
var songChartData:SongChartData = new SongChartData(scrollsMap, [], stepNoteMap);
|
||||
return songChartData;
|
||||
}
|
||||
}
|
||||
@@ -66,7 +66,7 @@ class StageData
|
||||
updateVersionToLatest();
|
||||
|
||||
var writer = new json2object.JsonWriter<StageData>();
|
||||
return writer.write(this, pretty ? ' ' : null);
|
||||
return writer.write(this, pretty ? ' ' : null);
|
||||
}
|
||||
|
||||
public function updateVersionToLatest():Void
|
||||
@@ -196,7 +196,7 @@ typedef StageDataProp =
|
||||
|
||||
/**
|
||||
* The animation type to use.
|
||||
* Options: "sparrow", "packer"
|
||||
* Options: "sparrow", "packer", "animateatlas"
|
||||
* @default "sparrow"
|
||||
*/
|
||||
@:default("sparrow")
|
||||
@@ -228,6 +228,63 @@ typedef StageDataProp =
|
||||
@:default("#FFFFFF")
|
||||
@:optional
|
||||
var color:String;
|
||||
|
||||
/**
|
||||
* Various settings for the prop.
|
||||
* Only available for texture atlases.
|
||||
*/
|
||||
@:optional
|
||||
var atlasSettings:TextureAtlasData;
|
||||
};
|
||||
|
||||
typedef TextureAtlasData =
|
||||
{
|
||||
/**
|
||||
* If true, the texture atlas will behave as if it was exported as an SWF file.
|
||||
* Notably, this allows MovieClip symbols to play.
|
||||
*/
|
||||
@:optional
|
||||
var swfMode:Bool;
|
||||
|
||||
/**
|
||||
* If true, filters and masks will be cached when the atlas is loaded, instead of during runtime.
|
||||
*/
|
||||
@:optional
|
||||
var cacheOnLoad:Bool;
|
||||
|
||||
/**
|
||||
* The filter quality.
|
||||
* Available values are: HIGH, MEDIUM, LOW, and RUDY.
|
||||
*
|
||||
* If you're making an atlas sprite in HScript, you pass an Int instead:
|
||||
*
|
||||
* HIGH - 0
|
||||
* MEDIUM - 1
|
||||
* LOW - 2
|
||||
* RUDY - 3
|
||||
*/
|
||||
@:optional
|
||||
var filterQuality:Int;
|
||||
|
||||
/**
|
||||
* Whether to apply the stage matrix, if it was exported from a symbol instance.
|
||||
* Also positions the Texture Atlas as it displays in Animate.
|
||||
* Turning this on is only recommended if you prepositioned the character in Animate.
|
||||
* For other cases, it should be turned off to act similarly to a normal FlxSprite.
|
||||
*/
|
||||
@:optional
|
||||
var applyStageMatrix:Bool;
|
||||
|
||||
/**
|
||||
* If enabled, the sprite will render as one texture instead of rendering multiple limbs.
|
||||
* This is useful for stuff like changing alpha, and shaders that require the whole sprite.
|
||||
*
|
||||
* Only enable this if your sprite either:
|
||||
* - Changes alpha to something other than 1.0
|
||||
* - Has a shader or blend mode
|
||||
*/
|
||||
@:optional
|
||||
var useRenderTexture:Bool;
|
||||
};
|
||||
|
||||
typedef StageDataCharacter =
|
||||
|
||||
@@ -114,4 +114,19 @@ typedef LevelPropData =
|
||||
@:default([])
|
||||
@:optional
|
||||
var animations:Array<AnimationData>;
|
||||
|
||||
/**
|
||||
* If animations are used, this is the name of the animation to play first.
|
||||
* @default idle
|
||||
*/
|
||||
@:default('')
|
||||
@:optional
|
||||
var startingAnimation:String;
|
||||
|
||||
/**
|
||||
* Flips the sprite on X axis.
|
||||
*/
|
||||
@:default(false)
|
||||
@:optional
|
||||
var flipX:Null<Bool>;
|
||||
}
|
||||
|
||||
@@ -40,7 +40,8 @@ class LevelRegistry extends BaseRegistry<Level, LevelData, LevelEntryParams> imp
|
||||
'week5',
|
||||
'week6',
|
||||
'week7',
|
||||
'weekend1'
|
||||
'weekend1',
|
||||
'sserafim'
|
||||
];
|
||||
}
|
||||
|
||||
|
||||
+51
-27
@@ -3,26 +3,18 @@ package funkin.external.android;
|
||||
#if android
|
||||
import lime.system.JNI;
|
||||
import flixel.util.FlxSignal;
|
||||
import haxe.ds.Map;
|
||||
|
||||
/**
|
||||
* A Utility class to handle Android API level callbacks and events.
|
||||
*/
|
||||
@:unreflective
|
||||
class CallbackUtil
|
||||
class CallbackUtil #if (lime >= "8.0.0") implements JNISafety #end
|
||||
{
|
||||
/**
|
||||
* The result code for `DATA_FOLDER_CLOSED` activity.
|
||||
*/
|
||||
public static var DATA_FOLDER_CLOSED(get, never):Int;
|
||||
|
||||
@:noCompletion
|
||||
static function get_DATA_FOLDER_CLOSED():Int
|
||||
{
|
||||
final field:Null<Dynamic> = JNIUtil.createStaticField('funkin/extensions/CallbackUtil', 'DATA_FOLDER_CLOSED', 'I');
|
||||
|
||||
return field != null ? field.get() : 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Signal triggered when an activity result is received.
|
||||
*
|
||||
@@ -30,6 +22,13 @@ class CallbackUtil
|
||||
*/
|
||||
public static var onActivityResult:FlxTypedSignal<Int->Int->Void> = new FlxTypedSignal<Int->Int->Void>();
|
||||
|
||||
/**
|
||||
* Signal triggered when the user opens a FNFC file with the game in runtime.
|
||||
*
|
||||
* First argument is the FNFC file path.
|
||||
*/
|
||||
public static var onFNFCOpen:FlxTypedSignal<String->Void> = new FlxTypedSignal<String->Void>();
|
||||
|
||||
/**
|
||||
* Initializes the callback utility.
|
||||
*/
|
||||
@@ -39,32 +38,57 @@ class CallbackUtil
|
||||
|
||||
if (initCallBackJNI != null)
|
||||
{
|
||||
initCallBackJNI(new CallbackHandler());
|
||||
initCallBackJNI(new CallbackUtil());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Internal class to handle native callback events.
|
||||
*/
|
||||
class CallbackHandler #if (lime >= "8.0.0") implements JNISafety #end
|
||||
{
|
||||
@:allow(funkin.external.android.CallbackUtil)
|
||||
function new():Void {}
|
||||
@:noCompletion
|
||||
static function get_DATA_FOLDER_CLOSED():Int
|
||||
{
|
||||
final field:Null<Dynamic> = JNIUtil.createStaticField('funkin/extensions/CallbackUtil', 'DATA_FOLDER_CLOSED', 'I');
|
||||
|
||||
/**
|
||||
* Handles the activity result callback from native code.
|
||||
*
|
||||
* @param requestCode The request code of the acitivty.
|
||||
* @param resultCode The result code of the acitivty.
|
||||
*/
|
||||
return field != null ? field.get() : 0;
|
||||
}
|
||||
|
||||
@:noCompletion
|
||||
private static var __staticFields:Array<Dynamic> = null;
|
||||
|
||||
@:noCompletion
|
||||
public static var __callbacksFields:Map<String, Dynamic> = new Map<String, Dynamic>();
|
||||
|
||||
@:noCompletion
|
||||
private static function listStaticFields():Array<Dynamic>
|
||||
{
|
||||
if (__staticFields != null) return __staticFields;
|
||||
__staticFields = Type.getClassFields(CallbackUtil);
|
||||
__staticFields = __staticFields.filter((field) -> Std.isOfType(Reflect.field(CallbackUtil, field), IFlxSignal));
|
||||
return __staticFields;
|
||||
}
|
||||
|
||||
@:noCompletion
|
||||
private function new() {}
|
||||
|
||||
@:noCompletion
|
||||
@:keep
|
||||
#if (lime >= "8.0.0")
|
||||
@:runOnMainThread
|
||||
#end
|
||||
public function onActivityResult(requestCode:Int, resultCode:Int):Void
|
||||
private function dispatchCallback(callbackName:String, arguments:Array<Dynamic>)
|
||||
{
|
||||
if (CallbackUtil.onActivityResult != null) CallbackUtil.onActivityResult.dispatch(requestCode, resultCode);
|
||||
if (listStaticFields().contains(callbackName))
|
||||
{
|
||||
final field = Reflect.field(CallbackUtil, callbackName);
|
||||
|
||||
if (!__callbacksFields.exists(callbackName))
|
||||
{
|
||||
__callbacksFields.set(callbackName, Reflect.field(field, "dispatch"));
|
||||
}
|
||||
Reflect.callMethod(field, __callbacksFields.get(callbackName), arguments);
|
||||
}
|
||||
else
|
||||
{
|
||||
trace('[CALLBACK] Callback "${callbackName}" not found.');
|
||||
}
|
||||
}
|
||||
}
|
||||
#end
|
||||
|
||||
@@ -4,7 +4,6 @@ package funkin.external.android;
|
||||
/**
|
||||
* A Utility class to manage the Application's Data folder on Android.
|
||||
*/
|
||||
@:unreflective
|
||||
class DataFolderUtil
|
||||
{
|
||||
/**
|
||||
|
||||
+26
@@ -0,0 +1,26 @@
|
||||
package funkin.external.android;
|
||||
|
||||
#if android
|
||||
import lime.system.JNI;
|
||||
|
||||
/**
|
||||
* Utility class for keyboard detection.
|
||||
*/
|
||||
class KeyboardUtil
|
||||
{
|
||||
/**
|
||||
* Returns `true` if a keyboard is currently connected to the device.
|
||||
*/
|
||||
public static var keyboardConnected(get, never):Bool;
|
||||
|
||||
@:noCompletion
|
||||
static function get_keyboardConnected():Bool
|
||||
{
|
||||
final method:Null<Dynamic> = JNIUtil.createStaticMethod('funkin/util/KeyboardUtil', 'isKeyboardConnected', '()Z');
|
||||
|
||||
if (method == null) return false;
|
||||
|
||||
return inline JNI.callStatic(method, []);
|
||||
}
|
||||
}
|
||||
#end
|
||||
@@ -7,7 +7,6 @@ import lime.system.JNI;
|
||||
/**
|
||||
* A Utility class to get Android screen related informations.
|
||||
*/
|
||||
@:unreflective
|
||||
class ScreenUtil
|
||||
{
|
||||
/**
|
||||
|
||||
@@ -0,0 +1,37 @@
|
||||
package funkin.extensions;
|
||||
|
||||
import android.content.Context;
|
||||
import android.media.AudioManager;
|
||||
import android.os.Bundle;
|
||||
import org.haxe.extension.Extension;
|
||||
|
||||
public class AudioSession extends Extension {
|
||||
private AudioManager audioManager;
|
||||
|
||||
@Override
|
||||
public void onCreate(Bundle state) {
|
||||
super.onCreate(state);
|
||||
audioManager = (AudioManager) Extension.mainContext.getSystemService(Context.AUDIO_SERVICE);
|
||||
requestAudioFocus();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onPause() {
|
||||
super.onPause();
|
||||
if (audioManager != null) {
|
||||
audioManager.abandonAudioFocus(null);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onResume() {
|
||||
super.onResume();
|
||||
requestAudioFocus();
|
||||
}
|
||||
|
||||
private void requestAudioFocus() {
|
||||
if (audioManager != null) {
|
||||
audioManager.requestAudioFocus(null, AudioManager.STREAM_MUSIC, AudioManager.AUDIOFOCUS_GAIN_TRANSIENT);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -25,11 +25,80 @@ public class CallbackUtil extends Extension
|
||||
CallbackUtil.haxeObject = haxeObject;
|
||||
}
|
||||
|
||||
public static void callMethod(String methodName)
|
||||
{
|
||||
if (haxeObject != null)
|
||||
{
|
||||
Object[] args = new Object[2];
|
||||
args[0] = methodName;
|
||||
args[1] = new Object[0];
|
||||
haxeObject.call("dispatchCallback", args);
|
||||
}
|
||||
}
|
||||
|
||||
public static void callMethod(String methodName, Object arg1)
|
||||
{
|
||||
if (haxeObject != null)
|
||||
{
|
||||
Object[] _args = {arg1};
|
||||
Object[] args = new Object[2];
|
||||
args[0] = methodName;
|
||||
args[1] = _args;
|
||||
haxeObject.call("dispatchCallback", args);
|
||||
}
|
||||
}
|
||||
|
||||
public static void callMethod(String methodName, Object arg1, Object arg2)
|
||||
{
|
||||
if (haxeObject != null)
|
||||
{
|
||||
Object[] _args = {arg1, arg2};
|
||||
Object[] args = new Object[2];
|
||||
args[0] = methodName;
|
||||
args[1] = _args;
|
||||
haxeObject.call("dispatchCallback", args);
|
||||
}
|
||||
}
|
||||
|
||||
public static void callMethod(String methodName, Object arg1, Object arg2, Object arg3)
|
||||
{
|
||||
if (haxeObject != null)
|
||||
{
|
||||
Object[] _args = {arg1, arg2, arg3};
|
||||
Object[] args = new Object[2];
|
||||
args[0] = methodName;
|
||||
args[1] = _args;
|
||||
haxeObject.call("dispatchCallback", args);
|
||||
}
|
||||
}
|
||||
|
||||
public static void callMethod(String methodName, Object arg1, Object arg2, Object arg3, Object arg4)
|
||||
{
|
||||
if (haxeObject != null)
|
||||
{
|
||||
Object[] _args = {arg1, arg2, arg3, arg4};
|
||||
Object[] args = new Object[2];
|
||||
args[0] = methodName;
|
||||
args[1] = _args;
|
||||
haxeObject.call("dispatchCallback", args);
|
||||
}
|
||||
}
|
||||
|
||||
public static void callMethod(String methodName, Object[] args)
|
||||
{
|
||||
if (haxeObject != null)
|
||||
{
|
||||
Object[] _args = new Object[2];
|
||||
args[0] = methodName;
|
||||
args[1] = args;
|
||||
haxeObject.call("dispatchCallback", _args);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onActivityResult(int requestCode, int resultCode, Intent data)
|
||||
{
|
||||
if (haxeObject != null)
|
||||
haxeObject.call2("onActivityResult", requestCode, resultCode);
|
||||
callMethod("onActivityResult", requestCode, resultCode);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,129 @@
|
||||
package funkin.extensions;
|
||||
|
||||
import android.content.Intent;
|
||||
import android.net.Uri;
|
||||
import android.os.ParcelFileDescriptor;
|
||||
import android.os.Bundle;
|
||||
import android.util.Log;
|
||||
|
||||
import funkin.extensions.CallbackUtil;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.FileInputStream;
|
||||
import java.io.FileOutputStream;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.io.OutputStream;
|
||||
|
||||
import org.haxe.extension.Extension;
|
||||
|
||||
import org.haxe.lime.HaxeObject;
|
||||
|
||||
public class FNFCExtension extends Extension
|
||||
{
|
||||
public static final String LOG_TAG = "FNFCExtension";
|
||||
|
||||
public static String lastFNFC = null;
|
||||
|
||||
@Override
|
||||
public void onCreate(Bundle savedInstanceState) {
|
||||
super.onCreate(savedInstanceState);
|
||||
|
||||
Intent intent = mainActivity.getIntent();
|
||||
if (intent != null && intent.getData() != null) {
|
||||
handleIntent(intent, false);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onNewIntent(Intent intent) {
|
||||
super.onNewIntent(intent);
|
||||
|
||||
if (intent != null && intent.getData() != null) {
|
||||
handleIntent(intent, true);
|
||||
}
|
||||
}
|
||||
|
||||
private static void handleIntent(Intent intent, boolean doCallback)
|
||||
{
|
||||
if (Intent.ACTION_VIEW.equals(intent.getAction()))
|
||||
{
|
||||
Uri uri = intent.getData();
|
||||
if (uri != null)
|
||||
{
|
||||
try
|
||||
{
|
||||
lastFNFC = copyFNFCToCache(uri);
|
||||
|
||||
if (doCallback)
|
||||
{
|
||||
CallbackUtil.callMethod("onFNFCOpen", lastFNFC);
|
||||
}
|
||||
}
|
||||
catch (IOException e)
|
||||
{
|
||||
Log.e(LOG_TAG, e.getMessage());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public static String copyFNFCToCache(Uri uri) throws IOException
|
||||
{
|
||||
if (uri != null)
|
||||
{
|
||||
String fileName = new File(uri.getPath()).getName();
|
||||
|
||||
// Clean some Uri leftovers
|
||||
if (fileName.contains(":"))
|
||||
fileName = fileName.split(":")[1];
|
||||
|
||||
File cacheFNFC = new File(mainContext.getCacheDir(), "fnfc");
|
||||
File output = new File(cacheFNFC, fileName);
|
||||
|
||||
if (!cacheFNFC.exists())
|
||||
cacheFNFC.mkdir();
|
||||
|
||||
if (output.exists())
|
||||
output.delete();
|
||||
|
||||
ParcelFileDescriptor parcelFileDescriptor = null;
|
||||
FileInputStream fileInputStream = null;
|
||||
OutputStream out = null;
|
||||
|
||||
try
|
||||
{
|
||||
parcelFileDescriptor = mainContext.getContentResolver().openFileDescriptor(uri, "r");
|
||||
fileInputStream = new FileInputStream(parcelFileDescriptor.getFileDescriptor());
|
||||
|
||||
byte[] fileBytes = new byte[(int) parcelFileDescriptor.getStatSize()];
|
||||
fileInputStream.read(fileBytes);
|
||||
|
||||
out = new FileOutputStream(output);
|
||||
out.write(fileBytes);
|
||||
|
||||
if (output.exists())
|
||||
output.deleteOnExit(); // makes the file get deleted when the app closes ig?
|
||||
}
|
||||
catch (IOException e)
|
||||
{
|
||||
Log.e(LOG_TAG, e.getMessage());
|
||||
}
|
||||
finally
|
||||
{
|
||||
if (fileInputStream != null)
|
||||
fileInputStream.close();
|
||||
|
||||
if (parcelFileDescriptor != null)
|
||||
parcelFileDescriptor.close();
|
||||
|
||||
if (out != null)
|
||||
out.close();
|
||||
}
|
||||
|
||||
return output.getAbsolutePath();
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
package funkin.util;
|
||||
|
||||
import org.haxe.extension.Extension;
|
||||
|
||||
public class KeyboardUtil
|
||||
{
|
||||
public static boolean isKeyboardConnected()
|
||||
{
|
||||
if (Extension.mainContext == null) return false;
|
||||
|
||||
// KEYBOARD_UNDEFINED = 0, KEYBOARD_NOKEYS = 1
|
||||
return Extension.mainContext.getResources().getConfiguration().keyboard > 1;
|
||||
}
|
||||
}
|
||||
+31
@@ -0,0 +1,31 @@
|
||||
package funkin.external.apple;
|
||||
|
||||
#if ((ios || macos) && cpp)
|
||||
/**
|
||||
* A utility class to manage the audio session on apple devices.
|
||||
*/
|
||||
@:build(funkin.util.macro.LinkerMacro.xml('project/Build.xml'))
|
||||
@:include('AudioSession.hpp')
|
||||
@:unreflective
|
||||
extern class AudioSession
|
||||
{
|
||||
/**
|
||||
* Initializes the audio session for playback.
|
||||
*
|
||||
* This function configures the shared AVAudioSession instance with the following settings:
|
||||
* - Sets the audio session category to Playback, allowing Bluetooth A2DP and mixing with spoken audio interruptions.
|
||||
* - For iOS 17.0 and above, disables interruption on route disconnect.
|
||||
* - For iOS 14.5 and above, prefers no interruptions from system alerts.
|
||||
*/
|
||||
@:native('Apple_AudioSession_Initialize')
|
||||
static function initialize():Void;
|
||||
|
||||
/**
|
||||
* Sets the active state of the shared AVAudioSession.
|
||||
*
|
||||
* @param active Whether to activate or deactivate the audio session.
|
||||
*/
|
||||
@:native('Apple_AudioSession_SetActive')
|
||||
static function setActive(active:Bool):Void;
|
||||
}
|
||||
#end
|
||||
+21
@@ -0,0 +1,21 @@
|
||||
package funkin.external.apple;
|
||||
|
||||
#if ((ios || macos) && cpp)
|
||||
/**
|
||||
* A utility class for FNFC loading on apple devices.
|
||||
*/
|
||||
@:build(funkin.util.macro.LinkerMacro.xml('project/Build.xml'))
|
||||
@:include('FNFCUtil.hpp')
|
||||
@:unreflective
|
||||
extern class FNFCUtil
|
||||
{
|
||||
/**
|
||||
* Copies the FNFC resource from the specified URL into the cache.
|
||||
*
|
||||
* @param url The URL of the FNFC resource to copy into the cache.
|
||||
* @param callback A function to be called when the copy operation completes.
|
||||
*/
|
||||
@:native('Apple_FNFCUtil_CopyFNFCIntoCache')
|
||||
static function copyFNFCIntoCache(url:cpp.ConstCharStar, callback:cpp.Callable<(event:cpp.ConstCharStar, value:cpp.ConstCharStar) -> Void>):Void;
|
||||
}
|
||||
#end
|
||||
+20
@@ -0,0 +1,20 @@
|
||||
package funkin.external.apple;
|
||||
|
||||
#if ((ios || macos) && cpp)
|
||||
/**
|
||||
* A utility class to get information about the mem usage.
|
||||
*/
|
||||
@:build(funkin.util.macro.LinkerMacro.xml('project/Build.xml'))
|
||||
@:include('MemoryUtil.hpp')
|
||||
@:unreflective
|
||||
extern class MemoryUtil
|
||||
{
|
||||
/**
|
||||
* Retrieves the current process's resident set size (RSS) in bytes on Apple platforms.
|
||||
*
|
||||
* @return The resident set size (RSS) in bytes if successful; otherwise, returns 0 on failure.
|
||||
*/
|
||||
@:native('Apple_MemoryUtil_GetCurrentProcessRss')
|
||||
static function getCurrentProcessRss():cpp.SizeT;
|
||||
}
|
||||
#end
|
||||
+36
@@ -0,0 +1,36 @@
|
||||
package funkin.external.apple;
|
||||
|
||||
#if ((ios || macos) && cpp)
|
||||
/**
|
||||
* A utility class to get screen related informations on apple devices.
|
||||
*/
|
||||
@:build(funkin.util.macro.LinkerMacro.xml('project/Build.xml'))
|
||||
@:include('ScreenUtil.hpp')
|
||||
@:unreflective
|
||||
extern class ScreenUtil
|
||||
{
|
||||
/**
|
||||
* Retrieves the safe area insets for the current screen on iOS devices.
|
||||
*
|
||||
* This function populates the provided pointers with the safe area insets (in pixels) for the top, bottom, left, and right edges of the screen.
|
||||
* On iOS 11 and later, it uses the main window's safeAreaInsets and converts them to pixel values using the screen scale.
|
||||
* On earlier iOS versions, all insets are set to 0.0.
|
||||
*
|
||||
* @param top Pointer to a double to receive the top safe area inset (in pixels).
|
||||
* @param bottom Pointer to a double to receive the bottom safe area inset (in pixels).
|
||||
* @param left Pointer to a double to receive the left safe area inset (in pixels).
|
||||
* @param right Pointer to a double to receive the right safe area inset (in pixels).
|
||||
*/
|
||||
@:native('Apple_ScreenUtil_GetSafeAreaInsets')
|
||||
static function getSafeAreaInsets(top:cpp.RawPointer<Float>, bottom:cpp.RawPointer<Float>, left:cpp.RawPointer<Float>, right:cpp.RawPointer<Float>):Void;
|
||||
|
||||
/**
|
||||
* Retrieves the size of the main screen in pixels.
|
||||
*
|
||||
* @param width Pointer to a double where the screen width (in pixels) will be stored.
|
||||
* @param height Pointer to a double where the screen height (in pixels) will be stored.
|
||||
*/
|
||||
@:native('Apple_ScreenUtil_GetScreenSize')
|
||||
static function getScreenSize(width:cpp.RawPointer<Float>, height:cpp.RawPointer<Float>):Void;
|
||||
}
|
||||
#end
|
||||
+29
@@ -0,0 +1,29 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<xml>
|
||||
<pragma once="true" />
|
||||
|
||||
<files id="haxe">
|
||||
<compilerflag value="-I${this_dir}/include" />
|
||||
</files>
|
||||
|
||||
<files id="__main__">
|
||||
<compilerflag value="-I${this_dir}/include" />
|
||||
</files>
|
||||
|
||||
<files id="external-apple">
|
||||
<compilerflag value="-I${this_dir}/include" />
|
||||
|
||||
<file name="${this_dir}/src/AudioSession.mm" />
|
||||
<file name="${this_dir}/src/MemoryUtil.mm" />
|
||||
<file name="${this_dir}/src/ScreenUtil.mm" />
|
||||
<file name="${this_dir}/src/FNFCUtil.mm" />
|
||||
</files>
|
||||
|
||||
<target id="haxe">
|
||||
<vflag name="-framework" value="UIKit" unless="macos" />
|
||||
<vflag name="-framework" value="Foundation" />
|
||||
<vflag name="-framework" value="AVFAudio" />
|
||||
|
||||
<files id="external-apple" />
|
||||
</target>
|
||||
</xml>
|
||||
@@ -0,0 +1,18 @@
|
||||
#pragma once
|
||||
|
||||
/**
|
||||
* Initializes the audio session for playback.
|
||||
*
|
||||
* This function configures the shared AVAudioSession instance with the following settings:
|
||||
* - Sets the audio session category to Playback, allowing Bluetooth A2DP and mixing with spoken audio interruptions.
|
||||
* - For iOS 17.0 and above, disables interruption on route disconnect.
|
||||
* - For iOS 14.5 and above, prefers no interruptions from system alerts.
|
||||
*/
|
||||
void Apple_AudioSession_Initialize();
|
||||
|
||||
/**
|
||||
* Sets the active state of the shared AVAudioSession.
|
||||
*
|
||||
* @param active Whether to activate or deactivate the audio session.
|
||||
*/
|
||||
void Apple_AudioSession_SetActive(bool active);
|
||||
@@ -0,0 +1,17 @@
|
||||
#pragma once
|
||||
|
||||
/**
|
||||
* Function pointer type for handling events with associated string values.
|
||||
*
|
||||
* @param event The name of the event as a null-terminated string.
|
||||
* @param value The value associated with the event as a null-terminated string.
|
||||
*/
|
||||
typedef void (*FNFCCallback)(const char* event, const char* value);
|
||||
|
||||
/**
|
||||
* Copies the FNFC resource from the specified URL into the cache.
|
||||
*
|
||||
* @param url The URL of the FNFC resource to copy into the cache.
|
||||
* @param callback A function to be called when the copy operation completes.
|
||||
*/
|
||||
void Apple_FNFCUtil_CopyFNFCIntoCache(const char *url, FNFCCallback callback);
|
||||
@@ -0,0 +1,10 @@
|
||||
#pragma once
|
||||
|
||||
#include <cstddef>
|
||||
|
||||
/**
|
||||
* Retrieves the current process's resident set size (RSS) in bytes on Apple platforms.
|
||||
*
|
||||
* @return The resident set size (RSS) in bytes if successful; otherwise, returns 0 on failure.
|
||||
*/
|
||||
size_t Apple_MemoryUtil_GetCurrentProcessRss();
|
||||
@@ -0,0 +1,23 @@
|
||||
#pragma once
|
||||
|
||||
/**
|
||||
* Retrieves the safe area insets for the current screen on iOS devices.
|
||||
*
|
||||
* This function populates the provided pointers with the safe area insets (in pixels) for the top, bottom, left, and right edges of the screen.
|
||||
* On iOS 11 and later, it uses the main window's safeAreaInsets and converts them to pixel values using the screen scale.
|
||||
* On earlier iOS versions, all insets are set to 0.0.
|
||||
*
|
||||
* @param top Pointer to a double to receive the top safe area inset (in pixels).
|
||||
* @param bottom Pointer to a double to receive the bottom safe area inset (in pixels).
|
||||
* @param left Pointer to a double to receive the left safe area inset (in pixels).
|
||||
* @param right Pointer to a double to receive the right safe area inset (in pixels).
|
||||
*/
|
||||
void Apple_ScreenUtil_GetSafeAreaInsets(double* top, double* bottom, double* left, double* right);
|
||||
|
||||
/**
|
||||
* Retrieves the size of the main screen in pixels.
|
||||
*
|
||||
* @param width Pointer to a double where the screen width (in pixels) will be stored.
|
||||
* @param height Pointer to a double where the screen height (in pixels) will be stored.
|
||||
*/
|
||||
void Apple_ScreenUtil_GetScreenSize(double* width, double* height);
|
||||
@@ -0,0 +1,49 @@
|
||||
#import "AudioSession.hpp"
|
||||
|
||||
#import <TargetConditionals.h>
|
||||
#import <Foundation/Foundation.h>
|
||||
#import <AVFAudio/AVFAudio.h>
|
||||
#if TARGET_OS_IOS
|
||||
#import <UIKit/UIKit.h>
|
||||
#endif
|
||||
|
||||
void Apple_AudioSession_Initialize()
|
||||
{
|
||||
#if TARGET_OS_IOS
|
||||
|
||||
AVAudioSession *session = [AVAudioSession sharedInstance];
|
||||
|
||||
NSError *error;
|
||||
|
||||
[session setCategory:AVAudioSessionCategoryPlayback mode:AVAudioSessionModeDefault options:AVAudioSessionCategoryOptionAllowBluetoothA2DP error:&error];
|
||||
|
||||
if (@available(iOS 17.0, *))
|
||||
{
|
||||
[session setPrefersInterruptionOnRouteDisconnect:false error:nil];
|
||||
}
|
||||
|
||||
if (@available(iOS 14.5, *))
|
||||
{
|
||||
[session setPrefersNoInterruptionsFromSystemAlerts:true error:nil];
|
||||
}
|
||||
|
||||
if (error)
|
||||
NSLog(@"Unable to set category of audio session: %@", error);
|
||||
else
|
||||
[session setActive:YES error:nil];
|
||||
#endif
|
||||
}
|
||||
|
||||
void Apple_AudioSession_SetActive(bool active)
|
||||
{
|
||||
#if TARGET_OS_IOS
|
||||
AVAudioSession *session = [AVAudioSession sharedInstance];
|
||||
|
||||
NSError *error;
|
||||
|
||||
[session setActive:active error:&error];
|
||||
|
||||
if (error)
|
||||
NSLog(@"Unable to set active of audio session: %@", error);
|
||||
#endif
|
||||
}
|
||||
@@ -0,0 +1,132 @@
|
||||
#include "FNFCUtil.hpp"
|
||||
#import <Foundation/Foundation.h>
|
||||
|
||||
static FNFCCallback fnfcCallback = nullptr;
|
||||
|
||||
void Apple_FNFCUtil_CopyFNFCIntoCache(const char *url, FNFCCallback callback)
|
||||
{
|
||||
#if TARGET_OS_IOS
|
||||
if (!fnfcCallback)
|
||||
{
|
||||
fnfcCallback = callback;
|
||||
}
|
||||
|
||||
if (!url)
|
||||
{
|
||||
if (fnfcCallback)
|
||||
{
|
||||
dispatch_async(dispatch_get_main_queue(), ^{
|
||||
fnfcCallback("FNFC_URL_NIL", "The URL passed to the conversion was null.");
|
||||
});
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
NSString *urlString = [NSString stringWithUTF8String:url];
|
||||
|
||||
NSURL *fileURL = [NSURL URLWithString:urlString];
|
||||
|
||||
if (!fileURL || !fileURL.isFileURL)
|
||||
{
|
||||
fileURL = [NSURL fileURLWithPath:urlString];
|
||||
}
|
||||
|
||||
if (!fileURL || !fileURL.isFileURL)
|
||||
{
|
||||
if (fnfcCallback)
|
||||
{
|
||||
dispatch_async(dispatch_get_main_queue(), ^{
|
||||
char *msg = strdup([[NSString stringWithFormat:@"The passed URL '%@' doesn't point at a FNFC file.", urlString] UTF8String]);
|
||||
fnfcCallback("FNFC_FILE_MISMATC", msg);
|
||||
free(msg);
|
||||
});
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
BOOL accessed = [fileURL startAccessingSecurityScopedResource];
|
||||
|
||||
if (!accessed)
|
||||
{
|
||||
if (fnfcCallback)
|
||||
{
|
||||
dispatch_async(dispatch_get_main_queue(), ^{
|
||||
char *msg = strdup([[NSString stringWithFormat:@"Failed to get access to URL '%@'.", urlString] UTF8String]);
|
||||
fnfcCallback("FNFC_FILE_ACCESS_ERR", msg);
|
||||
free(msg);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
NSString *sourcePath = [fileURL path];
|
||||
|
||||
NSString *filename = [sourcePath lastPathComponent];
|
||||
|
||||
NSArray *paths = NSSearchPathForDirectoriesInDomains(NSCachesDirectory, NSUserDomainMask, YES);
|
||||
|
||||
NSString *cacheDir = [[paths firstObject] stringByAppendingPathComponent:@"fnfc"];
|
||||
|
||||
NSFileManager *fm = [NSFileManager defaultManager];
|
||||
|
||||
NSError *dirError = nil;
|
||||
|
||||
if (![fm fileExistsAtPath:cacheDir])
|
||||
{
|
||||
if (![fm createDirectoryAtPath:cacheDir withIntermediateDirectories:YES attributes:nil error:&dirError])
|
||||
{
|
||||
if (fnfcCallback)
|
||||
{
|
||||
dispatch_async(dispatch_get_main_queue(), ^{
|
||||
char *msg = strdup([[NSString stringWithFormat:@"Failed to create directory %@: %@", cacheDir, dirError] UTF8String]);
|
||||
fnfcCallback("FNFC_DIR_CREATE_ERR", msg);
|
||||
free(msg);
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
NSString *destPath = [cacheDir stringByAppendingPathComponent:filename];
|
||||
|
||||
NSError *error = nil;
|
||||
|
||||
if ([fm fileExistsAtPath:destPath])
|
||||
{
|
||||
[fm removeItemAtPath:destPath error:nil];
|
||||
}
|
||||
|
||||
if (![fm copyItemAtPath:sourcePath toPath:destPath error:&error])
|
||||
{
|
||||
if (fnfcCallback)
|
||||
{
|
||||
dispatch_async(dispatch_get_main_queue(), ^{
|
||||
char *msg = strdup([[NSString stringWithFormat:@"Failed to copy FNFC from URL %@ to directory %@: %@", sourcePath, destPath, error] UTF8String]);
|
||||
fnfcCallback("FNFC_COPY_ERR", msg);
|
||||
free(msg);
|
||||
});
|
||||
}
|
||||
|
||||
if (accessed)
|
||||
{
|
||||
[fileURL stopAccessingSecurityScopedResource];
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
if (accessed)
|
||||
{
|
||||
[fileURL stopAccessingSecurityScopedResource];
|
||||
}
|
||||
|
||||
if (fnfcCallback)
|
||||
{
|
||||
dispatch_async(dispatch_get_main_queue(), ^{
|
||||
char *val = strdup([destPath UTF8String]);
|
||||
fnfcCallback("FNFC_RESULTS", val);
|
||||
free(val);
|
||||
});
|
||||
}
|
||||
#endif
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
#import "MemoryUtil.hpp"
|
||||
|
||||
#include <mach/mach.h>
|
||||
|
||||
size_t Apple_MemoryUtil_GetCurrentProcessRss()
|
||||
{
|
||||
struct task_basic_info info;
|
||||
|
||||
mach_msg_type_number_t count = TASK_BASIC_INFO_COUNT;
|
||||
|
||||
if (task_info(mach_task_self(), TASK_BASIC_INFO, (task_info_t)&info, &count) != KERN_SUCCESS)
|
||||
return 0;
|
||||
|
||||
return info.resident_size;
|
||||
}
|
||||
@@ -0,0 +1,45 @@
|
||||
#import "ScreenUtil.hpp"
|
||||
|
||||
#import <TargetConditionals.h>
|
||||
#if TARGET_OS_IOS
|
||||
#import <UIKit/UIKit.h>
|
||||
#endif
|
||||
#import <Foundation/Foundation.h>
|
||||
|
||||
void Apple_ScreenUtil_GetSafeAreaInsets(double* top, double* bottom, double* left, double* right)
|
||||
{
|
||||
#if TARGET_OS_IOS
|
||||
if (@available(iOS 11, *))
|
||||
{
|
||||
UIWindow* window = [UIApplication sharedApplication].windows[0];
|
||||
UIEdgeInsets safeAreaInsets = window.safeAreaInsets;
|
||||
float scale = [UIScreen mainScreen].scale;
|
||||
|
||||
(*top) = safeAreaInsets.top * scale;
|
||||
(*bottom) = safeAreaInsets.bottom * scale;
|
||||
(*left) = safeAreaInsets.left * scale;
|
||||
(*right) = safeAreaInsets.right * scale;
|
||||
|
||||
return;
|
||||
}
|
||||
#endif
|
||||
|
||||
(*top) = 0.0;
|
||||
(*bottom) = 0.0;
|
||||
(*left) = 0.0;
|
||||
(*right) = 0.0;
|
||||
}
|
||||
|
||||
void Apple_ScreenUtil_GetScreenSize(double* width, double* height)
|
||||
{
|
||||
#if TARGET_OS_IOS
|
||||
CGRect screenRect = [[UIScreen mainScreen] bounds];
|
||||
float scale = [UIScreen mainScreen].scale;
|
||||
|
||||
(*width) = (double)screenRect.size.width * scale;
|
||||
(*height) = (double)screenRect.size.height * scale;
|
||||
#else
|
||||
(*width) = 0.0;
|
||||
(*width) = 0.0;
|
||||
#endif
|
||||
}
|
||||
-17
@@ -1,17 +0,0 @@
|
||||
package funkin.external.ios;
|
||||
|
||||
#if ios
|
||||
/**
|
||||
* A Utility class to manage iOS audio.
|
||||
*/
|
||||
@:build(funkin.util.macro.LinkerMacro.xml('project/Build.xml'))
|
||||
@:include('AudioSession.hpp')
|
||||
@:unreflective
|
||||
extern class AudioSession
|
||||
{
|
||||
@:native('initialize')
|
||||
static function initialize():Void;
|
||||
@:native('setActive')
|
||||
static function setActive(active:Bool):Void;
|
||||
}
|
||||
#end
|
||||
-18
@@ -1,18 +0,0 @@
|
||||
package funkin.external.ios;
|
||||
|
||||
#if ios
|
||||
/**
|
||||
* A Utility class to get iOS screen related informations.
|
||||
*/
|
||||
@:build(funkin.util.macro.LinkerMacro.xml('project/Build.xml'))
|
||||
@:include('ScreenUtil.hpp')
|
||||
@:unreflective
|
||||
extern class ScreenUtil
|
||||
{
|
||||
@:native('getSafeAreaInsets')
|
||||
static function getSafeAreaInsets(top:cpp.RawPointer<Float>, bottom:cpp.RawPointer<Float>, left:cpp.RawPointer<Float>, right:cpp.RawPointer<Float>):Void;
|
||||
|
||||
@:native('getScreenSize')
|
||||
static function getScreenSize(width:cpp.RawPointer<Float>, height:cpp.RawPointer<Float>):Void;
|
||||
}
|
||||
#end
|
||||
-29
@@ -1,29 +0,0 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<xml>
|
||||
<pragma once="true" />
|
||||
|
||||
<files id="haxe">
|
||||
<compilerflag value="-I${this_dir}/ios/include" if="iphoneos || iphonesim" />
|
||||
</files>
|
||||
|
||||
<files id="__main__">
|
||||
<compilerflag value="-I${this_dir}/ios/include" if="iphoneos || iphonesim" />
|
||||
</files>
|
||||
|
||||
<files id="external-ios" dir="${this_dir}/ios" if="iphoneos || iphonesim">
|
||||
<compilerflag value="-I${this_dir}/ios/include" />
|
||||
|
||||
<file name="src/ScreenUtil.mm" />
|
||||
<file name="src/AudioSession.mm" />
|
||||
</files>
|
||||
|
||||
<target id="haxe">
|
||||
<section if="iphoneos || iphonesim">
|
||||
<vflag name="-framework" value="UIKit" />
|
||||
<vflag name="-framework" value="Foundation" />
|
||||
<vflag name="-framework" value="AVFAudio" />
|
||||
</section>
|
||||
|
||||
<files id="external-ios" if="iphoneos || iphonesim" />
|
||||
</target>
|
||||
</xml>
|
||||
@@ -1,4 +0,0 @@
|
||||
#pragma once
|
||||
|
||||
void initialize();
|
||||
void setActive(bool active);
|
||||
@@ -1,4 +0,0 @@
|
||||
#pragma once
|
||||
|
||||
void getSafeAreaInsets(double* top, double* bottom, double* left, double* right);
|
||||
void getScreenSize(double* width, double* height);
|
||||
@@ -1,41 +0,0 @@
|
||||
#import <Foundation/Foundation.h>
|
||||
#import <AVFAudio/AVFAudio.h>
|
||||
|
||||
void initialize()
|
||||
{
|
||||
AVAudioSession *session = [AVAudioSession sharedInstance];
|
||||
|
||||
NSError *error;
|
||||
[session setCategory:AVAudioSessionCategoryPlayback
|
||||
mode:AVAudioSessionModeDefault
|
||||
options:AVAudioSessionCategoryOptionAllowBluetoothA2DP|AVAudioSessionCategoryOptionInterruptSpokenAudioAndMixWithOthers
|
||||
error:&error];
|
||||
|
||||
if (@available(iOS 17.0, *))
|
||||
{
|
||||
[session setPrefersInterruptionOnRouteDisconnect:false error:nil];
|
||||
}
|
||||
|
||||
if (@available(iOS 14.5, *))
|
||||
{
|
||||
[session setPrefersNoInterruptionsFromSystemAlerts:true error:nil];
|
||||
}
|
||||
|
||||
if (error)
|
||||
{
|
||||
NSLog(@"Unable to set category of audio session: %@", error);
|
||||
}
|
||||
}
|
||||
|
||||
void setActive(bool active)
|
||||
{
|
||||
AVAudioSession *session = [AVAudioSession sharedInstance];
|
||||
NSError *error;
|
||||
|
||||
[session setActive:YES error:&error];
|
||||
|
||||
if (error)
|
||||
{
|
||||
NSLog(@"Unable to set active of audio session: %@", error);
|
||||
}
|
||||
}
|
||||
@@ -1,36 +0,0 @@
|
||||
#import "ScreenUtil.hpp"
|
||||
|
||||
#import <UIKit/UIKit.h>
|
||||
#import <Foundation/Foundation.h>
|
||||
|
||||
void getSafeAreaInsets(double* top, double* bottom, double* left, double* right)
|
||||
{
|
||||
if (@available(iOS 11, *))
|
||||
{
|
||||
UIWindow* window = [UIApplication sharedApplication].windows[0];
|
||||
UIEdgeInsets safeAreaInsets = window.safeAreaInsets;
|
||||
|
||||
float scale = [UIScreen mainScreen].scale;
|
||||
|
||||
(*top) = safeAreaInsets.top * scale;
|
||||
(*bottom) = safeAreaInsets.bottom * scale;
|
||||
(*left) = safeAreaInsets.left * scale;
|
||||
(*right) = safeAreaInsets.right * scale;
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
(*top) = 0.0;
|
||||
(*bottom) = 0.0;
|
||||
(*left) = 0.0;
|
||||
(*right) = 0.0;
|
||||
}
|
||||
|
||||
void getScreenSize(double* width, double* height)
|
||||
{
|
||||
CGRect screenRect = [[UIScreen mainScreen] bounds];
|
||||
float scale = [UIScreen mainScreen].scale;
|
||||
|
||||
(*width) = (double)screenRect.size.width * scale;
|
||||
(*height) = (double)screenRect.size.height * scale;
|
||||
}
|
||||
+17
-14
@@ -11,38 +11,32 @@ extern class WinAPI
|
||||
/**
|
||||
* Shows a message box with an error icon.
|
||||
*
|
||||
* @param handle A handle to the parent window.
|
||||
* @param message The message to display.
|
||||
* @param title The title of the message box.
|
||||
*/
|
||||
@:native('WINAPI_ShowError')
|
||||
static function showError(message:cpp.ConstCharStar, title:cpp.ConstCharStar):Void;
|
||||
static function showError(handle:cpp.RawPointer<cpp.Void>, message:cpp.ConstCharStar, title:cpp.ConstCharStar):Void;
|
||||
|
||||
/**
|
||||
* Shows a message box with a warning icon.
|
||||
*
|
||||
* @param handle A handle to the parent window.
|
||||
* @param message The message to display.
|
||||
* @param title The title of the message box.
|
||||
*/
|
||||
@:native('WINAPI_ShowWarning')
|
||||
static function showWarning(message:cpp.ConstCharStar, title:cpp.ConstCharStar):Void;
|
||||
static function showWarning(handle:cpp.RawPointer<cpp.Void>, message:cpp.ConstCharStar, title:cpp.ConstCharStar):Void;
|
||||
|
||||
/**
|
||||
* Shows a message box with an information icon.
|
||||
*
|
||||
* @param handle A handle to the parent window.
|
||||
* @param message The message to display.
|
||||
* @param title The title of the message box.
|
||||
*/
|
||||
@:native('WINAPI_ShowInformation')
|
||||
static function showInformation(message:cpp.ConstCharStar, title:cpp.ConstCharStar):Void;
|
||||
|
||||
/**
|
||||
* Shows a message box with a question icon.
|
||||
*
|
||||
* @param message The message to display.
|
||||
* @param title The title of the message box.
|
||||
*/
|
||||
@:native('WINAPI_ShowQuestion')
|
||||
static function showQuestion(message:cpp.ConstCharStar, title:cpp.ConstCharStar):Void;
|
||||
static function showInformation(handle:cpp.RawPointer<cpp.Void>, message:cpp.ConstCharStar, title:cpp.ConstCharStar):Void;
|
||||
|
||||
/**
|
||||
* Disables the "Report to Microsoft" dialog that appears when the application crashes.
|
||||
@@ -56,13 +50,22 @@ extern class WinAPI
|
||||
@:native('WINAPI_DisableWindowsGhosting')
|
||||
static function disableWindowsGhosting():Void;
|
||||
|
||||
/**
|
||||
* Retrieves the current working set size (in bytes) of the process.
|
||||
*
|
||||
* @return The size of the working set memory used by the process.
|
||||
*/
|
||||
@:native('WINAPI_GetProcessMemoryWorkingSetSize')
|
||||
static function getProcessMemoryWorkingSetSize():cpp.SizeT;
|
||||
|
||||
/**
|
||||
* Sets the dark mode for the active window.
|
||||
*
|
||||
* @param enable A boolean value indicating whether to enable (true) or disable (false) dark mode.
|
||||
* @param handle A handle to the parent window.
|
||||
* @param enable Whether to enable or disable dark mode.
|
||||
*/
|
||||
@:native('WINAPI_SetDarkMode')
|
||||
static function setDarkMode(enable:Bool):Void;
|
||||
static function setDarkMode(handle:cpp.RawPointer<cpp.Void>, enable:Bool):Void;
|
||||
|
||||
/**
|
||||
* Checks if the system is using dark mode.
|
||||
|
||||
+3
-3
@@ -10,7 +10,7 @@
|
||||
<compilerflag value="-I${this_dir}/include" />
|
||||
</files>
|
||||
|
||||
<files id="winapi">
|
||||
<files id="external-windows">
|
||||
<compilerflag value="-I${this_dir}/include" />
|
||||
|
||||
<file name="${this_dir}/src/winapi.cpp" />
|
||||
@@ -24,13 +24,13 @@
|
||||
<lib name="-ladvapi32" />
|
||||
</section>
|
||||
|
||||
<section if="windows" unless="mingw">
|
||||
<section unless="mingw">
|
||||
<lib name="user32.lib" />
|
||||
<lib name="kernel32.lib" />
|
||||
<lib name="dwmapi.lib" />
|
||||
<lib name="advapi32.lib" />
|
||||
</section>
|
||||
|
||||
<files id="winapi" />
|
||||
<files id="external-windows" />
|
||||
</target>
|
||||
</xml>
|
||||
|
||||
+30
-19
@@ -1,51 +1,62 @@
|
||||
#pragma once
|
||||
|
||||
/**
|
||||
* @brief Shows an error message box
|
||||
* Shows an error message box.
|
||||
*
|
||||
* @param handle A handle to the parent window
|
||||
* @param message The error message to display
|
||||
* @param title The title of the message box
|
||||
*/
|
||||
void WINAPI_ShowError(const char *message, const char *title);
|
||||
void WINAPI_ShowError(void* handle, const char *message, const char *title);
|
||||
|
||||
/**
|
||||
* @brief Shows a warning message box
|
||||
* Shows a warning message box.
|
||||
*
|
||||
* @param handle A handle to the parent window
|
||||
* @param message The warning message to display
|
||||
* @param title The title of the message box
|
||||
*/
|
||||
void WINAPI_ShowWarning(const char *message, const char *title);
|
||||
void WINAPI_ShowWarning(void* handle, const char *message, const char *title);
|
||||
|
||||
/**
|
||||
* @brief Shows an information message box
|
||||
* Shows an information message box.
|
||||
*
|
||||
* @param handle A handle to the parent window
|
||||
* @param message The information message to display
|
||||
* @param title The title of the message box
|
||||
*/
|
||||
void WINAPI_ShowInformation(const char *message, const char *title);
|
||||
void WINAPI_ShowInformation(void* handle, const char *message, const char *title);
|
||||
|
||||
/**
|
||||
* @brief Shows a question message box with OK/Cancel buttons
|
||||
* @param message The question message to display
|
||||
* @param title The title of the message box
|
||||
*/
|
||||
void WINAPI_ShowQuestion(const char *message, const char *title);
|
||||
|
||||
/**
|
||||
* @brief Disables Windows error reporting dialogs
|
||||
* Disables Windows error reporting dialogs.
|
||||
*/
|
||||
void WINAPI_DisableErrorReporting();
|
||||
|
||||
/**
|
||||
* @brief Disables Windows ghosting for the current process
|
||||
* Disables Windows ghosting for the current process.
|
||||
*/
|
||||
void WINAPI_DisableWindowsGhosting();
|
||||
|
||||
/**
|
||||
* @brief Sets dark mode for the active window
|
||||
* @param enable True to enable dark mode, false to disable
|
||||
* Retrieves the current working set size (in bytes) of the calling process.
|
||||
*
|
||||
* This function queries the operating system for the amount of physical memory currently allocated to the process (its working set).
|
||||
*
|
||||
* @return The working set size in bytes. Returns 0 if the query fails.
|
||||
*/
|
||||
void WINAPI_SetDarkMode(bool enable);
|
||||
size_t WINAPI_GetProcessMemoryWorkingSetSize();
|
||||
|
||||
/**
|
||||
* @brief Checks if the system is using dark mode
|
||||
* Sets dark mode on a window
|
||||
* @param handle A handle to the parent window.
|
||||
*
|
||||
* @param enable True to enable dark mode, false to disable
|
||||
*/
|
||||
void WINAPI_SetDarkMode(void* handle, bool enable);
|
||||
|
||||
/**
|
||||
* Checks if the system is using dark mode.
|
||||
*
|
||||
* @return True if system is in dark mode, false otherwise
|
||||
*/
|
||||
bool WINAPI_IsSystemDarkMode();
|
||||
|
||||
+39
-15
@@ -11,25 +11,36 @@
|
||||
#include <stdint.h>
|
||||
#include <stdio.h>
|
||||
|
||||
void WINAPI_ShowError(const char *message, const char *title)
|
||||
void WINAPI_ShowError(void* handle, const char *message, const char *title)
|
||||
{
|
||||
MessageBox(GetActiveWindow(), message, title, MB_OK | MB_ICONERROR);
|
||||
HWND hwnd = (HWND)handle;
|
||||
|
||||
if (!IsWindow(hwnd))
|
||||
hwnd = nullptr;
|
||||
|
||||
MessageBox(hwnd, message, title, MB_OK | MB_ICONERROR);
|
||||
}
|
||||
|
||||
void WINAPI_ShowWarning(const char *message, const char *title)
|
||||
void WINAPI_ShowWarning(void* handle, const char *message, const char *title)
|
||||
{
|
||||
MessageBox(GetActiveWindow(), message, title, MB_OK | MB_ICONWARNING);
|
||||
HWND hwnd = (HWND)handle;
|
||||
|
||||
if (!IsWindow(hwnd))
|
||||
hwnd = nullptr;
|
||||
|
||||
MessageBox(hwnd, message, title, MB_OK | MB_ICONWARNING);
|
||||
}
|
||||
|
||||
void WINAPI_ShowInformation(const char *message, const char *title)
|
||||
void WINAPI_ShowInformation(void* handle, const char *message, const char *title)
|
||||
{
|
||||
MessageBox(GetActiveWindow(), message, title, MB_OK | MB_ICONINFORMATION);
|
||||
HWND hwnd = (HWND)handle;
|
||||
|
||||
if (!IsWindow(hwnd))
|
||||
hwnd = nullptr;
|
||||
|
||||
MessageBox(hwnd, message, title, MB_OK | MB_ICONINFORMATION);
|
||||
}
|
||||
|
||||
void WINAPI_ShowQuestion(const char *message, const char *title)
|
||||
{
|
||||
MessageBox(GetActiveWindow(), message, title, MB_OKCANCEL | MB_ICONQUESTION);
|
||||
}
|
||||
|
||||
void WINAPI_DisableErrorReporting()
|
||||
{
|
||||
@@ -41,16 +52,29 @@ void WINAPI_DisableWindowsGhosting()
|
||||
DisableProcessWindowsGhosting();
|
||||
}
|
||||
|
||||
void WINAPI_SetDarkMode(bool enable)
|
||||
size_t WINAPI_GetProcessMemoryWorkingSetSize()
|
||||
{
|
||||
HWND window = GetActiveWindow();
|
||||
PROCESS_MEMORY_COUNTERS_EX pmc;
|
||||
|
||||
if (GetProcessMemoryInfo(GetCurrentProcess(), (PROCESS_MEMORY_COUNTERS*)&pmc, sizeof(pmc)))
|
||||
return pmc.WorkingSetSize;
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
void WINAPI_SetDarkMode(void* handle, bool enable)
|
||||
{
|
||||
HWND hwnd = (HWND)handle;
|
||||
|
||||
if (!IsWindow(hwnd))
|
||||
return;
|
||||
|
||||
int darkMode = enable ? 1 : 0;
|
||||
|
||||
if (DwmSetWindowAttribute(window, 20, &darkMode, sizeof(darkMode)) != S_OK)
|
||||
DwmSetWindowAttribute(window, 19, &darkMode, sizeof(darkMode));
|
||||
if (DwmSetWindowAttribute(hwnd, 20, &darkMode, sizeof(darkMode)) != S_OK)
|
||||
DwmSetWindowAttribute(hwnd, 19, &darkMode, sizeof(darkMode));
|
||||
|
||||
UpdateWindow(window);
|
||||
UpdateWindow(hwnd);
|
||||
}
|
||||
|
||||
bool WINAPI_IsSystemDarkMode()
|
||||
|
||||
@@ -0,0 +1,35 @@
|
||||
package funkin.graphics;
|
||||
|
||||
import funkin.graphics.FunkinSprite;
|
||||
import animate.FlxAnimateController;
|
||||
|
||||
class FunkinAnimationController extends FlxAnimateController
|
||||
{
|
||||
/**
|
||||
* The sprite that this animation controller is attached to.
|
||||
*/
|
||||
var _parentSprite:FunkinSprite;
|
||||
|
||||
public function new(sprite:FunkinSprite)
|
||||
{
|
||||
super(sprite);
|
||||
_parentSprite = sprite;
|
||||
}
|
||||
|
||||
/**
|
||||
* We override `FlxAnimationController`'s `play` method to account for texture atlases.
|
||||
*/
|
||||
public override function play(animName:String, force = false, reversed = false, frame = 0):Void
|
||||
{
|
||||
if (animName == null || animName == '') animName = _parentSprite.getDefaultSymbol();
|
||||
|
||||
if (!_parentSprite.hasAnimation(animName))
|
||||
{
|
||||
// Skip if the animation doesn't exist
|
||||
trace('Animation ${animName} does not exist!');
|
||||
return;
|
||||
}
|
||||
|
||||
super.play(animName, force, reversed, frame);
|
||||
}
|
||||
}
|
||||
@@ -254,7 +254,6 @@ class FunkinCamera extends FlxCamera
|
||||
|
||||
function disposeTextures():Void
|
||||
{
|
||||
trace('disposing textures');
|
||||
for (bitmap in grabbed)
|
||||
{
|
||||
bitmap.dispose();
|
||||
|
||||
@@ -9,28 +9,158 @@ import funkin.graphics.framebuffer.FixedBitmapData;
|
||||
import openfl.display.BitmapData;
|
||||
import flixel.math.FlxRect;
|
||||
import flixel.math.FlxPoint;
|
||||
import flixel.graphics.frames.FlxFrame.FlxFrameAngle;
|
||||
import flixel.math.FlxMatrix;
|
||||
import flixel.graphics.frames.FlxFrame;
|
||||
import flixel.FlxCamera;
|
||||
import openfl.system.System;
|
||||
import flixel.system.FlxAssets.FlxGraphicAsset;
|
||||
import funkin.FunkinMemory;
|
||||
import animate.internal.SymbolItem;
|
||||
import animate.internal.elements.Element;
|
||||
import animate.internal.elements.AtlasInstance;
|
||||
import animate.internal.elements.SymbolInstance;
|
||||
import animate.FlxAnimate;
|
||||
import animate.FlxAnimateFrames;
|
||||
import haxe.io.Path;
|
||||
|
||||
using StringTools;
|
||||
|
||||
typedef AtlasSpriteSettings =
|
||||
{
|
||||
/**
|
||||
* If true, the texture atlas will behave as if it was exported as an SWF file.
|
||||
* Notably, this allows MovieClip symbols to play.
|
||||
*/
|
||||
@:optional
|
||||
var swfMode:Bool;
|
||||
|
||||
/**
|
||||
* If true, filters and masks will be cached when the atlas is loaded, instead of during runtime.
|
||||
*/
|
||||
@:optional
|
||||
var cacheOnLoad:Bool;
|
||||
|
||||
/**
|
||||
* The filter quality.
|
||||
* Available values are: HIGH, MEDIUM, LOW, and RUDY.
|
||||
*
|
||||
* If you're making an atlas sprite in HScript, you pass an Int instead:
|
||||
*
|
||||
* HIGH - 0
|
||||
* MEDIUM - 1
|
||||
* LOW - 2
|
||||
* RUDY - 3
|
||||
*/
|
||||
@:optional
|
||||
var filterQuality:FilterQuality;
|
||||
|
||||
/**
|
||||
* Optional, an array of spritemaps for the atlas to load.
|
||||
*/
|
||||
@:optional
|
||||
var spritemaps:Array<SpritemapInput>;
|
||||
|
||||
/**
|
||||
* Optional, string of the metadata.json contents.
|
||||
*/
|
||||
@:optional
|
||||
var metadataJson:String;
|
||||
|
||||
/**
|
||||
* Optional, force the cache to use a specific key to index the texture atlas.
|
||||
*/
|
||||
@:optional
|
||||
var cacheKey:String;
|
||||
|
||||
/**
|
||||
* If true, the texture atlas will use a new slot in the cache.
|
||||
*/
|
||||
@:optional
|
||||
var uniqueInCache:Bool;
|
||||
|
||||
/**
|
||||
* Optional callback for when a symbol is created.
|
||||
*/
|
||||
@:optional
|
||||
var onSymbolCreate:animate.internal.SymbolItem->Void;
|
||||
|
||||
/**
|
||||
* Whether to apply the stage matrix, if it was exported from a symbol instance.
|
||||
* Also positions the Texture Atlas as it displays in Animate.
|
||||
* Turning this on is only recommended if you prepositioned the character in Animate.
|
||||
* For other cases, it should be turned off to act similarly to a normal FlxSprite.
|
||||
*/
|
||||
@:optional
|
||||
var applyStageMatrix:Bool;
|
||||
|
||||
/**
|
||||
* If enabled, the sprite will render as one texture instead of rendering multiple limbs.
|
||||
* This is useful for stuff like changing alpha, and shaders that require the whole sprite.
|
||||
*
|
||||
* Only enable this if your sprite either:
|
||||
* - Changes alpha to something other than 1.0
|
||||
* - Has a shader or blend mode
|
||||
*/
|
||||
@:optional
|
||||
var useRenderTexture:Bool;
|
||||
}
|
||||
|
||||
/**
|
||||
* An FlxSprite with additional functionality.
|
||||
* - A more efficient method for creating solid color sprites.
|
||||
* - TODO: Better cache handling for textures.
|
||||
*/
|
||||
@:nullSafety
|
||||
class FunkinSprite extends FlxSprite
|
||||
class FunkinSprite extends FlxAnimate
|
||||
{
|
||||
/**
|
||||
* @param x Starting X position
|
||||
* @param y Starting Y position
|
||||
* @param path The asset path for the graphic
|
||||
* @param atlasSettings The optional settings for the texture atlas
|
||||
*/
|
||||
public function new(?x:Float = 0, ?y:Float = 0)
|
||||
public function new(?x:Float = 0, ?y:Float = 0, ?path:String, ?atlasSettings:AtlasSpriteSettings)
|
||||
{
|
||||
super(x, y);
|
||||
|
||||
if (path != null)
|
||||
{
|
||||
var ext:String = Path.extension(path);
|
||||
|
||||
switch (ext)
|
||||
{
|
||||
case 'png':
|
||||
this.loadGraphic(path);
|
||||
|
||||
case '':
|
||||
// Do the opposite of Paths.animateAtlas since that function is called in loadTextureAtlas.
|
||||
var lib:String = Paths.getLibrary(path);
|
||||
|
||||
if (lib == 'preload')
|
||||
{
|
||||
path = path.replace('assets/images/', '');
|
||||
}
|
||||
else
|
||||
{
|
||||
path = path.replace('$lib:assets/$lib/images/', '');
|
||||
}
|
||||
|
||||
this.loadTextureAtlas(path, lib, atlasSettings);
|
||||
|
||||
default:
|
||||
FlxG.log.warn('Texture path $path is not a valid path. Make sure the path points to either an image or a folder with the texture atlas files!');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override function initVars():Void
|
||||
{
|
||||
super.initVars();
|
||||
|
||||
var newController:FunkinAnimationController = new FunkinAnimationController(this);
|
||||
|
||||
animation = newController;
|
||||
anim = newController;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -75,6 +205,20 @@ class FunkinSprite extends FlxSprite
|
||||
return sprite;
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a new FunkinSprite with an Adobe Animate texture atlas.
|
||||
* @param x The starting X position.
|
||||
* @param y The starting Y position.
|
||||
* @param key The key of the texture to load.
|
||||
* @return The new FunkinSprite.
|
||||
*/
|
||||
public static function createTextureAtlas(x:Float = 0.0, y:Float = 0.0, key:String, ?assetLibrary:Null<String>, ?settings:AtlasSpriteSettings):FunkinSprite
|
||||
{
|
||||
var sprite:FunkinSprite = new FunkinSprite(x, y);
|
||||
sprite.loadTextureAtlas(key, assetLibrary ?? "", settings);
|
||||
return sprite;
|
||||
}
|
||||
|
||||
/**
|
||||
* Load a static image as the sprite's texture.
|
||||
* @param key The key of the texture to load.
|
||||
@@ -83,7 +227,17 @@ class FunkinSprite extends FlxSprite
|
||||
public function loadTexture(key:String):FunkinSprite
|
||||
{
|
||||
var graphicKey:String = Paths.image(key);
|
||||
if (!isTextureCached(graphicKey)) FlxG.log.warn('Texture not cached, may experience stuttering! $graphicKey');
|
||||
|
||||
if (!Assets.exists(graphicKey, IMAGE))
|
||||
{
|
||||
FlxG.log.error('Texture not found, check your path! $graphicKey');
|
||||
return this;
|
||||
}
|
||||
|
||||
if (!FunkinMemory.isTextureCached(graphicKey))
|
||||
{
|
||||
FlxG.log.warn('Texture not cached, may experience stuttering! $graphicKey');
|
||||
}
|
||||
|
||||
loadGraphic(graphicKey);
|
||||
|
||||
@@ -163,6 +317,32 @@ class FunkinSprite extends FlxSprite
|
||||
return loadBitmapData(inputBitmap);
|
||||
}
|
||||
|
||||
/**
|
||||
* Loads an Adobe Animate texture atlas as the sprite's texture.
|
||||
* @param key The key of the texture to load.
|
||||
* @param settings Additional settings for loading the atlas.
|
||||
* @return This sprite, for chaining.
|
||||
*/
|
||||
public function loadTextureAtlas(key:Null<String>, ?assetLibrary:Null<String>, ?settings:AtlasSpriteSettings):FunkinSprite
|
||||
{
|
||||
if (key == null)
|
||||
{
|
||||
throw 'Null path specified for loadTextureAtlas()!';
|
||||
}
|
||||
|
||||
if (settings == null)
|
||||
{
|
||||
settings = getDefaultAtlasSettings();
|
||||
}
|
||||
|
||||
this.applyStageMatrix = settings.applyStageMatrix ?? false;
|
||||
this.useRenderTexture = settings.useRenderTexture ?? false;
|
||||
|
||||
frames = Paths.getAnimateAtlas(key, assetLibrary, settings);
|
||||
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Load an animated texture (Sparrow atlas spritesheet) as the sprite's texture.
|
||||
* @param key The key of the texture to load.
|
||||
@@ -171,7 +351,7 @@ class FunkinSprite extends FlxSprite
|
||||
public function loadSparrow(key:String):FunkinSprite
|
||||
{
|
||||
var graphicKey:String = Paths.image(key);
|
||||
if (!isTextureCached(graphicKey)) FlxG.log.warn('Texture not cached, may experience stuttering! $graphicKey');
|
||||
if (!FunkinMemory.isTextureCached(graphicKey)) FlxG.log.warn('Texture not cached, may experience stuttering! $graphicKey');
|
||||
|
||||
this.frames = Paths.getSparrowAtlas(key);
|
||||
|
||||
@@ -186,73 +366,13 @@ class FunkinSprite extends FlxSprite
|
||||
public function loadPacker(key:String):FunkinSprite
|
||||
{
|
||||
var graphicKey:String = Paths.image(key);
|
||||
if (!isTextureCached(graphicKey)) FlxG.log.warn('Texture not cached, may experience stuttering! $graphicKey');
|
||||
if (!FunkinMemory.isTextureCached(graphicKey)) FlxG.log.warn('Texture not cached, may experience stuttering! $graphicKey');
|
||||
|
||||
this.frames = Paths.getPackerAtlas(key);
|
||||
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Determine whether the texture with the given key is cached.
|
||||
* @param key The key of the texture to check.
|
||||
* @return Whether the texture is cached.
|
||||
*/
|
||||
public static function isTextureCached(key:String):Bool
|
||||
{
|
||||
return FlxG.bitmap.get(key) != null;
|
||||
}
|
||||
|
||||
@:deprecated("Use FunkinMemory.cacheTexture() instead")
|
||||
public static function cacheTexture(key:String):Void
|
||||
{
|
||||
FunkinMemory.cacheTexture(Paths.image(key));
|
||||
}
|
||||
|
||||
@:deprecated("Use FunkinMemory.permanentCacheTexture() instead")
|
||||
public static function permanentCacheTexture(key:String):Void
|
||||
{
|
||||
@:privateAccess FunkinMemory.permanentCacheTexture(Paths.image(key));
|
||||
}
|
||||
|
||||
@:deprecated("Use FunkinMemory.cacheTexture() instead")
|
||||
public static function cacheSparrow(key:String):Void
|
||||
{
|
||||
FunkinMemory.cacheTexture(Paths.image(key));
|
||||
}
|
||||
|
||||
@:deprecated("Use FunkinMemory.cacheTexture() instead")
|
||||
public static function cachePacker(key:String):Void
|
||||
{
|
||||
FunkinMemory.cacheTexture(Paths.image(key));
|
||||
}
|
||||
|
||||
@:deprecated("Use FunkinMemory.preparePurgeTextureCache() instead")
|
||||
public static function preparePurgeCache():Void
|
||||
{
|
||||
FunkinMemory.preparePurgeTextureCache();
|
||||
}
|
||||
|
||||
@:deprecated("Use FunkinMemory.purgeCache() instead")
|
||||
public static function purgeCache():Void
|
||||
{
|
||||
FunkinMemory.purgeCache();
|
||||
}
|
||||
|
||||
static function isGraphicCached(graphic:FlxGraphic):Bool
|
||||
{
|
||||
var result = null;
|
||||
if (graphic == null) return false;
|
||||
result = FlxG.bitmap.get(graphic.key);
|
||||
if (result == null) return false;
|
||||
if (result != graphic)
|
||||
{
|
||||
FlxG.log.warn('Cached graphic does not match original: ${graphic.key}');
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param id The animation ID to check.
|
||||
* @return Whether the animation is dynamic (has multiple frames). `false` for static, one-frame animations.
|
||||
@@ -266,6 +386,107 @@ class FunkinSprite extends FlxSprite
|
||||
return animData.numFrames > 1;
|
||||
}
|
||||
|
||||
/**
|
||||
* Whether or not this sprite has an animation with the given ID.
|
||||
* @param id The ID of the animation to check.
|
||||
*/
|
||||
public function hasAnimation(id:String):Bool
|
||||
{
|
||||
var animationList:Array<String> = this.animation?.getNameList() ?? [];
|
||||
if (animationList.contains(id))
|
||||
{
|
||||
return true;
|
||||
}
|
||||
else if (this.isAnimate && !animationList.contains(id))
|
||||
{
|
||||
return addAnimationIfMissing(id);
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds an animation if it doesn't exist.
|
||||
* @param id The animation ID to check.
|
||||
*/
|
||||
function addAnimationIfMissing(id:String):Bool
|
||||
{
|
||||
@:privateAccess
|
||||
var symbols:Array<String> = this.library.dictionary.keys().array();
|
||||
var frameLabels:Array<String> = listAnimations();
|
||||
|
||||
if (frameLabels.contains(id))
|
||||
{
|
||||
// Animation exists as a frame label but wasn't added, so we add it
|
||||
anim.addByFrameLabel(id, id, this.library.frameRate, false);
|
||||
return true;
|
||||
}
|
||||
else if (symbols.contains(id))
|
||||
{
|
||||
// Animation exists as a symbol but wasn't added, so we add it
|
||||
anim.addBySymbol(id, id, this.library.frameRate, false);
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets every frame on every symbol that starts with the given keyword.
|
||||
* @param keyword The keyword to search for.
|
||||
* @return An array of frames.
|
||||
*/
|
||||
public function getFramesWithKeyword(keyword:String):Array<animate.internal.Frame>
|
||||
{
|
||||
if (!this.isAnimate)
|
||||
{
|
||||
trace('WARNING: getFramesWithKeyword() only works texture atlases!');
|
||||
return [];
|
||||
}
|
||||
|
||||
var symbolItems:Array<animate.internal.SymbolItem> = [];
|
||||
var frames:Array<animate.internal.Frame> = [];
|
||||
|
||||
@:privateAccess
|
||||
for (symbol in this.library.dictionary.keys())
|
||||
{
|
||||
var symbolItem:Null<animate.internal.SymbolItem> = this.library.getSymbol(symbol);
|
||||
if (symbolItem == null) continue;
|
||||
|
||||
if (symbolItem.name.contains(keyword))
|
||||
{
|
||||
symbolItems.push(symbolItem);
|
||||
}
|
||||
}
|
||||
|
||||
for (symbolItem in symbolItems)
|
||||
{
|
||||
symbolItem.timeline.forEachLayer((layer) -> {
|
||||
layer.forEachFrame((frame) -> {
|
||||
frames.push(frame);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
return frames;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the current animation ID.
|
||||
*/
|
||||
public function getCurrentAnimation():String
|
||||
{
|
||||
return this.animation?.curAnim?.name ?? '';
|
||||
}
|
||||
|
||||
/**
|
||||
* Whether or not the current animation is finished.
|
||||
*/
|
||||
public function isAnimationFinished():Bool
|
||||
{
|
||||
return this.animation?.finished ?? false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Acts similarly to `makeGraphic`, but with improved memory usage,
|
||||
* at the expense of not being able to paint onto the resulting sprite.
|
||||
@@ -286,6 +507,221 @@ class FunkinSprite extends FlxSprite
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return A list of all the animations this sprite has available.
|
||||
*/
|
||||
public function listAnimations():Array<String>
|
||||
{
|
||||
var frameLabels:Array<String> = getFrameLabelList();
|
||||
var animationList:Array<String> = this.animation?.getNameList() ?? [];
|
||||
|
||||
return frameLabels.concat(animationList);
|
||||
}
|
||||
|
||||
/**
|
||||
* TEXTURE ATLAS-EXCLUSIVE FUNCTIONS
|
||||
* These functions only work if the sprite's texture is an Adobe Animate texture atlas.
|
||||
* Calling these functions on non-texture atlases will do nothing.
|
||||
*/
|
||||
/**
|
||||
* Gets a list of frame labels from the default timeline.
|
||||
*/
|
||||
public function getFrameLabelList():Array<String>
|
||||
{
|
||||
if (!this.isAnimate)
|
||||
{
|
||||
trace('WARNING: getFrameLabelList() only works texture atlases!');
|
||||
return [];
|
||||
}
|
||||
|
||||
var foundLabels:Array<String> = [];
|
||||
var mainTimeline:Null<animate.internal.Timeline> = this.library.timeline;
|
||||
|
||||
for (layer in mainTimeline.layers)
|
||||
{
|
||||
@:nullSafety(Off)
|
||||
for (frame in layer.frames)
|
||||
{
|
||||
if (frame.name.rtrim() != '')
|
||||
{
|
||||
foundLabels.push(frame.name);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return foundLabels;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets a frame label by its name.
|
||||
* @param name The name of the frame label to retrieve.
|
||||
* @return The frame label, or null if it doesn't exist.
|
||||
*/
|
||||
public function getFrameLabel(name:String):Null<animate.internal.Frame>
|
||||
{
|
||||
if (!this.isAnimate)
|
||||
{
|
||||
trace('WARNING: getFrameLabel() only works texture atlases!');
|
||||
return null;
|
||||
}
|
||||
|
||||
for (layer in this.timeline.layers)
|
||||
{
|
||||
@:nullSafety(Off)
|
||||
for (frame in layer.frames)
|
||||
{
|
||||
if (frame.name == name)
|
||||
{
|
||||
return frame;
|
||||
}
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the default symbol in the atlas.
|
||||
*/
|
||||
public function getDefaultSymbol():String
|
||||
{
|
||||
if (!this.isAnimate)
|
||||
{
|
||||
trace('WARNING: getDefaultSymbol() only works texture atlases!');
|
||||
return '';
|
||||
}
|
||||
|
||||
return library.timeline.name;
|
||||
}
|
||||
|
||||
/**
|
||||
* Replaces the graphic of a symbol in the atlas.
|
||||
* @param symbol The symbol to replace.
|
||||
* @param graphic The new graphic to use.
|
||||
* @param adjustScale Whether to adjust the scale of new frame to match the old one.
|
||||
*/
|
||||
public function replaceSymbolGraphic(symbol:String, ?graphic:Null<FlxGraphicAsset>, ?adjustScale:Bool = true):Void
|
||||
{
|
||||
if (!this.isAnimate)
|
||||
{
|
||||
trace('WARNING: replaceSymbolGraphic() only works texture atlases!');
|
||||
return;
|
||||
}
|
||||
|
||||
var elements:Array<Element> = getSymbolElements(symbol);
|
||||
|
||||
for (element in elements)
|
||||
{
|
||||
var atlasInstance:AtlasInstance = element.toAtlasInstance();
|
||||
var frame:Null<FlxFrame> = graphic != null ? FlxG.bitmap.add(graphic).imageFrame.frame : null;
|
||||
|
||||
atlasInstance.replaceFrame(frame, adjustScale);
|
||||
element = atlasInstance;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the first element of a symbol in the atlas.
|
||||
* @param symbol The symbol to get elements from.
|
||||
* @return The first element of the symbol. WARNING: Can be null.
|
||||
*/
|
||||
public function getFirstElement(symbol:String):Null<Element>
|
||||
{
|
||||
if (!this.isAnimate)
|
||||
{
|
||||
trace('WARNING: getFirstElement() only works texture atlases!');
|
||||
return null;
|
||||
}
|
||||
|
||||
var symbolElements:Array<Element> = getSymbolElements(symbol);
|
||||
return symbolElements.length > 0 ? symbolElements[0] : null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the elements of a symbol in the atlas.
|
||||
* @param symbol The symbol to get elements from.
|
||||
*/
|
||||
public function getSymbolElements(symbol:String):Array<Element>
|
||||
{
|
||||
if (!this.isAnimate)
|
||||
{
|
||||
trace('WARNING: getSymbolElements() only works texture atlases!');
|
||||
return [];
|
||||
}
|
||||
|
||||
var symbolInstance:Null<SymbolItem> = this.library.getSymbol(symbol);
|
||||
|
||||
if (symbolInstance == null)
|
||||
{
|
||||
throw 'Symbol not found in atlas: ${symbol}';
|
||||
return [];
|
||||
}
|
||||
|
||||
var elements:Array<Element> = symbolInstance.timeline.getElementsAtIndex(0);
|
||||
|
||||
if (elements?.length == 0)
|
||||
{
|
||||
trace('WARNING: No Atlas Elements found for "$symbol" symbol.');
|
||||
}
|
||||
|
||||
return elements ?? [];
|
||||
}
|
||||
|
||||
/**
|
||||
* Scales an element by a certain multiplier.
|
||||
* @param element The element to scale.
|
||||
* @param scale The scale multiplier.
|
||||
* @param positionOffset The offset to apply to `tx` and `ty` after scaling.
|
||||
* (Or in other words, the position of the element.)
|
||||
*/
|
||||
public function scaleElement(element:Element, scale:Float, positionOffset:Float = 0, scaleEverything:Bool = false):Void
|
||||
{
|
||||
if (!this.isAnimate)
|
||||
{
|
||||
trace('WARNING: scaleElement() only works texture atlases!');
|
||||
return;
|
||||
}
|
||||
|
||||
var elementMatrix:FlxMatrix = element.matrix;
|
||||
|
||||
if (scaleEverything)
|
||||
{
|
||||
elementMatrix.scale(scale, scale);
|
||||
return;
|
||||
}
|
||||
|
||||
var symbolInstance:SymbolInstance = element.parentFrame.convertToSymbol(0, 1);
|
||||
var transformPoint:FlxPoint = symbolInstance.transformationPoint;
|
||||
|
||||
elementMatrix.a += scale;
|
||||
elementMatrix.d += scale;
|
||||
|
||||
elementMatrix.tx -= transformPoint.x * scale;
|
||||
elementMatrix.ty -= transformPoint.y * scale;
|
||||
|
||||
elementMatrix.tx -= positionOffset;
|
||||
elementMatrix.ty -= positionOffset;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the default settings for a texture atlas sprite.
|
||||
* @return The default settings for a texture atlas sprite.
|
||||
*/
|
||||
public function getDefaultAtlasSettings():AtlasSpriteSettings
|
||||
{
|
||||
return {
|
||||
swfMode: false,
|
||||
cacheOnLoad: false,
|
||||
filterQuality: MEDIUM,
|
||||
spritemaps: null,
|
||||
metadataJson: null,
|
||||
cacheKey: null,
|
||||
uniqueInCache: false,
|
||||
onSymbolCreate: null,
|
||||
applyStageMatrix: false,
|
||||
useRenderTexture: false
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Ensure scale is applied when cloning a sprite.R
|
||||
* The default `clone()` method acts kinda weird TBH.
|
||||
@@ -325,77 +761,10 @@ class FunkinSprite extends FlxSprite
|
||||
return _rect;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the screen position of this object.
|
||||
*
|
||||
* @param result Optional arg for the returning point
|
||||
* @param camera The desired "screen" coordinate space. If `null`, `FlxG.camera` is used.
|
||||
* @return The screen position of this object.
|
||||
*/
|
||||
public override function getScreenPosition(?result:FlxPoint, ?camera:FlxCamera):FlxPoint
|
||||
override function preparePixelPerfectMatrix(matrix:FlxMatrix)
|
||||
{
|
||||
if (result == null) result = FlxPoint.get();
|
||||
|
||||
if (camera == null) camera = FlxG.camera;
|
||||
|
||||
result.set(x, y);
|
||||
if (pixelPerfectPosition)
|
||||
{
|
||||
_rect.width = _rect.width / this.scale.x;
|
||||
_rect.height = _rect.height / this.scale.y;
|
||||
_rect.x = _rect.x / this.scale.x;
|
||||
_rect.y = _rect.y / this.scale.y;
|
||||
_rect.round();
|
||||
_rect.x = _rect.x * this.scale.x;
|
||||
_rect.y = _rect.y * this.scale.y;
|
||||
_rect.width = _rect.width * this.scale.x;
|
||||
_rect.height = _rect.height * this.scale.y;
|
||||
}
|
||||
|
||||
return result.subtract(camera.scroll.x * scrollFactor.x, camera.scroll.y * scrollFactor.y);
|
||||
}
|
||||
|
||||
override function drawSimple(camera:FlxCamera):Void
|
||||
{
|
||||
getScreenPosition(_point, camera).subtractPoint(offset);
|
||||
if (isPixelPerfectRender(camera))
|
||||
{
|
||||
_point.x = _point.x / this.scale.x;
|
||||
_point.y = _point.y / this.scale.y;
|
||||
_point.round();
|
||||
|
||||
_point.x = _point.x * this.scale.x;
|
||||
_point.y = _point.y * this.scale.y;
|
||||
}
|
||||
|
||||
_point.copyToFlash(_flashPoint);
|
||||
camera.copyPixels(_frame, framePixels, _flashRect, _flashPoint, colorTransform, blend, antialiasing);
|
||||
}
|
||||
|
||||
override function drawComplex(camera:FlxCamera):Void
|
||||
{
|
||||
_frame.prepareMatrix(_matrix, FlxFrameAngle.ANGLE_0, checkFlipX(), checkFlipY());
|
||||
_matrix.translate(-origin.x, -origin.y);
|
||||
_matrix.scale(scale.x, scale.y);
|
||||
|
||||
if (bakedRotationAngle <= 0)
|
||||
{
|
||||
updateTrig();
|
||||
|
||||
if (angle != 0) _matrix.rotateWithTrig(_cosAngle, _sinAngle);
|
||||
}
|
||||
|
||||
getScreenPosition(_point, camera).subtractPoint(offset);
|
||||
_point.add(origin.x, origin.y);
|
||||
_matrix.translate(_point.x, _point.y);
|
||||
|
||||
if (isPixelPerfectRender(camera))
|
||||
{
|
||||
_matrix.tx = Math.round(_matrix.tx / this.scale.x) * this.scale.x;
|
||||
_matrix.ty = Math.round(_matrix.ty / this.scale.y) * this.scale.y;
|
||||
}
|
||||
|
||||
camera.drawPixels(_frame, framePixels, _matrix, colorTransform, blend, antialiasing, shader);
|
||||
matrix.tx = Math.round(matrix.tx / this.scale.x) * this.scale.x;
|
||||
matrix.ty = Math.round(matrix.ty / this.scale.y) * this.scale.y;
|
||||
}
|
||||
|
||||
public override function destroy():Void
|
||||
|
||||
@@ -1,419 +0,0 @@
|
||||
package funkin.graphics.adobeanimate;
|
||||
|
||||
import flixel.util.FlxSignal.FlxTypedSignal;
|
||||
import flxanimate.FlxAnimate;
|
||||
import flxanimate.FlxAnimate.Settings;
|
||||
import flixel.graphics.frames.FlxFrame;
|
||||
import flixel.system.FlxAssets.FlxGraphicAsset;
|
||||
import flixel.math.FlxPoint;
|
||||
import flxanimate.animate.FlxKeyFrame;
|
||||
|
||||
/**
|
||||
* A sprite which provides convenience functions for rendering a texture atlas with animations.
|
||||
*/
|
||||
@:nullSafety
|
||||
class FlxAtlasSprite extends FlxAnimate
|
||||
{
|
||||
static final SETTINGS:Settings =
|
||||
{
|
||||
// ?ButtonSettings:Map<String, flxanimate.animate.FlxAnim.ButtonSettings>,
|
||||
FrameRate: 24.0,
|
||||
Reversed: false,
|
||||
// ?OnComplete:Void -> Void,
|
||||
ShowPivot: false,
|
||||
Antialiasing: true,
|
||||
ScrollFactor: null,
|
||||
// Offset: new FlxPoint(0, 0), // This is just FlxSprite.offset
|
||||
};
|
||||
|
||||
/**
|
||||
* Signal dispatched when an animation advances to the next frame.
|
||||
*/
|
||||
public var onAnimationFrame:FlxTypedSignal<String->Int->Void> = new FlxTypedSignal();
|
||||
|
||||
/**
|
||||
* Signal dispatched when a non-looping animation finishes playing.
|
||||
*/
|
||||
public var onAnimationComplete:FlxTypedSignal<String->Void> = new FlxTypedSignal();
|
||||
|
||||
/**
|
||||
* Signal dispatched when a looping animation finishes playing.
|
||||
*/
|
||||
public var onAnimationLoop:FlxTypedSignal<String->Void> = new FlxTypedSignal();
|
||||
|
||||
var currentAnimation:String = '';
|
||||
|
||||
var canPlayOtherAnims:Bool = true;
|
||||
|
||||
@:nullSafety(Off) // null safety HATES new classes atm, it'll be fixed in haxe 4.0.0?
|
||||
public function new(x:Float, y:Float, ?path:String, ?settings:Settings)
|
||||
{
|
||||
if (settings == null) settings = SETTINGS;
|
||||
|
||||
if (path == null)
|
||||
{
|
||||
throw 'Null path specified for FlxAtlasSprite!';
|
||||
}
|
||||
|
||||
// Validate asset path.
|
||||
if (!Assets.exists('${path}/Animation.json'))
|
||||
{
|
||||
throw 'FlxAtlasSprite does not have an Animation.json file at the specified path (${path})';
|
||||
}
|
||||
|
||||
super(x, y, path, settings);
|
||||
|
||||
if (this.anim.stageInstance == null)
|
||||
{
|
||||
throw 'FlxAtlasSprite not initialized properly. Are you sure the path (${path}) exists?';
|
||||
}
|
||||
|
||||
onAnimationComplete.add(cleanupAnimation);
|
||||
|
||||
// This defaults the sprite to play the first animation in the atlas,
|
||||
// then pauses it. This ensures symbols are initialized properly.
|
||||
this.anim.play('');
|
||||
this.anim.pause();
|
||||
|
||||
this.anim.onComplete.add(_onAnimationComplete);
|
||||
this.anim.onFrame.add(_onAnimationFrame);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return A list of all the animations this sprite has available.
|
||||
*/
|
||||
public function listAnimations():Array<String>
|
||||
{
|
||||
var mainSymbol = this.anim.symbolDictionary[this.anim.stageInstance.symbol.name];
|
||||
if (mainSymbol == null)
|
||||
{
|
||||
FlxG.log.error('FlxAtlasSprite does not have its main symbol!');
|
||||
return [];
|
||||
}
|
||||
return mainSymbol.getFrameLabels().map(keyFrame -> keyFrame.name).filterNull();
|
||||
}
|
||||
|
||||
/**
|
||||
* @param id A string ID of the animation.
|
||||
* @return Whether the animation was found on this sprite.
|
||||
*/
|
||||
public function hasAnimation(id:String):Bool
|
||||
{
|
||||
return getLabelIndex(id) != -1 || anim.symbolDictionary.exists(id);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return The current animation being played.
|
||||
*/
|
||||
public function getCurrentAnimation():String
|
||||
{
|
||||
return this.currentAnimation;
|
||||
}
|
||||
|
||||
var _completeAnim:Bool = false;
|
||||
|
||||
var fr:Null<FlxKeyFrame> = null;
|
||||
|
||||
var looping:Bool = false;
|
||||
|
||||
public var ignoreExclusionPref:Array<String> = [];
|
||||
|
||||
/**
|
||||
* Plays an animation.
|
||||
* @param id A string ID of the animation to play.
|
||||
* @param restart Whether to restart the animation if it is already playing.
|
||||
* @param ignoreOther Whether to ignore all other animation inputs, until this one is done playing
|
||||
* @param loop Whether to loop the animation
|
||||
* @param startFrame The frame to start the animation on
|
||||
* NOTE: `loop` and `ignoreOther` are not compatible with each other!
|
||||
*/
|
||||
public function playAnimation(id:String, restart:Bool = false, ignoreOther:Bool = false, loop:Bool = false, startFrame:Int = 0):Void
|
||||
{
|
||||
// Skip if not allowed to play animations.
|
||||
if ((!canPlayOtherAnims))
|
||||
{
|
||||
if (this.currentAnimation == id && restart) {}
|
||||
else if (ignoreExclusionPref != null && ignoreExclusionPref.length > 0)
|
||||
{
|
||||
var detected:Bool = false;
|
||||
for (entry in ignoreExclusionPref)
|
||||
{
|
||||
if (StringTools.startsWith(id, entry))
|
||||
{
|
||||
detected = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (!detected) return;
|
||||
}
|
||||
else
|
||||
return;
|
||||
}
|
||||
|
||||
if (anim == null) return;
|
||||
|
||||
if (id == null || id == '') id = this.currentAnimation;
|
||||
|
||||
if (this.currentAnimation == id && !restart)
|
||||
{
|
||||
if (!anim.isPlaying)
|
||||
{
|
||||
if (fr != null) anim.curFrame = fr.index + startFrame;
|
||||
else
|
||||
anim.curFrame = startFrame;
|
||||
|
||||
// Resume animation if it's paused.
|
||||
anim.resume();
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
else if (!hasAnimation(id))
|
||||
{
|
||||
// Skip if the animation doesn't exist
|
||||
trace('Animation ' + id + ' not found');
|
||||
return;
|
||||
}
|
||||
|
||||
this.currentAnimation = id;
|
||||
anim.onComplete.removeAll();
|
||||
anim.onComplete.add(function() {
|
||||
_onAnimationComplete();
|
||||
});
|
||||
|
||||
looping = loop;
|
||||
|
||||
// Prevent other animations from playing if `ignoreOther` is true.
|
||||
if (ignoreOther) canPlayOtherAnims = false;
|
||||
|
||||
// Move to the first frame of the animation.
|
||||
// goToFrameLabel(id);
|
||||
// trace('Playing animation $id');
|
||||
if ((id == null || id == "") || this.anim.symbolDictionary.exists(id) || (this.anim.getByName(id) != null))
|
||||
{
|
||||
this.anim.play(id, restart, false, startFrame);
|
||||
|
||||
this.currentAnimation = anim.curSymbol.name;
|
||||
|
||||
fr = null;
|
||||
}
|
||||
var frameLabelNames = getFrameLabelNames();
|
||||
// Only call goToFrameLabel if there is a frame label with that name. This prevents annoying warnings!
|
||||
if (frameLabelNames != null && frameLabelNames.indexOf(id) != -1)
|
||||
{
|
||||
goToFrameLabel(id);
|
||||
fr = anim.getFrameLabel(id);
|
||||
anim.curFrame += startFrame;
|
||||
// Resume animation if it's paused.
|
||||
anim.resume();
|
||||
}
|
||||
}
|
||||
|
||||
override public function update(elapsed:Float):Void
|
||||
{
|
||||
super.update(elapsed);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns true if the animation has finished playing.
|
||||
* @return Whether the animation has finished playing.
|
||||
*/
|
||||
public function isAnimationFinished():Bool
|
||||
{
|
||||
return isLoopComplete();
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns true if the animation has reached the last frame.
|
||||
* Can be true even if animation is configured to loop.
|
||||
* @return Whether the animation has reached the last frame.
|
||||
*/
|
||||
public function isLoopComplete():Bool
|
||||
{
|
||||
if (this.anim == null) return false;
|
||||
if (!this.anim.isPlaying) return false;
|
||||
|
||||
if (fr != null)
|
||||
{
|
||||
var curFrame = anim.curFrame;
|
||||
|
||||
var startFrame = fr.index;
|
||||
var endFrame = (fr.index + fr.duration);
|
||||
|
||||
return (anim.reversed) ? (curFrame < startFrame) : (curFrame >= endFrame);
|
||||
}
|
||||
|
||||
return (anim.reversed && anim.curFrame == 0 || !(anim.reversed) && (anim.curFrame) >= (anim.length - 1));
|
||||
}
|
||||
|
||||
/**
|
||||
* Stops the current animation.
|
||||
*/
|
||||
public function stopAnimation():Void
|
||||
{
|
||||
if (this.currentAnimation == null) return;
|
||||
|
||||
this.anim.removeAllCallbacksFrom(getNextFrameLabel(this.currentAnimation));
|
||||
|
||||
goToFrameIndex(0);
|
||||
}
|
||||
|
||||
function addFrameCallback(label:String, callback:Void->Void):Void
|
||||
{
|
||||
var frameLabel = this.anim.getFrameLabel(label);
|
||||
frameLabel.add(callback);
|
||||
}
|
||||
|
||||
function goToFrameLabel(label:String):Void
|
||||
{
|
||||
this.anim.goToFrameLabel(label);
|
||||
}
|
||||
|
||||
function getFrameLabelNames(?layer:haxe.extern.EitherType<Int, String>):Null<Array<String>>
|
||||
{
|
||||
var labels = this.anim.getFrameLabels(layer);
|
||||
var array = [];
|
||||
for (label in labels)
|
||||
{
|
||||
array.push(label.name);
|
||||
}
|
||||
|
||||
return array;
|
||||
}
|
||||
|
||||
function getNextFrameLabel(label:String):String
|
||||
{
|
||||
return listAnimations()[(getLabelIndex(label) + 1) % listAnimations().length];
|
||||
}
|
||||
|
||||
function getLabelIndex(label:String):Int
|
||||
{
|
||||
return listAnimations().indexOf(label);
|
||||
}
|
||||
|
||||
function goToFrameIndex(index:Int):Void
|
||||
{
|
||||
this.anim.curFrame = index;
|
||||
}
|
||||
|
||||
public function cleanupAnimation(_:String):Void
|
||||
{
|
||||
canPlayOtherAnims = true;
|
||||
// this.currentAnimation = null;
|
||||
this.anim.pause();
|
||||
}
|
||||
|
||||
function _onAnimationFrame(frame:Int):Void
|
||||
{
|
||||
if (currentAnimation != null)
|
||||
{
|
||||
onAnimationFrame.dispatch(currentAnimation, frame);
|
||||
|
||||
if (isLoopComplete())
|
||||
{
|
||||
anim.pause();
|
||||
|
||||
if (looping)
|
||||
{
|
||||
anim.curFrame = (fr != null) ? fr.index : 0;
|
||||
anim.resume();
|
||||
_onAnimationLoop();
|
||||
}
|
||||
else if (fr != null && anim.curFrame != anim.length - 1)
|
||||
{
|
||||
anim.curFrame--;
|
||||
_onAnimationComplete();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function _onAnimationComplete():Void
|
||||
{
|
||||
if (currentAnimation != null)
|
||||
{
|
||||
onAnimationComplete.dispatch(currentAnimation);
|
||||
}
|
||||
else
|
||||
{
|
||||
onAnimationComplete.dispatch('');
|
||||
}
|
||||
}
|
||||
|
||||
function _onAnimationLoop():Void
|
||||
{
|
||||
if (currentAnimation != null)
|
||||
{
|
||||
onAnimationLoop.dispatch(currentAnimation);
|
||||
}
|
||||
else
|
||||
{
|
||||
onAnimationLoop.dispatch('');
|
||||
}
|
||||
}
|
||||
|
||||
var prevFrames:Map<Int, FlxFrame> = [];
|
||||
|
||||
public function replaceFrameGraphic(index:Int, ?graphic:FlxGraphicAsset):Void
|
||||
{
|
||||
var cond = false;
|
||||
|
||||
if (graphic == null) cond = true;
|
||||
else
|
||||
{
|
||||
if ((graphic is String)) cond = !Assets.exists(graphic)
|
||||
else
|
||||
cond = false;
|
||||
}
|
||||
if (cond)
|
||||
{
|
||||
var prevFrame:Null<FlxFrame> = prevFrames.get(index);
|
||||
if (prevFrame == null) return;
|
||||
|
||||
prevFrame.copyTo(frames.getByIndex(index));
|
||||
return;
|
||||
}
|
||||
|
||||
var prevFrame:FlxFrame = prevFrames.get(index) ?? frames.getByIndex(index).copyTo();
|
||||
prevFrames.set(index, prevFrame);
|
||||
|
||||
@:nullSafety(Off) // TODO: Remove this once flixel.system.frontEnds.BitmapFrontEnd has been null safed
|
||||
var frame = FlxG.bitmap.add(graphic).imageFrame.frame;
|
||||
frame.copyTo(frames.getByIndex(index));
|
||||
|
||||
// Additional sizing fix.
|
||||
@:privateAccess
|
||||
if (true)
|
||||
{
|
||||
var frame = frames.getByIndex(index);
|
||||
frame.tileMatrix[0] = prevFrame.frame.width / frame.frame.width;
|
||||
frame.tileMatrix[3] = prevFrame.frame.height / frame.frame.height;
|
||||
}
|
||||
}
|
||||
|
||||
public function getBasePosition():Null<FlxPoint>
|
||||
{
|
||||
// var stagePos = new FlxPoint(anim.stageInstance.matrix.tx, anim.stageInstance.matrix.ty);
|
||||
var instancePos = new FlxPoint(anim.curInstance.matrix.tx, anim.curInstance.matrix.ty);
|
||||
var firstElement = anim.curSymbol.timeline?.get(0)?.get(0)?.get(0);
|
||||
if (firstElement == null) return instancePos;
|
||||
var firstElementPos = new FlxPoint(firstElement.matrix.tx, firstElement.matrix.ty);
|
||||
|
||||
return instancePos + firstElementPos;
|
||||
}
|
||||
|
||||
public function getPivotPosition():Null<FlxPoint>
|
||||
{
|
||||
return anim.curInstance.symbol.transformationPoint;
|
||||
}
|
||||
|
||||
public override function destroy():Void
|
||||
{
|
||||
for (prevFrameId in prevFrames.keys())
|
||||
{
|
||||
replaceFrameGraphic(prevFrameId, null);
|
||||
}
|
||||
|
||||
super.destroy();
|
||||
}
|
||||
}
|
||||
@@ -68,7 +68,7 @@ class FrameBuffer
|
||||
{
|
||||
camera.clearDrawStack();
|
||||
camera.canvas.graphics.clear();
|
||||
camera.fill(camera.bgColor.to24Bit(), camera.useBgAlphaBlending, camera.bgColor.alphaFloat);
|
||||
camera.fill(camera.bgColor.rgb, camera.useBgAlphaBlending, camera.bgColor.alphaFloat);
|
||||
#if FLX_DEBUG
|
||||
camera.debugLayer.graphics.clear();
|
||||
#end
|
||||
|
||||
@@ -70,6 +70,11 @@ class MeshRender extends FlxStrip
|
||||
add_tri(a, c, d);
|
||||
}
|
||||
|
||||
public override function draw():Void
|
||||
{
|
||||
super.draw();
|
||||
}
|
||||
|
||||
/**
|
||||
* Build a quad from four points.
|
||||
*
|
||||
|
||||
@@ -1,230 +0,0 @@
|
||||
package funkin.graphics.shaders;
|
||||
|
||||
import flixel.system.FlxAssets.FlxShader;
|
||||
import flixel.util.FlxColor;
|
||||
import flixel.FlxSprite;
|
||||
import flixel.math.FlxAngle;
|
||||
import flixel.graphics.frames.FlxFrame;
|
||||
import openfl.display.BitmapData;
|
||||
|
||||
/*
|
||||
A screenspace version of the DropShadowShader.. currently the only way to use this effect with
|
||||
FlxAnimate :(
|
||||
*/
|
||||
class DropShadowScreenspace extends DropShadowShader
|
||||
{
|
||||
/*
|
||||
The current zoom of the camera. Needed to figure out how much to multiply the drop shadow size.
|
||||
*/
|
||||
public var curZoom(default, set):Float;
|
||||
|
||||
function set_curZoom(val:Float):Float
|
||||
{
|
||||
curZoom = val;
|
||||
zoom.value = [val];
|
||||
return val;
|
||||
}
|
||||
|
||||
@:glFragmentSource('
|
||||
#pragma header
|
||||
|
||||
// This shader aims to mostly recreate how Adobe Animate/Flash handles drop shadows, but its main use here is for rim lighting.
|
||||
|
||||
// this shader also includes a recreation of the Animate/Flash "Adjust Color" filter,
|
||||
// which was kindly provided and written by Rozebud https://github.com/ThatRozebudDude ( thank u rozebud :) )
|
||||
// Adapted from Andrey-Postelzhuks shader found here: https://forum.unity.com/threads/hue-saturation-brightness-contrast-shader.260649/
|
||||
// Hue rotation stuff is from here: https://www.w3.org/TR/filter-effects/#feColorMatrixElement
|
||||
|
||||
// equals (frame.left, frame.top, frame.right, frame.bottom)
|
||||
uniform vec4 uFrameBounds;
|
||||
|
||||
uniform float ang;
|
||||
uniform float dist;
|
||||
uniform float str;
|
||||
uniform float thr;
|
||||
|
||||
// need to account for rotated frames... oops
|
||||
uniform float angOffset;
|
||||
|
||||
uniform sampler2D altMask;
|
||||
uniform bool useMask;
|
||||
uniform float thr2;
|
||||
|
||||
uniform vec3 dropColor;
|
||||
|
||||
uniform float hue;
|
||||
uniform float saturation;
|
||||
uniform float brightness;
|
||||
uniform float contrast;
|
||||
|
||||
uniform float zoom;
|
||||
|
||||
uniform float AA_STAGES;
|
||||
|
||||
const vec3 grayscaleValues = vec3(0.3098039215686275, 0.607843137254902, 0.0823529411764706);
|
||||
const float e = 2.718281828459045;
|
||||
|
||||
vec3 applyHueRotate(vec3 aColor, float aHue){
|
||||
float angle = radians(aHue);
|
||||
|
||||
mat3 m1 = mat3(0.213, 0.213, 0.213, 0.715, 0.715, 0.715, 0.072, 0.072, 0.072);
|
||||
mat3 m2 = mat3(0.787, -0.213, -0.213, -0.715, 0.285, -0.715, -0.072, -0.072, 0.928);
|
||||
mat3 m3 = mat3(-0.213, 0.143, -0.787, -0.715, 0.140, 0.715, 0.928, -0.283, 0.072);
|
||||
mat3 m = m1 + cos(angle) * m2 + sin(angle) * m3;
|
||||
|
||||
return m * aColor;
|
||||
}
|
||||
|
||||
vec3 applySaturation(vec3 aColor, float value){
|
||||
if(value > 0.0){ value = value * 3.0; }
|
||||
value = (1.0 + (value / 100.0));
|
||||
vec3 grayscale = vec3(dot(aColor, grayscaleValues));
|
||||
return clamp(mix(grayscale, aColor, value), 0.0, 1.0);
|
||||
}
|
||||
|
||||
vec3 applyContrast(vec3 aColor, float value){
|
||||
value = (1.0 + (value / 100.0));
|
||||
if(value > 1.0){
|
||||
value = (((0.00852259 * pow(e, 4.76454 * (value - 1.0))) * 1.01) - 0.0086078159) * 10.0; //Just roll with it...
|
||||
value += 1.0;
|
||||
}
|
||||
return clamp((aColor - 0.25) * value + 0.25, 0.0, 1.0);
|
||||
}
|
||||
|
||||
vec3 applyHSBCEffect(vec3 color){
|
||||
|
||||
//Brightness
|
||||
color = color + ((brightness) / 255.0);
|
||||
|
||||
//Hue
|
||||
color = applyHueRotate(color, hue);
|
||||
|
||||
//Contrast
|
||||
color = applyContrast(color, contrast);
|
||||
|
||||
//Saturation
|
||||
color = applySaturation(color, saturation);
|
||||
|
||||
return color;
|
||||
}
|
||||
|
||||
vec2 hash22(vec2 p) {
|
||||
vec3 p3 = fract(vec3(p.xyx) * vec3(.1031, .1030, .0973));
|
||||
p3 += dot(p3, p3.yzx + 33.33);
|
||||
return fract((p3.xx + p3.yz) * p3.zy);
|
||||
}
|
||||
|
||||
float intensityPass(vec2 fragCoord, float curThreshold, bool useMask) {
|
||||
vec4 col = texture2D(bitmap, fragCoord);
|
||||
|
||||
float maskIntensity = 0.0;
|
||||
if(useMask == true){
|
||||
maskIntensity = mix(0.0, 1.0, texture2D(altMask, fragCoord).b);
|
||||
}
|
||||
|
||||
if(col.a == 0.0){
|
||||
return 0.0;
|
||||
}
|
||||
|
||||
float intensity = dot(col.rgb, vec3(0.3098, 0.6078, 0.0823));
|
||||
|
||||
intensity = maskIntensity > 0.0 ? float(intensity > thr2) : float(intensity > thr);
|
||||
|
||||
return intensity;
|
||||
}
|
||||
|
||||
// essentially just stole this from the AngleMask shader but repurposed it to smooth
|
||||
// the threshold because without any sort of smoothing it produces horrible edges
|
||||
float antialias(vec2 fragCoord, float curThreshold, bool useMask) {
|
||||
if (AA_STAGES == 0.0) {
|
||||
return intensityPass(fragCoord, curThreshold, useMask);
|
||||
}
|
||||
|
||||
// In GLSL 100, we need to use constant loop bounds
|
||||
// Well assume a reasonable maximum for AA_STAGES and use a fixed loop
|
||||
// The actual number of iterations will be controlled by a condition inside
|
||||
const int MAX_AA = 8; // This should be large enough for most uses
|
||||
|
||||
float AA_TOTAL_PASSES = AA_STAGES * AA_STAGES + 1.0;
|
||||
const float AA_JITTER = 0.5;
|
||||
|
||||
// Run the shader multiple times with a random subpixel offset each time and average the results
|
||||
float color = intensityPass(fragCoord, curThreshold, useMask);
|
||||
for (int i = 0; i < MAX_AA * MAX_AA; i++) {
|
||||
// Calculate x and y from i
|
||||
int x = i / MAX_AA;
|
||||
int y = i - (MAX_AA * int(i/MAX_AA)); // poor mans modulus
|
||||
|
||||
// Skip iterations beyond our desired AA_STAGES
|
||||
if (float(x) >= AA_STAGES || float(y) >= AA_STAGES) {
|
||||
continue;
|
||||
}
|
||||
|
||||
vec2 offset = AA_JITTER * (2.0 * hash22(vec2(float(x), float(y))) - 1.0) / openfl_TextureSize.xy;
|
||||
color += intensityPass(fragCoord + offset, curThreshold, useMask);
|
||||
}
|
||||
|
||||
return color / AA_TOTAL_PASSES;
|
||||
}
|
||||
|
||||
vec3 createDropShadow(vec3 col, float curThreshold, bool useMask) {
|
||||
|
||||
// essentially a mask so that areas under the threshold dont show the rimlight (mainly the outlines)
|
||||
float intensity = antialias(openfl_TextureCoordv, curThreshold, useMask);
|
||||
|
||||
// the distance the dropshadow moves needs to be correctly scaled based on the texture size
|
||||
vec2 imageRatio = vec2(1.0/openfl_TextureSize.x, 1.0/openfl_TextureSize.y);
|
||||
|
||||
// check the pixel in the direction and distance specified
|
||||
vec2 checkedPixel = vec2(openfl_TextureCoordv.x + ((dist*zoom) * cos(ang + angOffset) * imageRatio.x), openfl_TextureCoordv.y - ((dist*zoom) * sin(ang + angOffset) * imageRatio.y));
|
||||
|
||||
// multiplier for the intensity of the drop shadow
|
||||
float dropShadowAmount = 0.0;
|
||||
|
||||
dropShadowAmount = texture2D(bitmap, checkedPixel).a;
|
||||
|
||||
// add the dropshadow color based on the amount, strength, and intensity
|
||||
col.rgb += dropColor.rgb * ((1.0 - (dropShadowAmount * str))*intensity);
|
||||
|
||||
return col;
|
||||
}
|
||||
|
||||
void main()
|
||||
{
|
||||
vec4 col = texture2D(bitmap, openfl_TextureCoordv);
|
||||
|
||||
vec3 unpremultipliedColor = col.a > 0.0 ? col.rgb / col.a : col.rgb;
|
||||
|
||||
vec3 outColor = applyHSBCEffect(unpremultipliedColor);
|
||||
|
||||
outColor = createDropShadow(outColor, thr, useMask);
|
||||
|
||||
gl_FragColor = vec4(outColor.rgb * col.a, col.a);
|
||||
}
|
||||
|
||||
')
|
||||
override public function new()
|
||||
{
|
||||
super();
|
||||
|
||||
angle = 90;
|
||||
strength = 1;
|
||||
distance = 15;
|
||||
threshold = 0.1;
|
||||
|
||||
baseHue = 0;
|
||||
baseSaturation = 0;
|
||||
baseBrightness = 0;
|
||||
baseContrast = 0;
|
||||
|
||||
useAltMask = false;
|
||||
|
||||
color = 0xFFDFEF3C;
|
||||
|
||||
antialiasAmt = 2;
|
||||
|
||||
curZoom = 1;
|
||||
|
||||
angOffset.value = [0];
|
||||
}
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user