diff --git a/.gitignore b/.gitignore index 0cbeac3..2076ec5 100644 --- a/.gitignore +++ b/.gitignore @@ -11,3 +11,4 @@ Library/ Temp/ UserSettings/ Assets/StreamingAssets/AssetBundles/ +Builds/ diff --git a/Assets/Content/ClientData/music_tracks_used.txt b/Assets/Content/ClientData/music_tracks_used.txt new file mode 100644 index 0000000..db30b3a --- /dev/null +++ b/Assets/Content/ClientData/music_tracks_used.txt @@ -0,0 +1,84 @@ +akanon +akanon-1 +arena +attack_0 +attack_1 +attack_2 +bard +beethoven6 +brass_harmonies +cascade +cobaltscar +cottage +crouching +death +death_variant +eastkarana +eerie_0 +eerie_1 +eerie_10 +eerie_11 +eerie_2 +eerie_3 +eerie_4 +eerie_5 +eerie_6 +eerie_6-1 +eerie_7 +eerie_8 +eerie_9 +entrance_fanfare +entrance_fanfare-1 +entrance_fanfare_1 +entrance_fanfare_1-1 +eqtheme +erudsxing_0 +erudsxing_1 +felwithe_0 +felwithe_1 +felwithe_2 +fishsale +freeport +freportn +gfaydark +gfaydark-1 +gfaydark-2 +gfaydark-4 +gl_12 +guildmaster +gypsies +gypsies-1 +gypsies-2 +heroism +hogcaller +kael_0 +kael_1 +karana_river +karana_river-1 +lavastorm +lavastorm-1 +lionsmane +maidensfancy +merchant +militia +neriak_0 +neriak_1 +neriak_2 +nightchords +nro +qeynos_0 +qeynos_gates +qeynos_gates_brass +qeytoqrg +rivervale +sea +skyshrine +templeoflife +templeveeshan +thurgadina_0 +thurgadina_1 +thurgadinb +underwater +velketor_0 +velketor_1 +wakening \ No newline at end of file diff --git a/Assets/Content/ClientData/musictracks.txt.meta b/Assets/Content/ClientData/music_tracks_used.txt.meta similarity index 75% rename from Assets/Content/ClientData/musictracks.txt.meta rename to Assets/Content/ClientData/music_tracks_used.txt.meta index e16eb2f..ce04419 100644 --- a/Assets/Content/ClientData/musictracks.txt.meta +++ b/Assets/Content/ClientData/music_tracks_used.txt.meta @@ -1,5 +1,5 @@ fileFormatVersion: 2 -guid: 931834896ceace6478372fe59b4873a2 +guid: 5db0e8718d457ab42aeb735538af8b81 TextScriptImporter: externalObjects: {} userData: diff --git a/Assets/Content/ClientData/musictracks.txt b/Assets/Content/ClientData/musictracks.txt deleted file mode 100644 index 0225d68..0000000 --- a/Assets/Content/ClientData/musictracks.txt +++ /dev/null @@ -1,662 +0,0 @@ -#airplane -akanon-2 -airplane_0 -entrance_fanfare-1 -arpeggiated_runs -airplane_1 -heroism-1 - - -#akanon -akanon -gfaydark-1 -gfaydark-2 -felwithe_2 -felwithe_0 -felwithe_1 -arena -gypsies - - -#befallen -eerie_0 -eerie_1 -eerie_2 -eerie_3 -eerie_4 -eerie_5 -eerie_6 -eerie_7 -eerie_8 -eerie_9 -eerie_10 -eerie_11 - - -#blackburrow -eerie_0 -eerie_1 -eerie_2 -eerie_3 -eerie_4 -eerie_5 -eerie_6 -eerie_7 -eerie_8 -eerie_9 -eerie_10 -eerie_11 - - -#butcher -entrance_fanfare -entrance_fanfare_1 - - -#cauldron -entrance_fanfare -entrance_fanfare_1 -entrance_fanfare_1-1 -entrance_fanfare-1 -sea - - -#cobaltscar -cobaltscar -underwater - - -#crushbone -gfaydark-4 -airplane_0 -entrance_fanfare-1 -eerie_3 -arpeggiated_runs -neriak_0-1 - - -#crystal -erudsxing_0 -gfaydark-1 -cobaltscar -gfaydark - - -#damage -damage_0 -damage_1 -damage_2 -damage_3 -damage_4 - - -#damage1 -damage_0 -damage_1 -damage_2 -damage_3 -damage_4 - - -#damage2 -damage_0 -damage_1 -damage_2 -damage_3 -damage_4 - - -#eastkarana -cascade -eastkarana - - -#eerie -eerie_0 -eerie_1 -eerie_2 -eerie_3 -eerie_4 -eerie_5 -eerie_6 -eerie_7 -eerie_8 -eerie_9 -eerie_10 -eerie_11 - - -#erudnext -felwithe_0 -cascade -karana_river -entrance_fanfare_1-1 -bard -freportn -karana_river-1 -brass_harmonies - - -#eudnint -cascade -karana_river -entrance_fanfare_1-1 -bard -freportn -karana_river-1 -brass_harmonies -felwithe_1 - - -#erudsxing -erudsxing_0 -erudsxing_1 - - -#fearplane -eerie_8 -eerie_9 -eerie_0 -eerie_1 -eerie_2 -eerie_3 -eerie_4 -eerie_5 -eerie_7 - - -#felwithea -felwithe_0 -felwithe_1 -felwithe_2 - - -#freporte -freeport -lionsmane -fishsale -cottage -gypsies -death_variant -eerie_4 -eerie_5 - - -#freportn -harprun -gypsies-1 -gypsies -eqtheme -arena -brass_harmonies -akanon-1 -lionsmane -fishsale -bard -entrance_fanfare_1-1 -cottage -freportn -nightchords -string_fanfare -brass_fanfare - - -#freportw -heroism -hogcaller -militia -arena -entrance_fanfare_1-1 -eastkarana -karana_river-1 - - -#frozenshadow -eerie_7 -eerie_0 -lavastorm-1 -templeoflife -qeynos_0-1 -gfaydark - - -#gfaydark -entrance_fanfare -entrance_fanfare_1 -entrance_fanfare_1-1 -entrance_fanfare-1 -gfaydark -eerie_9 - - -#gl -attack_0 -attack_1 -attack_2 -death -airplane_0-1 -gl_5 -gl_6 -crouching -underwater -gl_9 -gl_10 -gl_11 -gl_12 -bard_intro -bard_main -gl_15 -gl_16 -gl_17 -gl_18 -gl_19 -sea -merchant -gfaydark-5 -guildmaster - - -#grobb -eerie_0 -eerie_1 -eerie_2 -eerie_3 -eerie_4 -eerie_5 -eerie_6 -eerie_7 -eerie_8 -eerie_9 -eerie_10 -eerie_11 - - -#guktop -eerie_0 -eerie_1 -eerie_5 -eerie_7 -arpeggiated_runs -neriak_0-1 -eerie_3 -maidensfancy -hogcaller -entrance_fanfare_1 - - -#halas -felwithe_0 -felwithe_1 -felwithe_2 -arena -hogcaller -maidensfancy -lionsmane -fishsale -cottage -eerie_4 - - -#hateplane -felwithe_0 -felwithe_2 -felwithe_1 -arpeggiated_runs -bard-2 -neriak_0-1 - - -#innothule -entrance_fanfare -entrance_fanfare_1 -eerie_3 -eerie_2 - - -#kael -kael_0 -kael_1 -attack_1 -arena - - -#kaladima -entrance_fanfare_1 -arena -nightchords -cascade -hogcaller -lionsmane -freportn -heroism -brass_harmonies-1 - - -#lavastorm -lavastorm -eerie_1 - - -#lfaydark -cascade -gfaydark-2 -entrance_fanfare_1-1 -gypsies-1 -eerie_5 -militia - - -#mistmoore -cascade -eastkarana -eerie_1 -entrance_fanfare -felwithe_1 -felwithe_2 -felwithe_0 -neriak_0-1 -karana_river -bard-2 -brass_harmonies -string_fanfare -freportn-1 -nightchords -gfaydark-3 -heroism-1 - - -#najena -eerie_0 -eerie_1 -eerie_2 -eerie_3 -eerie_4 -eerie_5 -eerie_6 -eerie_7 -eerie_8 -eerie_9 -eerie_10 -eerie_11 - - -#nektulos -lavastorm-1 - - -#neriaka -eerie_0 -eerie_1 -eerie_2 -eerie_3 -eerie_4 -eerie_5 -eerie_6 -eerie_7 -eerie_8 -eerie_9 -eerie_10 -eerie_11 -neriak_0 -neriak_1 -neriak_2 -arpeggiated_runs -fishsale -maidensfancy - - -#neriakb -eerie_0 -eerie_1 -eerie_2 -eerie_3 -eerie_4 -eerie_5 -eerie_6 -eerie_7 -eerie_8 -eerie_9 -eerie_10 -eerie_11 -neriak_0 -neriak_1 -neriak_2 -arpeggiated_runs -fishsale -maidensfancy - - -#neriakc -eerie_0 -eerie_1 -eerie_2 -eerie_3 -eerie_4 -eerie_5 -eerie_6 -eerie_7 -eerie_8 -eerie_9 -eerie_10 -eerie_11 -neriak_0 -neriak_1 -neriak_2 -arpeggiated_runs -fishsale -maidensfancy - - -#northkarana -cascade -eastkarana - - -#nro -nro -entrance_fanfare_1 -freeport -gypsies -eerie_7 - - -#opener -opener_0 - - -#opener2 -eastkarana -character_select - - -#opener3 -opener_3 -character_select - - -#opener4 -eqtheme -character_select - - -#paw -eerie_0 -eerie_1 -crouching - - -#pickchar -character_select-1 - - -#qcat -eerie_0 -eerie_1 -crouching - - -#qey2hh1 -cottage -maidensfancy -karana_river - - -#qeynos - - -arena -qeynos_gates -eerie_0 -eerie_1 -templeoflife -bard -eqtheme -gypsies -lionsmane -fishsale -cottage -freeport -qeynos_0 - - -#qeynos2 -arena -qeynos_gates -eerie_0 -eerie_1 -templeoflife -bard-1 - - -#qeytoqrg -arena -qeytoqrg -eerie_0 -eerie_1 -templeoflife -cottage -maidensfancy -entrance_fanfare - - -#qrg -arena -qeytoqrg -templeoflife -qeynos_gates_brass - - -#rathemtn -cascade -eastkarana -gypsies-2 -entrance_fanfare_1 -entrance_fanfare_1-1 -eerie_2 -eerie_3 - - -#rivervale -Rivervale - - -#runnyeye -eerie_0 -eerie_1 -crouching - - -#skyshrine -gl_12 -bard_intro -skyshrine -beethoven6 -nightchords -sea -gfaydark -gfaydark-2 - - -#soldungb -lavastorm -eerie_10 -eerie_11 -eerie_0 -eerie_1 -eerie_2 -eerie_3 -eerie_6-1 - - -#southkarana -cascade -eastkarana -eerie_2 -entrance_fanfare -maidensfancy -akanon-1 - - -#steamfont -gfaydark-4 -entrance_fanfare -entrance_fanfare_1-1 -akanon-1 -lavastorm -eerie_3 -eerie_5 - - -#templeveeshan -templeveeshan -gfaydark-5 -attack_1 -attack_0 - - -#thurgadina -templeveeshan -attack_1 -attack_0 -thurgadina_0 -templeoflife -thurgadina_1 - - -#thurgadinb -thurgadina_1 -thurgadinb - - -#tox -entrance_fanfare_1 -karana_river -gypsies-1 -cascade -karana_river-1 - - -#unrest -eastkarana -eerie_2 -eerie_1 -entrance_fanfare -felwithe_1 -felwithe_2 -neriak_0-1 -karana_river -brass_harmonies -string_fanfare -freportn-1 -gfaydark-3 -heroism-1 - - -#velketor -velketor_0 -velketor_1 - - -#wakening -wakening -gfaydark \ No newline at end of file diff --git a/Assets/Content/ClientData/zonelist_all.txt b/Assets/Content/ClientData/zonelist_all.txt index 03c5e1d..58fee9e 100644 --- a/Assets/Content/ClientData/zonelist_all.txt +++ b/Assets/Content/ClientData/zonelist_all.txt @@ -1,4 +1,4 @@ -# Lantern Zone List +# Lantern Zone List airplane akanon arena @@ -42,7 +42,7 @@ freportw frontiermtns frozenshadow gfaydark -freatdivide +greatdivide grobb growthplane gukbottom @@ -101,7 +101,7 @@ sirens skyfire skyshrine sleeper -solgunga +soldunga soldungb soltemple southkarana diff --git a/Assets/Content/ClientData/zonelist_other.txt b/Assets/Content/ClientData/zonelist_misc.txt similarity index 100% rename from Assets/Content/ClientData/zonelist_other.txt rename to Assets/Content/ClientData/zonelist_misc.txt diff --git a/Assets/Content/ClientData/zonelist_other.txt.meta b/Assets/Content/ClientData/zonelist_misc.txt.meta similarity index 75% rename from Assets/Content/ClientData/zonelist_other.txt.meta rename to Assets/Content/ClientData/zonelist_misc.txt.meta index a0d9a1c..537bc80 100644 --- a/Assets/Content/ClientData/zonelist_other.txt.meta +++ b/Assets/Content/ClientData/zonelist_misc.txt.meta @@ -1,5 +1,5 @@ fileFormatVersion: 2 -guid: 93ad09031bcb3514fbd796e69de438c3 +guid: 21e725e15ecdf8b4b8d9889b0a04e3b2 TextScriptImporter: externalObjects: {} userData: diff --git a/Assets/Plugins.meta b/Assets/Plugins.meta new file mode 100644 index 0000000..6789158 --- /dev/null +++ b/Assets/Plugins.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: 9fa2c912df3f6294392881ae51b6d40f +folderAsset: yes +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Plugins/EQ.meta b/Assets/Plugins/EQ.meta new file mode 100644 index 0000000..fea3572 --- /dev/null +++ b/Assets/Plugins/EQ.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: 4584511a81c938849833d85446fff51d +folderAsset: yes +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Plugins/EQ/DryWetMidi.meta b/Assets/Plugins/EQ/DryWetMidi.meta new file mode 100644 index 0000000..7fefa83 --- /dev/null +++ b/Assets/Plugins/EQ/DryWetMidi.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: 74dc29eff712ac04eae2c67bc8c03a50 +folderAsset: yes +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Plugins/EQ/DryWetMidi/LICENSE b/Assets/Plugins/EQ/DryWetMidi/LICENSE new file mode 100644 index 0000000..4a28173 --- /dev/null +++ b/Assets/Plugins/EQ/DryWetMidi/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2018 Maxim Dobroselsky + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/Assets/Plugins/EQ/DryWetMidi/LICENSE.meta b/Assets/Plugins/EQ/DryWetMidi/LICENSE.meta new file mode 100644 index 0000000..1e56ac9 --- /dev/null +++ b/Assets/Plugins/EQ/DryWetMidi/LICENSE.meta @@ -0,0 +1,7 @@ +fileFormatVersion: 2 +guid: 4d6124a5f1fd85d48b6bace18be23909 +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Plugins/EQ/DryWetMidi/Melanchall.DryWetMidi.dll b/Assets/Plugins/EQ/DryWetMidi/Melanchall.DryWetMidi.dll new file mode 100644 index 0000000..ffd7b9a --- /dev/null +++ b/Assets/Plugins/EQ/DryWetMidi/Melanchall.DryWetMidi.dll @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:53248d3db25cae4e4d308daef1f44e51b951a0dd46ac85b80309369980f0b7ba +size 655360 diff --git a/Assets/Plugins/EQ/DryWetMidi/Melanchall.DryWetMidi.dll.meta b/Assets/Plugins/EQ/DryWetMidi/Melanchall.DryWetMidi.dll.meta new file mode 100644 index 0000000..7fea852 --- /dev/null +++ b/Assets/Plugins/EQ/DryWetMidi/Melanchall.DryWetMidi.dll.meta @@ -0,0 +1,33 @@ +fileFormatVersion: 2 +guid: c993b9ff3998a1340abff7b2b30107a5 +PluginImporter: + externalObjects: {} + serializedVersion: 2 + iconMap: {} + executionOrder: {} + defineConstraints: [] + isPreloaded: 0 + isOverridable: 0 + isExplicitlyReferenced: 0 + validateReferences: 1 + platformData: + - first: + Any: + second: + enabled: 1 + settings: {} + - first: + Editor: Editor + second: + enabled: 0 + settings: + DefaultValueInitialized: true + - first: + Windows Store Apps: WindowsStoreApps + second: + enabled: 0 + settings: + CPU: AnyCPU + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Plugins/EQ/sqlite3.meta b/Assets/Plugins/EQ/sqlite3.meta new file mode 100644 index 0000000..a12ffdc --- /dev/null +++ b/Assets/Plugins/EQ/sqlite3.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: 2f9d0f92c29cf5d45a77291ab238e770 +folderAsset: yes +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Plugins/EQ/sqlite3/Android.meta b/Assets/Plugins/EQ/sqlite3/Android.meta new file mode 100644 index 0000000..41730fd --- /dev/null +++ b/Assets/Plugins/EQ/sqlite3/Android.meta @@ -0,0 +1,9 @@ +fileFormatVersion: 2 +guid: 335966ac406cbcd4a88311438fdfc300 +folderAsset: yes +timeCreated: 1510781584 +licenseType: Free +DefaultImporter: + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Plugins/EQ/sqlite3/Android/libs.meta b/Assets/Plugins/EQ/sqlite3/Android/libs.meta new file mode 100644 index 0000000..8010bd6 --- /dev/null +++ b/Assets/Plugins/EQ/sqlite3/Android/libs.meta @@ -0,0 +1,9 @@ +fileFormatVersion: 2 +guid: 02132c477ff6f6f4ea5a4328861274f4 +folderAsset: yes +timeCreated: 1510781584 +licenseType: Free +DefaultImporter: + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Plugins/EQ/sqlite3/Android/libs/armeabi-v7a.meta b/Assets/Plugins/EQ/sqlite3/Android/libs/armeabi-v7a.meta new file mode 100644 index 0000000..8d45cca --- /dev/null +++ b/Assets/Plugins/EQ/sqlite3/Android/libs/armeabi-v7a.meta @@ -0,0 +1,9 @@ +fileFormatVersion: 2 +guid: 25c5110ddb83da84d93b7c7633de52be +folderAsset: yes +timeCreated: 1510781584 +licenseType: Free +DefaultImporter: + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Plugins/EQ/sqlite3/Android/libs/armeabi-v7a/libsqlite3.so b/Assets/Plugins/EQ/sqlite3/Android/libs/armeabi-v7a/libsqlite3.so new file mode 100644 index 0000000..2c6e2bf Binary files /dev/null and b/Assets/Plugins/EQ/sqlite3/Android/libs/armeabi-v7a/libsqlite3.so differ diff --git a/Assets/Plugins/EQ/sqlite3/Android/libs/armeabi-v7a/libsqlite3.so.meta b/Assets/Plugins/EQ/sqlite3/Android/libs/armeabi-v7a/libsqlite3.so.meta new file mode 100644 index 0000000..2dc4a89 --- /dev/null +++ b/Assets/Plugins/EQ/sqlite3/Android/libs/armeabi-v7a/libsqlite3.so.meta @@ -0,0 +1,34 @@ +fileFormatVersion: 2 +guid: 0fafe71b3fa11bd4986429e3bd16b1f5 +timeCreated: 1510781589 +licenseType: Free +PluginImporter: + serializedVersion: 2 + iconMap: {} + executionOrder: {} + isPreloaded: 0 + isOverridable: 0 + platformData: + data: + first: + Android: Android + second: + enabled: 1 + settings: + CPU: ARMv7 + data: + first: + Any: + second: + enabled: 0 + settings: {} + data: + first: + Editor: Editor + second: + enabled: 0 + settings: + DefaultValueInitialized: true + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Plugins/EQ/sqlite3/Android/libs/x86.meta b/Assets/Plugins/EQ/sqlite3/Android/libs/x86.meta new file mode 100644 index 0000000..3397f16 --- /dev/null +++ b/Assets/Plugins/EQ/sqlite3/Android/libs/x86.meta @@ -0,0 +1,9 @@ +fileFormatVersion: 2 +guid: 82e47201a7d72b046ab5605a6017fb4c +folderAsset: yes +timeCreated: 1510781584 +licenseType: Free +DefaultImporter: + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Plugins/EQ/sqlite3/Android/libs/x86/libsqlite3.so b/Assets/Plugins/EQ/sqlite3/Android/libs/x86/libsqlite3.so new file mode 100644 index 0000000..9d08844 Binary files /dev/null and b/Assets/Plugins/EQ/sqlite3/Android/libs/x86/libsqlite3.so differ diff --git a/Assets/Plugins/EQ/sqlite3/Android/libs/x86/libsqlite3.so.meta b/Assets/Plugins/EQ/sqlite3/Android/libs/x86/libsqlite3.so.meta new file mode 100644 index 0000000..79afa9b --- /dev/null +++ b/Assets/Plugins/EQ/sqlite3/Android/libs/x86/libsqlite3.so.meta @@ -0,0 +1,34 @@ +fileFormatVersion: 2 +guid: ff8ccf4e24e4b1b40af1fd8125621ad1 +timeCreated: 1510781589 +licenseType: Free +PluginImporter: + serializedVersion: 2 + iconMap: {} + executionOrder: {} + isPreloaded: 0 + isOverridable: 0 + platformData: + data: + first: + Android: Android + second: + enabled: 1 + settings: + CPU: x86 + data: + first: + Any: + second: + enabled: 0 + settings: {} + data: + first: + Editor: Editor + second: + enabled: 0 + settings: + DefaultValueInitialized: true + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Plugins/EQ/sqlite3/x64.meta b/Assets/Plugins/EQ/sqlite3/x64.meta new file mode 100644 index 0000000..dfe5741 --- /dev/null +++ b/Assets/Plugins/EQ/sqlite3/x64.meta @@ -0,0 +1,9 @@ +fileFormatVersion: 2 +guid: b62a33d43cba07d45a76286dd8ab900a +folderAsset: yes +timeCreated: 1510781584 +licenseType: Free +DefaultImporter: + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Plugins/EQ/sqlite3/x64/sqlite3.dll b/Assets/Plugins/EQ/sqlite3/x64/sqlite3.dll new file mode 100644 index 0000000..832e2aa --- /dev/null +++ b/Assets/Plugins/EQ/sqlite3/x64/sqlite3.dll @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:49520982bd5e2aae881db1764b26ca7518ed766e147ee068ac54ead797987bf5 +size 2122240 diff --git a/Assets/Plugins/EQ/sqlite3/x64/sqlite3.dll.meta b/Assets/Plugins/EQ/sqlite3/x64/sqlite3.dll.meta new file mode 100644 index 0000000..e412a07 --- /dev/null +++ b/Assets/Plugins/EQ/sqlite3/x64/sqlite3.dll.meta @@ -0,0 +1,101 @@ +fileFormatVersion: 2 +guid: 30b498471cb1e134f932ad62296e655d +PluginImporter: + externalObjects: {} + serializedVersion: 2 + iconMap: {} + executionOrder: {} + defineConstraints: [] + isPreloaded: 0 + isOverridable: 0 + isExplicitlyReferenced: 0 + validateReferences: 1 + platformData: + - first: + '': Any + second: + enabled: 0 + settings: + Exclude Editor: 0 + Exclude Linux: 0 + Exclude Linux64: 0 + Exclude LinuxUniversal: 0 + Exclude OSXUniversal: 0 + Exclude Win: 1 + Exclude Win64: 0 + - first: + Any: + second: + enabled: 1 + settings: {} + - first: + Editor: Editor + second: + enabled: 1 + settings: + CPU: x86_64 + DefaultValueInitialized: true + OS: AnyOS + - first: + Facebook: Win + second: + enabled: 0 + settings: + CPU: None + - first: + Facebook: Win64 + second: + enabled: 1 + settings: + CPU: AnyCPU + - first: + Standalone: Linux + second: + enabled: 1 + settings: + CPU: None + - first: + Standalone: Linux64 + second: + enabled: 1 + settings: + CPU: AnyCPU + - first: + Standalone: LinuxUniversal + second: + enabled: 1 + settings: + CPU: AnyCPU + - first: + Standalone: OSXIntel + second: + enabled: 0 + settings: + CPU: None + - first: + Standalone: OSXIntel64 + second: + enabled: 1 + settings: + CPU: AnyCPU + - first: + Standalone: OSXUniversal + second: + enabled: 1 + settings: + CPU: x86_64 + - first: + Standalone: Win + second: + enabled: 0 + settings: + CPU: None + - first: + Standalone: Win64 + second: + enabled: 1 + settings: + CPU: AnyCPU + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Plugins/EQ/sqlite3/x86.meta b/Assets/Plugins/EQ/sqlite3/x86.meta new file mode 100644 index 0000000..eaaa5bf --- /dev/null +++ b/Assets/Plugins/EQ/sqlite3/x86.meta @@ -0,0 +1,9 @@ +fileFormatVersion: 2 +guid: bac43642f49374a4b8fd396f62bf4160 +folderAsset: yes +timeCreated: 1510781584 +licenseType: Free +DefaultImporter: + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Plugins/EQ/sqlite3/x86/sqlite3.dll b/Assets/Plugins/EQ/sqlite3/x86/sqlite3.dll new file mode 100644 index 0000000..7ed11ef --- /dev/null +++ b/Assets/Plugins/EQ/sqlite3/x86/sqlite3.dll @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:74e67b029eaf55a9b1e5ce8363c15cd51473eb87552c0a79d91cf0ea3f9efabb +size 1730048 diff --git a/Assets/Plugins/EQ/sqlite3/x86/sqlite3.dll.meta b/Assets/Plugins/EQ/sqlite3/x86/sqlite3.dll.meta new file mode 100644 index 0000000..1c1dfc5 --- /dev/null +++ b/Assets/Plugins/EQ/sqlite3/x86/sqlite3.dll.meta @@ -0,0 +1,108 @@ +fileFormatVersion: 2 +guid: ed911f4d5cfe16b4883008f447621db9 +PluginImporter: + externalObjects: {} + serializedVersion: 2 + iconMap: {} + executionOrder: {} + defineConstraints: [] + isPreloaded: 0 + isOverridable: 0 + isExplicitlyReferenced: 0 + validateReferences: 1 + platformData: + - first: + : Any + second: + enabled: 0 + settings: + Exclude Android: 1 + Exclude Editor: 0 + Exclude Linux: 0 + Exclude Linux64: 0 + Exclude LinuxUniversal: 0 + Exclude OSXUniversal: 0 + Exclude Win: 0 + Exclude Win64: 1 + - first: + Android: Android + second: + enabled: 0 + settings: + CPU: ARMv7 + - first: + Any: + second: + enabled: 1 + settings: {} + - first: + Editor: Editor + second: + enabled: 1 + settings: + CPU: x86 + DefaultValueInitialized: true + OS: AnyOS + - first: + Facebook: Win + second: + enabled: 1 + settings: + CPU: AnyCPU + - first: + Facebook: Win64 + second: + enabled: 0 + settings: + CPU: None + - first: + Standalone: Linux + second: + enabled: 1 + settings: + CPU: x86 + - first: + Standalone: Linux64 + second: + enabled: 1 + settings: + CPU: None + - first: + Standalone: LinuxUniversal + second: + enabled: 1 + settings: + CPU: AnyCPU + - first: + Standalone: OSXIntel + second: + enabled: 1 + settings: + CPU: AnyCPU + - first: + Standalone: OSXIntel64 + second: + enabled: 0 + settings: + CPU: None + - first: + Standalone: OSXUniversal + second: + enabled: 1 + settings: + CPU: x86 + - first: + Standalone: Win + second: + enabled: 1 + settings: + CPU: AnyCPU + - first: + Standalone: Win64 + second: + enabled: 0 + settings: + CPU: None + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Scripts/Infrastructure/EQ/MeltySynth.meta b/Assets/Scripts/Infrastructure/EQ/MeltySynth.meta new file mode 100644 index 0000000..29deeb6 --- /dev/null +++ b/Assets/Scripts/Infrastructure/EQ/MeltySynth.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: eedabe2f4545332478a56e2cd5529797 +folderAsset: yes +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Scripts/Infrastructure/EQ/MeltySynth/ArrayMath.cs b/Assets/Scripts/Infrastructure/EQ/MeltySynth/ArrayMath.cs new file mode 100644 index 0000000..41573ae --- /dev/null +++ b/Assets/Scripts/Infrastructure/EQ/MeltySynth/ArrayMath.cs @@ -0,0 +1,46 @@ +using System.Numerics; +using System.Runtime.InteropServices; + +namespace Infrastructure.EQ.MeltySynth +{ + internal static class ArrayMath + { + public static void MultiplyAdd(float a, float[] x, float[] destination) + { + // LANTERN: use older implementation with il2cpp + // See also: https://github.com/homy-game-studio/hgs-unity-tone/issues/4 +#if ENABLE_IL2CPP + for (var i = 0; i < destination.Length; i++) + { + destination[i] += a * x[i]; + } + return; +#endif + + var vx = MemoryMarshal.Cast>(x); + var vd = MemoryMarshal.Cast>(destination); + + var count = 0; + + for (var i = 0; i < vd.Length; i++) + { + vd[i] += a * vx[i]; + count += Vector.Count; + } + + for (var i = count; i < destination.Length; i++) + { + destination[i] += a * x[i]; + } + } + + public static void MultiplyAdd(float a, float step, float[] x, float[] destination) + { + for (var i = 0; i < destination.Length; i++) + { + destination[i] += a * x[i]; + a += step; + } + } + } +} diff --git a/Assets/Scripts/Infrastructure/EQ/MeltySynth/ArrayMath.cs.meta b/Assets/Scripts/Infrastructure/EQ/MeltySynth/ArrayMath.cs.meta new file mode 100644 index 0000000..9ec6db8 --- /dev/null +++ b/Assets/Scripts/Infrastructure/EQ/MeltySynth/ArrayMath.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 95f1e2e23dab06a42babe9cec08c319c +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Scripts/Infrastructure/EQ/MeltySynth/AudioRendererEx.cs b/Assets/Scripts/Infrastructure/EQ/MeltySynth/AudioRendererEx.cs new file mode 100644 index 0000000..c07ba00 --- /dev/null +++ b/Assets/Scripts/Infrastructure/EQ/MeltySynth/AudioRendererEx.cs @@ -0,0 +1,290 @@ +using System; +using System.Buffers; + +namespace Infrastructure.EQ.MeltySynth +{ + /// + /// Provides utility methods to convert the format of samples. + /// + public static class AudioRendererEx + { + /// + /// Renders the waveform as a stereo interleaved signal. + /// + /// The audio renderer. + /// The destination buffer. + /// + /// This utility method internally uses , + /// which may result in memory allocation on the first call. + /// To completely avoid memory allocation, + /// use . + /// + public static void RenderInterleaved(this IAudioRenderer renderer, Span destination) + { + if (renderer == null) + { + throw new ArgumentNullException(nameof(renderer)); + } + + if (destination.Length % 2 != 0) + { + throw new ArgumentException("The length of the destination buffer must be even.", nameof(destination)); + } + + var sampleCount = destination.Length / 2; + var bufferLength = destination.Length; + + var buffer = ArrayPool.Shared.Rent(bufferLength); + + try + { + var left = buffer.AsSpan(0, sampleCount); + var right = buffer.AsSpan(sampleCount, sampleCount); + renderer.Render(left, right); + + var pos = 0; + for (var t = 0; t < sampleCount; t++) + { + destination[pos++] = left[t]; + destination[pos++] = right[t]; + } + } + finally + { + ArrayPool.Shared.Return(buffer); + } + } + + /// + /// Renders the waveform as a monaural signal. + /// + /// The audio renderer. + /// The destination buffer. + /// + /// This utility method internally uses , + /// which may result in memory allocation on the first call. + /// To completely avoid memory allocation, + /// use . + /// + public static void RenderMono(this IAudioRenderer renderer, Span destination) + { + if (renderer == null) + { + throw new ArgumentNullException(nameof(renderer)); + } + + var sampleCount = destination.Length; + var bufferLength = 2 * destination.Length; + + var buffer = ArrayPool.Shared.Rent(bufferLength); + + try + { + var left = buffer.AsSpan(0, sampleCount); + var right = buffer.AsSpan(sampleCount, sampleCount); + renderer.Render(left, right); + + for (var t = 0; t < sampleCount; t++) + { + destination[t] = (left[t] + right[t]) / 2; + } + } + finally + { + ArrayPool.Shared.Return(buffer); + } + } + + /// + /// Renders the waveform with 16-bit quantization. + /// + /// The audio renderer. + /// The buffer of the left channel to store the rendered waveform. + /// The buffer of the right channel to store the rendered waveform. + /// + /// Out of range samples will be clipped. + /// This utility method internally uses , + /// which may result in memory allocation on the first call. + /// To completely avoid memory allocation, + /// use . + /// The output buffers for the left and right must be the same length. + /// + public static void RenderInt16(this IAudioRenderer renderer, Span left, Span right) + { + if (renderer == null) + { + throw new ArgumentNullException(nameof(renderer)); + } + + if (left.Length != right.Length) + { + throw new ArgumentException("The output buffers for the left and right must be the same length."); + } + + var sampleCount = left.Length; + var bufferLength = 2 * left.Length; + + var buffer = ArrayPool.Shared.Rent(bufferLength); + + try + { + var bufLeft = buffer.AsSpan(0, sampleCount); + var bufRight = buffer.AsSpan(sampleCount, sampleCount); + renderer.Render(bufLeft, bufRight); + + for (var t = 0; t < sampleCount; t++) + { + var sample = 32768 * bufLeft[t]; + if (sample < short.MinValue) + { + sample = short.MinValue; + } + else if (sample > short.MaxValue) + { + sample = short.MaxValue; + } + + left[t] = (short)sample; + } + + for (var t = 0; t < sampleCount; t++) + { + var sample = 32768 * bufRight[t]; + if (sample < short.MinValue) + { + sample = short.MinValue; + } + else if (sample > short.MaxValue) + { + sample = short.MaxValue; + } + + right[t] = (short)sample; + } + } + finally + { + ArrayPool.Shared.Return(buffer); + } + } + + /// + /// Renders the waveform as a stereo interleaved signal with 16-bit quantization. + /// + /// The audio renderer. + /// The destination buffer. + /// + /// Out of range samples will be clipped. + /// This utility method internally uses , + /// which may result in memory allocation on the first call. + /// To completely avoid memory allocation, + /// use . + /// + public static void RenderInterleavedInt16(this IAudioRenderer renderer, Span destination) + { + if (renderer == null) + { + throw new ArgumentNullException(nameof(renderer)); + } + + if (destination.Length % 2 != 0) + { + throw new ArgumentException("The length of the destination buffer must be even.", nameof(destination)); + } + + var sampleCount = destination.Length / 2; + var bufferLength = destination.Length; + + var buffer = ArrayPool.Shared.Rent(bufferLength); + + try + { + var left = buffer.AsSpan(0, sampleCount); + var right = buffer.AsSpan(sampleCount, sampleCount); + renderer.Render(left, right); + + var pos = 0; + for (var t = 0; t < sampleCount; t++) + { + var sampleLeft = (int)(32768 * left[t]); + if (sampleLeft < short.MinValue) + { + sampleLeft = short.MinValue; + } + else if (sampleLeft > short.MaxValue) + { + sampleLeft = short.MaxValue; + } + + var sampleRight = (int)(32768 * right[t]); + if (sampleRight < short.MinValue) + { + sampleRight = short.MinValue; + } + else if (sampleRight > short.MaxValue) + { + sampleRight = short.MaxValue; + } + + destination[pos++] = (short)sampleLeft; + destination[pos++] = (short)sampleRight; + } + } + finally + { + ArrayPool.Shared.Return(buffer); + } + } + + /// + /// Renders the waveform as a monaural signal with 16-bit quantization. + /// + /// The audio renderer. + /// The destination buffer. + /// + /// Out of range samples will be clipped. + /// This utility method internally uses , + /// which may result in memory allocation on the first call. + /// To completely avoid memory allocation, + /// use . + /// + public static void RenderMonoInt16(this IAudioRenderer renderer, Span destination) + { + if (renderer == null) + { + throw new ArgumentNullException(nameof(renderer)); + } + + var sampleCount = destination.Length; + var bufferLength = 2 * destination.Length; + + var buffer = ArrayPool.Shared.Rent(bufferLength); + + try + { + var left = buffer.AsSpan(0, sampleCount); + var right = buffer.AsSpan(sampleCount, sampleCount); + renderer.Render(left, right); + + for (var t = 0; t < sampleCount; t++) + { + var sample = (int)(16384 * (left[t] + right[t])); + if (sample < short.MinValue) + { + sample = short.MinValue; + } + else if (sample > short.MaxValue) + { + sample = short.MaxValue; + } + + destination[t] = (short)sample; + } + } + finally + { + ArrayPool.Shared.Return(buffer); + } + } + } +} diff --git a/Assets/Scripts/Infrastructure/EQ/MeltySynth/AudioRendererEx.cs.meta b/Assets/Scripts/Infrastructure/EQ/MeltySynth/AudioRendererEx.cs.meta new file mode 100644 index 0000000..2f49f77 --- /dev/null +++ b/Assets/Scripts/Infrastructure/EQ/MeltySynth/AudioRendererEx.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 86d82a3e04097d443b6974f26f75d548 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Scripts/Infrastructure/EQ/MeltySynth/BiQuadFilter.cs b/Assets/Scripts/Infrastructure/EQ/MeltySynth/BiQuadFilter.cs new file mode 100644 index 0000000..e5b8cba --- /dev/null +++ b/Assets/Scripts/Infrastructure/EQ/MeltySynth/BiQuadFilter.cs @@ -0,0 +1,101 @@ +using System; + +namespace Infrastructure.EQ.MeltySynth +{ + internal sealed class BiQuadFilter + { + private static readonly float resonancePeakOffset = 1 - 1 / MathF.Sqrt(2); + + private readonly Synthesizer synthesizer; + + private bool active; + + private float a0; + private float a1; + private float a2; + private float a3; + private float a4; + + private float x1; + private float x2; + private float y1; + private float y2; + + internal BiQuadFilter(Synthesizer synthesizer) + { + this.synthesizer = synthesizer; + } + + public void ClearBuffer() + { + x1 = 0; + x2 = 0; + y1 = 0; + y2 = 0; + } + + public void SetLowPassFilter(float cutoffFrequency, float resonance) + { + if (cutoffFrequency < 0.499F * synthesizer.SampleRate) + { + active = true; + + // This equation gives the Q value which makes the desired resonance peak. + // The error of the resultant peak height is less than 3%. + var q = resonance - resonancePeakOffset / (1 + 6 * (resonance - 1)); + + var w = 2 * MathF.PI * cutoffFrequency / synthesizer.SampleRate; + var cosw = MathF.Cos(w); + var alpha = MathF.Sin(w) / (2 * q); + + var b0 = (1 - cosw) / 2; + var b1 = 1 - cosw; + var b2 = (1 - cosw) / 2; + var a0 = 1 + alpha; + var a1 = -2 * cosw; + var a2 = 1 - alpha; + + SetCoefficients(a0, a1, a2, b0, b1, b2); + } + else + { + active = false; + } + } + + public void Process(float[] block) + { + if (active) + { + for (var t = 0; t < block.Length; t++) + { + var input = block[t]; + var output = a0 * input + a1 * x1 + a2 * x2 - a3 * y1 - a4 * y2; + + x2 = x1; + x1 = input; + y2 = y1; + y1 = output; + + block[t] = output; + } + } + else + { + x2 = block[block.Length - 2]; + x1 = block[block.Length - 1]; + y2 = x2; + y1 = x1; + } + } + + private void SetCoefficients(float a0, float a1, float a2, float b0, float b1, float b2) + { + this.a0 = b0 / a0; + this.a1 = b1 / a0; + this.a2 = b2 / a0; + this.a3 = a1 / a0; + this.a4 = a2 / a0; + } + } +} diff --git a/Assets/Scripts/Infrastructure/EQ/MeltySynth/BiQuadFilter.cs.meta b/Assets/Scripts/Infrastructure/EQ/MeltySynth/BiQuadFilter.cs.meta new file mode 100644 index 0000000..7dd28e5 --- /dev/null +++ b/Assets/Scripts/Infrastructure/EQ/MeltySynth/BiQuadFilter.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 569270e12ff9256419928f458f882d6f +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Scripts/Infrastructure/EQ/MeltySynth/BinaryReaderEx.cs b/Assets/Scripts/Infrastructure/EQ/MeltySynth/BinaryReaderEx.cs new file mode 100644 index 0000000..0e65342 --- /dev/null +++ b/Assets/Scripts/Infrastructure/EQ/MeltySynth/BinaryReaderEx.cs @@ -0,0 +1,79 @@ +using System.IO; +using System.Text; + +namespace Infrastructure.EQ.MeltySynth +{ + internal static class BinaryReaderEx + { + public static string ReadFourCC(this BinaryReader reader) + { + var data = reader.ReadBytes(4); + + for (var i = 0; i < data.Length; i++) + { + var value = data[i]; + if (!(32 <= value && value <= 126)) + { + data[i] = (byte)'?'; + } + } + + return Encoding.ASCII.GetString(data, 0, data.Length); + } + + public static string ReadFixedLengthString(this BinaryReader reader, int length) + { + var data = reader.ReadBytes(length); + + int actualLength; + for (actualLength = 0; actualLength < data.Length; actualLength++) + { + if (data[actualLength] == 0) + { + break; + } + } + + return Encoding.ASCII.GetString(data, 0, actualLength); + } + + public static short ReadInt16BigEndian(this BinaryReader reader) + { + var value = reader.ReadInt16(); + var b1 = 0xFF & (value >> 0); + var b2 = 0xFF & (value >> 8); + return (short)((b1 << 8) | b2); + } + + public static int ReadInt32BigEndian(this BinaryReader reader) + { + var value = reader.ReadInt32(); + var b1 = 0xFF & (value >> 0); + var b2 = 0xFF & (value >> 8); + var b3 = 0xFF & (value >> 16); + var b4 = 0xFF & (value >> 24); + return (b1 << 24) | (b2 << 16) | (b3 << 8) | b4; + } + + public static int ReadIntVariableLength(this BinaryReader reader) + { + var acc = 0; + var count = 0; + while (true) + { + var value = reader.ReadByte(); + acc = (acc << 7) | (value & 127); + if ((value & 128) == 0) + { + break; + } + count++; + if (count == 4) + { + throw new InvalidDataException("The length of the value must be equal to or less than 4."); + } + } + return acc; + } + } +} diff --git a/Assets/Scripts/Infrastructure/EQ/MeltySynth/BinaryReaderEx.cs.meta b/Assets/Scripts/Infrastructure/EQ/MeltySynth/BinaryReaderEx.cs.meta new file mode 100644 index 0000000..0dfaa68 --- /dev/null +++ b/Assets/Scripts/Infrastructure/EQ/MeltySynth/BinaryReaderEx.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 8f4ccc7f1d60b8f4cb1adfe26ecbf573 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Scripts/Infrastructure/EQ/MeltySynth/Channel.cs b/Assets/Scripts/Infrastructure/EQ/MeltySynth/Channel.cs new file mode 100644 index 0000000..a73a5c5 --- /dev/null +++ b/Assets/Scripts/Infrastructure/EQ/MeltySynth/Channel.cs @@ -0,0 +1,204 @@ +namespace Infrastructure.EQ.MeltySynth +{ + internal sealed class Channel + { + private readonly Synthesizer synthesizer; + private readonly bool isPercussionChannel; + + private int bankNumber; + private int patchNumber; + + private short modulation; + private short volume; + private short pan; + private short expression; + private bool holdPedal; + + private byte reverbSend; + private byte chorusSend; + + private short rpn; + private short pitchBendRange; + private short coarseTune; + private short fineTune; + + private float pitchBend; + + internal Channel(Synthesizer synthesizer, bool isPercussionChannel) + { + this.synthesizer = synthesizer; + this.isPercussionChannel = isPercussionChannel; + + Reset(); + } + + public void Reset() + { + bankNumber = isPercussionChannel ? 128 : 0; + patchNumber = 0; + + modulation = 0; + volume = 100 << 7; + pan = 64 << 7; + expression = 127 << 7; + holdPedal = false; + + reverbSend = 40; + chorusSend = 0; + + rpn = -1; + pitchBendRange = 2 << 7; + coarseTune = 0; + fineTune = 8192; + + pitchBend = 0F; + } + + public void ResetAllControllers() + { + modulation = 0; + expression = 127 << 7; + holdPedal = false; + + rpn = -1; + + pitchBend = 0F; + } + + public void SetBank(int value) + { + bankNumber = value; + + if (isPercussionChannel) + { + bankNumber += 128; + } + } + + public void SetPatch(int value) + { + patchNumber = value; + } + + public void SetModulationCoarse(int value) + { + modulation = (short)((modulation & 0x7F) | (value << 7)); + } + + public void SetModulationFine(int value) + { + modulation = (short)((modulation & 0xFF80) | value); + } + + public void SetVolumeCoarse(int value) + { + volume = (short)((volume & 0x7F) | (value << 7)); + } + + public void SetVolumeFine(int value) + { + volume = (short)((volume & 0xFF80) | value); + } + + public void SetPanCoarse(int value) + { + pan = (short)((pan & 0x7F) | (value << 7)); + } + + public void SetPanFine(int value) + { + pan = (short)((pan & 0xFF80) | value); + } + + public void SetExpressionCoarse(int value) + { + expression = (short)((expression & 0x7F) | (value << 7)); + } + + public void SetExpressionFine(int value) + { + expression = (short)((expression & 0xFF80) | value); + } + + public void SetHoldPedal(int value) + { + holdPedal = value >= 64; + } + + public void SetReverbSend(int value) + { + reverbSend = (byte)value; + } + + public void SetChorusSend(int value) + { + chorusSend = (byte)value; + } + + public void SetRpnCoarse(int value) + { + rpn = (short)((rpn & 0x7F) | (value << 7)); + } + + public void SetRpnFine(int value) + { + rpn = (short)((rpn & 0xFF80) | value); + } + + public void DataEntryCoarse(int value) + { + switch (rpn) + { + case 0: + pitchBendRange = (short)((pitchBendRange & 0x7F) | (value << 7)); + break; + + case 1: + fineTune = (short)((fineTune & 0x7F) | (value << 7)); + break; + + case 2: + coarseTune = (short)(value - 64); + break; + } + } + + public void DataEntryFine(int value) + { + switch (rpn) + { + case 0: + pitchBendRange = (short)((pitchBendRange & 0xFF80) | value); + break; + + case 1: + fineTune = (short)((fineTune & 0xFF80) | value); + break; + } + } + + public void SetPitchBend(int value1, int value2) + { + pitchBend = (1F / 8192F) * ((value1 | (value2 << 7)) - 8192); + } + + public bool IsPercussionChannel => isPercussionChannel; + + public int BankNumber => bankNumber; + public int PatchNumber => patchNumber; + + public float Modulation => (50F / 16383F) * modulation; + public float Volume => (1F / 16383F) * volume; + public float Pan => (100F / 16383F) * pan - 50F; + public float Expression => (1F / 16383F) * expression; + public bool HoldPedal => holdPedal; + + public float ReverbSend => (1F / 127F) * reverbSend; + public float ChorusSend => (1F / 127F) * chorusSend; + + public float PitchBendRange => (pitchBendRange >> 7) + 0.01F * (pitchBendRange & 0x7F); + public float Tune => coarseTune + (1F / 8192F) * (fineTune - 8192); + + public float PitchBend => PitchBendRange * pitchBend; + } +} diff --git a/Assets/Scripts/Infrastructure/EQ/MeltySynth/Channel.cs.meta b/Assets/Scripts/Infrastructure/EQ/MeltySynth/Channel.cs.meta new file mode 100644 index 0000000..b3ac101 --- /dev/null +++ b/Assets/Scripts/Infrastructure/EQ/MeltySynth/Channel.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 0b43771d248a85843b2f9c01c8080c03 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Scripts/Infrastructure/EQ/MeltySynth/Chorus.cs b/Assets/Scripts/Infrastructure/EQ/MeltySynth/Chorus.cs new file mode 100644 index 0000000..c77ca58 --- /dev/null +++ b/Assets/Scripts/Infrastructure/EQ/MeltySynth/Chorus.cs @@ -0,0 +1,109 @@ +using System; + +namespace Infrastructure.EQ.MeltySynth +{ + internal sealed class Chorus + { + private readonly float[] bufferL; + private readonly float[] bufferR; + + private readonly float[] delayTable; + + private int bufferIndex; + + private int delayTableIndexL; + private int delayTableIndexR; + + internal Chorus(int sampleRate, double delay, double depth, double frequency) + { + bufferL = new float[(int)(sampleRate * (delay + depth)) + 2]; + bufferR = new float[(int)(sampleRate * (delay + depth)) + 2]; + + delayTable = new float[(int)Math.Round(sampleRate / frequency)]; + for (var t = 0; t < delayTable.Length; t++) + { + var phase = 2 * Math.PI * t / delayTable.Length; + delayTable[t] = (float)(sampleRate * (delay + depth * Math.Sin(phase))); + } + + bufferIndex = 0; + + delayTableIndexL = 0; + delayTableIndexR = delayTable.Length / 4; + } + + public void Process(float[] inputLeft, float[] inputRight, float[] outputLeft, float[] outputRight) + { + for (var t = 0; t < outputLeft.Length; t++) + { + { + var position = bufferIndex - (double)delayTable[delayTableIndexL]; + if (position < 0.0) + { + position += bufferL.Length; + } + + var index1 = (int)position; + var index2 = index1 + 1; + + if (index2 == bufferL.Length) + { + index2 = 0; + } + + var x1 = (double)bufferL[index1]; + var x2 = (double)bufferL[index2]; + var a = position - index1; + outputLeft[t] = (float)(x1 + a * (x2 - x1)); + + delayTableIndexL++; + if (delayTableIndexL == delayTable.Length) + { + delayTableIndexL = 0; + } + } + + { + var position = bufferIndex - (double)delayTable[delayTableIndexR]; + if (position < 0.0) + { + position += bufferR.Length; + } + + var index1 = (int)position; + var index2 = index1 + 1; + + if (index2 == bufferR.Length) + { + index2 = 0; + } + + var x1 = (double)bufferR[index1]; + var x2 = (double)bufferR[index2]; + var a = position - index1; + outputRight[t] = (float)(x1 + a * (x2 - x1)); + + delayTableIndexR++; + if (delayTableIndexR == delayTable.Length) + { + delayTableIndexR = 0; + } + } + + bufferL[bufferIndex] = inputLeft[t]; + bufferR[bufferIndex] = inputRight[t]; + bufferIndex++; + if (bufferIndex == bufferL.Length) + { + bufferIndex = 0; + } + } + } + + public void Mute() + { + Array.Clear(bufferL, 0, bufferL.Length); + Array.Clear(bufferR, 0, bufferR.Length); + } + } +} diff --git a/Assets/Scripts/Infrastructure/EQ/MeltySynth/Chorus.cs.meta b/Assets/Scripts/Infrastructure/EQ/MeltySynth/Chorus.cs.meta new file mode 100644 index 0000000..a399afa --- /dev/null +++ b/Assets/Scripts/Infrastructure/EQ/MeltySynth/Chorus.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 2d08f91227eb52649ac686998ad0a341 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Scripts/Infrastructure/EQ/MeltySynth/Generator.cs b/Assets/Scripts/Infrastructure/EQ/MeltySynth/Generator.cs new file mode 100644 index 0000000..1fcc8a6 --- /dev/null +++ b/Assets/Scripts/Infrastructure/EQ/MeltySynth/Generator.cs @@ -0,0 +1,39 @@ +using System.IO; + +namespace Infrastructure.EQ.MeltySynth +{ + internal struct Generator + { + private readonly GeneratorType type; + private readonly ushort value; + + private Generator(BinaryReader reader) + { + type = (GeneratorType)reader.ReadUInt16(); + value = reader.ReadUInt16(); + } + + internal static Generator[] ReadFromChunk(BinaryReader reader, int size) + { + if (size % 4 != 0) + { + throw new InvalidDataException("The generator list is invalid."); + } + + var generators = new Generator[size / 4 - 1]; + + for (var i = 0; i < generators.Length; i++) + { + generators[i] = new Generator(reader); + } + + // The last one is the terminator. + new Generator(reader); + + return generators; + } + + public GeneratorType Type => type; + public ushort Value => value; + } +} diff --git a/Assets/Scripts/Infrastructure/EQ/MeltySynth/Generator.cs.meta b/Assets/Scripts/Infrastructure/EQ/MeltySynth/Generator.cs.meta new file mode 100644 index 0000000..412458e --- /dev/null +++ b/Assets/Scripts/Infrastructure/EQ/MeltySynth/Generator.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 2a593dfe78199a6448207a54fcbddae5 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Scripts/Infrastructure/EQ/MeltySynth/GeneratorType.cs b/Assets/Scripts/Infrastructure/EQ/MeltySynth/GeneratorType.cs new file mode 100644 index 0000000..ebb3a33 --- /dev/null +++ b/Assets/Scripts/Infrastructure/EQ/MeltySynth/GeneratorType.cs @@ -0,0 +1,67 @@ +namespace Infrastructure.EQ.MeltySynth +{ + internal enum GeneratorType : ushort + { + StartAddressOffset = 0, + EndAddressOffset = 1, + StartLoopAddressOffset = 2, + EndLoopAddressOffset = 3, + StartAddressCoarseOffset = 4, + ModulationLfoToPitch = 5, + VibratoLfoToPitch = 6, + ModulationEnvelopeToPitch = 7, + InitialFilterCutoffFrequency = 8, + InitialFilterQ = 9, + ModulationLfoToFilterCutoffFrequency = 10, + ModulationEnvelopeToFilterCutoffFrequency = 11, + EndAddressCoarseOffset = 12, + ModulationLfoToVolume = 13, + Unused1 = 14, + ChorusEffectsSend = 15, + ReverbEffectsSend = 16, + Pan = 17, + Unused2 = 18, + Unused3 = 19, + Unused4 = 20, + DelayModulationLfo = 21, + FrequencyModulationLfo = 22, + DelayVibratoLfo = 23, + FrequencyVibratoLfo = 24, + DelayModulationEnvelope = 25, + AttackModulationEnvelope = 26, + HoldModulationEnvelope = 27, + DecayModulationEnvelope = 28, + SustainModulationEnvelope = 29, + ReleaseModulationEnvelope = 30, + KeyNumberToModulationEnvelopeHold = 31, + KeyNumberToModulationEnvelopeDecay = 32, + DelayVolumeEnvelope = 33, + AttackVolumeEnvelope = 34, + HoldVolumeEnvelope = 35, + DecayVolumeEnvelope = 36, + SustainVolumeEnvelope = 37, + ReleaseVolumeEnvelope = 38, + KeyNumberToVolumeEnvelopeHold = 39, + KeyNumberToVolumeEnvelopeDecay = 40, + Instrument = 41, + Reserved1 = 42, + KeyRange = 43, + VelocityRange = 44, + StartLoopAddressCoarseOffset = 45, + KeyNumber = 46, + Velocity = 47, + InitialAttenuation = 48, + Reserved2 = 49, + EndLoopAddressCoarseOffset = 50, + CoarseTune = 51, + FineTune = 52, + SampleID = 53, + SampleModes = 54, + Reserved3 = 55, + ScaleTuning = 56, + ExclusiveClass = 57, + OverridingRootKey = 58, + Unused5 = 59, + UnusedEnd = 60 + } +} diff --git a/Assets/Scripts/Infrastructure/EQ/MeltySynth/GeneratorType.cs.meta b/Assets/Scripts/Infrastructure/EQ/MeltySynth/GeneratorType.cs.meta new file mode 100644 index 0000000..26807cb --- /dev/null +++ b/Assets/Scripts/Infrastructure/EQ/MeltySynth/GeneratorType.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 4dfc500381f8e384e811b46e824893e3 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Scripts/Infrastructure/EQ/MeltySynth/IAudioRenderer.cs b/Assets/Scripts/Infrastructure/EQ/MeltySynth/IAudioRenderer.cs new file mode 100644 index 0000000..5e8c131 --- /dev/null +++ b/Assets/Scripts/Infrastructure/EQ/MeltySynth/IAudioRenderer.cs @@ -0,0 +1,20 @@ +using System; + +namespace Infrastructure.EQ.MeltySynth +{ + /// + /// Defines a common interface for audio rendering. + /// + public interface IAudioRenderer + { + /// + /// Renders the waveform. + /// + /// The buffer of the left channel to store the rendered waveform. + /// The buffer of the right channel to store the rendered waveform. + /// + /// The output buffers for the left and right must be the same length. + /// + void Render(Span left, Span right); + } +} diff --git a/Assets/Scripts/Infrastructure/EQ/MeltySynth/IAudioRenderer.cs.meta b/Assets/Scripts/Infrastructure/EQ/MeltySynth/IAudioRenderer.cs.meta new file mode 100644 index 0000000..fbf8272 --- /dev/null +++ b/Assets/Scripts/Infrastructure/EQ/MeltySynth/IAudioRenderer.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 8e13fd2e96f6b96468b05f80b2a5f0b6 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Scripts/Infrastructure/EQ/MeltySynth/Instrument.cs b/Assets/Scripts/Infrastructure/EQ/MeltySynth/Instrument.cs new file mode 100644 index 0000000..5798eb3 --- /dev/null +++ b/Assets/Scripts/Infrastructure/EQ/MeltySynth/Instrument.cs @@ -0,0 +1,80 @@ +using System; +using System.Collections.Generic; +using System.IO; + +namespace Infrastructure.EQ.MeltySynth +{ + /// + /// Represents an instrument in the SoundFont. + /// + public sealed class Instrument + { + internal static readonly Instrument Default = new Instrument(); + + private readonly string name; + private readonly InstrumentRegion[] regions; + + private Instrument() + { + name = "Default"; + regions = Array.Empty(); + } + + private Instrument(InstrumentInfo info, Zone[] zones, SampleHeader[] samples) + { + this.name = info.Name; + + var zoneCount = info.ZoneEndIndex - info.ZoneStartIndex + 1; + if (zoneCount <= 0) + { + throw new InvalidDataException($"The instrument '{info.Name}' has no zone."); + } + + var zoneSpan = zones.AsSpan(info.ZoneStartIndex, zoneCount); + + regions = InstrumentRegion.Create(this, zoneSpan, samples); + } + + internal static Instrument[] Create(InstrumentInfo[] infos, Zone[] zones, SampleHeader[] samples) + { + if (infos.Length <= 1) + { + throw new InvalidDataException("No valid instrument was found."); + } + + // The last one is the terminator. + var instruments = new Instrument[infos.Length - 1]; + + for (var i = 0; i < instruments.Length; i++) + { + instruments[i] = new Instrument(infos[i], zones, samples); + } + + return instruments; + } + + /// + /// Gets the name of the instrument. + /// + /// + /// The name of the instrument. + /// + public override string ToString() + { + return name; + } + + /// + /// The name of the instrument. + /// + public string Name => name; + + /// + /// The regions of the instrument. + /// + public IReadOnlyList Regions => regions; + + // Internally exposes the raw array for fast enumeration. + internal InstrumentRegion[] RegionArray => regions; + } +} diff --git a/Assets/Scripts/Infrastructure/EQ/MeltySynth/Instrument.cs.meta b/Assets/Scripts/Infrastructure/EQ/MeltySynth/Instrument.cs.meta new file mode 100644 index 0000000..7ed9b0b --- /dev/null +++ b/Assets/Scripts/Infrastructure/EQ/MeltySynth/Instrument.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 0949c7101cdb83b40a1015b0bc3e6507 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Scripts/Infrastructure/EQ/MeltySynth/InstrumentInfo.cs b/Assets/Scripts/Infrastructure/EQ/MeltySynth/InstrumentInfo.cs new file mode 100644 index 0000000..b7f1f2e --- /dev/null +++ b/Assets/Scripts/Infrastructure/EQ/MeltySynth/InstrumentInfo.cs @@ -0,0 +1,45 @@ +using System.IO; + +namespace Infrastructure.EQ.MeltySynth +{ + internal sealed class InstrumentInfo + { + private string name; + private int zoneStartIndex; + private int zoneEndIndex; + + private InstrumentInfo(BinaryReader reader) + { + name = reader.ReadFixedLengthString(20); + zoneStartIndex = reader.ReadUInt16(); + } + + internal static InstrumentInfo[] ReadFromChunk(BinaryReader reader, int size) + { + if (size % 22 != 0) + { + throw new InvalidDataException("The instrument list is invalid."); + } + + var count = size / 22; + + var instruments = new InstrumentInfo[count]; + + for (var i = 0; i < count; i++) + { + instruments[i] = new InstrumentInfo(reader); + } + + for (var i = 0; i < count - 1; i++) + { + instruments[i].zoneEndIndex = instruments[i + 1].zoneStartIndex - 1; + } + + return instruments; + } + + public string Name => name; + public int ZoneStartIndex => zoneStartIndex; + public int ZoneEndIndex => zoneEndIndex; + } +} diff --git a/Assets/Scripts/Infrastructure/EQ/MeltySynth/InstrumentInfo.cs.meta b/Assets/Scripts/Infrastructure/EQ/MeltySynth/InstrumentInfo.cs.meta new file mode 100644 index 0000000..f04e43b --- /dev/null +++ b/Assets/Scripts/Infrastructure/EQ/MeltySynth/InstrumentInfo.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 3271c75a303649940949ca36fcdfea81 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Scripts/Infrastructure/EQ/MeltySynth/InstrumentRegion.cs b/Assets/Scripts/Infrastructure/EQ/MeltySynth/InstrumentRegion.cs new file mode 100644 index 0000000..958e2bf --- /dev/null +++ b/Assets/Scripts/Infrastructure/EQ/MeltySynth/InstrumentRegion.cs @@ -0,0 +1,204 @@ +using System; +using System.IO; +using System.Linq; + +namespace Infrastructure.EQ.MeltySynth +{ + /// + /// Represents an instrument region. + /// + /// + /// An instrument region contains all the parameters necessary to synthesize a note. + /// + public sealed class InstrumentRegion + { + internal static readonly InstrumentRegion Default = new InstrumentRegion(); + + private readonly short[] gs; + + private readonly SampleHeader sample; + + private InstrumentRegion() + { + gs = new short[61]; + gs[(int)GeneratorType.InitialFilterCutoffFrequency] = 13500; + gs[(int)GeneratorType.DelayModulationLfo] = -12000; + gs[(int)GeneratorType.DelayVibratoLfo] = -12000; + gs[(int)GeneratorType.DelayModulationEnvelope] = -12000; + gs[(int)GeneratorType.AttackModulationEnvelope] = -12000; + gs[(int)GeneratorType.HoldModulationEnvelope] = -12000; + gs[(int)GeneratorType.DecayModulationEnvelope] = -12000; + gs[(int)GeneratorType.ReleaseModulationEnvelope] = -12000; + gs[(int)GeneratorType.DelayVolumeEnvelope] = -12000; + gs[(int)GeneratorType.AttackVolumeEnvelope] = -12000; + gs[(int)GeneratorType.HoldVolumeEnvelope] = -12000; + gs[(int)GeneratorType.DecayVolumeEnvelope] = -12000; + gs[(int)GeneratorType.ReleaseVolumeEnvelope] = -12000; + gs[(int)GeneratorType.KeyRange] = 0x7F00; + gs[(int)GeneratorType.VelocityRange] = 0x7F00; + gs[(int)GeneratorType.KeyNumber] = -1; + gs[(int)GeneratorType.Velocity] = -1; + gs[(int)GeneratorType.ScaleTuning] = 100; + gs[(int)GeneratorType.OverridingRootKey] = -1; + + sample = SampleHeader.Default; + } + + private InstrumentRegion(Instrument instrument, Zone global, Zone local, SampleHeader[] samples) : this() + { + foreach (var generator in global.Generators) + { + SetParameter(generator); + } + + foreach (var generator in local.Generators) + { + SetParameter(generator); + } + + var id = gs[(int)GeneratorType.SampleID]; + if (!(0 <= id && id < samples.Length)) + { + throw new InvalidDataException($"The instrument '{instrument.Name}' contains an invalid sample ID '{id}'."); + } + + sample = samples[id]; + } + + internal static InstrumentRegion[] Create(Instrument instrument, Span zones, SampleHeader[] samples) + { + // Is the first one the global zone? + if (zones[0].Generators.Count == 0 || zones[0].Generators.Last().Type != GeneratorType.SampleID) + { + // The first one is the global zone. + var global = zones[0]; + + // The global zone is regarded as the base setting of subsequent zones. + var regions = new InstrumentRegion[zones.Length - 1]; + for (var i = 0; i < regions.Length; i++) + { + regions[i] = new InstrumentRegion(instrument, global, zones[i + 1], samples); + } + return regions; + } + else + { + // No global zone. + var regions = new InstrumentRegion[zones.Length]; + for (var i = 0; i < regions.Length; i++) + { + regions[i] = new InstrumentRegion(instrument, Zone.Empty, zones[i], samples); + } + return regions; + } + } + + private void SetParameter(Generator generator) + { + var index = (int)generator.Type; + + // Unknown generators should be ignored. + if (0 <= index && index < gs.Length) + { + gs[index] = (short)generator.Value; + } + } + + /// + /// Checks if the region covers the given key and velocity. + /// + /// The key of a note. + /// The velocity of a note. + /// + /// true if the region covers the given key and velocity. + /// + public bool Contains(int key, int velocity) + { + var containsKey = KeyRangeStart <= key && key <= KeyRangeEnd; + var containsVelocity = VelocityRangeStart <= velocity && velocity <= VelocityRangeEnd; + return containsKey && containsVelocity; + } + + /// + /// Gets the string representation of the region. + /// + /// + /// The string representation of the region. + /// + public override string ToString() + { + return $"{sample.Name} (Key: {KeyRangeStart}-{KeyRangeEnd}, Velocity: {VelocityRangeStart}-{VelocityRangeEnd})"; + } + + internal short this[GeneratorType generatortType] => gs[(int)generatortType]; + + /// + /// The sample corresponding to the region. + /// + public SampleHeader Sample => sample; + +#pragma warning disable CS1591 // I'm too lazy to add comments for all the following things. + + public int SampleStart => sample.Start + StartAddressOffset; + public int SampleEnd => sample.End + EndAddressOffset; + public int SampleStartLoop => sample.StartLoop + StartLoopAddressOffset; + public int SampleEndLoop => sample.EndLoop + EndLoopAddressOffset; + + public int StartAddressOffset => 32768 * this[GeneratorType.StartAddressCoarseOffset] + this[GeneratorType.StartAddressOffset]; + public int EndAddressOffset => 32768 * this[GeneratorType.EndAddressCoarseOffset] + this[GeneratorType.EndAddressOffset]; + public int StartLoopAddressOffset => 32768 * this[GeneratorType.StartLoopAddressCoarseOffset] + this[GeneratorType.StartLoopAddressOffset]; + public int EndLoopAddressOffset => 32768 * this[GeneratorType.EndLoopAddressCoarseOffset] + this[GeneratorType.EndLoopAddressOffset]; + + public int ModulationLfoToPitch => this[GeneratorType.ModulationLfoToPitch]; + public int VibratoLfoToPitch => this[GeneratorType.VibratoLfoToPitch]; + public int ModulationEnvelopeToPitch => this[GeneratorType.ModulationEnvelopeToPitch]; + public float InitialFilterCutoffFrequency => SoundFontMath.CentsToHertz(this[GeneratorType.InitialFilterCutoffFrequency]); + public float InitialFilterQ => 0.1F * this[GeneratorType.InitialFilterQ]; + public int ModulationLfoToFilterCutoffFrequency => this[GeneratorType.ModulationLfoToFilterCutoffFrequency]; + public int ModulationEnvelopeToFilterCutoffFrequency => this[GeneratorType.ModulationEnvelopeToFilterCutoffFrequency]; + + public float ModulationLfoToVolume => 0.1F * this[GeneratorType.ModulationLfoToVolume]; + + public float ChorusEffectsSend => 0.1F * this[GeneratorType.ChorusEffectsSend]; + public float ReverbEffectsSend => 0.1F * this[GeneratorType.ReverbEffectsSend]; + public float Pan => 0.1F * this[GeneratorType.Pan]; + + public float DelayModulationLfo => SoundFontMath.TimecentsToSeconds(this[GeneratorType.DelayModulationLfo]); + public float FrequencyModulationLfo => SoundFontMath.CentsToHertz(this[GeneratorType.FrequencyModulationLfo]); + public float DelayVibratoLfo => SoundFontMath.TimecentsToSeconds(this[GeneratorType.DelayVibratoLfo]); + public float FrequencyVibratoLfo => SoundFontMath.CentsToHertz(this[GeneratorType.FrequencyVibratoLfo]); + public float DelayModulationEnvelope => SoundFontMath.TimecentsToSeconds(this[GeneratorType.DelayModulationEnvelope]); + public float AttackModulationEnvelope => SoundFontMath.TimecentsToSeconds(this[GeneratorType.AttackModulationEnvelope]); + public float HoldModulationEnvelope => SoundFontMath.TimecentsToSeconds(this[GeneratorType.HoldModulationEnvelope]); + public float DecayModulationEnvelope => SoundFontMath.TimecentsToSeconds(this[GeneratorType.DecayModulationEnvelope]); + public float SustainModulationEnvelope => 0.1F * this[GeneratorType.SustainModulationEnvelope]; + public float ReleaseModulationEnvelope => SoundFontMath.TimecentsToSeconds(this[GeneratorType.ReleaseModulationEnvelope]); + public int KeyNumberToModulationEnvelopeHold => this[GeneratorType.KeyNumberToModulationEnvelopeHold]; + public int KeyNumberToModulationEnvelopeDecay => this[GeneratorType.KeyNumberToModulationEnvelopeDecay]; + public float DelayVolumeEnvelope => SoundFontMath.TimecentsToSeconds(this[GeneratorType.DelayVolumeEnvelope]); + public float AttackVolumeEnvelope => SoundFontMath.TimecentsToSeconds(this[GeneratorType.AttackVolumeEnvelope]); + public float HoldVolumeEnvelope => SoundFontMath.TimecentsToSeconds(this[GeneratorType.HoldVolumeEnvelope]); + public float DecayVolumeEnvelope => SoundFontMath.TimecentsToSeconds(this[GeneratorType.DecayVolumeEnvelope]); + public float SustainVolumeEnvelope => 0.1F * this[GeneratorType.SustainVolumeEnvelope]; + public float ReleaseVolumeEnvelope => SoundFontMath.TimecentsToSeconds(this[GeneratorType.ReleaseVolumeEnvelope]); + public int KeyNumberToVolumeEnvelopeHold => this[GeneratorType.KeyNumberToVolumeEnvelopeHold]; + public int KeyNumberToVolumeEnvelopeDecay => this[GeneratorType.KeyNumberToVolumeEnvelopeDecay]; + + public int KeyRangeStart => this[GeneratorType.KeyRange] & 0xFF; + public int KeyRangeEnd => (this[GeneratorType.KeyRange] >> 8) & 0xFF; + public int VelocityRangeStart => this[GeneratorType.VelocityRange] & 0xFF; + public int VelocityRangeEnd => (this[GeneratorType.VelocityRange] >> 8) & 0xFF; + + public float InitialAttenuation => 0.1F * this[GeneratorType.InitialAttenuation]; + + public int CoarseTune => this[GeneratorType.CoarseTune]; + public int FineTune => this[GeneratorType.FineTune] + sample.PitchCorrection; + public LoopMode SampleModes => this[GeneratorType.SampleModes] != 2 ? (LoopMode)this[GeneratorType.SampleModes] : LoopMode.NoLoop; + + public int ScaleTuning => this[GeneratorType.ScaleTuning]; + public int ExclusiveClass => this[GeneratorType.ExclusiveClass]; + public int RootKey => this[GeneratorType.OverridingRootKey] != -1 ? this[GeneratorType.OverridingRootKey] : sample.OriginalPitch; + +#pragma warning restore CS1591 + } +} diff --git a/Assets/Scripts/Infrastructure/EQ/MeltySynth/InstrumentRegion.cs.meta b/Assets/Scripts/Infrastructure/EQ/MeltySynth/InstrumentRegion.cs.meta new file mode 100644 index 0000000..660a802 --- /dev/null +++ b/Assets/Scripts/Infrastructure/EQ/MeltySynth/InstrumentRegion.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 9a761a7f13962ea4b9265ded0a2dbee2 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Scripts/Infrastructure/EQ/MeltySynth/LICENSE.txt b/Assets/Scripts/Infrastructure/EQ/MeltySynth/LICENSE.txt new file mode 100644 index 0000000..4e2e7de --- /dev/null +++ b/Assets/Scripts/Infrastructure/EQ/MeltySynth/LICENSE.txt @@ -0,0 +1,29 @@ +MIT License + +C# Synth (http://csharpsynthproject.codeplex.com): +Copyright (C) 2014 Alex Veltsistas + +TinySoundFont (https://github.com/schellingb/TinySoundFont): +Copyright (C) 2017, 2018 Bernhard Schelling +Based on SFZero, Copyright (C) 2012 Steve Folta (https://github.com/stevefolta/SFZero) + +MeltySynth: +Copyright (C) 2021 Nobuaki Tanaka + +Permission is hereby granted, free of charge, to any person obtaining a copy of +this software and associated documentation files (the "Software"), to deal in +the Software without restriction, including without limitation the rights to +use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies +of the Software, and to permit persons to whom the Software is furnished to do +so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/Assets/Scripts/Infrastructure/EQ/MeltySynth/LICENSE.txt.meta b/Assets/Scripts/Infrastructure/EQ/MeltySynth/LICENSE.txt.meta new file mode 100644 index 0000000..85f00cf --- /dev/null +++ b/Assets/Scripts/Infrastructure/EQ/MeltySynth/LICENSE.txt.meta @@ -0,0 +1,7 @@ +fileFormatVersion: 2 +guid: 64a69e0b3c78fbd4cafa9780fc4b9f99 +TextScriptImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Scripts/Infrastructure/EQ/MeltySynth/Lfo.cs b/Assets/Scripts/Infrastructure/EQ/MeltySynth/Lfo.cs new file mode 100644 index 0000000..0ca6bd3 --- /dev/null +++ b/Assets/Scripts/Infrastructure/EQ/MeltySynth/Lfo.cs @@ -0,0 +1,74 @@ +namespace Infrastructure.EQ.MeltySynth +{ + internal sealed class Lfo + { + private readonly Synthesizer synthesizer; + + private bool active; + + private double delay; + private double period; + + private int processedSampleCount; + private float value; + + internal Lfo(Synthesizer synthesizer) + { + this.synthesizer = synthesizer; + } + + public void Start(float delay, float frequency) + { + if (frequency > 1.0E-3) + { + active = true; + + this.delay = delay; + this.period = 1.0 / frequency; + + processedSampleCount = 0; + value = 0; + } + else + { + active = false; + value = 0; + } + } + + public void Process() + { + if (!active) + { + return; + } + + processedSampleCount += synthesizer.BlockSize; + + var currentTime = (double)processedSampleCount / synthesizer.SampleRate; + + if (currentTime < delay) + { + value = 0; + } + else + { + var phase = ((currentTime - delay) % period) / period; + if (phase < 0.25) + { + value = (float)(4 * phase); + } + else if (phase < 0.75) + { + value = (float)(4 * (0.5 - phase)); + } + else + { + value = (float)(4 * (phase - 1.0)); + } + } + } + + public float Value => value; + } +} diff --git a/Assets/Scripts/Infrastructure/EQ/MeltySynth/Lfo.cs.meta b/Assets/Scripts/Infrastructure/EQ/MeltySynth/Lfo.cs.meta new file mode 100644 index 0000000..40eb3df --- /dev/null +++ b/Assets/Scripts/Infrastructure/EQ/MeltySynth/Lfo.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 8c93a63ab0e885e4bb6234bf697ab13a +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Scripts/Infrastructure/EQ/MeltySynth/LoopMode.cs b/Assets/Scripts/Infrastructure/EQ/MeltySynth/LoopMode.cs new file mode 100644 index 0000000..804b602 --- /dev/null +++ b/Assets/Scripts/Infrastructure/EQ/MeltySynth/LoopMode.cs @@ -0,0 +1,23 @@ +namespace Infrastructure.EQ.MeltySynth +{ + /// + /// Specifies how the synthesizer loops the sample. + /// + public enum LoopMode + { + /// + /// The sample will be played without loop. + /// + NoLoop = 0, + + /// + /// The sample will continuously loop. + /// + Continuous = 1, + + /// + /// The sample will loop until the note stops. + /// + LoopUntilNoteOff = 3 + } +} diff --git a/Assets/Scripts/Infrastructure/EQ/MeltySynth/LoopMode.cs.meta b/Assets/Scripts/Infrastructure/EQ/MeltySynth/LoopMode.cs.meta new file mode 100644 index 0000000..e0e368f --- /dev/null +++ b/Assets/Scripts/Infrastructure/EQ/MeltySynth/LoopMode.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 940062622189d0644926f7e68be628d8 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Scripts/Infrastructure/EQ/MeltySynth/MidiFile.cs b/Assets/Scripts/Infrastructure/EQ/MeltySynth/MidiFile.cs new file mode 100644 index 0000000..70e9d10 --- /dev/null +++ b/Assets/Scripts/Infrastructure/EQ/MeltySynth/MidiFile.cs @@ -0,0 +1,557 @@ +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.IO; +using System.Linq; +using System.Text; + +namespace Infrastructure.EQ.MeltySynth +{ + /// + /// Represents a standard MIDI file. + /// + public sealed class MidiFile + { + private Message[] messages; + private TimeSpan[] times; + + /// + /// Loads a MIDI file from the stream. + /// + /// The data stream used to load the MIDI file. + public MidiFile(Stream stream) + { + if (stream == null) + { + throw new ArgumentNullException(nameof(stream)); + } + + Load(stream, 0, MidiFileLoopType.None); + + // Workaround for nullable warnings in .NET Standard 2.1. + Debug.Assert(messages != null); + Debug.Assert(times != null); + } + + /// + /// Loads a MIDI file from the stream. + /// + /// The data stream used to load the MIDI file. + /// The loop start point in ticks. + public MidiFile(Stream stream, int loopPoint) + { + if (stream == null) + { + throw new ArgumentNullException(nameof(stream)); + } + + if (loopPoint < 0) + { + throw new ArgumentException("The loop point must be a non-negative value.", nameof(loopPoint)); + } + + Load(stream, loopPoint, MidiFileLoopType.None); + + // Workaround for nullable warnings in .NET Standard 2.1. + Debug.Assert(messages != null); + Debug.Assert(times != null); + } + + /// + /// Loads a MIDI file from the stream. + /// + /// The data stream used to load the MIDI file. + /// The type of the loop extension to be used. + public MidiFile(Stream stream, MidiFileLoopType loopType) + { + if (stream == null) + { + throw new ArgumentNullException(nameof(stream)); + } + + Load(stream, 0, loopType); + + // Workaround for nullable warnings in .NET Standard 2.1. + Debug.Assert(messages != null); + Debug.Assert(times != null); + } + + /// + /// Loads a MIDI file from the file. + /// + /// The MIDI file name and path. + public MidiFile(string path) + { + if (path == null) + { + throw new ArgumentNullException(nameof(path)); + } + + using (var stream = new FileStream(path, FileMode.Open, FileAccess.Read)) + { + Load(stream, 0, MidiFileLoopType.None); + } + + // Workaround for nullable warnings in .NET Standard 2.1. + Debug.Assert(messages != null); + Debug.Assert(times != null); + } + + /// + /// Loads a MIDI file from the file. + /// + /// The MIDI file name and path. + /// The loop start point in ticks. + public MidiFile(string path, int loopPoint) + { + if (path == null) + { + throw new ArgumentNullException(nameof(path)); + } + + if (loopPoint < 0) + { + throw new ArgumentException("The loop point must be a non-negative value.", nameof(loopPoint)); + } + + using (var stream = new FileStream(path, FileMode.Open, FileAccess.Read)) + { + Load(stream, loopPoint, MidiFileLoopType.None); + } + + // Workaround for nullable warnings in .NET Standard 2.1. + Debug.Assert(messages != null); + Debug.Assert(times != null); + } + + /// + /// Loads a MIDI file from the file. + /// + /// The MIDI file name and path. + /// The type of the loop extension to be used. + public MidiFile(string path, MidiFileLoopType loopType) + { + if (path == null) + { + throw new ArgumentNullException(nameof(path)); + } + + using (var stream = new FileStream(path, FileMode.Open, FileAccess.Read)) + { + Load(stream, 0, loopType); + } + + // Workaround for nullable warnings in .NET Standard 2.1. + Debug.Assert(messages != null); + Debug.Assert(times != null); + } + + // Some .NET implementations round TimeSpan to the nearest millisecond, + // and the timing of MIDI messages will be wrong. + // This method makes TimeSpan without rounding. + internal static TimeSpan GetTimeSpanFromSeconds(double value) + { + return new TimeSpan((long)(TimeSpan.TicksPerSecond * value)); + } + + private void Load(Stream stream, int loopPoint, MidiFileLoopType loopType) + { + using (var reader = new BinaryReader(stream, Encoding.ASCII, true)) + { + var chunkType = reader.ReadFourCC(); + if (chunkType != "MThd") + { + throw new InvalidDataException($"The chunk type must be 'MThd', but was '{chunkType}'."); + } + + var size = reader.ReadInt32BigEndian(); + if (size != 6) + { + throw new InvalidDataException($"The MThd chunk has invalid data."); + } + + var format = reader.ReadInt16BigEndian(); + if (!(format == 0 || format == 1)) + { + throw new NotSupportedException($"The format {format} is not supported."); + } + + var trackCount = reader.ReadInt16BigEndian(); + var resolution = reader.ReadInt16BigEndian(); + + var messageLists = new List[trackCount]; + var tickLists = new List[trackCount]; + for (var i = 0; i < trackCount; i++) + { + (messageLists[i], tickLists[i]) = ReadTrack(reader, loopType); + } + + if (loopPoint != 0) + { + var tickList = tickLists[0]; + var messageList = messageLists[0]; + if (loopPoint <= tickList.Last()) + { + for (var i = 0; i < tickList.Count; i++) + { + if (tickList[i] >= loopPoint) + { + tickList.Insert(i, loopPoint); + messageList.Insert(i, Message.LoopStart()); + break; + } + } + } + else + { + tickList.Add(loopPoint); + messageList.Add(Message.LoopStart()); + } + } + + (messages, times) = MergeTracks(messageLists, tickLists, resolution); + } + } + + private static (List, List) ReadTrack(BinaryReader reader, MidiFileLoopType loopType) + { + var chunkType = reader.ReadFourCC(); + if (chunkType != "MTrk") + { + throw new InvalidDataException($"The chunk type must be 'MTrk', but was '{chunkType}'."); + } + + reader.ReadInt32BigEndian(); + + var messages = new List(); + var ticks = new List(); + + int tick = 0; + byte lastStatus = 0; + + while (true) + { + var delta = reader.ReadIntVariableLength(); + var first = reader.ReadByte(); + + try + { + tick = checked(tick + delta); + } + catch (OverflowException) + { + throw new NotSupportedException("Long MIDI file is not supported."); + } + + if ((first & 128) == 0) + { + var command = lastStatus & 0xF0; + if (command == 0xC0 || command == 0xD0) + { + messages.Add(Message.Common(lastStatus, first)); + ticks.Add(tick); + } + else + { + var data2 = reader.ReadByte(); + messages.Add(Message.Common(lastStatus, first, data2, loopType)); + ticks.Add(tick); + } + + continue; + } + + switch (first) + { + case 0xF0: // System Exclusive + DiscardData(reader); + break; + + case 0xF7: // System Exclusive + DiscardData(reader); + break; + + case 0xFF: // Meta Event + switch (reader.ReadByte()) + { + case 0x2F: // End of Track + reader.ReadByte(); + messages.Add(Message.EndOfTrack()); + ticks.Add(tick); + return (messages, ticks); + + case 0x51: // Tempo + messages.Add(Message.TempoChange(ReadTempo(reader))); + ticks.Add(tick); + break; + + default: + DiscardData(reader); + break; + } + break; + + default: + var command = first & 0xF0; + if (command == 0xC0 || command == 0xD0) + { + var data1 = reader.ReadByte(); + messages.Add(Message.Common(first, data1)); + ticks.Add(tick); + } + else + { + var data1 = reader.ReadByte(); + var data2 = reader.ReadByte(); + messages.Add(Message.Common(first, data1, data2, loopType)); + ticks.Add(tick); + } + break; + } + + lastStatus = first; + } + } + + private static (Message[], TimeSpan[]) MergeTracks(List[] messageLists, List[] tickLists, int resolution) + { + var mergedMessages = new List(); + var mergedTimes = new List(); + + var indices = new int[messageLists.Length]; + + var currentTick = 0; + var currentTime = TimeSpan.Zero; + + var tempo = 120.0; + + while (true) + { + var minTick = int.MaxValue; + var minIndex = -1; + for (var ch = 0; ch < tickLists.Length; ch++) + { + if (indices[ch] < tickLists[ch].Count) + { + var tick = tickLists[ch][indices[ch]]; + if (tick < minTick) + { + minTick = tick; + minIndex = ch; + } + } + } + + if (minIndex == -1) + { + break; + } + + var nextTick = tickLists[minIndex][indices[minIndex]]; + var deltaTick = nextTick - currentTick; + var deltaTime = GetTimeSpanFromSeconds(60.0 / (resolution * tempo) * deltaTick); + + currentTick += deltaTick; + currentTime += deltaTime; + + var message = messageLists[minIndex][indices[minIndex]]; + if (message.Type == MessageType.TempoChange) + { + tempo = message.Tempo; + } + else + { + mergedMessages.Add(message); + mergedTimes.Add(currentTime); + } + + indices[minIndex]++; + } + + return (mergedMessages.ToArray(), mergedTimes.ToArray()); + } + + private static int ReadTempo(BinaryReader reader) + { + var size = reader.ReadIntVariableLength(); + if (size != 3) + { + throw new InvalidDataException("Failed to read the tempo value."); + } + + var b1 = reader.ReadByte(); + var b2 = reader.ReadByte(); + var b3 = reader.ReadByte(); + return (b1 << 16) | (b2 << 8) | b3; + } + + private static void DiscardData(BinaryReader reader) + { + var size = reader.ReadIntVariableLength(); + reader.BaseStream.Position += size; + } + + /// + /// The length of the MIDI file. + /// + public TimeSpan Length => times.Last(); + + internal Message[] Messages => messages; + internal TimeSpan[] Times => times; + + + + internal struct Message + { + private byte channel; + private byte command; + private byte data1; + private byte data2; + + private Message(byte channel, byte command, byte data1, byte data2) + { + this.channel = channel; + this.command = command; + this.data1 = data1; + this.data2 = data2; + } + + public static Message Common(byte status, byte data1) + { + byte channel = (byte)(status & 0x0F); + byte command = (byte)(status & 0xF0); + byte data2 = 0; + return new Message(channel, command, data1, data2); + } + + public static Message Common(byte status, byte data1, byte data2, MidiFileLoopType loopType) + { + byte channel = (byte)(status & 0x0F); + byte command = (byte)(status & 0xF0); + + if (command == 0xB0) + { + switch (loopType) + { + case MidiFileLoopType.RpgMaker: + if (data1 == 111) + { + return LoopStart(); + } + break; + + case MidiFileLoopType.IncredibleMachine: + if (data1 == 110) + { + return LoopStart(); + } + if (data1 == 111) + { + return LoopEnd(); + } + break; + + case MidiFileLoopType.FinalFantasy: + if (data1 == 116) + { + return LoopStart(); + } + if (data1 == 117) + { + return LoopEnd(); + } + break; + } + } + + return new Message(channel, command, data1, data2); + } + + public static Message TempoChange(int tempo) + { + byte command = (byte)(tempo >> 16); + byte data1 = (byte)(tempo >> 8); + byte data2 = (byte)(tempo); + return new Message((int)MessageType.TempoChange, command, data1, data2); + } + + public static Message LoopStart() + { + return new Message((int)MessageType.LoopStart, 0, 0, 0); + } + + public static Message LoopEnd() + { + return new Message((int)MessageType.LoopEnd, 0, 0, 0); + } + + public static Message EndOfTrack() + { + return new Message((int)MessageType.EndOfTrack, 0, 0, 0); + } + + public override string ToString() + { + switch (channel) + { + case (int)MessageType.TempoChange: + return "Tempo: " + Tempo; + + case (int)MessageType.LoopStart: + return "LoopStart"; + + case (int)MessageType.LoopEnd: + return "LoopEnd"; + + case (int)MessageType.EndOfTrack: + return "EndOfTrack"; + + default: + return "CH" + channel + ": " + command.ToString("X2") + ", " + data1.ToString("X2") + ", " + data2.ToString("X2"); + } + } + + public MessageType Type + { + get + { + switch (channel) + { + case (int)MessageType.TempoChange: + return MessageType.TempoChange; + + case (int)MessageType.LoopStart: + return MessageType.LoopStart; + + case (int)MessageType.LoopEnd: + return MessageType.LoopEnd; + + case (int)MessageType.EndOfTrack: + return MessageType.EndOfTrack; + + default: + return MessageType.Normal; + } + } + } + + public byte Channel => channel; + public byte Command => command; + public byte Data1 => data1; + public byte Data2 => data2; + + public double Tempo => 60000000.0 / ((command << 16) | (data1 << 8) | data2); + } + + + + internal enum MessageType + { + Normal = 0, + TempoChange = 252, + LoopStart = 253, + LoopEnd = 254, + EndOfTrack = 255 + } + } +} diff --git a/Assets/Scripts/Infrastructure/EQ/MeltySynth/MidiFile.cs.meta b/Assets/Scripts/Infrastructure/EQ/MeltySynth/MidiFile.cs.meta new file mode 100644 index 0000000..b938dca --- /dev/null +++ b/Assets/Scripts/Infrastructure/EQ/MeltySynth/MidiFile.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 6b8d8a21cace10c42894ae7d3da21b87 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Scripts/Infrastructure/EQ/MeltySynth/MidiFileLoopType.cs b/Assets/Scripts/Infrastructure/EQ/MeltySynth/MidiFileLoopType.cs new file mode 100644 index 0000000..3d85947 --- /dev/null +++ b/Assets/Scripts/Infrastructure/EQ/MeltySynth/MidiFileLoopType.cs @@ -0,0 +1,31 @@ +namespace Infrastructure.EQ.MeltySynth +{ + /// + /// Specifies the non-standard loop extension for MIDI files. + /// + public enum MidiFileLoopType + { + /// + /// No special loop extension. + /// + None = 0, + + /// + /// The RPG Maker style loop. + /// CC #111 will be the loop start point. + /// + RpgMaker, + + /// + /// The Incredible Machine style loop. + /// CC #110 and #111 will be the loop start point and end point, respectively. + /// + IncredibleMachine, + + /// + /// The Final Fantasy style loop. + /// CC #116 and #117 will be the loop start point and end point, respectively. + /// + FinalFantasy + } +} diff --git a/Assets/Scripts/Infrastructure/EQ/MeltySynth/MidiFileLoopType.cs.meta b/Assets/Scripts/Infrastructure/EQ/MeltySynth/MidiFileLoopType.cs.meta new file mode 100644 index 0000000..92408b8 --- /dev/null +++ b/Assets/Scripts/Infrastructure/EQ/MeltySynth/MidiFileLoopType.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 8cc53b0f8fabd2c41bdb12fe6e9fbb05 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Scripts/Infrastructure/EQ/MeltySynth/MidiFileSequencer.cs b/Assets/Scripts/Infrastructure/EQ/MeltySynth/MidiFileSequencer.cs new file mode 100644 index 0000000..cc18ef9 --- /dev/null +++ b/Assets/Scripts/Infrastructure/EQ/MeltySynth/MidiFileSequencer.cs @@ -0,0 +1,275 @@ +using System; + +namespace Infrastructure.EQ.MeltySynth +{ + /// + /// An instance of the MIDI file sequencer. + /// + /// + /// Note that this class does not provide thread safety. + /// If you want to do playback control and render the waveform in separate threads, + /// you must ensure that the methods will not be called simultaneously. + /// + public sealed class MidiFileSequencer : IAudioRenderer + { + private readonly Synthesizer synthesizer; + + private float speed; + + private MidiFile? midiFile; + private bool loop; + + private int blockWrote; + + private TimeSpan currentTime; + private int msgIndex; + private int loopIndex; + + private MessageHook? onSendMessage; + + /// + /// Initializes a new instance of the sequencer. + /// + /// The synthesizer to be handled by the sequencer. + public MidiFileSequencer(Synthesizer synthesizer) + { + if (synthesizer == null) + { + throw new ArgumentNullException(nameof(synthesizer)); + } + + this.synthesizer = synthesizer; + + speed = 1F; + } + + /// + /// Plays the MIDI file. + /// + /// The MIDI file to be played. + /// If true, the MIDI file loops after reaching the end. + public void Play(MidiFile midiFile, bool loop) + { + if (midiFile == null) + { + throw new ArgumentNullException(nameof(midiFile)); + } + + this.midiFile = midiFile; + this.loop = loop; + + blockWrote = synthesizer.BlockSize; + + currentTime = TimeSpan.Zero; + msgIndex = 0; + loopIndex = 0; + + synthesizer.Reset(); + } + + /// + /// Stop playing. + /// + public void Stop() + { + midiFile = null; + + synthesizer.Reset(); + } + + /// + public void Render(Span left, Span right) + { + if (left.Length != right.Length) + { + throw new ArgumentException("The output buffers for the left and right must be the same length."); + } + + var wrote = 0; + while (wrote < left.Length) + { + if (blockWrote == synthesizer.BlockSize) + { + ProcessEvents(); + blockWrote = 0; + currentTime += MidiFile.GetTimeSpanFromSeconds((double)speed * synthesizer.BlockSize / synthesizer.SampleRate); + } + + // LANTERN + // quickly changing tracks can caused srcRem to be negative. + var srcRem = Math.Max(0, synthesizer.BlockSize - blockWrote); + var dstRem = Math.Max(0, left.Length - wrote); + // var srcRem = synthesizer.BlockSize - blockWrote; + // var dstRem = left.Length - wrote; + var rem = Math.Min(srcRem, dstRem); + + synthesizer.Render(left.Slice(wrote, rem), right.Slice(wrote, rem)); + + blockWrote += rem; + wrote += rem; + } + } + + /// + /// LANTERN + /// Seek playback. + /// + /// The index in ticks to seek to. + public void Seek(int seekIndex) + { + if (midiFile == null) + { + return; + } + + seekIndex = Math.Clamp(seekIndex, 0, midiFile.Messages.Length); + + currentTime = midiFile.Times[seekIndex]; + msgIndex = seekIndex; + synthesizer.NoteOffAll(false); + } + + private void ProcessEvents() + { + if (midiFile == null) + { + return; + } + + while (msgIndex < midiFile.Messages.Length) + { + var time = midiFile.Times[msgIndex]; + var msg = midiFile.Messages[msgIndex]; + if (time <= currentTime) + { + if (msg.Type == MidiFile.MessageType.Normal) + { + if (onSendMessage == null) + { + synthesizer.ProcessMidiMessage(msg.Channel, msg.Command, msg.Data1, msg.Data2); + } + else + { + onSendMessage(synthesizer, msg.Channel, msg.Command, msg.Data1, msg.Data2); + } + } + else if (loop) + { + if (msg.Type == MidiFile.MessageType.LoopStart) + { + loopIndex = msgIndex; + } + else if (msg.Type == MidiFile.MessageType.LoopEnd) + { + currentTime = midiFile.Times[loopIndex]; + msgIndex = loopIndex; + synthesizer.NoteOffAll(false); + } + } + msgIndex++; + } + else + { + break; + } + } + + if (msgIndex == midiFile.Messages.Length && loop) + { + currentTime = midiFile.Times[loopIndex]; + msgIndex = loopIndex; + synthesizer.NoteOffAll(false); + } + } + + /// + /// Gets the synthesizer handled by the sequencer. + /// + public Synthesizer Synthesizer => synthesizer; + + /// + /// Gets the current playback position. + /// + public TimeSpan Position => currentTime; + + /// + /// LANTERN + /// Gets the current playback message index. + /// + public int MessageIndex => msgIndex; + + /// + /// LANTERN + /// Gets the total count of messages for the current playback. + /// + public int MessageCount => midiFile?.Messages?.Length ?? 0; + + /// + /// Gets a value that indicates whether the current playback position is at the end of the sequence. + /// + /// + /// If the Play method has not yet been called, this value is true. + /// This value will never be true if loop playback is enabled. + /// + public bool EndOfSequence + { + get + { + if (midiFile == null) + { + return true; + } + else + { + return msgIndex == midiFile.Messages.Length; + } + } + } + + /// + /// Gets or sets the playback speed. + /// + /// + /// The default value is 1. + /// The tempo will be multiplied by this value. + /// + public float Speed + { + get => speed; + + set + { + if (value > 0) + { + speed = value; + } + else + { + throw new ArgumentOutOfRangeException("The playback speed must be a positive value."); + } + } + } + + /// + /// Gets or sets the method to alter MIDI messages during playback. + /// If null, MIDI messages will be sent to the synthesizer without any change. + /// + public MessageHook? OnSendMessage + { + get => onSendMessage; + set => onSendMessage = value; + } + + + + /// + /// Represents the method that is called each time a MIDI message is processed during playback. + /// + /// The synthesizer handled by the sequencer. + /// The channel to which the message will be sent. + /// The type of the message. + /// The first data part of the message. + /// The second data part of the message. + public delegate void MessageHook(Synthesizer synthesizer, int channel, int command, int data1, int data2); + } +} diff --git a/Assets/Scripts/Infrastructure/EQ/MeltySynth/MidiFileSequencer.cs.meta b/Assets/Scripts/Infrastructure/EQ/MeltySynth/MidiFileSequencer.cs.meta new file mode 100644 index 0000000..2a37f6e --- /dev/null +++ b/Assets/Scripts/Infrastructure/EQ/MeltySynth/MidiFileSequencer.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: f534ba861f278f8418c48aa8ede4690f +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Scripts/Infrastructure/EQ/MeltySynth/ModulationEnvelope.cs b/Assets/Scripts/Infrastructure/EQ/MeltySynth/ModulationEnvelope.cs new file mode 100644 index 0000000..97412c1 --- /dev/null +++ b/Assets/Scripts/Infrastructure/EQ/MeltySynth/ModulationEnvelope.cs @@ -0,0 +1,144 @@ +using System; + +namespace Infrastructure.EQ.MeltySynth +{ + internal sealed class ModulationEnvelope + { + private readonly Synthesizer synthesizer; + + private double attackSlope; + private double decaySlope; + private double releaseSlope; + + private double attackStartTime; + private double holdStartTime; + private double decayStartTime; + + private double decayEndTime; + private double releaseEndTime; + + private float sustainLevel; + private float releaseLevel; + + private int processedSampleCount; + private Stage stage; + private float value; + + internal ModulationEnvelope(Synthesizer synthesizer) + { + this.synthesizer = synthesizer; + } + + public void Start(float delay, float attack, float hold, float decay, float sustain, float release) + { + attackSlope = 1 / attack; + decaySlope = 1 / decay; + releaseSlope = 1 / release; + + attackStartTime = delay; + holdStartTime = attackStartTime + attack; + decayStartTime = holdStartTime + hold; + + decayEndTime = decayStartTime + decay; + releaseEndTime = release; + + sustainLevel = Math.Clamp(sustain, 0F, 1F); + releaseLevel = 0; + + processedSampleCount = 0; + stage = Stage.Delay; + value = 0; + + Process(0); + } + + public void Release() + { + stage = Stage.Release; + releaseEndTime += (double)processedSampleCount / synthesizer.SampleRate; + releaseLevel = value; + } + + public bool Process() + { + return Process(synthesizer.BlockSize); + } + + private bool Process(int sampleCount) + { + processedSampleCount += sampleCount; + + var currentTime = (double)processedSampleCount / synthesizer.SampleRate; + + while (stage <= Stage.Hold) + { + double endTime; + switch (stage) + { + case Stage.Delay: + endTime = attackStartTime; + break; + + case Stage.Attack: + endTime = holdStartTime; + break; + + case Stage.Hold: + endTime = decayStartTime; + break; + + default: + throw new InvalidOperationException("Invalid envelope stage."); + } + + if (currentTime < endTime) + { + break; + } + else + { + stage++; + } + } + + switch (stage) + { + case Stage.Delay: + value = 0; + return true; + + case Stage.Attack: + value = (float)(attackSlope * (currentTime - attackStartTime)); + return true; + + case Stage.Hold: + value = 1; + return true; + + case Stage.Decay: + value = Math.Max((float)(decaySlope * (decayEndTime - currentTime)), sustainLevel); + return value > SoundFontMath.NonAudible; + + case Stage.Release: + value = Math.Max((float)(releaseLevel * releaseSlope * (releaseEndTime - currentTime)), 0F); + return value > SoundFontMath.NonAudible; + + default: + throw new InvalidOperationException("Invalid envelope stage."); + } + } + + public float Value => value; + + + + private enum Stage + { + Delay, + Attack, + Hold, + Decay, + Release + } + } +} diff --git a/Assets/Scripts/Infrastructure/EQ/MeltySynth/ModulationEnvelope.cs.meta b/Assets/Scripts/Infrastructure/EQ/MeltySynth/ModulationEnvelope.cs.meta new file mode 100644 index 0000000..70e6c99 --- /dev/null +++ b/Assets/Scripts/Infrastructure/EQ/MeltySynth/ModulationEnvelope.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 4256df4a8b46593478522abee08be615 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Scripts/Infrastructure/EQ/MeltySynth/Modulator.cs b/Assets/Scripts/Infrastructure/EQ/MeltySynth/Modulator.cs new file mode 100644 index 0000000..f35dd86 --- /dev/null +++ b/Assets/Scripts/Infrastructure/EQ/MeltySynth/Modulator.cs @@ -0,0 +1,18 @@ +using System.IO; + +namespace Infrastructure.EQ.MeltySynth +{ + internal static class Modulator + { + // Since modulators will not be supported, we discard the data. + internal static void DiscardData(BinaryReader reader, int size) + { + if (size % 10 != 0) + { + throw new InvalidDataException("The modulator list is invalid."); + } + + reader.BaseStream.Position += size; + } + } +} diff --git a/Assets/Scripts/Infrastructure/EQ/MeltySynth/Modulator.cs.meta b/Assets/Scripts/Infrastructure/EQ/MeltySynth/Modulator.cs.meta new file mode 100644 index 0000000..d767dad --- /dev/null +++ b/Assets/Scripts/Infrastructure/EQ/MeltySynth/Modulator.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: f186622bda1029e4cbb8fa04b7555e0a +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Scripts/Infrastructure/EQ/MeltySynth/Oscillator.cs b/Assets/Scripts/Infrastructure/EQ/MeltySynth/Oscillator.cs new file mode 100644 index 0000000..e2b2188 --- /dev/null +++ b/Assets/Scripts/Infrastructure/EQ/MeltySynth/Oscillator.cs @@ -0,0 +1,159 @@ +using System; + +namespace Infrastructure.EQ.MeltySynth +{ + internal sealed class Oscillator + { + // In this class, fixed-point numbers are used for speed-up. + // A fixed-point number is expressed by Int64, whose lower 24 bits represent the fraction part, + // and the rest represent the integer part. + // For clarity, fixed-point number variables have a suffix "_fp". + + private const int fracBits = 24; + private const long fracUnit = 1L << fracBits; + private const float fpToSample = 1F / (32768 * fracUnit); + + private readonly Synthesizer synthesizer; + + private short[]? data; + private LoopMode loopMode; + private int sampleRate; + private int start; + private int end; + private int startLoop; + private int endLoop; + private int rootKey; + + private float tune; + private float pitchChangeScale; + private float sampleRateRatio; + + private bool looping; + + private long position_fp; + + internal Oscillator(Synthesizer synthesizer) + { + this.synthesizer = synthesizer; + } + + public void Start(short[] data, LoopMode loopMode, int sampleRate, int start, int end, int startLoop, int endLoop, int rootKey, int coarseTune, int fineTune, int scaleTuning) + { + this.data = data; + this.loopMode = loopMode; + this.sampleRate = sampleRate; + this.start = start; + this.end = end; + this.startLoop = startLoop; + this.endLoop = endLoop; + this.rootKey = rootKey; + + tune = coarseTune + 0.01F * fineTune; + pitchChangeScale = 0.01F * scaleTuning; + sampleRateRatio = (float)sampleRate / synthesizer.SampleRate; + + if (loopMode == LoopMode.NoLoop) + { + looping = false; + } + else + { + looping = true; + } + + position_fp = (long)start << fracBits; + } + + public void Release() + { + if (loopMode == LoopMode.LoopUntilNoteOff) + { + looping = false; + } + } + + public bool Process(float[] block, float pitch) + { + var pitchChange = pitchChangeScale * (pitch - rootKey) + tune; + var pitchRatio = sampleRateRatio * MathF.Pow(2, pitchChange / 12); + return FillBlock(block, pitchRatio); + } + + internal bool FillBlock(float[] block, double pitchRatio) + { + var pitchRatio_fp = (long)(fracUnit * pitchRatio); + + if (looping) + { + return FillBlock_Continuous(block, pitchRatio_fp); + } + else + { + return FillBlock_NoLoop(block, pitchRatio_fp); + } + } + + private bool FillBlock_NoLoop(float[] block, long pitchRatio_fp) + { + for (var t = 0; t < block.Length; t++) + { + var index = position_fp >> fracBits; + + if (index >= end) + { + if (t > 0) + { + Array.Clear(block, t, block.Length - t); + return true; + } + else + { + return false; + } + } + + var x1 = data![index]; + var x2 = data![index + 1]; + var a_fp = position_fp & (fracUnit - 1); + block[t] = fpToSample * (((long)x1 << fracBits) + a_fp * (x2 - x1)); + + position_fp += pitchRatio_fp; + } + + return true; + } + + private bool FillBlock_Continuous(float[] block, long pitchRatio_fp) + { + var endLoop_fp = (long)endLoop << fracBits; + + var loopLength = (long)(endLoop - startLoop); + var loopLength_fp = loopLength << fracBits; + + for (var t = 0; t < block.Length; t++) + { + if (position_fp >= endLoop_fp) + { + position_fp -= loopLength_fp; + } + + var index1 = position_fp >> fracBits; + var index2 = index1 + 1; + + if (index2 >= endLoop) + { + index2 -= loopLength; + } + + var x1 = data![index1]; + var x2 = data![index2]; + var a_fp = position_fp & (fracUnit - 1); + block[t] = fpToSample * (((long)x1 << fracBits) + a_fp * (x2 - x1)); + + position_fp += pitchRatio_fp; + } + + return true; + } + } +} diff --git a/Assets/Scripts/Infrastructure/EQ/MeltySynth/Oscillator.cs.meta b/Assets/Scripts/Infrastructure/EQ/MeltySynth/Oscillator.cs.meta new file mode 100644 index 0000000..875fdd2 --- /dev/null +++ b/Assets/Scripts/Infrastructure/EQ/MeltySynth/Oscillator.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: d3eb60a7a7092244fadb1ed4ff64eff2 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Scripts/Infrastructure/EQ/MeltySynth/Preset.cs b/Assets/Scripts/Infrastructure/EQ/MeltySynth/Preset.cs new file mode 100644 index 0000000..0a864f2 --- /dev/null +++ b/Assets/Scripts/Infrastructure/EQ/MeltySynth/Preset.cs @@ -0,0 +1,100 @@ +using System; +using System.Collections.Generic; +using System.IO; + +namespace Infrastructure.EQ.MeltySynth +{ + /// + /// Represents a preset in the SoundFont. + /// + public sealed class Preset + { + internal static readonly Preset Default = new Preset(); + + private readonly string name; + private readonly int patchNumber; + private readonly int bankNumber; + private readonly int library; + private readonly int genre; + private readonly int morphology; + private readonly PresetRegion[] regions; + + private Preset() + { + name = "Default"; + regions = Array.Empty(); + } + + private Preset(PresetInfo info, Zone[] zones, Instrument[] instruments) + { + this.name = info.Name; + this.patchNumber = info.PatchNumber; + this.bankNumber = info.BankNumber; + this.library = info.Library; + this.genre = info.Genre; + this.morphology = info.Morphology; + + var zoneCount = info.ZoneEndIndex - info.ZoneStartIndex + 1; + if (zoneCount <= 0) + { + throw new InvalidDataException($"The preset '{info.Name}' has no zone."); + } + + var zoneSpan = zones.AsSpan(info.ZoneStartIndex, zoneCount); + + regions = PresetRegion.Create(this, zoneSpan, instruments); + } + + internal static Preset[] Create(PresetInfo[] infos, Zone[] zones, Instrument[] instruments) + { + if (infos.Length <= 1) + { + throw new InvalidDataException("No valid preset was found."); + } + + // The last one is the terminator. + var presets = new Preset[infos.Length - 1]; + + for (var i = 0; i < presets.Length; i++) + { + presets[i] = new Preset(infos[i], zones, instruments); + } + + return presets; + } + + /// + /// Gets the name of the preset. + /// + /// + /// The name of the preset. + /// + public override string ToString() + { + return name; + } + + /// + /// The name of the preset. + /// + public string Name => name; + + /// + /// The patch number of the preset. + /// + public int PatchNumber => patchNumber; + + /// + /// The bank number of the preset. + /// + public int BankNumber => bankNumber; + + /// + /// The regions of the preset. + /// + public IReadOnlyList Regions => regions; + + // Internally exposes the raw array for fast enumeration. + internal PresetRegion[] RegionArray => regions; + } +} diff --git a/Assets/Scripts/Infrastructure/EQ/MeltySynth/Preset.cs.meta b/Assets/Scripts/Infrastructure/EQ/MeltySynth/Preset.cs.meta new file mode 100644 index 0000000..990d122 --- /dev/null +++ b/Assets/Scripts/Infrastructure/EQ/MeltySynth/Preset.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 9c19dc74f3d2a6347bf337a45ae79fb7 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Scripts/Infrastructure/EQ/MeltySynth/PresetInfo.cs b/Assets/Scripts/Infrastructure/EQ/MeltySynth/PresetInfo.cs new file mode 100644 index 0000000..7542970 --- /dev/null +++ b/Assets/Scripts/Infrastructure/EQ/MeltySynth/PresetInfo.cs @@ -0,0 +1,60 @@ +using System.IO; + +namespace Infrastructure.EQ.MeltySynth +{ + internal sealed class PresetInfo + { + private string name; + private int patchNumber; + private int bankNumber; + private int zoneStartIndex; + private int zoneEndIndex; + private int library; + private int genre; + private int morphology; + + private PresetInfo(BinaryReader reader) + { + name = reader.ReadFixedLengthString(20); + patchNumber = reader.ReadUInt16(); + bankNumber = reader.ReadUInt16(); + zoneStartIndex = reader.ReadUInt16(); + library = reader.ReadInt32(); + genre = reader.ReadInt32(); + morphology = reader.ReadInt32(); + } + + internal static PresetInfo[] ReadFromChunk(BinaryReader reader, int size) + { + if (size % 38 != 0) + { + throw new InvalidDataException("The preset list is invalid."); + } + + var count = size / 38; + + var presets = new PresetInfo[count]; + + for (var i = 0; i < count; i++) + { + presets[i] = new PresetInfo(reader); + } + + for (var i = 0; i < count - 1; i++) + { + presets[i].zoneEndIndex = presets[i + 1].zoneStartIndex - 1; + } + + return presets; + } + + public string Name => name; + public int PatchNumber => patchNumber; + public int BankNumber => bankNumber; + public int ZoneStartIndex => zoneStartIndex; + public int ZoneEndIndex => zoneEndIndex; + public int Library => library; + public int Genre => genre; + public int Morphology => morphology; + } +} diff --git a/Assets/Scripts/Infrastructure/EQ/MeltySynth/PresetInfo.cs.meta b/Assets/Scripts/Infrastructure/EQ/MeltySynth/PresetInfo.cs.meta new file mode 100644 index 0000000..20680c2 --- /dev/null +++ b/Assets/Scripts/Infrastructure/EQ/MeltySynth/PresetInfo.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 262a3d14dd71d1e46b3e952d268e1b16 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Scripts/Infrastructure/EQ/MeltySynth/PresetRegion.cs b/Assets/Scripts/Infrastructure/EQ/MeltySynth/PresetRegion.cs new file mode 100644 index 0000000..94ec0ef --- /dev/null +++ b/Assets/Scripts/Infrastructure/EQ/MeltySynth/PresetRegion.cs @@ -0,0 +1,182 @@ +using System; +using System.IO; +using System.Linq; + +namespace Infrastructure.EQ.MeltySynth +{ + /// + /// Represents a preset region. + /// + /// + /// A preset region indicates how the parameters of the instrument should be modified in the preset. + /// + public sealed class PresetRegion + { + internal static readonly PresetRegion Default = new PresetRegion(); + + private readonly short[] gs; + + private readonly Instrument instrument; + + private PresetRegion() + { + gs = new short[61]; + gs[(int)GeneratorType.KeyRange] = 0x7F00; + gs[(int)GeneratorType.VelocityRange] = 0x7F00; + + instrument = Instrument.Default; + } + + private PresetRegion(Preset preset, Zone global, Zone local, Instrument[] instruments) : this() + { + foreach (var generator in global.Generators) + { + SetParameter(generator); + } + + foreach (var generator in local.Generators) + { + SetParameter(generator); + } + + var id = gs[(int)GeneratorType.Instrument]; + if (!(0 <= id && id < instruments.Length)) + { + throw new InvalidDataException($"The preset '{preset.Name}' contains an invalid instrument ID '{id}'."); + } + + instrument = instruments[id]; + } + + internal static PresetRegion[] Create(Preset preset, Span zones, Instrument[] instruments) + { + // Is the first one the global zone? + if (zones[0].Generators.Count == 0 || zones[0].Generators.Last().Type != GeneratorType.Instrument) + { + // The first one is the global zone. + var global = zones[0]; + + // The global zone is regarded as the base setting of subsequent zones. + var regions = new PresetRegion[zones.Length - 1]; + for (var i = 0; i < regions.Length; i++) + { + regions[i] = new PresetRegion(preset, global, zones[i + 1], instruments); + } + return regions; + } + else + { + // No global zone. + var regions = new PresetRegion[zones.Length]; + for (var i = 0; i < regions.Length; i++) + { + regions[i] = new PresetRegion(preset, Zone.Empty, zones[i], instruments); + } + return regions; + } + } + + private void SetParameter(Generator generator) + { + var index = (int)generator.Type; + + // Unknown generators should be ignored. + if (0 <= index && index < gs.Length) + { + gs[index] = (short)generator.Value; + } + } + + /// + /// Checks if the region covers the given key and velocity. + /// + /// The key of a note. + /// The velocity of a note. + /// + /// true if the region covers the given key and velocity. + /// + public bool Contains(int key, int velocity) + { + var containsKey = KeyRangeStart <= key && key <= KeyRangeEnd; + var containsVelocity = VelocityRangeStart <= velocity && velocity <= VelocityRangeEnd; + return containsKey && containsVelocity; + } + + /// + /// Gets the string representation of the region. + /// + /// + /// The string representation of the region. + /// + public override string ToString() + { + return $"{instrument.Name} (Key: {KeyRangeStart}-{KeyRangeEnd}, Velocity: {VelocityRangeStart}-{VelocityRangeEnd})"; + } + + internal short this[GeneratorType generatortType] => gs[(int)generatortType]; + + /// + /// The instrument corresponding to the region. + /// + public Instrument Instrument => instrument; + +#pragma warning disable CS1591 // I'm too lazy to add comments for all the following things. + + // public int StartAddressOffset => 32768 * this[GeneratorParameterType.StartAddressCoarseOffset] + this[GeneratorParameterType.StartAddressOffset]; + // public int EndAddressOffset => 32768 * this[GeneratorParameterType.EndAddressCoarseOffset] + this[GeneratorParameterType.EndAddressOffset]; + // public int StartLoopAddressOffset => 32768 * this[GeneratorParameterType.StartLoopAddressCoarseOffset] + this[GeneratorParameterType.StartLoopAddressOffset]; + // public int EndLoopAddressOffset => 32768 * this[GeneratorParameterType.EndLoopAddressCoarseOffset] + this[GeneratorParameterType.EndLoopAddressOffset]; + + public int ModulationLfoToPitch => this[GeneratorType.ModulationLfoToPitch]; + public int VibratoLfoToPitch => this[GeneratorType.VibratoLfoToPitch]; + public int ModulationEnvelopeToPitch => this[GeneratorType.ModulationEnvelopeToPitch]; + public float InitialFilterCutoffFrequency => SoundFontMath.CentsToMultiplyingFactor(this[GeneratorType.InitialFilterCutoffFrequency]); + public float InitialFilterQ => 0.1F * this[GeneratorType.InitialFilterQ]; + public int ModulationLfoToFilterCutoffFrequency => this[GeneratorType.ModulationLfoToFilterCutoffFrequency]; + public int ModulationEnvelopeToFilterCutoffFrequency => this[GeneratorType.ModulationEnvelopeToFilterCutoffFrequency]; + + public float ModulationLfoToVolume => 0.1F * this[GeneratorType.ModulationLfoToVolume]; + + public float ChorusEffectsSend => 0.1F * this[GeneratorType.ChorusEffectsSend]; + public float ReverbEffectsSend => 0.1F * this[GeneratorType.ReverbEffectsSend]; + public float Pan => 0.1F * this[GeneratorType.Pan]; + + public float DelayModulationLfo => SoundFontMath.CentsToMultiplyingFactor(this[GeneratorType.DelayModulationLfo]); + public float FrequencyModulationLfo => SoundFontMath.CentsToMultiplyingFactor(this[GeneratorType.FrequencyModulationLfo]); + public float DelayVibratoLfo => SoundFontMath.CentsToMultiplyingFactor(this[GeneratorType.DelayVibratoLfo]); + public float FrequencyVibratoLfo => SoundFontMath.CentsToMultiplyingFactor(this[GeneratorType.FrequencyVibratoLfo]); + public float DelayModulationEnvelope => SoundFontMath.CentsToMultiplyingFactor(this[GeneratorType.DelayModulationEnvelope]); + public float AttackModulationEnvelope => SoundFontMath.CentsToMultiplyingFactor(this[GeneratorType.AttackModulationEnvelope]); + public float HoldModulationEnvelope => SoundFontMath.CentsToMultiplyingFactor(this[GeneratorType.HoldModulationEnvelope]); + public float DecayModulationEnvelope => SoundFontMath.CentsToMultiplyingFactor(this[GeneratorType.DecayModulationEnvelope]); + public float SustainModulationEnvelope => 0.1F * this[GeneratorType.SustainModulationEnvelope]; + public float ReleaseModulationEnvelope => SoundFontMath.CentsToMultiplyingFactor(this[GeneratorType.ReleaseModulationEnvelope]); + public int KeyNumberToModulationEnvelopeHold => this[GeneratorType.KeyNumberToModulationEnvelopeHold]; + public int KeyNumberToModulationEnvelopeDecay => this[GeneratorType.KeyNumberToModulationEnvelopeDecay]; + public float DelayVolumeEnvelope => SoundFontMath.CentsToMultiplyingFactor(this[GeneratorType.DelayVolumeEnvelope]); + public float AttackVolumeEnvelope => SoundFontMath.CentsToMultiplyingFactor(this[GeneratorType.AttackVolumeEnvelope]); + public float HoldVolumeEnvelope => SoundFontMath.CentsToMultiplyingFactor(this[GeneratorType.HoldVolumeEnvelope]); + public float DecayVolumeEnvelope => SoundFontMath.CentsToMultiplyingFactor(this[GeneratorType.DecayVolumeEnvelope]); + public float SustainVolumeEnvelope => 0.1F * this[GeneratorType.SustainVolumeEnvelope]; + public float ReleaseVolumeEnvelope => SoundFontMath.CentsToMultiplyingFactor(this[GeneratorType.ReleaseVolumeEnvelope]); + public int KeyNumberToVolumeEnvelopeHold => this[GeneratorType.KeyNumberToVolumeEnvelopeHold]; + public int KeyNumberToVolumeEnvelopeDecay => this[GeneratorType.KeyNumberToVolumeEnvelopeDecay]; + + public int KeyRangeStart => this[GeneratorType.KeyRange] & 0xFF; + public int KeyRangeEnd => (this[GeneratorType.KeyRange] >> 8) & 0xFF; + public int VelocityRangeStart => this[GeneratorType.VelocityRange] & 0xFF; + public int VelocityRangeEnd => (this[GeneratorType.VelocityRange] >> 8) & 0xFF; + + public float InitialAttenuation => 0.1F * this[GeneratorType.InitialAttenuation]; + + public int CoarseTune => this[GeneratorType.CoarseTune]; + public int FineTune => this[GeneratorType.FineTune]; + // public LoopMode SampleModes => this[GeneratorParameterType.SampleModes]; + + public int ScaleTuning => this[GeneratorType.ScaleTuning]; + // public int ExclusiveClass => this[GeneratorParameterType.ExclusiveClass]; + // public int RootKey => this[GeneratorParameterType.OverridingRootKey]; + +#pragma warning restore CS1591 + } +} diff --git a/Assets/Scripts/Infrastructure/EQ/MeltySynth/PresetRegion.cs.meta b/Assets/Scripts/Infrastructure/EQ/MeltySynth/PresetRegion.cs.meta new file mode 100644 index 0000000..c370866 --- /dev/null +++ b/Assets/Scripts/Infrastructure/EQ/MeltySynth/PresetRegion.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: d465d964caa4dd04a86cfa3f00b764ff +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Scripts/Infrastructure/EQ/MeltySynth/RegionEx.cs b/Assets/Scripts/Infrastructure/EQ/MeltySynth/RegionEx.cs new file mode 100644 index 0000000..4cbf9b3 --- /dev/null +++ b/Assets/Scripts/Infrastructure/EQ/MeltySynth/RegionEx.cs @@ -0,0 +1,86 @@ +using System; + +namespace Infrastructure.EQ.MeltySynth +{ + internal static class RegionEx + { + public static void Start(this Oscillator oscillator, short[] data, InstrumentRegion region) + { + Start(oscillator, data, new RegionPair(PresetRegion.Default, region)); + } + + public static void Start(this Oscillator oscillator, short[] data, RegionPair region) + { + var sampleRate = region.Instrument.Sample.SampleRate; + var loopMode = region.SampleModes; + var start = region.SampleStart; + var end = region.SampleEnd; + var startLoop = region.SampleStartLoop; + var endLoop = region.SampleEndLoop; + var rootKey = region.RootKey; + var coarseTune = region.CoarseTune; + var fineTune = region.FineTune; + var scaleTuning = region.ScaleTuning; + + oscillator.Start(data, loopMode, sampleRate, start, end, startLoop, endLoop, rootKey, coarseTune, fineTune, scaleTuning); + } + + public static void Start(this VolumeEnvelope envelope, InstrumentRegion region, int key, int velocity) + { + Start(envelope, new RegionPair(PresetRegion.Default, region), key, velocity); + } + + public static void Start(this VolumeEnvelope envelope, RegionPair region, int key, int velocity) + { + // If the release time is shorter than 10 ms, it will be clamped to 10 ms to avoid pop noise. + + var delay = region.DelayVolumeEnvelope; + var attack = region.AttackVolumeEnvelope; + var hold = region.HoldVolumeEnvelope * SoundFontMath.KeyNumberToMultiplyingFactor(region.KeyNumberToVolumeEnvelopeHold, key); + var decay = region.DecayVolumeEnvelope * SoundFontMath.KeyNumberToMultiplyingFactor(region.KeyNumberToVolumeEnvelopeDecay, key); + var sustain = SoundFontMath.DecibelsToLinear(-region.SustainVolumeEnvelope); + var release = Math.Max(region.ReleaseVolumeEnvelope, 0.01F); + + envelope.Start(delay, attack, hold, decay, sustain, release); + } + + public static void Start(this ModulationEnvelope envelope, InstrumentRegion region, int key, int velocity) + { + Start(envelope, new RegionPair(PresetRegion.Default, region), key, velocity); + } + + public static void Start(this ModulationEnvelope envelope, RegionPair region, int key, int velocity) + { + // According to the implementation of TinySoundFont, the attack time should be adjusted by the velocity. + + var delay = region.DelayModulationEnvelope; + var attack = region.AttackModulationEnvelope * ((145 - velocity) / 144F); + var hold = region.HoldModulationEnvelope * SoundFontMath.KeyNumberToMultiplyingFactor(region.KeyNumberToModulationEnvelopeHold, key); + var decay = region.DecayModulationEnvelope * SoundFontMath.KeyNumberToMultiplyingFactor(region.KeyNumberToModulationEnvelopeDecay, key); + var sustain = 1F - region.SustainModulationEnvelope / 100F; + var release = region.ReleaseModulationEnvelope; + + envelope.Start(delay, attack, hold, decay, sustain, release); + } + + public static void StartVibrato(this Lfo lfo, InstrumentRegion region, int key, int velocity) + { + StartVibrato(lfo, new RegionPair(PresetRegion.Default, region), key, velocity); + } + + public static void StartVibrato(this Lfo lfo, RegionPair region, int key, int velocity) + { + lfo.Start(region.DelayVibratoLfo, region.FrequencyVibratoLfo); + } + + public static void StartModulation(this Lfo lfo, InstrumentRegion region, int key, int velocity) + { + StartModulation(lfo, new RegionPair(PresetRegion.Default, region), key, velocity); + } + + public static void StartModulation(this Lfo lfo, RegionPair region, int key, int velocity) + { + lfo.Start(region.DelayModulationLfo, region.FrequencyModulationLfo); + } + } +} diff --git a/Assets/Scripts/Infrastructure/EQ/MeltySynth/RegionEx.cs.meta b/Assets/Scripts/Infrastructure/EQ/MeltySynth/RegionEx.cs.meta new file mode 100644 index 0000000..174cd44 --- /dev/null +++ b/Assets/Scripts/Infrastructure/EQ/MeltySynth/RegionEx.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 6a2d3c81b0cf935448e29e9074a0b783 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Scripts/Infrastructure/EQ/MeltySynth/RegionPair.cs b/Assets/Scripts/Infrastructure/EQ/MeltySynth/RegionPair.cs new file mode 100644 index 0000000..6cfb1c0 --- /dev/null +++ b/Assets/Scripts/Infrastructure/EQ/MeltySynth/RegionPair.cs @@ -0,0 +1,79 @@ +namespace Infrastructure.EQ.MeltySynth +{ + internal struct RegionPair + { + private readonly PresetRegion preset; + private readonly InstrumentRegion instrument; + + internal RegionPair(PresetRegion preset, InstrumentRegion instrument) + { + this.preset = preset; + this.instrument = instrument; + } + + private int this[GeneratorType generatortType] => instrument[generatortType] + preset[generatortType]; + + public PresetRegion Preset => preset; + public InstrumentRegion Instrument => instrument; + + public int SampleStart => instrument.SampleStart; + public int SampleEnd => instrument.SampleEnd; + public int SampleStartLoop => instrument.SampleStartLoop; + public int SampleEndLoop => instrument.SampleEndLoop; + + public int StartAddressOffset => instrument.StartAddressOffset; + public int EndAddressOffset => instrument.EndAddressOffset; + public int StartLoopAddressOffset => instrument.StartLoopAddressOffset; + public int EndLoopAddressOffset => instrument.EndLoopAddressOffset; + + public int ModulationLfoToPitch => this[GeneratorType.ModulationLfoToPitch]; + public int VibratoLfoToPitch => this[GeneratorType.VibratoLfoToPitch]; + public int ModulationEnvelopeToPitch => this[GeneratorType.ModulationEnvelopeToPitch]; + public float InitialFilterCutoffFrequency => SoundFontMath.CentsToHertz(this[GeneratorType.InitialFilterCutoffFrequency]); + public float InitialFilterQ => 0.1F * this[GeneratorType.InitialFilterQ]; + public int ModulationLfoToFilterCutoffFrequency => this[GeneratorType.ModulationLfoToFilterCutoffFrequency]; + public int ModulationEnvelopeToFilterCutoffFrequency => this[GeneratorType.ModulationEnvelopeToFilterCutoffFrequency]; + + public float ModulationLfoToVolume => 0.1F * this[GeneratorType.ModulationLfoToVolume]; + + public float ChorusEffectsSend => 0.1F * this[GeneratorType.ChorusEffectsSend]; + public float ReverbEffectsSend => 0.1F * this[GeneratorType.ReverbEffectsSend]; + public float Pan => 0.1F * this[GeneratorType.Pan]; + + public float DelayModulationLfo => SoundFontMath.TimecentsToSeconds(this[GeneratorType.DelayModulationLfo]); + public float FrequencyModulationLfo => SoundFontMath.CentsToHertz(this[GeneratorType.FrequencyModulationLfo]); + public float DelayVibratoLfo => SoundFontMath.TimecentsToSeconds(this[GeneratorType.DelayVibratoLfo]); + public float FrequencyVibratoLfo => SoundFontMath.CentsToHertz(this[GeneratorType.FrequencyVibratoLfo]); + public float DelayModulationEnvelope => SoundFontMath.TimecentsToSeconds(this[GeneratorType.DelayModulationEnvelope]); + public float AttackModulationEnvelope => SoundFontMath.TimecentsToSeconds(this[GeneratorType.AttackModulationEnvelope]); + public float HoldModulationEnvelope => SoundFontMath.TimecentsToSeconds(this[GeneratorType.HoldModulationEnvelope]); + public float DecayModulationEnvelope => SoundFontMath.TimecentsToSeconds(this[GeneratorType.DecayModulationEnvelope]); + public float SustainModulationEnvelope => 0.1F * this[GeneratorType.SustainModulationEnvelope]; + public float ReleaseModulationEnvelope => SoundFontMath.TimecentsToSeconds(this[GeneratorType.ReleaseModulationEnvelope]); + public int KeyNumberToModulationEnvelopeHold => this[GeneratorType.KeyNumberToModulationEnvelopeHold]; + public int KeyNumberToModulationEnvelopeDecay => this[GeneratorType.KeyNumberToModulationEnvelopeDecay]; + public float DelayVolumeEnvelope => SoundFontMath.TimecentsToSeconds(this[GeneratorType.DelayVolumeEnvelope]); + public float AttackVolumeEnvelope => SoundFontMath.TimecentsToSeconds(this[GeneratorType.AttackVolumeEnvelope]); + public float HoldVolumeEnvelope => SoundFontMath.TimecentsToSeconds(this[GeneratorType.HoldVolumeEnvelope]); + public float DecayVolumeEnvelope => SoundFontMath.TimecentsToSeconds(this[GeneratorType.DecayVolumeEnvelope]); + public float SustainVolumeEnvelope => 0.1F * this[GeneratorType.SustainVolumeEnvelope]; + public float ReleaseVolumeEnvelope => SoundFontMath.TimecentsToSeconds(this[GeneratorType.ReleaseVolumeEnvelope]); + public int KeyNumberToVolumeEnvelopeHold => this[GeneratorType.KeyNumberToVolumeEnvelopeHold]; + public int KeyNumberToVolumeEnvelopeDecay => this[GeneratorType.KeyNumberToVolumeEnvelopeDecay]; + + // public int KeyRangeStart => this[GeneratorParameterType.KeyRange] & 0xFF; + // public int KeyRangeEnd => (this[GeneratorParameterType.KeyRange] >> 8) & 0xFF; + // public int VelocityRangeStart => this[GeneratorParameterType.VelocityRange] & 0xFF; + // public int VelocityRangeEnd => (this[GeneratorParameterType.VelocityRange] >> 8) & 0xFF; + + public float InitialAttenuation => 0.1F * this[GeneratorType.InitialAttenuation]; + + public int CoarseTune => this[GeneratorType.CoarseTune]; + public int FineTune => this[GeneratorType.FineTune] + instrument.Sample.PitchCorrection; + public LoopMode SampleModes => instrument.SampleModes; + + public int ScaleTuning => this[GeneratorType.ScaleTuning]; + public int ExclusiveClass => instrument.ExclusiveClass; + public int RootKey => instrument.RootKey; + } +} diff --git a/Assets/Scripts/Infrastructure/EQ/MeltySynth/RegionPair.cs.meta b/Assets/Scripts/Infrastructure/EQ/MeltySynth/RegionPair.cs.meta new file mode 100644 index 0000000..745e5fd --- /dev/null +++ b/Assets/Scripts/Infrastructure/EQ/MeltySynth/RegionPair.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: ce0675303a1c46845aa69cd21f1bfba0 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Scripts/Infrastructure/EQ/MeltySynth/Reverb.cs b/Assets/Scripts/Infrastructure/EQ/MeltySynth/Reverb.cs new file mode 100644 index 0000000..e8d3eea --- /dev/null +++ b/Assets/Scripts/Infrastructure/EQ/MeltySynth/Reverb.cs @@ -0,0 +1,424 @@ +// This reverb implementation is based on Freeverb, a public domain reverb +// implementation by Jezar at Dreampoint. + +using System; + +namespace Infrastructure.EQ.MeltySynth +{ + internal sealed class Reverb + { + private const float fixedGain = 0.015F; + private const float scaleWet = 3F; + private const float scaleDamp = 0.4F; + private const float scaleRoom = 0.28F; + private const float offsetRoom = 0.7F; + private const float initialRoom = 0.5F; + private const float initialDamp = 0.5F; + private const float initialWet = 1F / scaleWet; + private const float initialWidth = 1F; + private const int stereoSpread = 23; + + private const int cfTuningL1 = 1116; + private const int cfTuningR1 = 1116 + stereoSpread; + private const int cfTuningL2 = 1188; + private const int cfTuningR2 = 1188 + stereoSpread; + private const int cfTuningL3 = 1277; + private const int cfTuningR3 = 1277 + stereoSpread; + private const int cfTuningL4 = 1356; + private const int cfTuningR4 = 1356 + stereoSpread; + private const int cfTuningL5 = 1422; + private const int cfTuningR5 = 1422 + stereoSpread; + private const int cfTuningL6 = 1491; + private const int cfTuningR6 = 1491 + stereoSpread; + private const int cfTuningL7 = 1557; + private const int cfTuningR7 = 1557 + stereoSpread; + private const int cfTuningL8 = 1617; + private const int cfTuningR8 = 1617 + stereoSpread; + private const int apfTuningL1 = 556; + private const int apfTuningR1 = 556 + stereoSpread; + private const int apfTuningL2 = 441; + private const int apfTuningR2 = 441 + stereoSpread; + private const int apfTuningL3 = 341; + private const int apfTuningR3 = 341 + stereoSpread; + private const int apfTuningL4 = 225; + private const int apfTuningR4 = 225 + stereoSpread; + + private readonly CombFilter[] cfsL; + private readonly CombFilter[] cfsR; + private readonly AllPassFilter[] apfsL; + private readonly AllPassFilter[] apfsR; + + private float gain; + private float roomSize, roomSize1; + private float damp, damp1; + private float wet, wet1, wet2; + private float width; + + internal Reverb(int sampleRate) + { + cfsL = new CombFilter[] + { + new CombFilter(ScaleTuning(sampleRate, cfTuningL1)), + new CombFilter(ScaleTuning(sampleRate, cfTuningL2)), + new CombFilter(ScaleTuning(sampleRate, cfTuningL3)), + new CombFilter(ScaleTuning(sampleRate, cfTuningL4)), + new CombFilter(ScaleTuning(sampleRate, cfTuningL5)), + new CombFilter(ScaleTuning(sampleRate, cfTuningL6)), + new CombFilter(ScaleTuning(sampleRate, cfTuningL7)), + new CombFilter(ScaleTuning(sampleRate, cfTuningL8)) + }; + + cfsR = new CombFilter[] + { + new CombFilter(ScaleTuning(sampleRate, cfTuningR1)), + new CombFilter(ScaleTuning(sampleRate, cfTuningR2)), + new CombFilter(ScaleTuning(sampleRate, cfTuningR3)), + new CombFilter(ScaleTuning(sampleRate, cfTuningR4)), + new CombFilter(ScaleTuning(sampleRate, cfTuningR5)), + new CombFilter(ScaleTuning(sampleRate, cfTuningR6)), + new CombFilter(ScaleTuning(sampleRate, cfTuningR7)), + new CombFilter(ScaleTuning(sampleRate, cfTuningR8)) + }; + + apfsL = new AllPassFilter[] + { + new AllPassFilter(ScaleTuning(sampleRate, apfTuningL1)), + new AllPassFilter(ScaleTuning(sampleRate, apfTuningL2)), + new AllPassFilter(ScaleTuning(sampleRate, apfTuningL3)), + new AllPassFilter(ScaleTuning(sampleRate, apfTuningL4)) + }; + + apfsR = new AllPassFilter[] + { + new AllPassFilter(ScaleTuning(sampleRate, apfTuningR1)), + new AllPassFilter(ScaleTuning(sampleRate, apfTuningR2)), + new AllPassFilter(ScaleTuning(sampleRate, apfTuningR3)), + new AllPassFilter(ScaleTuning(sampleRate, apfTuningR4)) + }; + + foreach (var apf in apfsL) + { + apf.Feedback = 0.5F; + } + + foreach (var apf in apfsR) + { + apf.Feedback = 0.5F; + } + + Wet = initialWet; + RoomSize = initialRoom; + Damp = initialDamp; + Width = initialWidth; + } + + private int ScaleTuning(int sampleRate, int tuning) + { + return (int)Math.Round((double)sampleRate / 44100 * tuning); + } + + public void Process(float[] input, float[] outputLeft, float[] outputRight) + { + Array.Clear(outputLeft, 0, outputLeft.Length); + Array.Clear(outputRight, 0, outputRight.Length); + + foreach (var cf in cfsL) + { + cf.Process(input, outputLeft); + } + + foreach (var apf in apfsL) + { + apf.Process(outputLeft); + } + + foreach (var cf in cfsR) + { + cf.Process(input, outputRight); + } + + foreach (var apf in apfsR) + { + apf.Process(outputRight); + } + + // With the default settings, we can skip this part. + if (1F - wet1 > 1.0E-3 || wet2 > 1.0E-3) + { + for (var t = 0; t < input.Length; t++) + { + var left = outputLeft[t]; + var right = outputRight[t]; + outputLeft[t] = left * wet1 + right * wet2; + outputRight[t] = right * wet1 + left * wet2; + } + } + } + + public void Mute() + { + foreach (var cf in cfsL) + { + cf.Mute(); + } + + foreach (var cf in cfsR) + { + cf.Mute(); + } + + foreach (var apf in apfsL) + { + apf.Mute(); + } + + foreach (var apf in apfsR) + { + apf.Mute(); + } + } + + private void Update() + { + wet1 = wet * (width / 2F + 0.5F); + wet2 = wet * ((1F - width) / 2F); + + roomSize1 = roomSize; + damp1 = damp; + gain = fixedGain; + + foreach (var cf in cfsL) + { + cf.Feedback = roomSize1; + cf.Damp = damp1; + } + + foreach (var cf in cfsR) + { + cf.Feedback = roomSize1; + cf.Damp = damp1; + } + } + + public float InputGain => gain; + + public float RoomSize + { + get + { + return (roomSize - offsetRoom) / scaleRoom; + } + + set + { + roomSize = (value * scaleRoom) + offsetRoom; + Update(); + } + } + + public float Damp + { + get + { + return damp / scaleDamp; + } + + set + { + damp = value * scaleDamp; + Update(); + } + } + + public float Wet + { + get + { + return wet / scaleWet; + } + + set + { + wet = value * scaleWet; + Update(); + } + } + + public float Width + { + get + { + return width; + } + + set + { + width = value; + Update(); + } + } + + + + internal sealed class CombFilter + { + private readonly float[] buffer; + + private int bufferIndex; + private float filterStore; + + private float feedback; + private float damp1; + private float damp2; + + public CombFilter(int bufferSize) + { + buffer = new float[bufferSize]; + + bufferIndex = 0; + filterStore = 0F; + + feedback = 0F; + damp1 = 0F; + damp2 = 0F; + } + + public void Mute() + { + Array.Clear(buffer, 0, buffer.Length); + filterStore = 0F; + } + + public void Process(float[] inputBlock, float[] outputBlock) + { + var blockIndex = 0; + while (blockIndex < outputBlock.Length) + { + if (bufferIndex == buffer.Length) + { + bufferIndex = 0; + } + + var srcRem = buffer.Length - bufferIndex; + var dstRem = outputBlock.Length - blockIndex; + var rem = Math.Min(srcRem, dstRem); + + for (var t = 0; t < rem; t++) + { + var blockPos = blockIndex + t; + var bufferPos = bufferIndex + t; + + var input = inputBlock[blockPos]; + + // The following ifs are to avoid performance problem due to denormalized number. + // The original implementation uses unsafe cast to detect denormalized number. + // I tried to reproduce the original implementation using Unsafe.As, + // but the simple Math.Abs version was faster according to some benchmarks. + + var output = buffer[bufferPos]; + if (MathF.Abs(output) < 1.0E-6F) + { + output = 0F; + } + + filterStore = (output * damp2) + (filterStore * damp1); + if (MathF.Abs(filterStore) < 1.0E-6F) + { + filterStore = 0F; + } + + buffer[bufferPos] = input + (filterStore * feedback); + outputBlock[blockPos] += output; + } + + bufferIndex += rem; + blockIndex += rem; + } + } + + public float Feedback + { + get => feedback; + set => feedback = value; + } + + public float Damp + { + get => damp1; + + set + { + damp1 = value; + damp2 = 1F - value; + } + } + } + + + + internal sealed class AllPassFilter + { + private readonly float[] buffer; + + private int bufferIndex; + + private float feedback; + + public AllPassFilter(int bufferSize) + { + buffer = new float[bufferSize]; + + bufferIndex = 0; + + feedback = 0F; + } + + public void Mute() + { + Array.Clear(buffer, 0, buffer.Length); + } + + public void Process(float[] block) + { + var blockIndex = 0; + while (blockIndex < block.Length) + { + if (bufferIndex == buffer.Length) + { + bufferIndex = 0; + } + + var srcRem = buffer.Length - bufferIndex; + var dstRem = block.Length - blockIndex; + var rem = Math.Min(srcRem, dstRem); + + for (var t = 0; t < rem; t++) + { + var blockPos = blockIndex + t; + var bufferPos = bufferIndex + t; + + var input = block[blockPos]; + + var bufout = buffer[bufferPos]; + if (MathF.Abs(bufout) < 1.0E-6F) + { + bufout = 0F; + } + + block[blockPos] = bufout - input; + buffer[bufferPos] = input + (bufout * feedback); + } + + bufferIndex += rem; + blockIndex += rem; + } + } + + public float Feedback + { + get => feedback; + set => feedback = value; + } + } + } +} diff --git a/Assets/Scripts/Infrastructure/EQ/MeltySynth/Reverb.cs.meta b/Assets/Scripts/Infrastructure/EQ/MeltySynth/Reverb.cs.meta new file mode 100644 index 0000000..35a5103 --- /dev/null +++ b/Assets/Scripts/Infrastructure/EQ/MeltySynth/Reverb.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: fb273845a430d0d48b918fa06b631e93 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Scripts/Infrastructure/EQ/MeltySynth/SampleHeader.cs b/Assets/Scripts/Infrastructure/EQ/MeltySynth/SampleHeader.cs new file mode 100644 index 0000000..12a0bc1 --- /dev/null +++ b/Assets/Scripts/Infrastructure/EQ/MeltySynth/SampleHeader.cs @@ -0,0 +1,113 @@ +using System.IO; + +namespace Infrastructure.EQ.MeltySynth +{ + /// + /// Represents a sample in the SoundFont. + /// + public sealed class SampleHeader + { + internal static readonly SampleHeader Default = new SampleHeader(); + + private readonly string name; + private readonly int start; + private readonly int end; + private readonly int startLoop; + private readonly int endLoop; + private readonly int sampleRate; + private readonly byte originalPitch; + private readonly sbyte pitchCorrection; + private readonly ushort link; + private readonly SampleType type; + + private SampleHeader() + { + name = "Default"; + } + + private SampleHeader(BinaryReader reader) + { + name = reader.ReadFixedLengthString(20); + start = reader.ReadInt32(); + end = reader.ReadInt32(); + startLoop = reader.ReadInt32(); + endLoop = reader.ReadInt32(); + sampleRate = reader.ReadInt32(); + originalPitch = reader.ReadByte(); + pitchCorrection = reader.ReadSByte(); + link = reader.ReadUInt16(); + type = (SampleType)reader.ReadUInt16(); + } + + internal static SampleHeader[] ReadFromChunk(BinaryReader reader, int size) + { + if (size % 46 != 0) + { + throw new InvalidDataException("The sample header list is invalid."); + } + + var headers = new SampleHeader[size / 46 - 1]; + + for (var i = 0; i < headers.Length; i++) + { + headers[i] = new SampleHeader(reader); + } + + // The last one is the terminator. + new SampleHeader(reader); + + return headers; + } + + /// + /// Gets the name of the sample. + /// + /// + /// The name of the sample. + /// + public override string ToString() + { + return name; + } + + /// + /// The name of the sample. + /// + public string Name => name; + + /// + /// The start point of the sample in the sample data. + /// + public int Start => start; + + /// + /// The end point of the sample in the sample data. + /// + public int End => end; + + /// + /// The loop start point of the sample in the sample data. + /// + public int StartLoop => startLoop; + + /// + /// The loop end point of the sample in the sample data. + /// + public int EndLoop => endLoop; + + /// + /// The sample rate of the sample. + /// + public int SampleRate => sampleRate; + + /// + /// The key number of the recorded pitch of the sample. + /// + public byte OriginalPitch => originalPitch; + + /// + /// The pitch correction in cents that should be applied to the sample on playback. + /// + public sbyte PitchCorrection => pitchCorrection; + } +} diff --git a/Assets/Scripts/Infrastructure/EQ/MeltySynth/SampleHeader.cs.meta b/Assets/Scripts/Infrastructure/EQ/MeltySynth/SampleHeader.cs.meta new file mode 100644 index 0000000..653fd3d --- /dev/null +++ b/Assets/Scripts/Infrastructure/EQ/MeltySynth/SampleHeader.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 392fbf3bd49ac51499ca7767de3ae064 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Scripts/Infrastructure/EQ/MeltySynth/SampleType.cs b/Assets/Scripts/Infrastructure/EQ/MeltySynth/SampleType.cs new file mode 100644 index 0000000..3e51038 --- /dev/null +++ b/Assets/Scripts/Infrastructure/EQ/MeltySynth/SampleType.cs @@ -0,0 +1,14 @@ +namespace Infrastructure.EQ.MeltySynth +{ + internal enum SampleType + { + Mono = 1, + Right = 2, + Left = 4, + Linked = 8, + RomMono = 0x8001, + RomRight = 0x8002, + RomLeft = 0x8004, + RomLinked = 0x8008 + } +} diff --git a/Assets/Scripts/Infrastructure/EQ/MeltySynth/SampleType.cs.meta b/Assets/Scripts/Infrastructure/EQ/MeltySynth/SampleType.cs.meta new file mode 100644 index 0000000..329d940 --- /dev/null +++ b/Assets/Scripts/Infrastructure/EQ/MeltySynth/SampleType.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 3cabd35ffd10f1b429174c33c4fde27e +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Scripts/Infrastructure/EQ/MeltySynth/SoundFont.cs b/Assets/Scripts/Infrastructure/EQ/MeltySynth/SoundFont.cs new file mode 100644 index 0000000..b15e718 --- /dev/null +++ b/Assets/Scripts/Infrastructure/EQ/MeltySynth/SoundFont.cs @@ -0,0 +1,221 @@ +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.IO; +using System.Text; + +namespace Infrastructure.EQ.MeltySynth +{ + /// + /// Reperesents a SoundFont. + /// + public sealed class SoundFont + { + private SoundFontInfo info; + private int bitsPerSample; + private short[] waveData; + private SampleHeader[] sampleHeaders; + private Preset[] presets; + private Instrument[] instruments; + + /// + /// Loads a SoundFont from the stream. + /// + /// + /// The data stream used to load the SoundFont. + /// + public SoundFont(Stream stream) + { + if (stream == null) + { + throw new ArgumentNullException(nameof(stream)); + } + + Load(stream); + + // Workaround for nullable warnings in .NET Standard 2.1. + Debug.Assert(info != null); + Debug.Assert(waveData != null); + Debug.Assert(sampleHeaders != null); + Debug.Assert(presets != null); + Debug.Assert(instruments != null); + } + + /// + /// Loads a SoundFont from the file. + /// + /// + /// The SoundFont file name and path. + /// + public SoundFont(string path) + { + if (path == null) + { + throw new ArgumentNullException(nameof(path)); + } + + using (var stream = new FileStream(path, FileMode.Open, FileAccess.Read)) + { + Load(stream); + } + + // Workaround for nullable warnings in .NET Standard 2.1. + Debug.Assert(info != null); + Debug.Assert(waveData != null); + Debug.Assert(sampleHeaders != null); + Debug.Assert(presets != null); + Debug.Assert(instruments != null); + } + + private void Load(Stream stream) + { + using (var reader = new BinaryReader(stream, Encoding.ASCII, true)) + { + var chunkId = reader.ReadFourCC(); + if (chunkId != "RIFF") + { + throw new InvalidDataException("The RIFF chunk was not found."); + } + + var size = reader.ReadInt32(); + + var formType = reader.ReadFourCC(); + if (formType != "sfbk") + { + throw new InvalidDataException($"The type of the RIFF chunk must be 'sfbk', but was '{formType}'."); + } + + info = new SoundFontInfo(reader); + + var sampleData = new SoundFontSampleData(reader); + bitsPerSample = sampleData.BitsPerSample; + waveData = sampleData.Samples; + + var parameters = new SoundFontParameters(reader); + sampleHeaders = parameters.SampleHeaders; + presets = parameters.Presets; + instruments = parameters.Instruments; + } + + CheckSamples(); + CheckRegions(); + } + + /// + /// Gets the name of the SoundFont. + /// + /// + /// The name of the SoundFont. + /// + public override string ToString() + { + return info.BankName; + } + + private void CheckSamples() + { + var sampleCount = waveData.Length - 4; // This offset is to ensure that out of range access is safe. + + foreach (var sample in sampleHeaders) + { + if (!(0 <= sample.Start && sample.Start < sampleCount)) + { + throw new InvalidDataException($"The start position of the sample '{sample.Name}' is out of range."); + } + if (!(0 <= sample.StartLoop && sample.StartLoop < sampleCount)) + { + throw new InvalidDataException($"The loop start position of the sample '{sample.Name}' is out of range."); + } + if (!(0 < sample.End && sample.End <= sampleCount)) + { + throw new InvalidDataException($"The end position of the sample '{sample.Name}' is out of range."); + } + if (!(0 <= sample.EndLoop && sample.EndLoop <= sampleCount)) + { + throw new InvalidDataException($"The loop end position of the sample '{sample.Name}' is out of range."); + } + } + } + + private void CheckRegions() + { + var sampleCount = waveData.Length - 4; // This offset is to ensure that out of range access is safe. + + foreach (var instrument in instruments) + { + foreach (var region in instrument.RegionArray) + { + if (!(0 <= region.SampleStart && region.SampleStart < sampleCount)) + { + throw new InvalidDataException($"The start position of the sample '{region.Sample.Name}' in the instrument '{instrument.Name}' is out of range."); + } + if (!(0 <= region.SampleStartLoop && region.SampleStartLoop < sampleCount)) + { + throw new InvalidDataException($"The loop start position of the sample '{region.Sample.Name}' in the instrument '{instrument.Name}' is out of range."); + } + if (!(0 < region.SampleEnd && region.SampleEnd <= sampleCount)) + { + throw new InvalidDataException($"The end position of the sample '{region.Sample.Name}' in the instrument '{instrument.Name}' is out of range."); + } + if (!(0 <= region.SampleEndLoop && region.SampleEndLoop <= sampleCount)) + { + throw new InvalidDataException($"The loop end position of the sample '{region.Sample.Name}' in the instrument '{instrument.Name}' is out of range."); + } + + switch (region.SampleModes) + { + case LoopMode.NoLoop: + case LoopMode.Continuous: + case LoopMode.LoopUntilNoteOff: + break; + default: + throw new InvalidDataException($"The sample '{region.Sample.Name}' in the instrument '{instrument.Name}' has an invalid loop mode."); + } + } + } + } + + /// + /// The information of the SoundFont. + /// + public SoundFontInfo Info => info; + + /// + /// The bits per sample of the sample data. + /// + /// + /// This value is always 16. + /// + public int BitsPerSample => bitsPerSample; + + /// + /// The sample data. + /// + /// + /// This single array contains all the waveform data in the SoundFont. + /// An instance of indicates a range of the array corresponding to a sample. + /// + public ReadOnlySpan WaveData => waveData; + + /// + /// The samples of the SoundFont. + /// + public IReadOnlyList SampleHeaders => sampleHeaders; + + /// + /// The presets of the SoundFont. + /// + public IReadOnlyList Presets => presets; + + /// + /// The instruments of the SoundFont. + /// + public IReadOnlyList Instruments => instruments; + + // Internally exposes the raw arrays for fast enumeration. + internal short[] WaveDataArray => waveData; + internal SampleHeader[] SampleHeaderArray => sampleHeaders; + internal Preset[] PresetArray => presets; + internal Instrument[] InstrumentArray => instruments; + } +} diff --git a/Assets/Scripts/Infrastructure/EQ/MeltySynth/SoundFont.cs.meta b/Assets/Scripts/Infrastructure/EQ/MeltySynth/SoundFont.cs.meta new file mode 100644 index 0000000..6186ca0 --- /dev/null +++ b/Assets/Scripts/Infrastructure/EQ/MeltySynth/SoundFont.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 1b518946bb91dac4cb18beef5a99f5b8 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Scripts/Infrastructure/EQ/MeltySynth/SoundFontInfo.cs b/Assets/Scripts/Infrastructure/EQ/MeltySynth/SoundFontInfo.cs new file mode 100644 index 0000000..272bfec --- /dev/null +++ b/Assets/Scripts/Infrastructure/EQ/MeltySynth/SoundFontInfo.cs @@ -0,0 +1,150 @@ +using System.IO; + +namespace Infrastructure.EQ.MeltySynth +{ + /// + /// The information of a SoundFont. + /// + public sealed class SoundFontInfo + { + private readonly SoundFontVersion version = default; + private readonly string targetSoundEngine = string.Empty; + private readonly string bankName = string.Empty; + private readonly string romName = string.Empty; + private readonly SoundFontVersion romVersion = default; + private readonly string creationDate = string.Empty; + private readonly string author = string.Empty; + private readonly string targetProduct = string.Empty; + private readonly string copyright = string.Empty; + private readonly string comments = string.Empty; + private readonly string tools = string.Empty; + + internal SoundFontInfo(BinaryReader reader) + { + var chunkId = reader.ReadFourCC(); + if (chunkId != "LIST") + { + throw new InvalidDataException("The LIST chunk was not found."); + } + + var end = reader.BaseStream.Position + reader.ReadInt32(); + + var listType = reader.ReadFourCC(); + if (listType != "INFO") + { + throw new InvalidDataException($"The type of the LIST chunk must be 'INFO', but was '{listType}'."); + } + + while (reader.BaseStream.Position < end) + { + var id = reader.ReadFourCC(); + var size = reader.ReadInt32(); + + switch (id) + { + case "ifil": + version = new SoundFontVersion(reader.ReadInt16(), reader.ReadInt16()); + break; + case "isng": + targetSoundEngine = reader.ReadFixedLengthString(size); + break; + case "INAM": + bankName = reader.ReadFixedLengthString(size); + break; + case "irom": + romName = reader.ReadFixedLengthString(size); + break; + case "iver": + romVersion = new SoundFontVersion(reader.ReadInt16(), reader.ReadInt16()); + break; + case "ICRD": + creationDate = reader.ReadFixedLengthString(size); + break; + case "IENG": + author = reader.ReadFixedLengthString(size); + break; + case "IPRD": + targetProduct = reader.ReadFixedLengthString(size); + break; + case "ICOP": + copyright = reader.ReadFixedLengthString(size); + break; + case "ICMT": + comments = reader.ReadFixedLengthString(size); + break; + case "ISFT": + tools = reader.ReadFixedLengthString(size); + break; + default: + throw new InvalidDataException($"The INFO list contains an unknown ID '{id}'."); + } + } + } + + /// + /// Gets the name of the SoundFont. + /// + /// + /// The name of the SoundFont. + /// + public override string ToString() + { + return bankName; + } + + /// + /// The version of the SoundFont. + /// + public SoundFontVersion Version => version; + + /// + /// The target sound engine of the SoundFont. + /// + public string TargetSoundEngine => targetSoundEngine; + + /// + /// The bank name of the SoundFont. + /// + public string BankName => bankName; + + /// + /// The ROM name of the SoundFont. + /// + public string RomName => romName; + + /// + /// The ROM version of the SoundFont. + /// + public SoundFontVersion RomVersion => romVersion; + + /// + /// The creation date of the SoundFont. + /// + public string CeationDate => creationDate; + + /// + /// The auther of the SoundFont. + /// + public string Author => author; + + /// + /// The target product of the SoundFont. + /// + public string TargetProduct => targetProduct; + + /// + /// The copyright message for the SoundFont. + /// + public string Copyright => copyright; + + /// + /// The comments for the SoundFont. + /// + public string Comments => comments; + + /// + /// The tools used to create the SoundFont. + /// + public string Tools => tools; + } +} diff --git a/Assets/Scripts/Infrastructure/EQ/MeltySynth/SoundFontInfo.cs.meta b/Assets/Scripts/Infrastructure/EQ/MeltySynth/SoundFontInfo.cs.meta new file mode 100644 index 0000000..677b917 --- /dev/null +++ b/Assets/Scripts/Infrastructure/EQ/MeltySynth/SoundFontInfo.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: f7aba8ea57018484aa53fd2d06061ce8 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Scripts/Infrastructure/EQ/MeltySynth/SoundFontMath.cs b/Assets/Scripts/Infrastructure/EQ/MeltySynth/SoundFontMath.cs new file mode 100644 index 0000000..5ab12e4 --- /dev/null +++ b/Assets/Scripts/Infrastructure/EQ/MeltySynth/SoundFontMath.cs @@ -0,0 +1,55 @@ +using System; + +namespace Infrastructure.EQ.MeltySynth +{ + internal static class SoundFontMath + { + public const float HalfPi = MathF.PI / 2; + + public static readonly float NonAudible = 1.0E-3F; + + private static readonly double logNonAudible = Math.Log(1.0E-3); + + public static float TimecentsToSeconds(float x) + { + return MathF.Pow(2F, (1F / 1200F) * x); + } + + public static float CentsToHertz(float x) + { + return 8.176F * MathF.Pow(2F, (1F / 1200F) * x); + } + + public static float CentsToMultiplyingFactor(float x) + { + return MathF.Pow(2F, (1F / 1200F) * x); + } + + public static float DecibelsToLinear(float x) + { + return MathF.Pow(10F, 0.05F * x); + } + + public static float LinearToDecibels(float x) + { + return 20F * MathF.Log10(x); + } + + public static float KeyNumberToMultiplyingFactor(int cents, int key) + { + return TimecentsToSeconds(cents * (60 - key)); + } + + public static double ExpCutoff(double x) + { + if (x < logNonAudible) + { + return 0.0; + } + else + { + return Math.Exp(x); + } + } + } +} diff --git a/Assets/Scripts/Infrastructure/EQ/MeltySynth/SoundFontMath.cs.meta b/Assets/Scripts/Infrastructure/EQ/MeltySynth/SoundFontMath.cs.meta new file mode 100644 index 0000000..c045169 --- /dev/null +++ b/Assets/Scripts/Infrastructure/EQ/MeltySynth/SoundFontMath.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 6d4a2a545a6ae6349bcdfd8c3c18e33c +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Scripts/Infrastructure/EQ/MeltySynth/SoundFontParameters.cs b/Assets/Scripts/Infrastructure/EQ/MeltySynth/SoundFontParameters.cs new file mode 100644 index 0000000..191ef25 --- /dev/null +++ b/Assets/Scripts/Infrastructure/EQ/MeltySynth/SoundFontParameters.cs @@ -0,0 +1,92 @@ +using System.IO; + +namespace Infrastructure.EQ.MeltySynth +{ + internal sealed class SoundFontParameters + { + private readonly SampleHeader[] sampleHeaders; + private readonly Preset[] presets; + private readonly Instrument[] instruments; + + internal SoundFontParameters(BinaryReader reader) + { + var chunkId = reader.ReadFourCC(); + if (chunkId != "LIST") + { + throw new InvalidDataException("The LIST chunk was not found."); + } + + var end = reader.BaseStream.Position + reader.ReadInt32(); + + var listType = reader.ReadFourCC(); + if (listType != "pdta") + { + throw new InvalidDataException($"The type of the LIST chunk must be 'pdta', but was '{listType}'."); + } + + PresetInfo[]? presetInfos = null; + ZoneInfo[]? presetBag = null; + Generator[]? presetGenerators = null; + InstrumentInfo[]? instrumentInfos = null; + ZoneInfo[]? instrumentBag = null; + Generator[]? instrumentGenerators = null; + + while (reader.BaseStream.Position < end) + { + var id = reader.ReadFourCC(); + var size = reader.ReadInt32(); + + switch (id) + { + case "phdr": + presetInfos = PresetInfo.ReadFromChunk(reader, size); + break; + case "pbag": + presetBag = ZoneInfo.ReadFromChunk(reader, size); + break; + case "pmod": + Modulator.DiscardData(reader, size); + break; + case "pgen": + presetGenerators = Generator.ReadFromChunk(reader, size); + break; + case "inst": + instrumentInfos = InstrumentInfo.ReadFromChunk(reader, size); + break; + case "ibag": + instrumentBag = ZoneInfo.ReadFromChunk(reader, size); + break; + case "imod": + Modulator.DiscardData(reader, size); + break; + case "igen": + instrumentGenerators = Generator.ReadFromChunk(reader, size); + break; + case "shdr": + sampleHeaders = SampleHeader.ReadFromChunk(reader, size); + break; + default: + throw new InvalidDataException($"The INFO list contains an unknown ID '{id}'."); + } + } + + if (presetInfos == null) throw new InvalidDataException("The PHDR sub-chunk was not found."); + if (presetBag == null) throw new InvalidDataException("The PBAG sub-chunk was not found."); + if (presetGenerators == null) throw new InvalidDataException("The PGEN sub-chunk was not found."); + if (instrumentInfos == null) throw new InvalidDataException("The INST sub-chunk was not found."); + if (instrumentBag == null) throw new InvalidDataException("The IBAG sub-chunk was not found."); + if (instrumentGenerators == null) throw new InvalidDataException("The IGEN sub-chunk was not found."); + if (sampleHeaders == null) throw new InvalidDataException("The SHDR sub-chunk was not found."); + + var instrumentZones = Zone.Create(instrumentBag, instrumentGenerators); + instruments = Instrument.Create(instrumentInfos, instrumentZones, sampleHeaders); + + var presetZones = Zone.Create(presetBag, presetGenerators); + presets = Preset.Create(presetInfos, presetZones, instruments); + } + + public SampleHeader[] SampleHeaders => sampleHeaders; + public Preset[] Presets => presets; + public Instrument[] Instruments => instruments; + } +} diff --git a/Assets/Scripts/Infrastructure/EQ/MeltySynth/SoundFontParameters.cs.meta b/Assets/Scripts/Infrastructure/EQ/MeltySynth/SoundFontParameters.cs.meta new file mode 100644 index 0000000..e28202d --- /dev/null +++ b/Assets/Scripts/Infrastructure/EQ/MeltySynth/SoundFontParameters.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: d0977bc48f70d0b498c750d82257dfb9 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Scripts/Infrastructure/EQ/MeltySynth/SoundFontSampleData.cs b/Assets/Scripts/Infrastructure/EQ/MeltySynth/SoundFontSampleData.cs new file mode 100644 index 0000000..159ca94 --- /dev/null +++ b/Assets/Scripts/Infrastructure/EQ/MeltySynth/SoundFontSampleData.cs @@ -0,0 +1,64 @@ +using System; +using System.IO; +using System.Runtime.InteropServices; + +namespace Infrastructure.EQ.MeltySynth +{ + internal sealed class SoundFontSampleData + { + private readonly int bitsPerSample; + private readonly short[] samples; + + internal SoundFontSampleData(BinaryReader reader) + { + var chunkId = reader.ReadFourCC(); + if (chunkId != "LIST") + { + throw new InvalidDataException("The LIST chunk was not found."); + } + + var end = reader.BaseStream.Position + reader.ReadInt32(); + + var listType = reader.ReadFourCC(); + if (listType != "sdta") + { + throw new InvalidDataException($"The type of the LIST chunk must be 'sdta', but was '{listType}'."); + } + + while (reader.BaseStream.Position < end) + { + var id = reader.ReadFourCC(); + var size = reader.ReadInt32(); + + switch (id) + { + case "smpl": + bitsPerSample = 16; + samples = new short[size / 2]; + reader.Read(MemoryMarshal.Cast(samples)); + break; + case "sm24": + // 24 bit audio is not supported. + reader.BaseStream.Position += size; + break; + default: + throw new InvalidDataException($"The INFO list contains an unknown ID '{id}'."); + } + } + + if (samples == null) + { + throw new InvalidDataException("No valid sample data was found."); + } + + if (!BitConverter.IsLittleEndian) + { + // TODO: Insert the byte swapping code here. + throw new NotSupportedException("Big endian architectures are not yet supported."); + } + } + + public int BitsPerSample => bitsPerSample; + public short[] Samples => samples; + } +} diff --git a/Assets/Scripts/Infrastructure/EQ/MeltySynth/SoundFontSampleData.cs.meta b/Assets/Scripts/Infrastructure/EQ/MeltySynth/SoundFontSampleData.cs.meta new file mode 100644 index 0000000..6a4d405 --- /dev/null +++ b/Assets/Scripts/Infrastructure/EQ/MeltySynth/SoundFontSampleData.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: abb004b3816a10e40a74987af7f2f8ad +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Scripts/Infrastructure/EQ/MeltySynth/SoundFontVersion.cs b/Assets/Scripts/Infrastructure/EQ/MeltySynth/SoundFontVersion.cs new file mode 100644 index 0000000..92ed636 --- /dev/null +++ b/Assets/Scripts/Infrastructure/EQ/MeltySynth/SoundFontVersion.cs @@ -0,0 +1,36 @@ +namespace Infrastructure.EQ.MeltySynth +{ + /// + /// Reperesents the version of a SoundFont. + /// + public struct SoundFontVersion + { + private readonly short major; + private readonly short minor; + + internal SoundFontVersion(short major, short minor) + { + this.major = major; + this.minor = minor; + } + + /// + /// Gets the string representation of the version. + /// + /// + public override string ToString() + { + return $"{major}.{minor}"; + } + + /// + /// The major version. + /// + public short Major => major; + + /// + /// The minor version. + /// + public short Minor => minor; + } +} diff --git a/Assets/Scripts/Infrastructure/EQ/MeltySynth/SoundFontVersion.cs.meta b/Assets/Scripts/Infrastructure/EQ/MeltySynth/SoundFontVersion.cs.meta new file mode 100644 index 0000000..04037d9 --- /dev/null +++ b/Assets/Scripts/Infrastructure/EQ/MeltySynth/SoundFontVersion.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 59ec4ba488ef8a84998e3cdcf3e18559 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Scripts/Infrastructure/EQ/MeltySynth/Synthesizer.cs b/Assets/Scripts/Infrastructure/EQ/MeltySynth/Synthesizer.cs new file mode 100644 index 0000000..5149a10 --- /dev/null +++ b/Assets/Scripts/Infrastructure/EQ/MeltySynth/Synthesizer.cs @@ -0,0 +1,600 @@ +using System; +using System.Collections.Generic; +using System.Diagnostics; + +namespace Infrastructure.EQ.MeltySynth +{ + /// + /// An instance of the SoundFont synthesizer. + /// + /// + /// Note that this class does not provide thread safety. + /// If you want to send notes and render the waveform in separate threads, + /// you must ensure that the methods will not be called simultaneously. + /// + public sealed class Synthesizer : IAudioRenderer + { + private static readonly int channelCount = 16; + private static readonly int percussionChannel = 9; + + private readonly SoundFont soundFont; + private readonly int sampleRate; + private readonly int blockSize; + private readonly int maximumPolyphony; + private readonly bool enableReverbAndChorus; + + private readonly int minimumVoiceDuration; + + private readonly Dictionary presetLookup; + private readonly Preset defaultPreset; + + private readonly Channel[] channels; + + private readonly VoiceCollection voices; + + private readonly float[] blockLeft; + private readonly float[] blockRight; + + private readonly float inverseBlockSize; + + private int blockRead; + + private float masterVolume; + + private Reverb? reverb; + private float[]? reverbInput; + private float[]? reverbOutputLeft; + private float[]? reverbOutputRight; + + private Chorus? chorus; + private float[]? chorusInputLeft; + private float[]? chorusInputRight; + private float[]? chorusOutputLeft; + private float[]? chorusOutputRight; + + /// + /// Initializes a new synthesizer using a specified SoundFont and sample rate. + /// + /// The SoundFont file name and path. + /// The sample rate for synthesis. + public Synthesizer(string soundFontPath, int sampleRate) : this(new SoundFont(soundFontPath), new SynthesizerSettings(sampleRate)) + { + } + + /// + /// Initializes a new synthesizer using a specified SoundFont and sample rate. + /// + /// The SoundFont instance. + /// The sample rate for synthesis. + public Synthesizer(SoundFont soundFont, int sampleRate) : this(soundFont, new SynthesizerSettings(sampleRate)) + { + } + + /// + /// Initializes a new synthesizer using a specified SoundFont and settings. + /// + /// The SoundFont file name and path. + /// The settings for synthesis. + public Synthesizer(string soundFontPath, SynthesizerSettings settings) : this(new SoundFont(soundFontPath), settings) + { + } + + /// + /// Initializes a new synthesizer using a specified SoundFont and settings. + /// + /// The SoundFont instance. + /// The settings for synthesis. + public Synthesizer(SoundFont soundFont, SynthesizerSettings settings) + { + if (soundFont == null) + { + throw new ArgumentNullException(nameof(soundFont)); + } + + if (settings == null) + { + throw new ArgumentNullException(nameof(settings)); + } + + this.soundFont = soundFont; + this.sampleRate = settings.SampleRate; + this.blockSize = settings.BlockSize; + this.maximumPolyphony = settings.MaximumPolyphony; + this.enableReverbAndChorus = settings.EnableReverbAndChorus; + + minimumVoiceDuration = sampleRate / 500; + + presetLookup = new Dictionary(); + + var minPresetId = int.MaxValue; + foreach (var preset in soundFont.PresetArray) + { + // The preset ID is Int32, where the upper 16 bits represent the bank number + // and the lower 16 bits represent the patch number. + // This ID is used to search for presets by the combination of bank number + // and patch number. + var presetId = (preset.BankNumber << 16) | preset.PatchNumber; + presetLookup.TryAdd(presetId, preset); + + // The preset with the minimum ID number will be default. + // If the SoundFont is GM compatible, the piano will be chosen. + if (presetId < minPresetId) + { + defaultPreset = preset; + minPresetId = presetId; + } + } + // Default preset will never be null. + // This assertion suppresses the nullable warning. + Debug.Assert(defaultPreset != null); + + channels = new Channel[channelCount]; + for (var i = 0; i < channels.Length; i++) + { + channels[i] = new Channel(this, i == percussionChannel); + } + + voices = new VoiceCollection(this, maximumPolyphony); + + blockLeft = new float[blockSize]; + blockRight = new float[blockSize]; + + inverseBlockSize = 1F / blockSize; + + blockRead = blockSize; + + masterVolume = 0.5F; + + if (enableReverbAndChorus) + { + reverb = new Reverb(sampleRate); + reverbInput = new float[blockSize]; + reverbOutputLeft = new float[blockSize]; + reverbOutputRight = new float[blockSize]; + + chorus = new Chorus(sampleRate, 0.002, 0.0019, 0.4); + chorusInputLeft = new float[blockSize]; + chorusInputRight = new float[blockSize]; + chorusOutputLeft = new float[blockSize]; + chorusOutputRight = new float[blockSize]; + } + } + + /// + /// Processes a MIDI message. + /// + /// The channel to which the message will be sent. + /// The type of the message. + /// The first data part of the message. + /// The second data part of the message. + public void ProcessMidiMessage(int channel, int command, int data1, int data2) + { + if (!(0 <= channel && channel < channels.Length)) + { + return; + } + + var channelInfo = channels[channel]; + + switch (command) + { + case 0x80: // Note Off + NoteOff(channel, data1); + break; + + case 0x90: // Note On + NoteOn(channel, data1, data2); + break; + + case 0xB0: // Controller + switch (data1) + { + case 0x00: // Bank Selection + channelInfo.SetBank(data2); + break; + + case 0x01: // Modulation Coarse + channelInfo.SetModulationCoarse(data2); + break; + + case 0x21: // Modulation Fine + channelInfo.SetModulationFine(data2); + break; + + case 0x06: // Data Entry Coarse + channelInfo.DataEntryCoarse(data2); + break; + + case 0x26: // Data Entry Fine + channelInfo.DataEntryFine(data2); + break; + + case 0x07: // Channel Volume Coarse + channelInfo.SetVolumeCoarse(data2); + break; + + case 0x27: // Channel Volume Fine + channelInfo.SetVolumeFine(data2); + break; + + case 0x0A: // Pan Coarse + channelInfo.SetPanCoarse(data2); + break; + + case 0x2A: // Pan Fine + channelInfo.SetPanFine(data2); + break; + + case 0x0B: // Expression Coarse + channelInfo.SetExpressionCoarse(data2); + break; + + case 0x2B: // Expression Fine + channelInfo.SetExpressionFine(data2); + break; + + case 0x40: // Hold Pedal + channelInfo.SetHoldPedal(data2); + break; + + case 0x5B: // Reverb Send + channelInfo.SetReverbSend(data2); + break; + + case 0x5D: // Chorus Send + channelInfo.SetChorusSend(data2); + break; + + case 0x65: // RPN Coarse + channelInfo.SetRpnCoarse(data2); + break; + + case 0x64: // RPN Fine + channelInfo.SetRpnFine(data2); + break; + + case 0x78: // All Sound Off + NoteOffAll(channel, true); + break; + + case 0x79: // Reset All Controllers + ResetAllControllers(channel); + break; + + case 0x7B: // All Note Off + NoteOffAll(channel, false); + break; + } + break; + + case 0xC0: // Program Change + channelInfo.SetPatch(data1); + break; + + case 0xE0: // Pitch Bend + channelInfo.SetPitchBend(data1, data2); + break; + } + } + + /// + /// Stops a note. + /// + /// The channel of the note. + /// The key of the note. + public void NoteOff(int channel, int key) + { + if (!(0 <= channel && channel < channels.Length)) + { + return; + } + + foreach (var voice in voices) + { + if (voice.Channel == channel && voice.Key == key) + { + voice.End(); + } + } + } + + /// + /// Starts a note. + /// + /// The channel of the note. + /// The key of the note. + /// The velocity of the note. + public void NoteOn(int channel, int key, int velocity) + { + if (velocity == 0) + { + NoteOff(channel, key); + return; + } + + if (!(0 <= channel && channel < channels.Length)) + { + return; + } + + var channelInfo = channels[channel]; + + var presetId = (channelInfo.BankNumber << 16) | channelInfo.PatchNumber; + + Preset preset; + if (!presetLookup.TryGetValue(presetId, out preset)) + { + // Try fallback to the GM sound set. + // Normally, the given patch number + the bank number 0 will work. + // For drums (bank number >= 128), it seems to be better to select the standard set (128:0). + var gmPresetId = channelInfo.BankNumber < 128 ? channelInfo.PatchNumber : (128 << 16); + + if (!presetLookup.TryGetValue(gmPresetId, out preset)) + { + // No corresponding preset was found. Use the default one... + preset = defaultPreset; + } + } + + foreach (var presetRegion in preset.RegionArray) + { + if (presetRegion.Contains(key, velocity)) + { + foreach (var instrumentRegion in presetRegion.Instrument.RegionArray) + { + if (instrumentRegion.Contains(key, velocity)) + { + var regionPair = new RegionPair(presetRegion, instrumentRegion); + + var voice = voices.RequestNew(instrumentRegion, channel); + if (voice != null) + { + voice.Start(regionPair, channel, key, velocity); + } + } + } + } + } + } + + /// + /// Stops all the notes. + /// + /// If true, notes will stop immediately without the release sound. + public void NoteOffAll(bool immediate) + { + if (immediate) + { + voices.Clear(); + } + else + { + foreach (var voice in voices) + { + voice.End(); + } + } + } + + /// + /// Stops all the notes in the specified channel. + /// + /// The channel in which the notes will be stopped. + /// If true, notes will stop immediately without the release sound. + public void NoteOffAll(int channel, bool immediate) + { + if (immediate) + { + foreach (var voice in voices) + { + if (voice.Channel == channel) + { + voice.Kill(); + } + } + } + else + { + foreach (var voice in voices) + { + if (voice.Channel == channel) + { + voice.End(); + } + } + } + } + + /// + /// Resets all the controllers. + /// + public void ResetAllControllers() + { + foreach (var channel in channels) + { + channel.ResetAllControllers(); + } + } + + /// + /// Resets all the controllers of the specified channel. + /// + /// The channel to be reset. + public void ResetAllControllers(int channel) + { + if (!(0 <= channel && channel < channels.Length)) + { + return; + } + + channels[channel].ResetAllControllers(); + } + + /// + /// Resets the synthesizer. + /// + public void Reset() + { + voices.Clear(); + + foreach (var channel in channels) + { + channel.Reset(); + } + + if (enableReverbAndChorus) + { + reverb!.Mute(); + chorus!.Mute(); + } + + blockRead = blockSize; + } + + /// + public void Render(Span left, Span right) + { + if (left.Length != right.Length) + { + throw new ArgumentException("The output buffers for the left and right must be the same length."); + } + + var wrote = 0; + while (wrote < left.Length) + { + if (blockRead == blockSize) + { + RenderBlock(); + blockRead = 0; + } + + var srcRem = blockSize - blockRead; + var dstRem = left.Length - wrote; + var rem = Math.Min(srcRem, dstRem); + + blockLeft.AsSpan(blockRead, rem).CopyTo(left.Slice(wrote, rem)); + blockRight.AsSpan(blockRead, rem).CopyTo(right.Slice(wrote, rem)); + + blockRead += rem; + wrote += rem; + } + } + + private void RenderBlock() + { + voices.Process(); + + Array.Clear(blockLeft, 0, blockLeft.Length); + Array.Clear(blockRight, 0, blockRight.Length); + foreach (var voice in voices) + { + var previousGainLeft = masterVolume * voice.PreviousMixGainLeft; + var currentGainLeft = masterVolume * voice.CurrentMixGainLeft; + WriteBlock(previousGainLeft, currentGainLeft, voice.Block, blockLeft); + var previousGainRight = masterVolume * voice.PreviousMixGainRight; + var currentGainRight = masterVolume * voice.CurrentMixGainRight; + WriteBlock(previousGainRight, currentGainRight, voice.Block, blockRight); + } + + if (enableReverbAndChorus) + { + Array.Clear(chorusInputLeft!, 0, chorusInputLeft!.Length); + Array.Clear(chorusInputRight!, 0, chorusInputRight!.Length); + foreach (var voice in voices) + { + var previousGainLeft = voice.PreviousChorusSend * voice.PreviousMixGainLeft; + var currentGainLeft = voice.CurrentChorusSend * voice.CurrentMixGainLeft; + WriteBlock(previousGainLeft, currentGainLeft, voice.Block, chorusInputLeft); + var previousGainRight = voice.PreviousChorusSend * voice.PreviousMixGainRight; + var currentGainRight = voice.CurrentChorusSend * voice.CurrentMixGainRight; + WriteBlock(previousGainRight, currentGainRight, voice.Block, chorusInputRight); + } + chorus!.Process(chorusInputLeft, chorusInputRight, chorusOutputLeft!, chorusOutputRight!); + ArrayMath.MultiplyAdd(masterVolume, chorusOutputLeft!, blockLeft); + ArrayMath.MultiplyAdd(masterVolume, chorusOutputRight!, blockRight); + + Array.Clear(reverbInput!, 0, reverbInput!.Length); + foreach (var voice in voices) + { + var previousGain = reverb!.InputGain * voice.PreviousReverbSend * (voice.PreviousMixGainLeft + voice.PreviousMixGainRight); + var currentGain = reverb!.InputGain * voice.CurrentReverbSend * (voice.CurrentMixGainLeft + voice.CurrentMixGainRight); + WriteBlock(previousGain, currentGain, voice.Block, reverbInput); + } + reverb!.Process(reverbInput, reverbOutputLeft!, reverbOutputRight!); + ArrayMath.MultiplyAdd(masterVolume, reverbOutputLeft!, blockLeft); + ArrayMath.MultiplyAdd(masterVolume, reverbOutputRight!, blockRight); + } + } + + private void WriteBlock(float previousGain, float currentGain, float[] source, float[] destination) + { + if (Math.Max(previousGain, currentGain) < SoundFontMath.NonAudible) + { + return; + } + + if (MathF.Abs(currentGain - previousGain) < 1.0E-3) + { + ArrayMath.MultiplyAdd(currentGain, source, destination); + } + else + { + var step = inverseBlockSize * (currentGain - previousGain); + ArrayMath.MultiplyAdd(previousGain, step, source, destination); + } + } + + /// + /// The block size for rendering waveform. + /// + public int BlockSize => blockSize; + + /// + /// The number of maximum polyphony. + /// + public int MaximumPolyphony => maximumPolyphony; + + /// + /// The number of channels. + /// + /// + /// This value is always 16. + /// + public int ChannelCount => channelCount; + + /// + /// The percussion channel. + /// + /// + /// This value is always 9. + /// + public int PercussionChannel => percussionChannel; + + /// + /// The SoundFont used as the audio source. + /// + public SoundFont SoundFont => soundFont; + + /// + /// The sample rate for synthesis. + /// + public int SampleRate => sampleRate; + + /// + /// The number of voices currently being played. + /// + public int ActiveVoiceCount => voices.ActiveVoiceCount; + + /// + /// Gets or sets the master volume. + /// + public float MasterVolume + { + get => masterVolume; + set => masterVolume = value; + } + + internal int MinimumVoiceDuration => minimumVoiceDuration; + internal Channel[] Channels => channels; + } +} diff --git a/Assets/Scripts/Infrastructure/EQ/MeltySynth/Synthesizer.cs.meta b/Assets/Scripts/Infrastructure/EQ/MeltySynth/Synthesizer.cs.meta new file mode 100644 index 0000000..12f7b07 --- /dev/null +++ b/Assets/Scripts/Infrastructure/EQ/MeltySynth/Synthesizer.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 7ff56ef98aa40314b88f2ccae38a9f86 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Scripts/Infrastructure/EQ/MeltySynth/SynthesizerSettings.cs b/Assets/Scripts/Infrastructure/EQ/MeltySynth/SynthesizerSettings.cs new file mode 100644 index 0000000..00cfad9 --- /dev/null +++ b/Assets/Scripts/Infrastructure/EQ/MeltySynth/SynthesizerSettings.cs @@ -0,0 +1,108 @@ +using System; + +namespace Infrastructure.EQ.MeltySynth +{ + /// + /// Specifies a set of parameters for synthesis. + /// + public sealed class SynthesizerSettings + { + internal static int DefaultBlockSize = 64; + internal static int DefaultMaximumPolyphony = 64; + internal static bool DefaultEnableReverbAndChorus = true; + + private int sampleRate; + private int blockSize; + private int maximumPolyphony; + private bool enableReverbAndChorus; + + /// + /// Initializes a new instance of synthesizer settings. + /// + /// The sample rate for synthesis. + public SynthesizerSettings(int sampleRate) + { + CheckSampleRate(sampleRate); + + this.sampleRate = sampleRate; + this.blockSize = DefaultBlockSize; + this.maximumPolyphony = DefaultMaximumPolyphony; + this.enableReverbAndChorus = DefaultEnableReverbAndChorus; + } + + private static void CheckSampleRate(int value) + { + if (!(16000 <= value && value <= 192000)) + { + throw new ArgumentOutOfRangeException("The sample rate must be between 16000 and 192000."); + } + } + + private static void CheckBlockSize(int value) + { + if (!(8 <= value && value <= 1024)) + { + throw new ArgumentOutOfRangeException("The block size must be between 8 and 1024."); + } + } + + private static void CheckMaximumPolyphony(int value) + { + if (!(8 <= value && value <= 256)) + { + throw new ArgumentOutOfRangeException("The maximum number of polyphony must be between 8 and 256."); + } + } + + /// + /// Gets or sets the sample rate for synthesis. + /// + public int SampleRate + { + get => sampleRate; + + set + { + CheckSampleRate(value); + sampleRate = value; + } + } + + /// + /// Gets or sets the block size for rendering waveform. + /// + public int BlockSize + { + get => blockSize; + + set + { + CheckBlockSize(value); + blockSize = value; + } + } + + /// + /// Gets or sets the number of maximum polyphony. + /// + public int MaximumPolyphony + { + get => maximumPolyphony; + + set + { + CheckMaximumPolyphony(value); + maximumPolyphony = value; + } + } + + /// + /// Gets or sets whether reverb and chorus are enabled. + /// + public bool EnableReverbAndChorus + { + get => enableReverbAndChorus; + set => enableReverbAndChorus = value; + } + } +} diff --git a/Assets/Scripts/Infrastructure/EQ/MeltySynth/SynthesizerSettings.cs.meta b/Assets/Scripts/Infrastructure/EQ/MeltySynth/SynthesizerSettings.cs.meta new file mode 100644 index 0000000..eb36267 --- /dev/null +++ b/Assets/Scripts/Infrastructure/EQ/MeltySynth/SynthesizerSettings.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 08a3bfb8b97ec0b468c6ad44d58bf373 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Scripts/Infrastructure/EQ/MeltySynth/Voice.cs b/Assets/Scripts/Infrastructure/EQ/MeltySynth/Voice.cs new file mode 100644 index 0000000..7c9a60d --- /dev/null +++ b/Assets/Scripts/Infrastructure/EQ/MeltySynth/Voice.cs @@ -0,0 +1,302 @@ +using System; + +namespace Infrastructure.EQ.MeltySynth +{ + internal sealed class Voice + { + private readonly Synthesizer synthesizer; + + private readonly VolumeEnvelope volEnv; + private readonly ModulationEnvelope modEnv; + + private readonly Lfo vibLfo; + private readonly Lfo modLfo; + + private readonly Oscillator oscillator; + private readonly BiQuadFilter filter; + + private readonly float[] block; + + // A sudden change in the mix gain will cause pop noise. + // To avoid this, we save the mix gain of the previous block, + // and smooth out the gain if the gap between the current and previous gain is too large. + // The actual smoothing process is done in the WriteBlock method of the Synthesizer class. + + private float previousMixGainLeft; + private float previousMixGainRight; + private float currentMixGainLeft; + private float currentMixGainRight; + + private float previousReverbSend; + private float previousChorusSend; + private float currentReverbSend; + private float currentChorusSend; + + private int exclusiveClass; + private int channel; + private int key; + private int velocity; + + private float noteGain; + + private float cutoff; + private float resonance; + + private float vibLfoToPitch; + private float modLfoToPitch; + private float modEnvToPitch; + + private int modLfoToCutoff; + private int modEnvToCutoff; + private bool dynamicCutoff; + + private float modLfoToVolume; + private bool dynamicVolume; + + private float instrumentPan; + private float instrumentReverb; + private float instrumentChorus; + + // Some instruments require fast cutoff change, which can cause pop noise. + // This is used to smooth out the cutoff frequency. + private float smoothedCutoff; + + private VoiceState voiceState; + private int voiceLength; + + internal Voice(Synthesizer synthesizer) + { + this.synthesizer = synthesizer; + + volEnv = new VolumeEnvelope(synthesizer); + modEnv = new ModulationEnvelope(synthesizer); + + vibLfo = new Lfo(synthesizer); + modLfo = new Lfo(synthesizer); + + oscillator = new Oscillator(synthesizer); + filter = new BiQuadFilter(synthesizer); + + block = new float[synthesizer.BlockSize]; + } + + public void Start(RegionPair region, int channel, int key, int velocity) + { + this.exclusiveClass = region.ExclusiveClass; + this.channel = channel; + this.key = key; + this.velocity = velocity; + + if (velocity > 0) + { + // According to the Polyphone's implementation, the initial attenuation should be reduced to 40%. + // I'm not sure why, but this indeed improves the loudness variability. + var sampleAttenuation = 0.4F * region.InitialAttenuation; + var filterAttenuation = 0.5F * region.InitialFilterQ; + var decibels = 2 * SoundFontMath.LinearToDecibels(velocity / 127F) - sampleAttenuation - filterAttenuation; + noteGain = SoundFontMath.DecibelsToLinear(decibels); + } + else + { + noteGain = 0F; + } + + cutoff = region.InitialFilterCutoffFrequency; + resonance = SoundFontMath.DecibelsToLinear(region.InitialFilterQ); + + vibLfoToPitch = 0.01F * region.VibratoLfoToPitch; + modLfoToPitch = 0.01F * region.ModulationLfoToPitch; + modEnvToPitch = 0.01F * region.ModulationEnvelopeToPitch; + + modLfoToCutoff = region.ModulationLfoToFilterCutoffFrequency; + modEnvToCutoff = region.ModulationEnvelopeToFilterCutoffFrequency; + dynamicCutoff = modLfoToCutoff != 0 || modEnvToCutoff != 0; + + modLfoToVolume = region.ModulationLfoToVolume; + dynamicVolume = modLfoToVolume > 0.05F; + + instrumentPan = Math.Clamp(region.Pan, -50F, 50F); + instrumentReverb = 0.01F * region.ReverbEffectsSend; + instrumentChorus = 0.01F * region.ChorusEffectsSend; + + volEnv.Start(region, key, velocity); + modEnv.Start(region, key, velocity); + vibLfo.StartVibrato(region, key, velocity); + modLfo.StartModulation(region, key, velocity); + oscillator.Start(synthesizer.SoundFont.WaveDataArray, region); + filter.ClearBuffer(); + filter.SetLowPassFilter(cutoff, resonance); + + smoothedCutoff = cutoff; + + voiceState = VoiceState.Playing; + voiceLength = 0; + } + + public void End() + { + if (voiceState == VoiceState.Playing) + { + voiceState = VoiceState.ReleaseRequested; + } + } + + public void Kill() + { + noteGain = 0F; + } + + public bool Process() + { + if (noteGain < SoundFontMath.NonAudible) + { + return false; + } + + var channelInfo = synthesizer.Channels[channel]; + + ReleaseIfNecessary(channelInfo); + + if (!volEnv.Process()) + { + return false; + } + + modEnv.Process(); + vibLfo.Process(); + modLfo.Process(); + + var vibPitchChange = (0.01F * channelInfo.Modulation + vibLfoToPitch) * vibLfo.Value; + var modPitchChange = modLfoToPitch * modLfo.Value + modEnvToPitch * modEnv.Value; + var channelPitchChange = channelInfo.Tune + channelInfo.PitchBend; + var pitch = key + vibPitchChange + modPitchChange + channelPitchChange; + if (!oscillator.Process(block, pitch)) + { + return false; + } + + if (dynamicCutoff) + { + var cents = modLfoToCutoff * modLfo.Value + modEnvToCutoff * modEnv.Value; + var factor = SoundFontMath.CentsToMultiplyingFactor(cents); + var newCutoff = factor * cutoff; + + // The cutoff change is limited within x0.5 and x2 to reduce pop noise. + var lowerLimit = 0.5F * smoothedCutoff; + var upperLimit = 2F * smoothedCutoff; + smoothedCutoff = Math.Clamp(newCutoff, lowerLimit, upperLimit); + + filter.SetLowPassFilter(smoothedCutoff, resonance); + } + filter.Process(block); + + previousMixGainLeft = currentMixGainLeft; + previousMixGainRight = currentMixGainRight; + previousReverbSend = currentReverbSend; + previousChorusSend = currentChorusSend; + + // According to the GM spec, the following value should be squared. + var ve = channelInfo.Volume * channelInfo.Expression; + var channelGain = ve * ve; + + var mixGain = noteGain * channelGain * volEnv.Value; + if (dynamicVolume) + { + var decibels = modLfoToVolume * modLfo.Value; + mixGain *= SoundFontMath.DecibelsToLinear(decibels); + } + + var angle = (MathF.PI / 200F) * (channelInfo.Pan + instrumentPan + 50F); + if (angle <= 0F) + { + currentMixGainLeft = mixGain; + currentMixGainRight = 0F; + } + else if (angle >= SoundFontMath.HalfPi) + { + currentMixGainLeft = 0F; + currentMixGainRight = mixGain; + } + else + { + currentMixGainLeft = mixGain * MathF.Cos(angle); + currentMixGainRight = mixGain * MathF.Sin(angle); + } + + currentReverbSend = Math.Clamp(channelInfo.ReverbSend + instrumentReverb, 0F, 1F); + currentChorusSend = Math.Clamp(channelInfo.ChorusSend + instrumentChorus, 0F, 1F); + + if (voiceLength == 0) + { + previousMixGainLeft = currentMixGainLeft; + previousMixGainRight = currentMixGainRight; + previousReverbSend = currentReverbSend; + previousChorusSend = currentChorusSend; + } + + voiceLength += synthesizer.BlockSize; + + return true; + } + + private void ReleaseIfNecessary(Channel channelInfo) + { + if (voiceLength < synthesizer.MinimumVoiceDuration) + { + return; + } + + if (voiceState == VoiceState.ReleaseRequested && !channelInfo.HoldPedal) + { + volEnv.Release(); + modEnv.Release(); + oscillator.Release(); + + voiceState = VoiceState.Released; + } + } + + public float Priority + { + get + { + if (noteGain < SoundFontMath.NonAudible) + { + return 0F; + } + else + { + return volEnv.Priority; + } + } + } + + public float[] Block => block; + + public float PreviousMixGainLeft => previousMixGainLeft; + public float PreviousMixGainRight => previousMixGainRight; + public float CurrentMixGainLeft => currentMixGainLeft; + public float CurrentMixGainRight => currentMixGainRight; + + public float PreviousReverbSend => previousReverbSend; + public float PreviousChorusSend => previousChorusSend; + public float CurrentReverbSend => currentReverbSend; + public float CurrentChorusSend => currentChorusSend; + + public int ExclusiveClass => exclusiveClass; + public int Channel => channel; + public int Key => key; + public int Velocity => velocity; + + public int VoiceLength => voiceLength; + + + + private enum VoiceState + { + Playing, + ReleaseRequested, + Released + } + } +} diff --git a/Assets/Scripts/Infrastructure/EQ/MeltySynth/Voice.cs.meta b/Assets/Scripts/Infrastructure/EQ/MeltySynth/Voice.cs.meta new file mode 100644 index 0000000..272013a --- /dev/null +++ b/Assets/Scripts/Infrastructure/EQ/MeltySynth/Voice.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: afaad5364c090604ab6e9af076b02c4e +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Scripts/Infrastructure/EQ/MeltySynth/VoiceCollection.cs b/Assets/Scripts/Infrastructure/EQ/MeltySynth/VoiceCollection.cs new file mode 100644 index 0000000..18410b9 --- /dev/null +++ b/Assets/Scripts/Infrastructure/EQ/MeltySynth/VoiceCollection.cs @@ -0,0 +1,163 @@ +using System; +using System.Collections; +using System.Collections.Generic; + +namespace Infrastructure.EQ.MeltySynth +{ + internal sealed class VoiceCollection + { + private readonly Synthesizer synthesizer; + + private readonly Voice[] voices; + + private int activeVoiceCount; + + internal VoiceCollection(Synthesizer synthesizer, int maxActiveVoiceCount) + { + this.synthesizer = synthesizer; + + voices = new Voice[maxActiveVoiceCount]; + for (var i = 0; i < voices.Length; i++) + { + voices[i] = new Voice(synthesizer); + } + + activeVoiceCount = 0; + } + + public Voice? RequestNew(InstrumentRegion region, int channel) + { + // If an exclusive class is assigned to the region, find a voice with the same class. + // If found, reuse it to avoid playing multiple voices with the same class at a time. + var exclusiveClass = region.ExclusiveClass; + if (exclusiveClass != 0) + { + for (var i = 0; i < activeVoiceCount; i++) + { + var voice = voices[i]; + if (voice.ExclusiveClass == exclusiveClass && voice.Channel == channel) + { + return voice; + } + } + } + + // If the number of active voices is less than the limit, use a free one. + if (activeVoiceCount < voices.Length) + { + var free = voices[activeVoiceCount]; + activeVoiceCount++; + return free; + } + + // Too many active voices... + // Find one which has the lowest priority. + Voice? candidate = null; + var lowestPriority = float.MaxValue; + for (var i = 0; i < activeVoiceCount; i++) + { + var voice = voices[i]; + var priority = voice.Priority; + if (priority < lowestPriority) + { + lowestPriority = priority; + candidate = voice; + } + else if (priority == lowestPriority) + { + // Same priority... + // The older one should be more suitable for reuse. + if (voice.VoiceLength > candidate!.VoiceLength) + { + candidate = voice; + } + } + } + return candidate; + } + + public void Process() + { + var i = 0; + + while (true) + { + if (i == activeVoiceCount) + { + return; + } + + if (voices[i].Process()) + { + i++; + } + else + { + activeVoiceCount--; + + var tmp = voices[i]; + voices[i] = voices[activeVoiceCount]; + voices[activeVoiceCount] = tmp; + } + } + } + + public void Clear() + { + activeVoiceCount = 0; + } + + public Enumerator GetEnumerator() + { + return new Enumerator(this); + } + + public int ActiveVoiceCount => activeVoiceCount; + + + + public struct Enumerator : IEnumerator + { + private VoiceCollection collection; + + private int index; + private Voice? current; + + internal Enumerator(VoiceCollection collection) + { + this.collection = collection; + + index = 0; + current = null; + } + + public void Dispose() + { + } + + public bool MoveNext() + { + if (index < collection.activeVoiceCount) + { + current = collection.voices[index]; + index++; + return true; + } + else + { + return false; + } + } + + public void Reset() + { + index = 0; + current = null; + } + + public Voice Current => current!; + + object IEnumerator.Current => throw new NotSupportedException(); + } + } +} diff --git a/Assets/Scripts/Infrastructure/EQ/MeltySynth/VoiceCollection.cs.meta b/Assets/Scripts/Infrastructure/EQ/MeltySynth/VoiceCollection.cs.meta new file mode 100644 index 0000000..2d16e4f --- /dev/null +++ b/Assets/Scripts/Infrastructure/EQ/MeltySynth/VoiceCollection.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 006ec17f8f267584b9a652e978dc7924 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Scripts/Infrastructure/EQ/MeltySynth/VolumeEnvelope.cs b/Assets/Scripts/Infrastructure/EQ/MeltySynth/VolumeEnvelope.cs new file mode 100644 index 0000000..b0baad0 --- /dev/null +++ b/Assets/Scripts/Infrastructure/EQ/MeltySynth/VolumeEnvelope.cs @@ -0,0 +1,148 @@ +using System; + +namespace Infrastructure.EQ.MeltySynth +{ + internal sealed class VolumeEnvelope + { + private readonly Synthesizer synthesizer; + + private double attackSlope; + private double decaySlope; + private double releaseSlope; + + private double attackStartTime; + private double holdStartTime; + private double decayStartTime; + private double releaseStartTime; + + private float sustainLevel; + private float releaseLevel; + + private int processedSampleCount; + private Stage stage; + private float value; + + private float priority; + + internal VolumeEnvelope(Synthesizer synthesizer) + { + this.synthesizer = synthesizer; + } + + public void Start(float delay, float attack, float hold, float decay, float sustain, float release) + { + attackSlope = 1 / attack; + decaySlope = -9.226 / decay; + releaseSlope = -9.226 / release; + + attackStartTime = delay; + holdStartTime = attackStartTime + attack; + decayStartTime = holdStartTime + hold; + releaseStartTime = 0; + + sustainLevel = Math.Clamp(sustain, 0F, 1F); + releaseLevel = 0; + + processedSampleCount = 0; + stage = Stage.Delay; + value = 0; + + Process(0); + } + + public void Release() + { + stage = Stage.Release; + releaseStartTime = (double)processedSampleCount / synthesizer.SampleRate; + releaseLevel = value; + } + + public bool Process() + { + return Process(synthesizer.BlockSize); + } + + private bool Process(int sampleCount) + { + processedSampleCount += sampleCount; + + var currentTime = (double)processedSampleCount / synthesizer.SampleRate; + + while (stage <= Stage.Hold) + { + double endTime; + switch (stage) + { + case Stage.Delay: + endTime = attackStartTime; + break; + + case Stage.Attack: + endTime = holdStartTime; + break; + + case Stage.Hold: + endTime = decayStartTime; + break; + + default: + throw new InvalidOperationException("Invalid envelope stage."); + } + + if (currentTime < endTime) + { + break; + } + else + { + stage++; + } + } + + switch (stage) + { + case Stage.Delay: + value = 0; + priority = 4F + value; + return true; + + case Stage.Attack: + value = (float)(attackSlope * (currentTime - attackStartTime)); + priority = 3F + value; + return true; + + case Stage.Hold: + value = 1; + priority = 2F + value; + return true; + + case Stage.Decay: + value = Math.Max((float)SoundFontMath.ExpCutoff(decaySlope * (currentTime - decayStartTime)), sustainLevel); + priority = 1F + value; + return value > SoundFontMath.NonAudible; + + case Stage.Release: + value = (float)(releaseLevel * SoundFontMath.ExpCutoff(releaseSlope * (currentTime - releaseStartTime))); + priority = value; + return value > SoundFontMath.NonAudible; + + default: + throw new InvalidOperationException("Invalid envelope stage."); + } + } + + public float Value => value; + public float Priority => priority; + + + + private enum Stage + { + Delay, + Attack, + Hold, + Decay, + Release + } + } +} diff --git a/Assets/Scripts/Infrastructure/EQ/MeltySynth/VolumeEnvelope.cs.meta b/Assets/Scripts/Infrastructure/EQ/MeltySynth/VolumeEnvelope.cs.meta new file mode 100644 index 0000000..4a488a7 --- /dev/null +++ b/Assets/Scripts/Infrastructure/EQ/MeltySynth/VolumeEnvelope.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 69e58c39953be6244b993f4409e5c2a7 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Scripts/Infrastructure/EQ/MeltySynth/Zone.cs b/Assets/Scripts/Infrastructure/EQ/MeltySynth/Zone.cs new file mode 100644 index 0000000..a6be853 --- /dev/null +++ b/Assets/Scripts/Infrastructure/EQ/MeltySynth/Zone.cs @@ -0,0 +1,42 @@ +using System; +using System.IO; + +namespace Infrastructure.EQ.MeltySynth +{ + internal struct Zone + { + private ArraySegment generators; + + private Zone(ArraySegment generators) + { + this.generators = generators; + } + + private Zone(ZoneInfo info, Generator[] generators) + { + this.generators = new ArraySegment(generators, info.GeneratorIndex, info.GeneratorCount); + } + + internal static Zone[] Create(ZoneInfo[] infos, Generator[] generators) + { + if (infos.Length <= 1) + { + throw new InvalidDataException("No valid zone was found."); + } + + // The last one is the terminator. + var zones = new Zone[infos.Length - 1]; + + for (var i = 0; i < zones.Length; i++) + { + zones[i] = new Zone(infos[i], generators); + } + + return zones; + } + + public static Zone Empty => new Zone(ArraySegment.Empty); + + public ArraySegment Generators => generators; + } +} diff --git a/Assets/Scripts/Infrastructure/EQ/MeltySynth/Zone.cs.meta b/Assets/Scripts/Infrastructure/EQ/MeltySynth/Zone.cs.meta new file mode 100644 index 0000000..542105c --- /dev/null +++ b/Assets/Scripts/Infrastructure/EQ/MeltySynth/Zone.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 126f5336e7b499c48b53813d236f7ca2 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Scripts/Infrastructure/EQ/MeltySynth/ZoneInfo.cs b/Assets/Scripts/Infrastructure/EQ/MeltySynth/ZoneInfo.cs new file mode 100644 index 0000000..fd4c842 --- /dev/null +++ b/Assets/Scripts/Infrastructure/EQ/MeltySynth/ZoneInfo.cs @@ -0,0 +1,48 @@ +using System.IO; + +namespace Infrastructure.EQ.MeltySynth +{ + internal sealed class ZoneInfo + { + private int generatorIndex; + private int modulatorIndex; + private int generatorCount; + private int modulatorCount; + + private ZoneInfo(BinaryReader reader) + { + generatorIndex = reader.ReadUInt16(); + modulatorIndex = reader.ReadUInt16(); + } + + internal static ZoneInfo[] ReadFromChunk(BinaryReader reader, int size) + { + if (size % 4 != 0) + { + throw new InvalidDataException("The zone list is invalid."); + } + + var count = size / 4; + + var zones = new ZoneInfo[count]; + + for (var i = 0; i < count; i++) + { + zones[i] = new ZoneInfo(reader); + } + + for (var i = 0; i < count - 1; i++) + { + zones[i].generatorCount = zones[i + 1].generatorIndex - zones[i].generatorIndex; + zones[i].modulatorCount = zones[i + 1].modulatorIndex - zones[i].modulatorIndex; + } + + return zones; + } + + public int GeneratorIndex => generatorIndex; + public int ModulatorIndex => modulatorIndex; + public int GeneratorCount => generatorCount; + public int ModulatorCount => modulatorCount; + } +} diff --git a/Assets/Scripts/Infrastructure/EQ/MeltySynth/ZoneInfo.cs.meta b/Assets/Scripts/Infrastructure/EQ/MeltySynth/ZoneInfo.cs.meta new file mode 100644 index 0000000..6a102c5 --- /dev/null +++ b/Assets/Scripts/Infrastructure/EQ/MeltySynth/ZoneInfo.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 7b6d6fd9972d81c4a89d7b6f4d78b5dc +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Scripts/Infrastructure/EQ/SQLite.meta b/Assets/Scripts/Infrastructure/EQ/SQLite.meta new file mode 100644 index 0000000..0bdd528 --- /dev/null +++ b/Assets/Scripts/Infrastructure/EQ/SQLite.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: 428dc8e6cae2a154cab3483f3495bcab +folderAsset: yes +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Scripts/Infrastructure/EQ/SQLite/SQLite.cs b/Assets/Scripts/Infrastructure/EQ/SQLite/SQLite.cs new file mode 100644 index 0000000..06f84f5 --- /dev/null +++ b/Assets/Scripts/Infrastructure/EQ/SQLite/SQLite.cs @@ -0,0 +1,5234 @@ +// +// Copyright (c) 2009-2021 Krueger Systems, Inc. +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. +// +#if WINDOWS_PHONE && !USE_WP8_NATIVE_SQLITE +#define USE_CSHARP_SQLITE +#endif + +#if !USE_SQLITEPCL_RAW +#endif + +#if USE_CSHARP_SQLITE +using Sqlite3 = Community.CsharpSqlite.Sqlite3; +using Sqlite3DatabaseHandle = Community.CsharpSqlite.Sqlite3.sqlite3; +using Sqlite3Statement = Community.CsharpSqlite.Sqlite3.Vdbe; +#elif USE_WP8_NATIVE_SQLITE +using Sqlite3 = Sqlite.Sqlite3; +using Sqlite3DatabaseHandle = Sqlite.Database; +using Sqlite3Statement = Sqlite.Statement; +#elif USE_SQLITEPCL_RAW +using Sqlite3DatabaseHandle = SQLitePCL.sqlite3; +using Sqlite3BackupHandle = SQLitePCL.sqlite3_backup; +using Sqlite3Statement = SQLitePCL.sqlite3_stmt; +using Sqlite3 = SQLitePCL.raw; +#else +using Sqlite3DatabaseHandle = System.IntPtr; +using Sqlite3BackupHandle = System.IntPtr; +using Sqlite3Statement = System.IntPtr; +#endif +using System; +using System.Collections; +using System.Collections.Generic; +using System.Diagnostics; +using System.Linq; +using System.Linq.Expressions; +using System.Reflection; +using System.Runtime.InteropServices; +using System.Text; +using System.Threading; + +#pragma warning disable 1591 // XML Doc Comments + +namespace Infrastructure.Lantern.SQLite +{ + public class SQLiteException : Exception + { + public SQLite3.Result Result { get; private set; } + + protected SQLiteException(SQLite3.Result r, string message) : base(message) + { + Result = r; + } + + public static SQLiteException New(SQLite3.Result r, string message) + { + return new SQLiteException(r, message); + } + } + + public class NotNullConstraintViolationException : SQLiteException + { + public IEnumerable Columns { get; protected set; } + + protected NotNullConstraintViolationException(SQLite3.Result r, string message) + : this(r, message, null, null) + { + + } + + protected NotNullConstraintViolationException(SQLite3.Result r, string message, TableMapping mapping, object obj) + : base(r, message) + { + if (mapping != null && obj != null) + { + this.Columns = from c in mapping.Columns + where c.IsNullable == false && c.GetValue(obj) == null + select c; + } + } + + public static new NotNullConstraintViolationException New(SQLite3.Result r, string message) + { + return new NotNullConstraintViolationException(r, message); + } + + public static NotNullConstraintViolationException New(SQLite3.Result r, string message, TableMapping mapping, object obj) + { + return new NotNullConstraintViolationException(r, message, mapping, obj); + } + + public static NotNullConstraintViolationException New(SQLiteException exception, TableMapping mapping, object obj) + { + return new NotNullConstraintViolationException(exception.Result, exception.Message, mapping, obj); + } + } + + [Flags] + public enum SQLiteOpenFlags + { + ReadOnly = 1, ReadWrite = 2, Create = 4, + NoMutex = 0x8000, FullMutex = 0x10000, + SharedCache = 0x20000, PrivateCache = 0x40000, + ProtectionComplete = 0x00100000, + ProtectionCompleteUnlessOpen = 0x00200000, + ProtectionCompleteUntilFirstUserAuthentication = 0x00300000, + ProtectionNone = 0x00400000 + } + + [Flags] + public enum CreateFlags + { + /// + /// Use the default creation options + /// + None = 0x000, + /// + /// Create a primary key index for a property called 'Id' (case-insensitive). + /// This avoids the need for the [PrimaryKey] attribute. + /// + ImplicitPK = 0x001, + /// + /// Create indices for properties ending in 'Id' (case-insensitive). + /// + ImplicitIndex = 0x002, + /// + /// Create a primary key for a property called 'Id' and + /// create an indices for properties ending in 'Id' (case-insensitive). + /// + AllImplicit = 0x003, + /// + /// Force the primary key property to be auto incrementing. + /// This avoids the need for the [AutoIncrement] attribute. + /// The primary key property on the class should have type int or long. + /// + AutoIncPK = 0x004, + /// + /// Create virtual table using FTS3 + /// + FullTextSearch3 = 0x100, + /// + /// Create virtual table using FTS4 + /// + FullTextSearch4 = 0x200 + } + + /// + /// An open connection to a SQLite database. + /// + [Preserve(AllMembers = true)] + public partial class SQLiteConnection : IDisposable + { + private bool _open; + private TimeSpan _busyTimeout; + readonly static Dictionary _mappings = new Dictionary(); + private System.Diagnostics.Stopwatch _sw; + private long _elapsedMilliseconds = 0; + + private int _transactionDepth = 0; + private Random _rand = new Random(); + + public Sqlite3DatabaseHandle Handle { get; private set; } + static readonly Sqlite3DatabaseHandle NullHandle = default(Sqlite3DatabaseHandle); + static readonly Sqlite3BackupHandle NullBackupHandle = default(Sqlite3BackupHandle); + + /// + /// Gets the database path used by this connection. + /// + public string DatabasePath { get; private set; } + + /// + /// Gets the SQLite library version number. 3007014 would be v3.7.14 + /// + public int LibVersionNumber { get; private set; } + + /// + /// Whether Trace lines should be written that show the execution time of queries. + /// + public bool TimeExecution { get; set; } + + /// + /// Whether to write queries to during execution. + /// + public bool Trace { get; set; } + + /// + /// The delegate responsible for writing trace lines. + /// + /// The tracer. + public Action Tracer { get; set; } + + /// + /// Whether to store DateTime properties as ticks (true) or strings (false). + /// + public bool StoreDateTimeAsTicks { get; private set; } + + /// + /// Whether to store TimeSpan properties as ticks (true) or strings (false). + /// + public bool StoreTimeSpanAsTicks { get; private set; } + + /// + /// The format to use when storing DateTime properties as strings. Ignored if StoreDateTimeAsTicks is true. + /// + /// The date time string format. + public string DateTimeStringFormat { get; private set; } + + /// + /// The DateTimeStyles value to use when parsing a DateTime property string. + /// + /// The date time style. + internal System.Globalization.DateTimeStyles DateTimeStyle { get; private set; } + +#if USE_SQLITEPCL_RAW && !NO_SQLITEPCL_RAW_BATTERIES + static SQLiteConnection () + { + SQLitePCL.Batteries_V2.Init (); + } +#endif + + /// + /// Constructs a new SQLiteConnection and opens a SQLite database specified by databasePath. + /// + /// + /// Specifies the path to the database file. + /// + /// + /// Specifies whether to store DateTime properties as ticks (true) or strings (false). You + /// absolutely do want to store them as Ticks in all new projects. The value of false is + /// only here for backwards compatibility. There is a *significant* speed advantage, with no + /// down sides, when setting storeDateTimeAsTicks = true. + /// If you use DateTimeOffset properties, it will be always stored as ticks regardingless + /// the storeDateTimeAsTicks parameter. + /// + public SQLiteConnection(string databasePath, bool storeDateTimeAsTicks = true) + : this(new SQLiteConnectionString(databasePath, SQLiteOpenFlags.ReadWrite | SQLiteOpenFlags.Create, storeDateTimeAsTicks)) + { + } + + /// + /// Constructs a new SQLiteConnection and opens a SQLite database specified by databasePath. + /// + /// + /// Specifies the path to the database file. + /// + /// + /// Flags controlling how the connection should be opened. + /// + /// + /// Specifies whether to store DateTime properties as ticks (true) or strings (false). You + /// absolutely do want to store them as Ticks in all new projects. The value of false is + /// only here for backwards compatibility. There is a *significant* speed advantage, with no + /// down sides, when setting storeDateTimeAsTicks = true. + /// If you use DateTimeOffset properties, it will be always stored as ticks regardingless + /// the storeDateTimeAsTicks parameter. + /// + public SQLiteConnection(string databasePath, SQLiteOpenFlags openFlags, bool storeDateTimeAsTicks = true) + : this(new SQLiteConnectionString(databasePath, openFlags, storeDateTimeAsTicks)) + { + } + + /// + /// Constructs a new SQLiteConnection and opens a SQLite database specified by databasePath. + /// + /// + /// Details on how to find and open the database. + /// + public SQLiteConnection(SQLiteConnectionString connectionString) + { + if (connectionString == null) + throw new ArgumentNullException(nameof(connectionString)); + if (connectionString.DatabasePath == null) + throw new InvalidOperationException("DatabasePath must be specified"); + + DatabasePath = connectionString.DatabasePath; + + LibVersionNumber = SQLite3.LibVersionNumber(); + +#if NETFX_CORE + SQLite3.SetDirectory(/*temp directory type*/2, Windows.Storage.ApplicationData.Current.TemporaryFolder.Path); +#endif + + Sqlite3DatabaseHandle handle; + +#if SILVERLIGHT || USE_CSHARP_SQLITE || USE_SQLITEPCL_RAW + var r = SQLite3.Open (connectionString.DatabasePath, out handle, (int)connectionString.OpenFlags, connectionString.VfsName); +#else + // open using the byte[] + // in the case where the path may include Unicode + // force open to using UTF-8 using sqlite3_open_v2 + var databasePathAsBytes = GetNullTerminatedUtf8(connectionString.DatabasePath); + var r = SQLite3.Open(databasePathAsBytes, out handle, (int)connectionString.OpenFlags, connectionString.VfsName); +#endif + + Handle = handle; + if (r != SQLite3.Result.OK) + { + throw SQLiteException.New(r, String.Format("Could not open database file: {0} ({1})", DatabasePath, r)); + } + _open = true; + + StoreDateTimeAsTicks = connectionString.StoreDateTimeAsTicks; + StoreTimeSpanAsTicks = connectionString.StoreTimeSpanAsTicks; + DateTimeStringFormat = connectionString.DateTimeStringFormat; + DateTimeStyle = connectionString.DateTimeStyle; + + BusyTimeout = TimeSpan.FromSeconds(1.0); + Tracer = line => Debug.WriteLine(line); + + connectionString.PreKeyAction?.Invoke(this); + if (connectionString.Key is string stringKey) + { + SetKey(stringKey); + } + else if (connectionString.Key is byte[] bytesKey) + { + SetKey(bytesKey); + } + else if (connectionString.Key != null) + { + throw new InvalidOperationException("Encryption keys must be strings or byte arrays"); + } + connectionString.PostKeyAction?.Invoke(this); + } + + /// + /// Enables the write ahead logging. WAL is significantly faster in most scenarios + /// by providing better concurrency and better disk IO performance than the normal + /// journal mode. You only need to call this function once in the lifetime of the database. + /// + public void EnableWriteAheadLogging() + { + ExecuteScalar("PRAGMA journal_mode=WAL"); + } + + /// + /// Convert an input string to a quoted SQL string that can be safely used in queries. + /// + /// The quoted string. + /// The unsafe string to quote. + static string Quote(string unsafeString) + { + // TODO: Doesn't call sqlite3_mprintf("%Q", u) because we're waiting on https://github.com/ericsink/SQLitePCL.raw/issues/153 + if (unsafeString == null) + return "NULL"; + var safe = unsafeString.Replace("'", "''"); + return "'" + safe + "'"; + } + + /// + /// Sets the key used to encrypt/decrypt the database with "pragma key = ...". + /// This must be the first thing you call before doing anything else with this connection + /// if your database is encrypted. + /// This only has an effect if you are using the SQLCipher nuget package. + /// + /// Ecryption key plain text that is converted to the real encryption key using PBKDF2 key derivation + void SetKey(string key) + { + if (key == null) + throw new ArgumentNullException(nameof(key)); + var q = Quote(key); + ExecuteScalar("pragma key = " + q); + } + + /// + /// Sets the key used to encrypt/decrypt the database. + /// This must be the first thing you call before doing anything else with this connection + /// if your database is encrypted. + /// This only has an effect if you are using the SQLCipher nuget package. + /// + /// 256-bit (32 byte) ecryption key data + void SetKey(byte[] key) + { + if (key == null) + throw new ArgumentNullException(nameof(key)); + if (key.Length != 32 && key.Length != 48) + throw new ArgumentException("Key must be 32 bytes (256-bit) or 48 bytes (384-bit)", nameof(key)); + var s = String.Join("", key.Select(x => x.ToString("X2"))); + ExecuteScalar("pragma key = \"x'" + s + "'\""); + } + + /// + /// Enable or disable extension loading. + /// + public void EnableLoadExtension(bool enabled) + { + SQLite3.Result r = SQLite3.EnableLoadExtension(Handle, enabled ? 1 : 0); + if (r != SQLite3.Result.OK) + { + string msg = SQLite3.GetErrmsg(Handle); + throw SQLiteException.New(r, msg); + } + } + +#if !USE_SQLITEPCL_RAW + static byte[] GetNullTerminatedUtf8(string s) + { + var utf8Length = System.Text.Encoding.UTF8.GetByteCount(s); + var bytes = new byte[utf8Length + 1]; + utf8Length = System.Text.Encoding.UTF8.GetBytes(s, 0, s.Length, bytes, 0); + return bytes; + } +#endif + + /// + /// Sets a busy handler to sleep the specified amount of time when a table is locked. + /// The handler will sleep multiple times until a total time of has accumulated. + /// + public TimeSpan BusyTimeout + { + get { return _busyTimeout; } + set + { + _busyTimeout = value; + if (Handle != NullHandle) + { + SQLite3.BusyTimeout(Handle, (int)_busyTimeout.TotalMilliseconds); + } + } + } + + /// + /// Returns the mappings from types to tables that the connection + /// currently understands. + /// + public IEnumerable TableMappings + { + get + { + lock (_mappings) + { + return new List(_mappings.Values); + } + } + } + + /// + /// Retrieves the mapping that is automatically generated for the given type. + /// + /// + /// The type whose mapping to the database is returned. + /// + /// + /// Optional flags allowing implicit PK and indexes based on naming conventions + /// + /// + /// The mapping represents the schema of the columns of the database and contains + /// methods to set and get properties of objects. + /// + public TableMapping GetMapping(Type type, CreateFlags createFlags = CreateFlags.None) + { + TableMapping map; + var key = type.FullName; + lock (_mappings) + { + if (_mappings.TryGetValue(key, out map)) + { + if (createFlags != CreateFlags.None && createFlags != map.CreateFlags) + { + map = new TableMapping(type, createFlags); + _mappings[key] = map; + } + } + else + { + map = new TableMapping(type, createFlags); + _mappings.Add(key, map); + } + } + return map; + } + + /// + /// Retrieves the mapping that is automatically generated for the given type. + /// + /// + /// Optional flags allowing implicit PK and indexes based on naming conventions + /// + /// + /// The mapping represents the schema of the columns of the database and contains + /// methods to set and get properties of objects. + /// + public TableMapping GetMapping(CreateFlags createFlags = CreateFlags.None) + { + return GetMapping(typeof(T), createFlags); + } + + private struct IndexedColumn + { + public int Order; + public string ColumnName; + } + + private struct IndexInfo + { + public string IndexName; + public string TableName; + public bool Unique; + public List Columns; + } + + /// + /// Executes a "drop table" on the database. This is non-recoverable. + /// + public int DropTable() + { + return DropTable(GetMapping(typeof(T))); + } + + /// + /// Executes a "drop table" on the database. This is non-recoverable. + /// + /// + /// The TableMapping used to identify the table. + /// + public int DropTable(TableMapping map) + { + var query = string.Format("drop table if exists \"{0}\"", map.TableName); + return Execute(query); + } + + /// + /// Executes a "create table if not exists" on the database. It also + /// creates any specified indexes on the columns of the table. It uses + /// a schema automatically generated from the specified type. You can + /// later access this schema by calling GetMapping. + /// + /// + /// Whether the table was created or migrated. + /// + public CreateTableResult CreateTable(CreateFlags createFlags = CreateFlags.None) + { + return CreateTable(typeof(T), createFlags); + } + + /// + /// Executes a "create table if not exists" on the database. It also + /// creates any specified indexes on the columns of the table. It uses + /// a schema automatically generated from the specified type. You can + /// later access this schema by calling GetMapping. + /// + /// Type to reflect to a database table. + /// Optional flags allowing implicit PK and indexes based on naming conventions. + /// + /// Whether the table was created or migrated. + /// + public CreateTableResult CreateTable(Type ty, CreateFlags createFlags = CreateFlags.None) + { + var map = GetMapping(ty, createFlags); + + // Present a nice error if no columns specified + if (map.Columns.Length == 0) + { + throw new Exception(string.Format("Cannot create a table without columns (does '{0}' have public properties?)", ty.FullName)); + } + + // Check if the table exists + var result = CreateTableResult.Created; + var existingCols = GetTableInfo(map.TableName); + + // Create or migrate it + if (existingCols.Count == 0) + { + + // Facilitate virtual tables a.k.a. full-text search. + bool fts3 = (createFlags & CreateFlags.FullTextSearch3) != 0; + bool fts4 = (createFlags & CreateFlags.FullTextSearch4) != 0; + bool fts = fts3 || fts4; + var @virtual = fts ? "virtual " : string.Empty; + var @using = fts3 ? "using fts3 " : fts4 ? "using fts4 " : string.Empty; + + // Build query. + var query = "create " + @virtual + "table if not exists \"" + map.TableName + "\" " + @using + "(\n"; + var decls = map.Columns.Select(p => Orm.SqlDecl(p, StoreDateTimeAsTicks, StoreTimeSpanAsTicks)); + var decl = string.Join(",\n", decls.ToArray()); + query += decl; + query += ")"; + if (map.WithoutRowId) + { + query += " without rowid"; + } + + Execute(query); + } + else + { + result = CreateTableResult.Migrated; + MigrateTable(map, existingCols); + } + + var indexes = new Dictionary(); + foreach (var c in map.Columns) + { + foreach (var i in c.Indices) + { + var iname = i.Name ?? map.TableName + "_" + c.Name; + IndexInfo iinfo; + if (!indexes.TryGetValue(iname, out iinfo)) + { + iinfo = new IndexInfo + { + IndexName = iname, + TableName = map.TableName, + Unique = i.Unique, + Columns = new List() + }; + indexes.Add(iname, iinfo); + } + + if (i.Unique != iinfo.Unique) + throw new Exception("All the columns in an index must have the same value for their Unique property"); + + iinfo.Columns.Add(new IndexedColumn + { + Order = i.Order, + ColumnName = c.Name + }); + } + } + + foreach (var indexName in indexes.Keys) + { + var index = indexes[indexName]; + var columns = index.Columns.OrderBy(i => i.Order).Select(i => i.ColumnName).ToArray(); + CreateIndex(indexName, index.TableName, columns, index.Unique); + } + + return result; + } + + /// + /// Executes a "create table if not exists" on the database for each type. It also + /// creates any specified indexes on the columns of the table. It uses + /// a schema automatically generated from the specified type. You can + /// later access this schema by calling GetMapping. + /// + /// + /// Whether the table was created or migrated for each type. + /// + public CreateTablesResult CreateTables(CreateFlags createFlags = CreateFlags.None) + where T : new() + where T2 : new() + { + return CreateTables(createFlags, typeof(T), typeof(T2)); + } + + /// + /// Executes a "create table if not exists" on the database for each type. It also + /// creates any specified indexes on the columns of the table. It uses + /// a schema automatically generated from the specified type. You can + /// later access this schema by calling GetMapping. + /// + /// + /// Whether the table was created or migrated for each type. + /// + public CreateTablesResult CreateTables(CreateFlags createFlags = CreateFlags.None) + where T : new() + where T2 : new() + where T3 : new() + { + return CreateTables(createFlags, typeof(T), typeof(T2), typeof(T3)); + } + + /// + /// Executes a "create table if not exists" on the database for each type. It also + /// creates any specified indexes on the columns of the table. It uses + /// a schema automatically generated from the specified type. You can + /// later access this schema by calling GetMapping. + /// + /// + /// Whether the table was created or migrated for each type. + /// + public CreateTablesResult CreateTables(CreateFlags createFlags = CreateFlags.None) + where T : new() + where T2 : new() + where T3 : new() + where T4 : new() + { + return CreateTables(createFlags, typeof(T), typeof(T2), typeof(T3), typeof(T4)); + } + + /// + /// Executes a "create table if not exists" on the database for each type. It also + /// creates any specified indexes on the columns of the table. It uses + /// a schema automatically generated from the specified type. You can + /// later access this schema by calling GetMapping. + /// + /// + /// Whether the table was created or migrated for each type. + /// + public CreateTablesResult CreateTables(CreateFlags createFlags = CreateFlags.None) + where T : new() + where T2 : new() + where T3 : new() + where T4 : new() + where T5 : new() + { + return CreateTables(createFlags, typeof(T), typeof(T2), typeof(T3), typeof(T4), typeof(T5)); + } + + /// + /// Executes a "create table if not exists" on the database for each type. It also + /// creates any specified indexes on the columns of the table. It uses + /// a schema automatically generated from the specified type. You can + /// later access this schema by calling GetMapping. + /// + /// + /// Whether the table was created or migrated for each type. + /// + public CreateTablesResult CreateTables(CreateFlags createFlags = CreateFlags.None, params Type[] types) + { + var result = new CreateTablesResult(); + foreach (Type type in types) + { + var aResult = CreateTable(type, createFlags); + result.Results[type] = aResult; + } + return result; + } + + /// + /// Creates an index for the specified table and columns. + /// + /// Name of the index to create + /// Name of the database table + /// An array of column names to index + /// Whether the index should be unique + /// Zero on success. + public int CreateIndex(string indexName, string tableName, string[] columnNames, bool unique = false) + { + const string sqlFormat = "create {2} index if not exists \"{3}\" on \"{0}\"(\"{1}\")"; + var sql = String.Format(sqlFormat, tableName, string.Join("\", \"", columnNames), unique ? "unique" : "", indexName); + return Execute(sql); + } + + /// + /// Creates an index for the specified table and column. + /// + /// Name of the index to create + /// Name of the database table + /// Name of the column to index + /// Whether the index should be unique + /// Zero on success. + public int CreateIndex(string indexName, string tableName, string columnName, bool unique = false) + { + return CreateIndex(indexName, tableName, new string[] { columnName }, unique); + } + + /// + /// Creates an index for the specified table and column. + /// + /// Name of the database table + /// Name of the column to index + /// Whether the index should be unique + /// Zero on success. + public int CreateIndex(string tableName, string columnName, bool unique = false) + { + return CreateIndex(tableName + "_" + columnName, tableName, columnName, unique); + } + + /// + /// Creates an index for the specified table and columns. + /// + /// Name of the database table + /// An array of column names to index + /// Whether the index should be unique + /// Zero on success. + public int CreateIndex(string tableName, string[] columnNames, bool unique = false) + { + return CreateIndex(tableName + "_" + string.Join("_", columnNames), tableName, columnNames, unique); + } + + /// + /// Creates an index for the specified object property. + /// e.g. CreateIndex<Client>(c => c.Name); + /// + /// Type to reflect to a database table. + /// Property to index + /// Whether the index should be unique + /// Zero on success. + public int CreateIndex(Expression> property, bool unique = false) + { + MemberExpression mx; + if (property.Body.NodeType == ExpressionType.Convert) + { + mx = ((UnaryExpression)property.Body).Operand as MemberExpression; + } + else + { + mx = (property.Body as MemberExpression); + } + var propertyInfo = mx.Member as PropertyInfo; + if (propertyInfo == null) + { + throw new ArgumentException("The lambda expression 'property' should point to a valid Property"); + } + + var propName = propertyInfo.Name; + + var map = GetMapping(); + var colName = map.FindColumnWithPropertyName(propName).Name; + + return CreateIndex(map.TableName, colName, unique); + } + + [Preserve(AllMembers = true)] + public class ColumnInfo + { + // public int cid { get; set; } + + [Column("name")] + public string Name { get; set; } + + // [Column ("type")] + // public string ColumnType { get; set; } + + public int notnull { get; set; } + + // public string dflt_value { get; set; } + + // public int pk { get; set; } + + public override string ToString() + { + return Name; + } + } + + /// + /// Query the built-in sqlite table_info table for a specific tables columns. + /// + /// The columns contains in the table. + /// Table name. + public List GetTableInfo(string tableName) + { + var query = "pragma table_info(\"" + tableName + "\")"; + return Query(query); + } + + void MigrateTable(TableMapping map, List existingCols) + { + var toBeAdded = new List(); + + foreach (var p in map.Columns) + { + var found = false; + foreach (var c in existingCols) + { + found = (string.Compare(p.Name, c.Name, StringComparison.OrdinalIgnoreCase) == 0); + if (found) + break; + } + if (!found) + { + toBeAdded.Add(p); + } + } + + foreach (var p in toBeAdded) + { + var addCol = "alter table \"" + map.TableName + "\" add column " + Orm.SqlDecl(p, StoreDateTimeAsTicks, StoreTimeSpanAsTicks); + Execute(addCol); + } + } + + /// + /// Creates a new SQLiteCommand. Can be overridden to provide a sub-class. + /// + /// + protected virtual SQLiteCommand NewCommand() + { + return new SQLiteCommand(this); + } + + /// + /// Creates a new SQLiteCommand given the command text with arguments. Place a '?' + /// in the command text for each of the arguments. + /// + /// + /// The fully escaped SQL. + /// + /// + /// Arguments to substitute for the occurences of '?' in the command text. + /// + /// + /// A + /// + public SQLiteCommand CreateCommand(string cmdText, params object[] ps) + { + if (!_open) + throw SQLiteException.New(SQLite3.Result.Error, "Cannot create commands from unopened database"); + + var cmd = NewCommand(); + cmd.CommandText = cmdText; + foreach (var o in ps) + { + cmd.Bind(o); + } + return cmd; + } + + /// + /// Creates a new SQLiteCommand given the command text with named arguments. Place a "[@:$]VVV" + /// in the command text for each of the arguments. VVV represents an alphanumeric identifier. + /// For example, @name :name and $name can all be used in the query. + /// + /// + /// The fully escaped SQL. + /// + /// + /// Arguments to substitute for the occurences of "[@:$]VVV" in the command text. + /// + /// + /// A + /// + public SQLiteCommand CreateCommand(string cmdText, Dictionary args) + { + if (!_open) + throw SQLiteException.New(SQLite3.Result.Error, "Cannot create commands from unopened database"); + + SQLiteCommand cmd = NewCommand(); + cmd.CommandText = cmdText; + foreach (var kv in args) + { + cmd.Bind(kv.Key, kv.Value); + } + return cmd; + } + + /// + /// Creates a SQLiteCommand given the command text (SQL) with arguments. Place a '?' + /// in the command text for each of the arguments and then executes that command. + /// Use this method instead of Query when you don't expect rows back. Such cases include + /// INSERTs, UPDATEs, and DELETEs. + /// You can set the Trace or TimeExecution properties of the connection + /// to profile execution. + /// + /// + /// The fully escaped SQL. + /// + /// + /// Arguments to substitute for the occurences of '?' in the query. + /// + /// + /// The number of rows modified in the database as a result of this execution. + /// + public int Execute(string query, params object[] args) + { + var cmd = CreateCommand(query, args); + + if (TimeExecution) + { + if (_sw == null) + { + _sw = new Stopwatch(); + } + _sw.Reset(); + _sw.Start(); + } + + var r = cmd.ExecuteNonQuery(); + + if (TimeExecution) + { + _sw.Stop(); + _elapsedMilliseconds += _sw.ElapsedMilliseconds; + Tracer?.Invoke(string.Format("Finished in {0} ms ({1:0.0} s total)", _sw.ElapsedMilliseconds, _elapsedMilliseconds / 1000.0)); + } + + return r; + } + + /// + /// Creates a SQLiteCommand given the command text (SQL) with arguments. Place a '?' + /// in the command text for each of the arguments and then executes that command. + /// Use this method when return primitive values. + /// You can set the Trace or TimeExecution properties of the connection + /// to profile execution. + /// + /// + /// The fully escaped SQL. + /// + /// + /// Arguments to substitute for the occurences of '?' in the query. + /// + /// + /// The number of rows modified in the database as a result of this execution. + /// + public T ExecuteScalar(string query, params object[] args) + { + var cmd = CreateCommand(query, args); + + if (TimeExecution) + { + if (_sw == null) + { + _sw = new Stopwatch(); + } + _sw.Reset(); + _sw.Start(); + } + + var r = cmd.ExecuteScalar(); + + if (TimeExecution) + { + _sw.Stop(); + _elapsedMilliseconds += _sw.ElapsedMilliseconds; + Tracer?.Invoke(string.Format("Finished in {0} ms ({1:0.0} s total)", _sw.ElapsedMilliseconds, _elapsedMilliseconds / 1000.0)); + } + + return r; + } + + /// + /// Creates a SQLiteCommand given the command text (SQL) with arguments. Place a '?' + /// in the command text for each of the arguments and then executes that command. + /// It returns each row of the result using the mapping automatically generated for + /// the given type. + /// + /// + /// The fully escaped SQL. + /// + /// + /// Arguments to substitute for the occurences of '?' in the query. + /// + /// + /// An enumerable with one result for each row returned by the query. + /// + public List Query(string query, params object[] args) where T : new() + { + var cmd = CreateCommand(query, args); + return cmd.ExecuteQuery(); + } + + /// + /// Creates a SQLiteCommand given the command text (SQL) with arguments. Place a '?' + /// in the command text for each of the arguments and then executes that command. + /// It returns the first column of each row of the result. + /// + /// + /// The fully escaped SQL. + /// + /// + /// Arguments to substitute for the occurences of '?' in the query. + /// + /// + /// An enumerable with one result for the first column of each row returned by the query. + /// + public List QueryScalars(string query, params object[] args) + { + var cmd = CreateCommand(query, args); + return cmd.ExecuteQueryScalars().ToList(); + } + + /// + /// Creates a SQLiteCommand given the command text (SQL) with arguments. Place a '?' + /// in the command text for each of the arguments and then executes that command. + /// It returns each row of the result using the mapping automatically generated for + /// the given type. + /// + /// + /// The fully escaped SQL. + /// + /// + /// Arguments to substitute for the occurences of '?' in the query. + /// + /// + /// An enumerable with one result for each row returned by the query. + /// The enumerator (retrieved by calling GetEnumerator() on the result of this method) + /// will call sqlite3_step on each call to MoveNext, so the database + /// connection must remain open for the lifetime of the enumerator. + /// + public IEnumerable DeferredQuery(string query, params object[] args) where T : new() + { + var cmd = CreateCommand(query, args); + return cmd.ExecuteDeferredQuery(); + } + + /// + /// Creates a SQLiteCommand given the command text (SQL) with arguments. Place a '?' + /// in the command text for each of the arguments and then executes that command. + /// It returns each row of the result using the specified mapping. This function is + /// only used by libraries in order to query the database via introspection. It is + /// normally not used. + /// + /// + /// A to use to convert the resulting rows + /// into objects. + /// + /// + /// The fully escaped SQL. + /// + /// + /// Arguments to substitute for the occurences of '?' in the query. + /// + /// + /// An enumerable with one result for each row returned by the query. + /// + public List Query(TableMapping map, string query, params object[] args) + { + var cmd = CreateCommand(query, args); + return cmd.ExecuteQuery(map); + } + + /// + /// Creates a SQLiteCommand given the command text (SQL) with arguments. Place a '?' + /// in the command text for each of the arguments and then executes that command. + /// It returns each row of the result using the specified mapping. This function is + /// only used by libraries in order to query the database via introspection. It is + /// normally not used. + /// + /// + /// A to use to convert the resulting rows + /// into objects. + /// + /// + /// The fully escaped SQL. + /// + /// + /// Arguments to substitute for the occurences of '?' in the query. + /// + /// + /// An enumerable with one result for each row returned by the query. + /// The enumerator (retrieved by calling GetEnumerator() on the result of this method) + /// will call sqlite3_step on each call to MoveNext, so the database + /// connection must remain open for the lifetime of the enumerator. + /// + public IEnumerable DeferredQuery(TableMapping map, string query, params object[] args) + { + var cmd = CreateCommand(query, args); + return cmd.ExecuteDeferredQuery(map); + } + + /// + /// Returns a queryable interface to the table represented by the given type. + /// + /// + /// A queryable object that is able to translate Where, OrderBy, and Take + /// queries into native SQL. + /// + public TableQuery Table() where T : new() + { + return new TableQuery(this); + } + + /// + /// Attempts to retrieve an object with the given primary key from the table + /// associated with the specified type. Use of this method requires that + /// the given type have a designated PrimaryKey (using the PrimaryKeyAttribute). + /// + /// + /// The primary key. + /// + /// + /// The object with the given primary key. Throws a not found exception + /// if the object is not found. + /// + public T Get(object pk) where T : new() + { + var map = GetMapping(typeof(T)); + return Query(map.GetByPrimaryKeySql, pk).First(); + } + + /// + /// Attempts to retrieve an object with the given primary key from the table + /// associated with the specified type. Use of this method requires that + /// the given type have a designated PrimaryKey (using the PrimaryKeyAttribute). + /// + /// + /// The primary key. + /// + /// + /// The TableMapping used to identify the table. + /// + /// + /// The object with the given primary key. Throws a not found exception + /// if the object is not found. + /// + public object Get(object pk, TableMapping map) + { + return Query(map, map.GetByPrimaryKeySql, pk).First(); + } + + /// + /// Attempts to retrieve the first object that matches the predicate from the table + /// associated with the specified type. + /// + /// + /// A predicate for which object to find. + /// + /// + /// The object that matches the given predicate. Throws a not found exception + /// if the object is not found. + /// + public T Get(Expression> predicate) where T : new() + { + return Table().Where(predicate).First(); + } + + /// + /// Attempts to retrieve an object with the given primary key from the table + /// associated with the specified type. Use of this method requires that + /// the given type have a designated PrimaryKey (using the PrimaryKeyAttribute). + /// + /// + /// The primary key. + /// + /// + /// The object with the given primary key or null + /// if the object is not found. + /// + public T Find(object pk) where T : new() + { + var map = GetMapping(typeof(T)); + return Query(map.GetByPrimaryKeySql, pk).FirstOrDefault(); + } + + /// + /// Attempts to retrieve an object with the given primary key from the table + /// associated with the specified type. Use of this method requires that + /// the given type have a designated PrimaryKey (using the PrimaryKeyAttribute). + /// + /// + /// The primary key. + /// + /// + /// The TableMapping used to identify the table. + /// + /// + /// The object with the given primary key or null + /// if the object is not found. + /// + public object Find(object pk, TableMapping map) + { + return Query(map, map.GetByPrimaryKeySql, pk).FirstOrDefault(); + } + + /// + /// Attempts to retrieve the first object that matches the predicate from the table + /// associated with the specified type. + /// + /// + /// A predicate for which object to find. + /// + /// + /// The object that matches the given predicate or null + /// if the object is not found. + /// + public T Find(Expression> predicate) where T : new() + { + return Table().Where(predicate).FirstOrDefault(); + } + + /// + /// Attempts to retrieve the first object that matches the query from the table + /// associated with the specified type. + /// + /// + /// The fully escaped SQL. + /// + /// + /// Arguments to substitute for the occurences of '?' in the query. + /// + /// + /// The object that matches the given predicate or null + /// if the object is not found. + /// + public T FindWithQuery(string query, params object[] args) where T : new() + { + return Query(query, args).FirstOrDefault(); + } + + /// + /// Attempts to retrieve the first object that matches the query from the table + /// associated with the specified type. + /// + /// + /// The TableMapping used to identify the table. + /// + /// + /// The fully escaped SQL. + /// + /// + /// Arguments to substitute for the occurences of '?' in the query. + /// + /// + /// The object that matches the given predicate or null + /// if the object is not found. + /// + public object FindWithQuery(TableMapping map, string query, params object[] args) + { + return Query(map, query, args).FirstOrDefault(); + } + + /// + /// Whether has been called and the database is waiting for a . + /// + public bool IsInTransaction + { + get { return _transactionDepth > 0; } + } + + /// + /// Begins a new transaction. Call to end the transaction. + /// + /// Throws if a transaction has already begun. + public void BeginTransaction() + { + // The BEGIN command only works if the transaction stack is empty, + // or in other words if there are no pending transactions. + // If the transaction stack is not empty when the BEGIN command is invoked, + // then the command fails with an error. + // Rather than crash with an error, we will just ignore calls to BeginTransaction + // that would result in an error. + if (Interlocked.CompareExchange(ref _transactionDepth, 1, 0) == 0) + { + try + { + Execute("begin transaction"); + } + catch (Exception ex) + { + var sqlExp = ex as SQLiteException; + if (sqlExp != null) + { + // It is recommended that applications respond to the errors listed below + // by explicitly issuing a ROLLBACK command. + // TODO: This rollback failsafe should be localized to all throw sites. + switch (sqlExp.Result) + { + case SQLite3.Result.IOError: + case SQLite3.Result.Full: + case SQLite3.Result.Busy: + case SQLite3.Result.NoMem: + case SQLite3.Result.Interrupt: + RollbackTo(null, true); + break; + } + } + else + { + // Call decrement and not VolatileWrite in case we've already + // created a transaction point in SaveTransactionPoint since the catch. + Interlocked.Decrement(ref _transactionDepth); + } + + throw; + } + } + else + { + // Calling BeginTransaction on an already open transaction is invalid + throw new InvalidOperationException("Cannot begin a transaction while already in a transaction."); + } + } + + /// + /// Creates a savepoint in the database at the current point in the transaction timeline. + /// Begins a new transaction if one is not in progress. + /// + /// Call to undo transactions since the returned savepoint. + /// Call to commit transactions after the savepoint returned here. + /// Call to end the transaction, committing all changes. + /// + /// A string naming the savepoint. + public string SaveTransactionPoint() + { + int depth = Interlocked.Increment(ref _transactionDepth) - 1; + string retVal = "S" + _rand.Next(short.MaxValue) + "D" + depth; + + try + { + Execute("savepoint " + retVal); + } + catch (Exception ex) + { + var sqlExp = ex as SQLiteException; + if (sqlExp != null) + { + // It is recommended that applications respond to the errors listed below + // by explicitly issuing a ROLLBACK command. + // TODO: This rollback failsafe should be localized to all throw sites. + switch (sqlExp.Result) + { + case SQLite3.Result.IOError: + case SQLite3.Result.Full: + case SQLite3.Result.Busy: + case SQLite3.Result.NoMem: + case SQLite3.Result.Interrupt: + RollbackTo(null, true); + break; + } + } + else + { + Interlocked.Decrement(ref _transactionDepth); + } + + throw; + } + + return retVal; + } + + /// + /// Rolls back the transaction that was begun by or . + /// + public void Rollback() + { + RollbackTo(null, false); + } + + /// + /// Rolls back the savepoint created by or SaveTransactionPoint. + /// + /// The name of the savepoint to roll back to, as returned by . If savepoint is null or empty, this method is equivalent to a call to + public void RollbackTo(string savepoint) + { + RollbackTo(savepoint, false); + } + + /// + /// Rolls back the transaction that was begun by . + /// + /// The name of the savepoint to roll back to, as returned by . If savepoint is null or empty, this method is equivalent to a call to + /// true to avoid throwing exceptions, false otherwise + void RollbackTo(string savepoint, bool noThrow) + { + // Rolling back without a TO clause rolls backs all transactions + // and leaves the transaction stack empty. + try + { + if (String.IsNullOrEmpty(savepoint)) + { + if (Interlocked.Exchange(ref _transactionDepth, 0) > 0) + { + Execute("rollback"); + } + } + else + { + DoSavePointExecute(savepoint, "rollback to "); + } + } + catch (SQLiteException) + { + if (!noThrow) + throw; + + } + // No need to rollback if there are no transactions open. + } + + /// + /// Releases a savepoint returned from . Releasing a savepoint + /// makes changes since that savepoint permanent if the savepoint began the transaction, + /// or otherwise the changes are permanent pending a call to . + /// + /// The RELEASE command is like a COMMIT for a SAVEPOINT. + /// + /// The name of the savepoint to release. The string should be the result of a call to + public void Release(string savepoint) + { + try + { + DoSavePointExecute(savepoint, "release "); + } + catch (SQLiteException ex) + { + if (ex.Result == SQLite3.Result.Busy) + { + // Force a rollback since most people don't know this function can fail + // Don't call Rollback() since the _transactionDepth is 0 and it won't try + // Calling rollback makes our _transactionDepth variable correct. + // Writes to the database only happen at depth=0, so this failure will only happen then. + try + { + Execute("rollback"); + } + catch + { + // rollback can fail in all sorts of wonderful version-dependent ways. Let's just hope for the best + } + } + throw; + } + } + + void DoSavePointExecute(string savepoint, string cmd) + { + // Validate the savepoint + int firstLen = savepoint.IndexOf('D'); + if (firstLen >= 2 && savepoint.Length > firstLen + 1) + { + int depth; + if (Int32.TryParse(savepoint.Substring(firstLen + 1), out depth)) + { + // TODO: Mild race here, but inescapable without locking almost everywhere. + if (0 <= depth && depth < _transactionDepth) + { +#if NETFX_CORE || USE_SQLITEPCL_RAW || NETCORE + Volatile.Write (ref _transactionDepth, depth); +#elif SILVERLIGHT + _transactionDepth = depth; +#else + Thread.VolatileWrite(ref _transactionDepth, depth); +#endif + Execute(cmd + savepoint); + return; + } + } + } + + throw new ArgumentException("savePoint is not valid, and should be the result of a call to SaveTransactionPoint.", "savePoint"); + } + + /// + /// Commits the transaction that was begun by . + /// + public void Commit() + { + if (Interlocked.Exchange(ref _transactionDepth, 0) != 0) + { + try + { + Execute("commit"); + } + catch + { + // Force a rollback since most people don't know this function can fail + // Don't call Rollback() since the _transactionDepth is 0 and it won't try + // Calling rollback makes our _transactionDepth variable correct. + try + { + Execute("rollback"); + } + catch + { + // rollback can fail in all sorts of wonderful version-dependent ways. Let's just hope for the best + } + throw; + } + } + // Do nothing on a commit with no open transaction + } + + /// + /// Executes within a (possibly nested) transaction by wrapping it in a SAVEPOINT. If an + /// exception occurs the whole transaction is rolled back, not just the current savepoint. The exception + /// is rethrown. + /// + /// + /// The to perform within a transaction. can contain any number + /// of operations on the connection but should never call or + /// . + /// + public void RunInTransaction(Action action) + { + try + { + var savePoint = SaveTransactionPoint(); + action(); + Release(savePoint); + } + catch (Exception) + { + Rollback(); + throw; + } + } + + /// + /// Inserts all specified objects. + /// + /// + /// An of the objects to insert. + /// + /// A boolean indicating if the inserts should be wrapped in a transaction. + /// + /// + /// The number of rows added to the table. + /// + public int InsertAll(System.Collections.IEnumerable objects, bool runInTransaction = true) + { + var c = 0; + if (runInTransaction) + { + RunInTransaction(() => { + foreach (var r in objects) + { + c += Insert(r); + } + }); + } + else + { + foreach (var r in objects) + { + c += Insert(r); + } + } + return c; + } + + /// + /// Inserts all specified objects. + /// + /// + /// An of the objects to insert. + /// + /// + /// Literal SQL code that gets placed into the command. INSERT {extra} INTO ... + /// + /// + /// A boolean indicating if the inserts should be wrapped in a transaction. + /// + /// + /// The number of rows added to the table. + /// + public int InsertAll(System.Collections.IEnumerable objects, string extra, bool runInTransaction = true) + { + var c = 0; + if (runInTransaction) + { + RunInTransaction(() => { + foreach (var r in objects) + { + c += Insert(r, extra); + } + }); + } + else + { + foreach (var r in objects) + { + c += Insert(r, extra); + } + } + return c; + } + + /// + /// Inserts all specified objects. + /// + /// + /// An of the objects to insert. + /// + /// + /// The type of object to insert. + /// + /// + /// A boolean indicating if the inserts should be wrapped in a transaction. + /// + /// + /// The number of rows added to the table. + /// + public int InsertAll(System.Collections.IEnumerable objects, Type objType, bool runInTransaction = true) + { + var c = 0; + if (runInTransaction) + { + RunInTransaction(() => { + foreach (var r in objects) + { + c += Insert(r, objType); + } + }); + } + else + { + foreach (var r in objects) + { + c += Insert(r, objType); + } + } + return c; + } + + /// + /// Inserts the given object (and updates its + /// auto incremented primary key if it has one). + /// The return value is the number of rows added to the table. + /// + /// + /// The object to insert. + /// + /// + /// The number of rows added to the table. + /// + public int Insert(object obj) + { + if (obj == null) + { + return 0; + } + return Insert(obj, "", Orm.GetType(obj)); + } + + /// + /// Inserts the given object (and updates its + /// auto incremented primary key if it has one). + /// The return value is the number of rows added to the table. + /// If a UNIQUE constraint violation occurs with + /// some pre-existing object, this function deletes + /// the old object. + /// + /// + /// The object to insert. + /// + /// + /// The number of rows modified. + /// + public int InsertOrReplace(object obj) + { + if (obj == null) + { + return 0; + } + return Insert(obj, "OR REPLACE", Orm.GetType(obj)); + } + + /// + /// Inserts the given object (and updates its + /// auto incremented primary key if it has one). + /// The return value is the number of rows added to the table. + /// + /// + /// The object to insert. + /// + /// + /// The type of object to insert. + /// + /// + /// The number of rows added to the table. + /// + public int Insert(object obj, Type objType) + { + return Insert(obj, "", objType); + } + + /// + /// Inserts the given object (and updates its + /// auto incremented primary key if it has one). + /// The return value is the number of rows added to the table. + /// If a UNIQUE constraint violation occurs with + /// some pre-existing object, this function deletes + /// the old object. + /// + /// + /// The object to insert. + /// + /// + /// The type of object to insert. + /// + /// + /// The number of rows modified. + /// + public int InsertOrReplace(object obj, Type objType) + { + return Insert(obj, "OR REPLACE", objType); + } + + /// + /// Inserts the given object (and updates its + /// auto incremented primary key if it has one). + /// The return value is the number of rows added to the table. + /// + /// + /// The object to insert. + /// + /// + /// Literal SQL code that gets placed into the command. INSERT {extra} INTO ... + /// + /// + /// The number of rows added to the table. + /// + public int Insert(object obj, string extra) + { + if (obj == null) + { + return 0; + } + return Insert(obj, extra, Orm.GetType(obj)); + } + + /// + /// Inserts the given object (and updates its + /// auto incremented primary key if it has one). + /// The return value is the number of rows added to the table. + /// + /// + /// The object to insert. + /// + /// + /// Literal SQL code that gets placed into the command. INSERT {extra} INTO ... + /// + /// + /// The type of object to insert. + /// + /// + /// The number of rows added to the table. + /// + public int Insert(object obj, string extra, Type objType) + { + if (obj == null || objType == null) + { + return 0; + } + + var map = GetMapping(objType); + + if (map.PK != null && map.PK.IsAutoGuid) + { + if (map.PK.GetValue(obj).Equals(Guid.Empty)) + { + map.PK.SetValue(obj, Guid.NewGuid()); + } + } + + var replacing = string.Compare(extra, "OR REPLACE", StringComparison.OrdinalIgnoreCase) == 0; + + var cols = replacing ? map.InsertOrReplaceColumns : map.InsertColumns; + var vals = new object[cols.Length]; + for (var i = 0; i < vals.Length; i++) + { + vals[i] = cols[i].GetValue(obj); + } + + var insertCmd = GetInsertCommand(map, extra); + int count; + + lock (insertCmd) + { + // We lock here to protect the prepared statement returned via GetInsertCommand. + // A SQLite prepared statement can be bound for only one operation at a time. + try + { + count = insertCmd.ExecuteNonQuery(vals); + } + catch (SQLiteException ex) + { + if (SQLite3.ExtendedErrCode(this.Handle) == SQLite3.ExtendedResult.ConstraintNotNull) + { + throw NotNullConstraintViolationException.New(ex.Result, ex.Message, map, obj); + } + throw; + } + + if (map.HasAutoIncPK) + { + var id = SQLite3.LastInsertRowid(Handle); + map.SetAutoIncPK(obj, id); + } + } + if (count > 0) + OnTableChanged(map, NotifyTableChangedAction.Insert); + + return count; + } + + readonly Dictionary, PreparedSqlLiteInsertCommand> _insertCommandMap = new Dictionary, PreparedSqlLiteInsertCommand>(); + + PreparedSqlLiteInsertCommand GetInsertCommand(TableMapping map, string extra) + { + PreparedSqlLiteInsertCommand prepCmd; + + var key = Tuple.Create(map.MappedType.FullName, extra); + + lock (_insertCommandMap) + { + if (_insertCommandMap.TryGetValue(key, out prepCmd)) + { + return prepCmd; + } + } + + prepCmd = CreateInsertCommand(map, extra); + + lock (_insertCommandMap) + { + if (_insertCommandMap.TryGetValue(key, out var existing)) + { + prepCmd.Dispose(); + return existing; + } + + _insertCommandMap.Add(key, prepCmd); + } + + return prepCmd; + } + + PreparedSqlLiteInsertCommand CreateInsertCommand(TableMapping map, string extra) + { + var cols = map.InsertColumns; + string insertSql; + if (cols.Length == 0 && map.Columns.Length == 1 && map.Columns[0].IsAutoInc) + { + insertSql = string.Format("insert {1} into \"{0}\" default values", map.TableName, extra); + } + else + { + var replacing = string.Compare(extra, "OR REPLACE", StringComparison.OrdinalIgnoreCase) == 0; + + if (replacing) + { + cols = map.InsertOrReplaceColumns; + } + + insertSql = string.Format("insert {3} into \"{0}\"({1}) values ({2})", map.TableName, + string.Join(",", (from c in cols + select "\"" + c.Name + "\"").ToArray()), + string.Join(",", (from c in cols + select "?").ToArray()), extra); + + } + + var insertCommand = new PreparedSqlLiteInsertCommand(this, insertSql); + return insertCommand; + } + + /// + /// Updates all of the columns of a table using the specified object + /// except for its primary key. + /// The object is required to have a primary key. + /// + /// + /// The object to update. It must have a primary key designated using the PrimaryKeyAttribute. + /// + /// + /// The number of rows updated. + /// + public int Update(object obj) + { + if (obj == null) + { + return 0; + } + return Update(obj, Orm.GetType(obj)); + } + + /// + /// Updates all of the columns of a table using the specified object + /// except for its primary key. + /// The object is required to have a primary key. + /// + /// + /// The object to update. It must have a primary key designated using the PrimaryKeyAttribute. + /// + /// + /// The type of object to insert. + /// + /// + /// The number of rows updated. + /// + public int Update(object obj, Type objType) + { + int rowsAffected = 0; + if (obj == null || objType == null) + { + return 0; + } + + var map = GetMapping(objType); + + var pk = map.PK; + + if (pk == null) + { + throw new NotSupportedException("Cannot update " + map.TableName + ": it has no PK"); + } + + var cols = from p in map.Columns + where p != pk + select p; + var vals = from c in cols + select c.GetValue(obj); + var ps = new List(vals); + if (ps.Count == 0) + { + // There is a PK but no accompanying data, + // so reset the PK to make the UPDATE work. + cols = map.Columns; + vals = from c in cols + select c.GetValue(obj); + ps = new List(vals); + } + ps.Add(pk.GetValue(obj)); + var q = string.Format("update \"{0}\" set {1} where \"{2}\" = ? ", map.TableName, string.Join(",", (from c in cols + select "\"" + c.Name + "\" = ? ").ToArray()), pk.Name); + + try + { + rowsAffected = Execute(q, ps.ToArray()); + } + catch (SQLiteException ex) + { + + if (ex.Result == SQLite3.Result.Constraint && SQLite3.ExtendedErrCode(this.Handle) == SQLite3.ExtendedResult.ConstraintNotNull) + { + throw NotNullConstraintViolationException.New(ex, map, obj); + } + + throw ex; + } + + if (rowsAffected > 0) + OnTableChanged(map, NotifyTableChangedAction.Update); + + return rowsAffected; + } + + /// + /// Updates all specified objects. + /// + /// + /// An of the objects to insert. + /// + /// + /// A boolean indicating if the inserts should be wrapped in a transaction + /// + /// + /// The number of rows modified. + /// + public int UpdateAll(System.Collections.IEnumerable objects, bool runInTransaction = true) + { + var c = 0; + if (runInTransaction) + { + RunInTransaction(() => { + foreach (var r in objects) + { + c += Update(r); + } + }); + } + else + { + foreach (var r in objects) + { + c += Update(r); + } + } + return c; + } + + /// + /// Deletes the given object from the database using its primary key. + /// + /// + /// The object to delete. It must have a primary key designated using the PrimaryKeyAttribute. + /// + /// + /// The number of rows deleted. + /// + public int Delete(object objectToDelete) + { + var map = GetMapping(Orm.GetType(objectToDelete)); + var pk = map.PK; + if (pk == null) + { + throw new NotSupportedException("Cannot delete " + map.TableName + ": it has no PK"); + } + var q = string.Format("delete from \"{0}\" where \"{1}\" = ?", map.TableName, pk.Name); + var count = Execute(q, pk.GetValue(objectToDelete)); + if (count > 0) + OnTableChanged(map, NotifyTableChangedAction.Delete); + return count; + } + + /// + /// Deletes the object with the specified primary key. + /// + /// + /// The primary key of the object to delete. + /// + /// + /// The number of objects deleted. + /// + /// + /// The type of object. + /// + public int Delete(object primaryKey) + { + return Delete(primaryKey, GetMapping(typeof(T))); + } + + /// + /// Deletes the object with the specified primary key. + /// + /// + /// The primary key of the object to delete. + /// + /// + /// The TableMapping used to identify the table. + /// + /// + /// The number of objects deleted. + /// + public int Delete(object primaryKey, TableMapping map) + { + var pk = map.PK; + if (pk == null) + { + throw new NotSupportedException("Cannot delete " + map.TableName + ": it has no PK"); + } + var q = string.Format("delete from \"{0}\" where \"{1}\" = ?", map.TableName, pk.Name); + var count = Execute(q, primaryKey); + if (count > 0) + OnTableChanged(map, NotifyTableChangedAction.Delete); + return count; + } + + /// + /// Deletes all the objects from the specified table. + /// WARNING WARNING: Let me repeat. It deletes ALL the objects from the + /// specified table. Do you really want to do that? + /// + /// + /// The number of objects deleted. + /// + /// + /// The type of objects to delete. + /// + public int DeleteAll() + { + var map = GetMapping(typeof(T)); + return DeleteAll(map); + } + + /// + /// Deletes all the objects from the specified table. + /// WARNING WARNING: Let me repeat. It deletes ALL the objects from the + /// specified table. Do you really want to do that? + /// + /// + /// The TableMapping used to identify the table. + /// + /// + /// The number of objects deleted. + /// + public int DeleteAll(TableMapping map) + { + var query = string.Format("delete from \"{0}\"", map.TableName); + var count = Execute(query); + if (count > 0) + OnTableChanged(map, NotifyTableChangedAction.Delete); + return count; + } + + /// + /// Backup the entire database to the specified path. + /// + /// Path to backup file. + /// The name of the database to backup (usually "main"). + public void Backup(string destinationDatabasePath, string databaseName = "main") + { + // Open the destination + var r = SQLite3.Open(destinationDatabasePath, out var destHandle); + if (r != SQLite3.Result.OK) + { + throw SQLiteException.New(r, "Failed to open destination database"); + } + + // Init the backup + var backup = SQLite3.BackupInit(destHandle, databaseName, Handle, databaseName); + if (backup == NullBackupHandle) + { + SQLite3.Close(destHandle); + throw new Exception("Failed to create backup"); + } + + // Perform it + SQLite3.BackupStep(backup, -1); + SQLite3.BackupFinish(backup); + + // Check for errors + r = SQLite3.GetResult(destHandle); + string msg = ""; + if (r != SQLite3.Result.OK) + { + msg = SQLite3.GetErrmsg(destHandle); + } + + // Close everything and report errors + SQLite3.Close(destHandle); + if (r != SQLite3.Result.OK) + { + throw SQLiteException.New(r, msg); + } + } + + ~SQLiteConnection() + { + Dispose(false); + } + + public void Dispose() + { + Dispose(true); + GC.SuppressFinalize(this); + } + + public void Close() + { + Dispose(true); + } + + protected virtual void Dispose(bool disposing) + { + var useClose2 = LibVersionNumber >= 3007014; + + if (_open && Handle != NullHandle) + { + try + { + if (disposing) + { + lock (_insertCommandMap) + { + foreach (var sqlInsertCommand in _insertCommandMap.Values) + { + sqlInsertCommand.Dispose(); + } + _insertCommandMap.Clear(); + } + + var r = useClose2 ? SQLite3.Close2(Handle) : SQLite3.Close(Handle); + if (r != SQLite3.Result.OK) + { + string msg = SQLite3.GetErrmsg(Handle); + throw SQLiteException.New(r, msg); + } + } + else + { + var r = useClose2 ? SQLite3.Close2(Handle) : SQLite3.Close(Handle); + } + } + finally + { + Handle = NullHandle; + _open = false; + } + } + } + + void OnTableChanged(TableMapping table, NotifyTableChangedAction action) + { + var ev = TableChanged; + if (ev != null) + ev(this, new NotifyTableChangedEventArgs(table, action)); + } + + public event EventHandler TableChanged; + } + + public class NotifyTableChangedEventArgs : EventArgs + { + public TableMapping Table { get; private set; } + public NotifyTableChangedAction Action { get; private set; } + + public NotifyTableChangedEventArgs(TableMapping table, NotifyTableChangedAction action) + { + Table = table; + Action = action; + } + } + + public enum NotifyTableChangedAction + { + Insert, + Update, + Delete, + } + + /// + /// Represents a parsed connection string. + /// + public class SQLiteConnectionString + { + const string DateTimeSqliteDefaultFormat = "yyyy'-'MM'-'dd'T'HH':'mm':'ss'.'fff"; + + public string UniqueKey { get; } + public string DatabasePath { get; } + public bool StoreDateTimeAsTicks { get; } + public bool StoreTimeSpanAsTicks { get; } + public string DateTimeStringFormat { get; } + public System.Globalization.DateTimeStyles DateTimeStyle { get; } + public object Key { get; } + public SQLiteOpenFlags OpenFlags { get; } + public Action PreKeyAction { get; } + public Action PostKeyAction { get; } + public string VfsName { get; } + +#if NETFX_CORE + static readonly string MetroStyleDataPath = Windows.Storage.ApplicationData.Current.LocalFolder.Path; + + public static readonly string[] InMemoryDbPaths = new[] + { + ":memory:", + "file::memory:" + }; + + public static bool IsInMemoryPath(string databasePath) + { + return InMemoryDbPaths.Any(i => i.Equals(databasePath, StringComparison.OrdinalIgnoreCase)); + } + +#endif + + /// + /// Constructs a new SQLiteConnectionString with all the data needed to open an SQLiteConnection. + /// + /// + /// Specifies the path to the database file. + /// + /// + /// Specifies whether to store DateTime properties as ticks (true) or strings (false). You + /// absolutely do want to store them as Ticks in all new projects. The value of false is + /// only here for backwards compatibility. There is a *significant* speed advantage, with no + /// down sides, when setting storeDateTimeAsTicks = true. + /// If you use DateTimeOffset properties, it will be always stored as ticks regardingless + /// the storeDateTimeAsTicks parameter. + /// + public SQLiteConnectionString(string databasePath, bool storeDateTimeAsTicks = true) + : this(databasePath, SQLiteOpenFlags.Create | SQLiteOpenFlags.ReadWrite, storeDateTimeAsTicks) + { + } + + /// + /// Constructs a new SQLiteConnectionString with all the data needed to open an SQLiteConnection. + /// + /// + /// Specifies the path to the database file. + /// + /// + /// Specifies whether to store DateTime properties as ticks (true) or strings (false). You + /// absolutely do want to store them as Ticks in all new projects. The value of false is + /// only here for backwards compatibility. There is a *significant* speed advantage, with no + /// down sides, when setting storeDateTimeAsTicks = true. + /// If you use DateTimeOffset properties, it will be always stored as ticks regardingless + /// the storeDateTimeAsTicks parameter. + /// + /// + /// Specifies the encryption key to use on the database. Should be a string or a byte[]. + /// + /// + /// Executes prior to setting key for SQLCipher databases + /// + /// + /// Executes after setting key for SQLCipher databases + /// + /// + /// Specifies the Virtual File System to use on the database. + /// + public SQLiteConnectionString(string databasePath, bool storeDateTimeAsTicks, object key = null, Action preKeyAction = null, Action postKeyAction = null, string vfsName = null) + : this(databasePath, SQLiteOpenFlags.Create | SQLiteOpenFlags.ReadWrite, storeDateTimeAsTicks, key, preKeyAction, postKeyAction, vfsName) + { + } + + /// + /// Constructs a new SQLiteConnectionString with all the data needed to open an SQLiteConnection. + /// + /// + /// Specifies the path to the database file. + /// + /// + /// Flags controlling how the connection should be opened. + /// + /// + /// Specifies whether to store DateTime properties as ticks (true) or strings (false). You + /// absolutely do want to store them as Ticks in all new projects. The value of false is + /// only here for backwards compatibility. There is a *significant* speed advantage, with no + /// down sides, when setting storeDateTimeAsTicks = true. + /// If you use DateTimeOffset properties, it will be always stored as ticks regardingless + /// the storeDateTimeAsTicks parameter. + /// + /// + /// Specifies the encryption key to use on the database. Should be a string or a byte[]. + /// + /// + /// Executes prior to setting key for SQLCipher databases + /// + /// + /// Executes after setting key for SQLCipher databases + /// + /// + /// Specifies the Virtual File System to use on the database. + /// + /// + /// Specifies the format to use when storing DateTime properties as strings. + /// + /// + /// Specifies whether to store TimeSpan properties as ticks (true) or strings (false). You + /// absolutely do want to store them as Ticks in all new projects. The value of false is + /// only here for backwards compatibility. There is a *significant* speed advantage, with no + /// down sides, when setting storeTimeSpanAsTicks = true. + /// + public SQLiteConnectionString(string databasePath, SQLiteOpenFlags openFlags, bool storeDateTimeAsTicks, object key = null, Action preKeyAction = null, Action postKeyAction = null, string vfsName = null, string dateTimeStringFormat = DateTimeSqliteDefaultFormat, bool storeTimeSpanAsTicks = true) + { + if (key != null && !((key is byte[]) || (key is string))) + throw new ArgumentException("Encryption keys must be strings or byte arrays", nameof(key)); + + UniqueKey = string.Format("{0}_{1:X8}", databasePath, (uint)openFlags); + StoreDateTimeAsTicks = storeDateTimeAsTicks; + StoreTimeSpanAsTicks = storeTimeSpanAsTicks; + DateTimeStringFormat = dateTimeStringFormat; + DateTimeStyle = "o".Equals(DateTimeStringFormat, StringComparison.OrdinalIgnoreCase) || "r".Equals(DateTimeStringFormat, StringComparison.OrdinalIgnoreCase) ? System.Globalization.DateTimeStyles.RoundtripKind : System.Globalization.DateTimeStyles.None; + Key = key; + PreKeyAction = preKeyAction; + PostKeyAction = postKeyAction; + OpenFlags = openFlags; + VfsName = vfsName; + +#if NETFX_CORE + DatabasePath = IsInMemoryPath(databasePath) + ? databasePath + : System.IO.Path.Combine(MetroStyleDataPath, databasePath); + +#else + DatabasePath = databasePath; +#endif + } + } + + [AttributeUsage(AttributeTargets.Class)] + public class TableAttribute : Attribute + { + public string Name { get; set; } + + /// + /// Flag whether to create the table without rowid (see https://sqlite.org/withoutrowid.html) + /// + /// The default is false so that sqlite adds an implicit rowid to every table created. + /// + public bool WithoutRowId { get; set; } + + public TableAttribute(string name) + { + Name = name; + } + } + + [AttributeUsage(AttributeTargets.Property)] + public class ColumnAttribute : Attribute + { + public string Name { get; set; } + + public ColumnAttribute(string name) + { + Name = name; + } + } + + [AttributeUsage(AttributeTargets.Property)] + public class PrimaryKeyAttribute : Attribute + { + } + + [AttributeUsage(AttributeTargets.Property)] + public class AutoIncrementAttribute : Attribute + { + } + + [AttributeUsage(AttributeTargets.Property)] + public class IndexedAttribute : Attribute + { + public string Name { get; set; } + public int Order { get; set; } + public virtual bool Unique { get; set; } + + public IndexedAttribute() + { + } + + public IndexedAttribute(string name, int order) + { + Name = name; + Order = order; + } + } + + [AttributeUsage(AttributeTargets.Property)] + public class IgnoreAttribute : Attribute + { + } + + [AttributeUsage(AttributeTargets.Property)] + public class UniqueAttribute : IndexedAttribute + { + public override bool Unique + { + get { return true; } + set { /* throw? */ } + } + } + + [AttributeUsage(AttributeTargets.Property)] + public class MaxLengthAttribute : Attribute + { + public int Value { get; private set; } + + public MaxLengthAttribute(int length) + { + Value = length; + } + } + + public sealed class PreserveAttribute : System.Attribute + { + public bool AllMembers; + public bool Conditional; + } + + /// + /// Select the collating sequence to use on a column. + /// "BINARY", "NOCASE", and "RTRIM" are supported. + /// "BINARY" is the default. + /// + [AttributeUsage(AttributeTargets.Property)] + public class CollationAttribute : Attribute + { + public string Value { get; private set; } + + public CollationAttribute(string collation) + { + Value = collation; + } + } + + [AttributeUsage(AttributeTargets.Property)] + public class NotNullAttribute : Attribute + { + } + + [AttributeUsage(AttributeTargets.Enum)] + public class StoreAsTextAttribute : Attribute + { + } + + public class TableMapping + { + public Type MappedType { get; private set; } + + public string TableName { get; private set; } + + public bool WithoutRowId { get; private set; } + + public Column[] Columns { get; private set; } + + public Column PK { get; private set; } + + public string GetByPrimaryKeySql { get; private set; } + + public CreateFlags CreateFlags { get; private set; } + + internal MapMethod Method { get; private set; } = MapMethod.ByName; + + readonly Column _autoPk; + readonly Column[] _insertColumns; + readonly Column[] _insertOrReplaceColumns; + + public TableMapping(Type type, CreateFlags createFlags = CreateFlags.None) + { + MappedType = type; + CreateFlags = createFlags; + + var typeInfo = type.GetTypeInfo(); +#if ENABLE_IL2CPP + var tableAttr = typeInfo.GetCustomAttribute (); +#else + var tableAttr = + typeInfo.CustomAttributes + .Where(x => x.AttributeType == typeof(TableAttribute)) + .Select(x => (TableAttribute)Orm.InflateAttribute(x)) + .FirstOrDefault(); +#endif + + TableName = (tableAttr != null && !string.IsNullOrEmpty(tableAttr.Name)) ? tableAttr.Name : MappedType.Name; + WithoutRowId = tableAttr != null ? tableAttr.WithoutRowId : false; + + var members = GetPublicMembers(type); + var cols = new List(members.Count); + foreach (var m in members) + { + var ignore = m.IsDefined(typeof(IgnoreAttribute), true); + if (!ignore) + cols.Add(new Column(m, createFlags)); + } + Columns = cols.ToArray(); + foreach (var c in Columns) + { + if (c.IsAutoInc && c.IsPK) + { + _autoPk = c; + } + if (c.IsPK) + { + PK = c; + } + } + + HasAutoIncPK = _autoPk != null; + + if (PK != null) + { + GetByPrimaryKeySql = string.Format("select * from \"{0}\" where \"{1}\" = ?", TableName, PK.Name); + } + else + { + // People should not be calling Get/Find without a PK + GetByPrimaryKeySql = string.Format("select * from \"{0}\" limit 1", TableName); + } + + _insertColumns = Columns.Where(c => !c.IsAutoInc).ToArray(); + _insertOrReplaceColumns = Columns.ToArray(); + } + + private IReadOnlyCollection GetPublicMembers(Type type) + { + if (type.Name.StartsWith("ValueTuple`")) + return GetFieldsFromValueTuple(type); + + var members = new List(); + var memberNames = new HashSet(); + var newMembers = new List(); + do + { + var ti = type.GetTypeInfo(); + newMembers.Clear(); + + newMembers.AddRange( + from p in ti.DeclaredProperties + where !memberNames.Contains(p.Name) && + p.CanRead && p.CanWrite && + p.GetMethod != null && p.SetMethod != null && + p.GetMethod.IsPublic && p.SetMethod.IsPublic && + !p.GetMethod.IsStatic && !p.SetMethod.IsStatic + select p); + + members.AddRange(newMembers); + foreach (var m in newMembers) + memberNames.Add(m.Name); + + type = ti.BaseType; + } + while (type != typeof(object)); + + return members; + } + + private IReadOnlyCollection GetFieldsFromValueTuple(Type type) + { + Method = MapMethod.ByPosition; + var fields = type.GetFields(); + + // https://docs.microsoft.com/en-us/dotnet/api/system.valuetuple-8.rest + if (fields.Length >= 8) + throw new NotSupportedException("ValueTuple with more than 7 members not supported due to nesting; see https://docs.microsoft.com/en-us/dotnet/api/system.valuetuple-8.rest"); + + return fields; + } + + public bool HasAutoIncPK { get; private set; } + + public void SetAutoIncPK(object obj, long id) + { + if (_autoPk != null) + { + _autoPk.SetValue(obj, Convert.ChangeType(id, _autoPk.ColumnType, null)); + } + } + + public Column[] InsertColumns + { + get + { + return _insertColumns; + } + } + + public Column[] InsertOrReplaceColumns + { + get + { + return _insertOrReplaceColumns; + } + } + + public Column FindColumnWithPropertyName(string propertyName) + { + var exact = Columns.FirstOrDefault(c => c.PropertyName == propertyName); + return exact; + } + + public Column FindColumn(string columnName) + { + if (Method != MapMethod.ByName) + throw new InvalidOperationException($"This {nameof(TableMapping)} is not mapped by name, but {Method}."); + + var exact = Columns.FirstOrDefault(c => c.Name.ToLower() == columnName.ToLower()); + return exact; + } + + public class Column + { + MemberInfo _member; + + public string Name { get; private set; } + + public PropertyInfo PropertyInfo => _member as PropertyInfo; + + public string PropertyName { get { return _member.Name; } } + + public Type ColumnType { get; private set; } + + public string Collation { get; private set; } + + public bool IsAutoInc { get; private set; } + public bool IsAutoGuid { get; private set; } + + public bool IsPK { get; private set; } + + public IEnumerable Indices { get; set; } + + public bool IsNullable { get; private set; } + + public int? MaxStringLength { get; private set; } + + public bool StoreAsText { get; private set; } + + public Column(MemberInfo member, CreateFlags createFlags = CreateFlags.None) + { + _member = member; + var memberType = GetMemberType(member); + + var colAttr = member.CustomAttributes.FirstOrDefault(x => x.AttributeType == typeof(ColumnAttribute)); +#if ENABLE_IL2CPP + var ca = member.GetCustomAttribute(typeof(ColumnAttribute)) as ColumnAttribute; + Name = ca == null ? member.Name : ca.Name; +#else + Name = (colAttr != null && colAttr.ConstructorArguments.Count > 0) ? + colAttr.ConstructorArguments[0].Value?.ToString() : + member.Name; +#endif + //If this type is Nullable then Nullable.GetUnderlyingType returns the T, otherwise it returns null, so get the actual type instead + ColumnType = Nullable.GetUnderlyingType(memberType) ?? memberType; + Collation = Orm.Collation(member); + + IsPK = Orm.IsPK(member) || + (((createFlags & CreateFlags.ImplicitPK) == CreateFlags.ImplicitPK) && + string.Compare(member.Name, Orm.ImplicitPkName, StringComparison.OrdinalIgnoreCase) == 0); + + var isAuto = Orm.IsAutoInc(member) || (IsPK && ((createFlags & CreateFlags.AutoIncPK) == CreateFlags.AutoIncPK)); + IsAutoGuid = isAuto && ColumnType == typeof(Guid); + IsAutoInc = isAuto && !IsAutoGuid; + + Indices = Orm.GetIndices(member); + if (!Indices.Any() + && !IsPK + && ((createFlags & CreateFlags.ImplicitIndex) == CreateFlags.ImplicitIndex) + && Name.EndsWith(Orm.ImplicitIndexSuffix, StringComparison.OrdinalIgnoreCase) + ) + { + Indices = new IndexedAttribute[] { new IndexedAttribute() }; + } + IsNullable = !(IsPK || Orm.IsMarkedNotNull(member)); + MaxStringLength = Orm.MaxStringLength(member); + + StoreAsText = memberType.GetTypeInfo().CustomAttributes.Any(x => x.AttributeType == typeof(StoreAsTextAttribute)); + } + + public Column(PropertyInfo member, CreateFlags createFlags = CreateFlags.None) + : this((MemberInfo)member, createFlags) + { } + + public void SetValue(object obj, object val) + { + if (_member is PropertyInfo propy) + { + if (val != null && ColumnType.GetTypeInfo().IsEnum) + propy.SetValue(obj, Enum.ToObject(ColumnType, val)); + else + propy.SetValue(obj, val); + } + else if (_member is FieldInfo field) + { + if (val != null && ColumnType.GetTypeInfo().IsEnum) + field.SetValue(obj, Enum.ToObject(ColumnType, val)); + else + field.SetValue(obj, val); + } + else + throw new InvalidProgramException("unreachable condition"); + } + + public object GetValue(object obj) + { + if (_member is PropertyInfo propy) + return propy.GetValue(obj); + else if (_member is FieldInfo field) + return field.GetValue(obj); + else + throw new InvalidProgramException("unreachable condition"); + } + + private static Type GetMemberType(MemberInfo m) + { + switch (m.MemberType) + { + case MemberTypes.Property: return ((PropertyInfo)m).PropertyType; + case MemberTypes.Field: return ((FieldInfo)m).FieldType; + default: throw new InvalidProgramException($"{nameof(TableMapping)} supports properties or fields only."); + } + } + } + + internal enum MapMethod + { + ByName, + ByPosition + } + } + + class EnumCacheInfo + { + public EnumCacheInfo(Type type) + { + var typeInfo = type.GetTypeInfo(); + + IsEnum = typeInfo.IsEnum; + + if (IsEnum) + { + StoreAsText = typeInfo.CustomAttributes.Any(x => x.AttributeType == typeof(StoreAsTextAttribute)); + + if (StoreAsText) + { + EnumValues = new Dictionary(); + foreach (object e in Enum.GetValues(type)) + { + EnumValues[Convert.ToInt32(e)] = e.ToString(); + } + } + } + } + + public bool IsEnum { get; private set; } + + public bool StoreAsText { get; private set; } + + public Dictionary EnumValues { get; private set; } + } + + static class EnumCache + { + static readonly Dictionary Cache = new Dictionary(); + + public static EnumCacheInfo GetInfo() + { + return GetInfo(typeof(T)); + } + + public static EnumCacheInfo GetInfo(Type type) + { + lock (Cache) + { + EnumCacheInfo info = null; + if (!Cache.TryGetValue(type, out info)) + { + info = new EnumCacheInfo(type); + Cache[type] = info; + } + + return info; + } + } + } + + public static class Orm + { + public const int DefaultMaxStringLength = 140; + public const string ImplicitPkName = "Id"; + public const string ImplicitIndexSuffix = "Id"; + + public static Type GetType(object obj) + { + if (obj == null) + return typeof(object); + var rt = obj as IReflectableType; + if (rt != null) + return rt.GetTypeInfo().AsType(); + return obj.GetType(); + } + + public static string SqlDecl(TableMapping.Column p, bool storeDateTimeAsTicks, bool storeTimeSpanAsTicks) + { + string decl = "\"" + p.Name + "\" " + SqlType(p, storeDateTimeAsTicks, storeTimeSpanAsTicks) + " "; + + if (p.IsPK) + { + decl += "primary key "; + } + if (p.IsAutoInc) + { + decl += "autoincrement "; + } + if (!p.IsNullable) + { + decl += "not null "; + } + if (!string.IsNullOrEmpty(p.Collation)) + { + decl += "collate " + p.Collation + " "; + } + + return decl; + } + + public static string SqlType(TableMapping.Column p, bool storeDateTimeAsTicks, bool storeTimeSpanAsTicks) + { + var clrType = p.ColumnType; + if (clrType == typeof(Boolean) || clrType == typeof(Byte) || clrType == typeof(UInt16) || clrType == typeof(SByte) || clrType == typeof(Int16) || clrType == typeof(Int32) || clrType == typeof(UInt32) || clrType == typeof(Int64)) + { + return "integer"; + } + else if (clrType == typeof(Single) || clrType == typeof(Double) || clrType == typeof(Decimal)) + { + return "float"; + } + else if (clrType == typeof(String) || clrType == typeof(StringBuilder) || clrType == typeof(Uri) || clrType == typeof(UriBuilder)) + { + int? len = p.MaxStringLength; + + if (len.HasValue) + return "varchar(" + len.Value + ")"; + + return "varchar"; + } + else if (clrType == typeof(TimeSpan)) + { + return storeTimeSpanAsTicks ? "bigint" : "time"; + } + else if (clrType == typeof(DateTime)) + { + return storeDateTimeAsTicks ? "bigint" : "datetime"; + } + else if (clrType == typeof(DateTimeOffset)) + { + return "bigint"; + } + else if (clrType.GetTypeInfo().IsEnum) + { + if (p.StoreAsText) + return "varchar"; + else + return "integer"; + } + else if (clrType == typeof(byte[])) + { + return "blob"; + } + else if (clrType == typeof(Guid)) + { + return "varchar(36)"; + } + else + { + throw new NotSupportedException("Don't know about " + clrType); + } + } + + public static bool IsPK(MemberInfo p) + { + return p.CustomAttributes.Any(x => x.AttributeType == typeof(PrimaryKeyAttribute)); + } + + public static string Collation(MemberInfo p) + { +#if ENABLE_IL2CPP + return (p.GetCustomAttribute ()?.Value) ?? ""; +#else + return + (p.CustomAttributes + .Where(x => typeof(CollationAttribute) == x.AttributeType) + .Select(x => { + var args = x.ConstructorArguments; + return args.Count > 0 ? ((args[0].Value as string) ?? "") : ""; + }) + .FirstOrDefault()) ?? ""; +#endif + } + + public static bool IsAutoInc(MemberInfo p) + { + return p.CustomAttributes.Any(x => x.AttributeType == typeof(AutoIncrementAttribute)); + } + + public static FieldInfo GetField(TypeInfo t, string name) + { + var f = t.GetDeclaredField(name); + if (f != null) + return f; + return GetField(t.BaseType.GetTypeInfo(), name); + } + + public static PropertyInfo GetProperty(TypeInfo t, string name) + { + var f = t.GetDeclaredProperty(name); + if (f != null) + return f; + return GetProperty(t.BaseType.GetTypeInfo(), name); + } + + public static object InflateAttribute(CustomAttributeData x) + { + var atype = x.AttributeType; + var typeInfo = atype.GetTypeInfo(); +#if ENABLE_IL2CPP + var r = Activator.CreateInstance (x.AttributeType); +#else + var args = x.ConstructorArguments.Select(a => a.Value).ToArray(); + var r = Activator.CreateInstance(x.AttributeType, args); + foreach (var arg in x.NamedArguments) + { + if (arg.IsField) + { + GetField(typeInfo, arg.MemberName).SetValue(r, arg.TypedValue.Value); + } + else + { + GetProperty(typeInfo, arg.MemberName).SetValue(r, arg.TypedValue.Value); + } + } +#endif + return r; + } + + public static IEnumerable GetIndices(MemberInfo p) + { +#if ENABLE_IL2CPP + return p.GetCustomAttributes (); +#else + var indexedInfo = typeof(IndexedAttribute).GetTypeInfo(); + return + p.CustomAttributes + .Where(x => indexedInfo.IsAssignableFrom(x.AttributeType.GetTypeInfo())) + .Select(x => (IndexedAttribute)InflateAttribute(x)); +#endif + } + + public static int? MaxStringLength(MemberInfo p) + { +#if ENABLE_IL2CPP + return p.GetCustomAttribute ()?.Value; +#else + var attr = p.CustomAttributes.FirstOrDefault(x => x.AttributeType == typeof(MaxLengthAttribute)); + if (attr != null) + { + var attrv = (MaxLengthAttribute)InflateAttribute(attr); + return attrv.Value; + } + return null; +#endif + } + + public static int? MaxStringLength(PropertyInfo p) => MaxStringLength((MemberInfo)p); + + public static bool IsMarkedNotNull(MemberInfo p) + { + return p.CustomAttributes.Any(x => x.AttributeType == typeof(NotNullAttribute)); + } + } + + public partial class SQLiteCommand + { + SQLiteConnection _conn; + private List _bindings; + + public string CommandText { get; set; } + + public SQLiteCommand(SQLiteConnection conn) + { + _conn = conn; + _bindings = new List(); + CommandText = ""; + } + + public int ExecuteNonQuery() + { + if (_conn.Trace) + { + _conn.Tracer?.Invoke("Executing: " + this); + } + + var r = SQLite3.Result.OK; + var stmt = Prepare(); + r = SQLite3.Step(stmt); + Finalize(stmt); + if (r == SQLite3.Result.Done) + { + int rowsAffected = SQLite3.Changes(_conn.Handle); + return rowsAffected; + } + else if (r == SQLite3.Result.Error) + { + string msg = SQLite3.GetErrmsg(_conn.Handle); + throw SQLiteException.New(r, msg); + } + else if (r == SQLite3.Result.Constraint) + { + if (SQLite3.ExtendedErrCode(_conn.Handle) == SQLite3.ExtendedResult.ConstraintNotNull) + { + throw NotNullConstraintViolationException.New(r, SQLite3.GetErrmsg(_conn.Handle)); + } + } + + throw SQLiteException.New(r, SQLite3.GetErrmsg(_conn.Handle)); + } + + public IEnumerable ExecuteDeferredQuery() + { + return ExecuteDeferredQuery(_conn.GetMapping(typeof(T))); + } + + public List ExecuteQuery() + { + return ExecuteDeferredQuery(_conn.GetMapping(typeof(T))).ToList(); + } + + public List ExecuteQuery(TableMapping map) + { + return ExecuteDeferredQuery(map).ToList(); + } + + /// + /// Invoked every time an instance is loaded from the database. + /// + /// + /// The newly created object. + /// + /// + /// This can be overridden in combination with the + /// method to hook into the life-cycle of objects. + /// + protected virtual void OnInstanceCreated(object obj) + { + // Can be overridden. + } + + public IEnumerable ExecuteDeferredQuery(TableMapping map) + { + if (_conn.Trace) + { + _conn.Tracer?.Invoke("Executing Query: " + this); + } + + var stmt = Prepare(); + try + { + var cols = new TableMapping.Column[SQLite3.ColumnCount(stmt)]; + var fastColumnSetters = new Action[SQLite3.ColumnCount(stmt)]; + + if (map.Method == TableMapping.MapMethod.ByPosition) + { + Array.Copy(map.Columns, cols, Math.Min(cols.Length, map.Columns.Length)); + } + else if (map.Method == TableMapping.MapMethod.ByName) + { + MethodInfo getSetter = null; + if (typeof(T) != map.MappedType) + { + getSetter = typeof(FastColumnSetter) + .GetMethod(nameof(FastColumnSetter.GetFastSetter), + BindingFlags.NonPublic | BindingFlags.Static).MakeGenericMethod(map.MappedType); + } + + for (int i = 0; i < cols.Length; i++) + { + var name = SQLite3.ColumnName16(stmt, i); + cols[i] = map.FindColumn(name); + if (cols[i] != null) + if (getSetter != null) + { + fastColumnSetters[i] = (Action)getSetter.Invoke(null, new object[] { _conn, cols[i] }); + } + else + { + fastColumnSetters[i] = FastColumnSetter.GetFastSetter(_conn, cols[i]); + } + } + } + + while (SQLite3.Step(stmt) == SQLite3.Result.Row) + { + var obj = Activator.CreateInstance(map.MappedType); + for (int i = 0; i < cols.Length; i++) + { + if (cols[i] == null) + continue; + + if (fastColumnSetters[i] != null) + { + fastColumnSetters[i].Invoke(obj, stmt, i); + } + else + { + var colType = SQLite3.ColumnType(stmt, i); + var val = ReadCol(stmt, i, colType, cols[i].ColumnType); + cols[i].SetValue(obj, val); + } + } + OnInstanceCreated(obj); + yield return (T)obj; + } + } + finally + { + SQLite3.Finalize(stmt); + } + } + + public T ExecuteScalar() + { + if (_conn.Trace) + { + _conn.Tracer?.Invoke("Executing Query: " + this); + } + + T val = default(T); + + var stmt = Prepare(); + + try + { + var r = SQLite3.Step(stmt); + if (r == SQLite3.Result.Row) + { + var colType = SQLite3.ColumnType(stmt, 0); + var colval = ReadCol(stmt, 0, colType, typeof(T)); + if (colval != null) + { + val = (T)colval; + } + } + else if (r == SQLite3.Result.Done) + { + } + else + { + throw SQLiteException.New(r, SQLite3.GetErrmsg(_conn.Handle)); + } + } + finally + { + Finalize(stmt); + } + + return val; + } + + public IEnumerable ExecuteQueryScalars() + { + if (_conn.Trace) + { + _conn.Tracer?.Invoke("Executing Query: " + this); + } + var stmt = Prepare(); + try + { + if (SQLite3.ColumnCount(stmt) < 1) + { + throw new InvalidOperationException("QueryScalars should return at least one column"); + } + while (SQLite3.Step(stmt) == SQLite3.Result.Row) + { + var colType = SQLite3.ColumnType(stmt, 0); + var val = ReadCol(stmt, 0, colType, typeof(T)); + if (val == null) + { + yield return default(T); + } + else + { + yield return (T)val; + } + } + } + finally + { + Finalize(stmt); + } + } + + public void Bind(string name, object val) + { + _bindings.Add(new Binding + { + Name = name, + Value = val + }); + } + + public void Bind(object val) + { + Bind(null, val); + } + + public override string ToString() + { + var parts = new string[1 + _bindings.Count]; + parts[0] = CommandText; + var i = 1; + foreach (var b in _bindings) + { + parts[i] = string.Format(" {0}: {1}", i - 1, b.Value); + i++; + } + return string.Join(Environment.NewLine, parts); + } + + Sqlite3Statement Prepare() + { + var stmt = SQLite3.Prepare2(_conn.Handle, CommandText); + BindAll(stmt); + return stmt; + } + + void Finalize(Sqlite3Statement stmt) + { + SQLite3.Finalize(stmt); + } + + void BindAll(Sqlite3Statement stmt) + { + int nextIdx = 1; + foreach (var b in _bindings) + { + if (b.Name != null) + { + b.Index = SQLite3.BindParameterIndex(stmt, b.Name); + } + else + { + b.Index = nextIdx++; + } + + BindParameter(stmt, b.Index, b.Value, _conn.StoreDateTimeAsTicks, _conn.DateTimeStringFormat, _conn.StoreTimeSpanAsTicks); + } + } + + static IntPtr NegativePointer = new IntPtr(-1); + + internal static void BindParameter(Sqlite3Statement stmt, int index, object value, bool storeDateTimeAsTicks, string dateTimeStringFormat, bool storeTimeSpanAsTicks) + { + if (value == null) + { + SQLite3.BindNull(stmt, index); + } + else + { + if (value is Int32) + { + SQLite3.BindInt(stmt, index, (int)value); + } + else if (value is String) + { + SQLite3.BindText(stmt, index, (string)value, -1, NegativePointer); + } + else if (value is Byte || value is UInt16 || value is SByte || value is Int16) + { + SQLite3.BindInt(stmt, index, Convert.ToInt32(value)); + } + else if (value is Boolean) + { + SQLite3.BindInt(stmt, index, (bool)value ? 1 : 0); + } + else if (value is UInt32 || value is Int64) + { + SQLite3.BindInt64(stmt, index, Convert.ToInt64(value)); + } + else if (value is Single || value is Double || value is Decimal) + { + SQLite3.BindDouble(stmt, index, Convert.ToDouble(value)); + } + else if (value is TimeSpan) + { + if (storeTimeSpanAsTicks) + { + SQLite3.BindInt64(stmt, index, ((TimeSpan)value).Ticks); + } + else + { + SQLite3.BindText(stmt, index, ((TimeSpan)value).ToString(), -1, NegativePointer); + } + } + else if (value is DateTime) + { + if (storeDateTimeAsTicks) + { + SQLite3.BindInt64(stmt, index, ((DateTime)value).Ticks); + } + else + { + SQLite3.BindText(stmt, index, ((DateTime)value).ToString(dateTimeStringFormat, System.Globalization.CultureInfo.InvariantCulture), -1, NegativePointer); + } + } + else if (value is DateTimeOffset) + { + SQLite3.BindInt64(stmt, index, ((DateTimeOffset)value).UtcTicks); + } + else if (value is byte[]) + { + SQLite3.BindBlob(stmt, index, (byte[])value, ((byte[])value).Length, NegativePointer); + } + else if (value is Guid) + { + SQLite3.BindText(stmt, index, ((Guid)value).ToString(), 72, NegativePointer); + } + else if (value is Uri) + { + SQLite3.BindText(stmt, index, ((Uri)value).ToString(), -1, NegativePointer); + } + else if (value is StringBuilder) + { + SQLite3.BindText(stmt, index, ((StringBuilder)value).ToString(), -1, NegativePointer); + } + else if (value is UriBuilder) + { + SQLite3.BindText(stmt, index, ((UriBuilder)value).ToString(), -1, NegativePointer); + } + else + { + // Now we could possibly get an enum, retrieve cached info + var valueType = value.GetType(); + var enumInfo = EnumCache.GetInfo(valueType); + if (enumInfo.IsEnum) + { + var enumIntValue = Convert.ToInt32(value); + if (enumInfo.StoreAsText) + SQLite3.BindText(stmt, index, enumInfo.EnumValues[enumIntValue], -1, NegativePointer); + else + SQLite3.BindInt(stmt, index, enumIntValue); + } + else + { + throw new NotSupportedException("Cannot store type: " + Orm.GetType(value)); + } + } + } + } + + class Binding + { + public string Name { get; set; } + + public object Value { get; set; } + + public int Index { get; set; } + } + + object ReadCol(Sqlite3Statement stmt, int index, SQLite3.ColType type, Type clrType) + { + if (type == SQLite3.ColType.Null) + { + return null; + } + else + { + var clrTypeInfo = clrType.GetTypeInfo(); + if (clrTypeInfo.IsGenericType && clrTypeInfo.GetGenericTypeDefinition() == typeof(Nullable<>)) + { + clrType = clrTypeInfo.GenericTypeArguments[0]; + clrTypeInfo = clrType.GetTypeInfo(); + } + + if (clrType == typeof(String)) + { + return SQLite3.ColumnString(stmt, index); + } + else if (clrType == typeof(Int32)) + { + return (int)SQLite3.ColumnInt(stmt, index); + } + else if (clrType == typeof(Boolean)) + { + return SQLite3.ColumnInt(stmt, index) == 1; + } + else if (clrType == typeof(double)) + { + return SQLite3.ColumnDouble(stmt, index); + } + else if (clrType == typeof(float)) + { + return (float)SQLite3.ColumnDouble(stmt, index); + } + else if (clrType == typeof(TimeSpan)) + { + if (_conn.StoreTimeSpanAsTicks) + { + return new TimeSpan(SQLite3.ColumnInt64(stmt, index)); + } + else + { + var text = SQLite3.ColumnString(stmt, index); + TimeSpan resultTime; + if (!TimeSpan.TryParseExact(text, "c", System.Globalization.CultureInfo.InvariantCulture, System.Globalization.TimeSpanStyles.None, out resultTime)) + { + resultTime = TimeSpan.Parse(text); + } + return resultTime; + } + } + else if (clrType == typeof(DateTime)) + { + if (_conn.StoreDateTimeAsTicks) + { + return new DateTime(SQLite3.ColumnInt64(stmt, index)); + } + else + { + var text = SQLite3.ColumnString(stmt, index); + DateTime resultDate; + if (!DateTime.TryParseExact(text, _conn.DateTimeStringFormat, System.Globalization.CultureInfo.InvariantCulture, _conn.DateTimeStyle, out resultDate)) + { + resultDate = DateTime.Parse(text); + } + return resultDate; + } + } + else if (clrType == typeof(DateTimeOffset)) + { + return new DateTimeOffset(SQLite3.ColumnInt64(stmt, index), TimeSpan.Zero); + } + else if (clrTypeInfo.IsEnum) + { + if (type == SQLite3.ColType.Text) + { + var value = SQLite3.ColumnString(stmt, index); + return Enum.Parse(clrType, value.ToString(), true); + } + else + return SQLite3.ColumnInt(stmt, index); + } + else if (clrType == typeof(Int64)) + { + return SQLite3.ColumnInt64(stmt, index); + } + else if (clrType == typeof(UInt32)) + { + return (uint)SQLite3.ColumnInt64(stmt, index); + } + else if (clrType == typeof(decimal)) + { + return (decimal)SQLite3.ColumnDouble(stmt, index); + } + else if (clrType == typeof(Byte)) + { + return (byte)SQLite3.ColumnInt(stmt, index); + } + else if (clrType == typeof(UInt16)) + { + return (ushort)SQLite3.ColumnInt(stmt, index); + } + else if (clrType == typeof(Int16)) + { + return (short)SQLite3.ColumnInt(stmt, index); + } + else if (clrType == typeof(sbyte)) + { + return (sbyte)SQLite3.ColumnInt(stmt, index); + } + else if (clrType == typeof(byte[])) + { + return SQLite3.ColumnByteArray(stmt, index); + } + else if (clrType == typeof(Guid)) + { + var text = SQLite3.ColumnString(stmt, index); + return new Guid(text); + } + else if (clrType == typeof(Uri)) + { + var text = SQLite3.ColumnString(stmt, index); + return new Uri(text); + } + else if (clrType == typeof(StringBuilder)) + { + var text = SQLite3.ColumnString(stmt, index); + return new StringBuilder(text); + } + else if (clrType == typeof(UriBuilder)) + { + var text = SQLite3.ColumnString(stmt, index); + return new UriBuilder(text); + } + else + { + throw new NotSupportedException("Don't know how to read " + clrType); + } + } + } + } + + internal class FastColumnSetter + { + /// + /// Creates a delegate that can be used to quickly set object members from query columns. + /// + /// Note that this frontloads the slow reflection-based type checking for columns to only happen once at the beginning of a query, + /// and then afterwards each row of the query can invoke the delegate returned by this function to get much better performance (up to 10x speed boost, depending on query size and platform). + /// + /// The type of the destination object that the query will read into + /// The active connection. Note that this is primarily needed in order to read preferences regarding how certain data types (such as TimeSpan / DateTime) should be encoded in the database. + /// The table mapping used to map the statement column to a member of the destination object type + /// + /// A delegate for fast-setting of object members from statement columns. + /// + /// If no fast setter is available for the requested column (enums in particular cause headache), then this function returns null. + /// + internal static Action GetFastSetter(SQLiteConnection conn, TableMapping.Column column) + { + Action fastSetter = null; + + Type clrType = column.PropertyInfo.PropertyType; + + var clrTypeInfo = clrType.GetTypeInfo(); + if (clrTypeInfo.IsGenericType && clrTypeInfo.GetGenericTypeDefinition() == typeof(Nullable<>)) + { + clrType = clrTypeInfo.GenericTypeArguments[0]; + clrTypeInfo = clrType.GetTypeInfo(); + } + + if (clrType == typeof(String)) + { + fastSetter = CreateTypedSetterDelegate(column, (stmt, index) => { + return SQLite3.ColumnString(stmt, index); + }); + } + else if (clrType == typeof(Int32)) + { + fastSetter = CreateNullableTypedSetterDelegate(column, (stmt, index) => { + return SQLite3.ColumnInt(stmt, index); + }); + } + else if (clrType == typeof(Boolean)) + { + fastSetter = CreateNullableTypedSetterDelegate(column, (stmt, index) => { + return SQLite3.ColumnInt(stmt, index) == 1; + }); + } + else if (clrType == typeof(double)) + { + fastSetter = CreateNullableTypedSetterDelegate(column, (stmt, index) => { + return SQLite3.ColumnDouble(stmt, index); + }); + } + else if (clrType == typeof(float)) + { + fastSetter = CreateNullableTypedSetterDelegate(column, (stmt, index) => { + return (float)SQLite3.ColumnDouble(stmt, index); + }); + } + else if (clrType == typeof(TimeSpan)) + { + if (conn.StoreTimeSpanAsTicks) + { + fastSetter = CreateNullableTypedSetterDelegate(column, (stmt, index) => { + return new TimeSpan(SQLite3.ColumnInt64(stmt, index)); + }); + } + else + { + fastSetter = CreateNullableTypedSetterDelegate(column, (stmt, index) => { + var text = SQLite3.ColumnString(stmt, index); + TimeSpan resultTime; + if (!TimeSpan.TryParseExact(text, "c", System.Globalization.CultureInfo.InvariantCulture, System.Globalization.TimeSpanStyles.None, out resultTime)) + { + resultTime = TimeSpan.Parse(text); + } + return resultTime; + }); + } + } + else if (clrType == typeof(DateTime)) + { + if (conn.StoreDateTimeAsTicks) + { + fastSetter = CreateNullableTypedSetterDelegate(column, (stmt, index) => { + return new DateTime(SQLite3.ColumnInt64(stmt, index)); + }); + } + else + { + fastSetter = CreateNullableTypedSetterDelegate(column, (stmt, index) => { + var text = SQLite3.ColumnString(stmt, index); + DateTime resultDate; + if (!DateTime.TryParseExact(text, conn.DateTimeStringFormat, System.Globalization.CultureInfo.InvariantCulture, conn.DateTimeStyle, out resultDate)) + { + resultDate = DateTime.Parse(text); + } + return resultDate; + }); + } + } + else if (clrType == typeof(DateTimeOffset)) + { + fastSetter = CreateNullableTypedSetterDelegate(column, (stmt, index) => { + return new DateTimeOffset(SQLite3.ColumnInt64(stmt, index), TimeSpan.Zero); + }); + } + else if (clrTypeInfo.IsEnum) + { + // NOTE: Not sure of a good way (if any?) to do a strongly-typed fast setter like this for enumerated types -- for now, return null and column sets will revert back to the safe (but slow) Reflection-based method of column prop.Set() + } + else if (clrType == typeof(Int64)) + { + fastSetter = CreateNullableTypedSetterDelegate(column, (stmt, index) => { + return SQLite3.ColumnInt64(stmt, index); + }); + } + else if (clrType == typeof(UInt32)) + { + fastSetter = CreateNullableTypedSetterDelegate(column, (stmt, index) => { + return (uint)SQLite3.ColumnInt64(stmt, index); + }); + } + else if (clrType == typeof(decimal)) + { + fastSetter = CreateNullableTypedSetterDelegate(column, (stmt, index) => { + return (decimal)SQLite3.ColumnDouble(stmt, index); + }); + } + else if (clrType == typeof(Byte)) + { + fastSetter = CreateNullableTypedSetterDelegate(column, (stmt, index) => { + return (byte)SQLite3.ColumnInt(stmt, index); + }); + } + else if (clrType == typeof(UInt16)) + { + fastSetter = CreateNullableTypedSetterDelegate(column, (stmt, index) => { + return (ushort)SQLite3.ColumnInt(stmt, index); + }); + } + else if (clrType == typeof(Int16)) + { + fastSetter = CreateNullableTypedSetterDelegate(column, (stmt, index) => { + return (short)SQLite3.ColumnInt(stmt, index); + }); + } + else if (clrType == typeof(sbyte)) + { + fastSetter = CreateNullableTypedSetterDelegate(column, (stmt, index) => { + return (sbyte)SQLite3.ColumnInt(stmt, index); + }); + } + else if (clrType == typeof(byte[])) + { + fastSetter = CreateTypedSetterDelegate(column, (stmt, index) => { + return SQLite3.ColumnByteArray(stmt, index); + }); + } + else if (clrType == typeof(Guid)) + { + fastSetter = CreateNullableTypedSetterDelegate(column, (stmt, index) => { + var text = SQLite3.ColumnString(stmt, index); + return new Guid(text); + }); + } + else if (clrType == typeof(Uri)) + { + fastSetter = CreateTypedSetterDelegate(column, (stmt, index) => { + var text = SQLite3.ColumnString(stmt, index); + return new Uri(text); + }); + } + else if (clrType == typeof(StringBuilder)) + { + fastSetter = CreateTypedSetterDelegate(column, (stmt, index) => { + var text = SQLite3.ColumnString(stmt, index); + return new StringBuilder(text); + }); + } + else if (clrType == typeof(UriBuilder)) + { + fastSetter = CreateTypedSetterDelegate(column, (stmt, index) => { + var text = SQLite3.ColumnString(stmt, index); + return new UriBuilder(text); + }); + } + else + { + // NOTE: Will fall back to the slow setter method in the event that we are unable to create a fast setter delegate for a particular column type + } + return fastSetter; + } + + /// + /// This creates a strongly typed delegate that will permit fast setting of column values given a Sqlite3Statement and a column index. + /// + /// Note that this is identical to CreateTypedSetterDelegate(), but has an extra check to see if it should create a nullable version of the delegate. + /// + /// The type of the object whose member column is being set + /// The CLR type of the member in the object which corresponds to the given SQLite columnn + /// The column mapping that identifies the target member of the destination object + /// A lambda that can be used to retrieve the column value at query-time + /// A strongly-typed delegate + private static Action CreateNullableTypedSetterDelegate(TableMapping.Column column, Func getColumnValue) where ColumnMemberType : struct + { + var clrTypeInfo = column.PropertyInfo.PropertyType.GetTypeInfo(); + bool isNullable = false; + + if (clrTypeInfo.IsGenericType && clrTypeInfo.GetGenericTypeDefinition() == typeof(Nullable<>)) + { + isNullable = true; + } + + if (isNullable) + { + var setProperty = (Action)Delegate.CreateDelegate( + typeof(Action), null, + column.PropertyInfo.GetSetMethod()); + + return (o, stmt, i) => { + var colType = SQLite3.ColumnType(stmt, i); + if (colType != SQLite3.ColType.Null) + setProperty.Invoke((ObjectType)o, getColumnValue.Invoke(stmt, i)); + }; + } + + return CreateTypedSetterDelegate(column, getColumnValue); + } + + /// + /// This creates a strongly typed delegate that will permit fast setting of column values given a Sqlite3Statement and a column index. + /// + /// The type of the object whose member column is being set + /// The CLR type of the member in the object which corresponds to the given SQLite columnn + /// The column mapping that identifies the target member of the destination object + /// A lambda that can be used to retrieve the column value at query-time + /// A strongly-typed delegate + private static Action CreateTypedSetterDelegate(TableMapping.Column column, Func getColumnValue) + { + var setProperty = (Action)Delegate.CreateDelegate( + typeof(Action), null, + column.PropertyInfo.GetSetMethod()); + + return (o, stmt, i) => { + var colType = SQLite3.ColumnType(stmt, i); + if (colType != SQLite3.ColType.Null) + setProperty.Invoke((ObjectType)o, getColumnValue.Invoke(stmt, i)); + }; + } + } + + /// + /// Since the insert never changed, we only need to prepare once. + /// + class PreparedSqlLiteInsertCommand : IDisposable + { + bool Initialized; + + SQLiteConnection Connection; + + string CommandText; + + Sqlite3Statement Statement; + static readonly Sqlite3Statement NullStatement = default(Sqlite3Statement); + + public PreparedSqlLiteInsertCommand(SQLiteConnection conn, string commandText) + { + Connection = conn; + CommandText = commandText; + } + + public int ExecuteNonQuery(object[] source) + { + if (Initialized && Statement == NullStatement) + { + throw new ObjectDisposedException(nameof(PreparedSqlLiteInsertCommand)); + } + + if (Connection.Trace) + { + Connection.Tracer?.Invoke("Executing: " + CommandText); + } + + var r = SQLite3.Result.OK; + + if (!Initialized) + { + Statement = SQLite3.Prepare2(Connection.Handle, CommandText); + Initialized = true; + } + + //bind the values. + if (source != null) + { + for (int i = 0; i < source.Length; i++) + { + SQLiteCommand.BindParameter(Statement, i + 1, source[i], Connection.StoreDateTimeAsTicks, Connection.DateTimeStringFormat, Connection.StoreTimeSpanAsTicks); + } + } + r = SQLite3.Step(Statement); + + if (r == SQLite3.Result.Done) + { + int rowsAffected = SQLite3.Changes(Connection.Handle); + SQLite3.Reset(Statement); + return rowsAffected; + } + else if (r == SQLite3.Result.Error) + { + string msg = SQLite3.GetErrmsg(Connection.Handle); + SQLite3.Reset(Statement); + throw SQLiteException.New(r, msg); + } + else if (r == SQLite3.Result.Constraint && SQLite3.ExtendedErrCode(Connection.Handle) == SQLite3.ExtendedResult.ConstraintNotNull) + { + SQLite3.Reset(Statement); + throw NotNullConstraintViolationException.New(r, SQLite3.GetErrmsg(Connection.Handle)); + } + else + { + SQLite3.Reset(Statement); + throw SQLiteException.New(r, SQLite3.GetErrmsg(Connection.Handle)); + } + } + + public void Dispose() + { + Dispose(true); + GC.SuppressFinalize(this); + } + + void Dispose(bool disposing) + { + var s = Statement; + Statement = NullStatement; + Connection = null; + if (s != NullStatement) + { + SQLite3.Finalize(s); + } + } + + ~PreparedSqlLiteInsertCommand() + { + Dispose(false); + } + } + + public enum CreateTableResult + { + Created, + Migrated, + } + + public class CreateTablesResult + { + public Dictionary Results { get; private set; } + + public CreateTablesResult() + { + Results = new Dictionary(); + } + } + + public abstract class BaseTableQuery + { + protected class Ordering + { + public string ColumnName { get; set; } + public bool Ascending { get; set; } + } + } + + public class TableQuery : BaseTableQuery, IEnumerable + { + public SQLiteConnection Connection { get; private set; } + + public TableMapping Table { get; private set; } + + Expression _where; + List _orderBys; + int? _limit; + int? _offset; + + BaseTableQuery _joinInner; + Expression _joinInnerKeySelector; + BaseTableQuery _joinOuter; + Expression _joinOuterKeySelector; + Expression _joinSelector; + + Expression _selector; + + TableQuery(SQLiteConnection conn, TableMapping table) + { + Connection = conn; + Table = table; + } + + public TableQuery(SQLiteConnection conn) + { + Connection = conn; + Table = Connection.GetMapping(typeof(T)); + } + + public TableQuery Clone() + { + var q = new TableQuery(Connection, Table); + q._where = _where; + q._deferred = _deferred; + if (_orderBys != null) + { + q._orderBys = new List(_orderBys); + } + q._limit = _limit; + q._offset = _offset; + q._joinInner = _joinInner; + q._joinInnerKeySelector = _joinInnerKeySelector; + q._joinOuter = _joinOuter; + q._joinOuterKeySelector = _joinOuterKeySelector; + q._joinSelector = _joinSelector; + q._selector = _selector; + return q; + } + + /// + /// Filters the query based on a predicate. + /// + public TableQuery Where(Expression> predExpr) + { + if (predExpr.NodeType == ExpressionType.Lambda) + { + var lambda = (LambdaExpression)predExpr; + var pred = lambda.Body; + var q = Clone(); + q.AddWhere(pred); + return q; + } + else + { + throw new NotSupportedException("Must be a predicate"); + } + } + + /// + /// Delete all the rows that match this query. + /// + public int Delete() + { + return Delete(null); + } + + /// + /// Delete all the rows that match this query and the given predicate. + /// + public int Delete(Expression> predExpr) + { + if (_limit.HasValue || _offset.HasValue) + throw new InvalidOperationException("Cannot delete with limits or offsets"); + + if (_where == null && predExpr == null) + throw new InvalidOperationException("No condition specified"); + + var pred = _where; + + if (predExpr != null && predExpr.NodeType == ExpressionType.Lambda) + { + var lambda = (LambdaExpression)predExpr; + pred = pred != null ? Expression.AndAlso(pred, lambda.Body) : lambda.Body; + } + + var args = new List(); + var cmdText = "delete from \"" + Table.TableName + "\""; + var w = CompileExpr(pred, args); + cmdText += " where " + w.CommandText; + + var command = Connection.CreateCommand(cmdText, args.ToArray()); + + int result = command.ExecuteNonQuery(); + return result; + } + + /// + /// Yields a given number of elements from the query and then skips the remainder. + /// + public TableQuery Take(int n) + { + var q = Clone(); + q._limit = n; + return q; + } + + /// + /// Skips a given number of elements from the query and then yields the remainder. + /// + public TableQuery Skip(int n) + { + var q = Clone(); + q._offset = n; + return q; + } + + /// + /// Returns the element at a given index + /// + public T ElementAt(int index) + { + return Skip(index).Take(1).First(); + } + + bool _deferred; + public TableQuery Deferred() + { + var q = Clone(); + q._deferred = true; + return q; + } + + /// + /// Order the query results according to a key. + /// + public TableQuery OrderBy(Expression> orderExpr) + { + return AddOrderBy(orderExpr, true); + } + + /// + /// Order the query results according to a key. + /// + public TableQuery OrderByDescending(Expression> orderExpr) + { + return AddOrderBy(orderExpr, false); + } + + /// + /// Order the query results according to a key. + /// + public TableQuery ThenBy(Expression> orderExpr) + { + return AddOrderBy(orderExpr, true); + } + + /// + /// Order the query results according to a key. + /// + public TableQuery ThenByDescending(Expression> orderExpr) + { + return AddOrderBy(orderExpr, false); + } + + TableQuery AddOrderBy(Expression> orderExpr, bool asc) + { + if (orderExpr.NodeType == ExpressionType.Lambda) + { + var lambda = (LambdaExpression)orderExpr; + + MemberExpression mem = null; + + var unary = lambda.Body as UnaryExpression; + if (unary != null && unary.NodeType == ExpressionType.Convert) + { + mem = unary.Operand as MemberExpression; + } + else + { + mem = lambda.Body as MemberExpression; + } + + if (mem != null && (mem.Expression.NodeType == ExpressionType.Parameter)) + { + var q = Clone(); + if (q._orderBys == null) + { + q._orderBys = new List(); + } + q._orderBys.Add(new Ordering + { + ColumnName = Table.FindColumnWithPropertyName(mem.Member.Name).Name, + Ascending = asc + }); + return q; + } + else + { + throw new NotSupportedException("Order By does not support: " + orderExpr); + } + } + else + { + throw new NotSupportedException("Must be a predicate"); + } + } + + private void AddWhere(Expression pred) + { + if (_where == null) + { + _where = pred; + } + else + { + _where = Expression.AndAlso(_where, pred); + } + } + + ///// + ///// Performs an inner join of two queries based on matching keys extracted from the elements. + ///// + //public TableQuery Join ( + // TableQuery inner, + // Expression> outerKeySelector, + // Expression> innerKeySelector, + // Expression> resultSelector) + //{ + // var q = new TableQuery (Connection, Connection.GetMapping (typeof (TResult))) { + // _joinOuter = this, + // _joinOuterKeySelector = outerKeySelector, + // _joinInner = inner, + // _joinInnerKeySelector = innerKeySelector, + // _joinSelector = resultSelector, + // }; + // return q; + //} + + // Not needed until Joins are supported + // Keeping this commented out forces the default Linq to objects processor to run + //public TableQuery Select (Expression> selector) + //{ + // var q = Clone (); + // q._selector = selector; + // return q; + //} + + private SQLiteCommand GenerateCommand(string selectionList) + { + if (_joinInner != null && _joinOuter != null) + { + throw new NotSupportedException("Joins are not supported."); + } + else + { + var cmdText = "select " + selectionList + " from \"" + Table.TableName + "\""; + var args = new List(); + if (_where != null) + { + var w = CompileExpr(_where, args); + cmdText += " where " + w.CommandText; + } + if ((_orderBys != null) && (_orderBys.Count > 0)) + { + var t = string.Join(", ", _orderBys.Select(o => "\"" + o.ColumnName + "\"" + (o.Ascending ? "" : " desc")).ToArray()); + cmdText += " order by " + t; + } + if (_limit.HasValue) + { + cmdText += " limit " + _limit.Value; + } + if (_offset.HasValue) + { + if (!_limit.HasValue) + { + cmdText += " limit -1 "; + } + cmdText += " offset " + _offset.Value; + } + return Connection.CreateCommand(cmdText, args.ToArray()); + } + } + + class CompileResult + { + public string CommandText { get; set; } + + public object Value { get; set; } + } + + private CompileResult CompileExpr(Expression expr, List queryArgs) + { + if (expr == null) + { + throw new NotSupportedException("Expression is NULL"); + } + else if (expr is BinaryExpression) + { + var bin = (BinaryExpression)expr; + + // VB turns 'x=="foo"' into 'CompareString(x,"foo",true/false)==0', so we need to unwrap it + // http://blogs.msdn.com/b/vbteam/archive/2007/09/18/vb-expression-trees-string-comparisons.aspx + if (bin.Left.NodeType == ExpressionType.Call) + { + var call = (MethodCallExpression)bin.Left; + if (call.Method.DeclaringType.FullName == "Microsoft.VisualBasic.CompilerServices.Operators" + && call.Method.Name == "CompareString") + bin = Expression.MakeBinary(bin.NodeType, call.Arguments[0], call.Arguments[1]); + } + + + var leftr = CompileExpr(bin.Left, queryArgs); + var rightr = CompileExpr(bin.Right, queryArgs); + + //If either side is a parameter and is null, then handle the other side specially (for "is null"/"is not null") + string text; + if (leftr.CommandText == "?" && leftr.Value == null) + text = CompileNullBinaryExpression(bin, rightr); + else if (rightr.CommandText == "?" && rightr.Value == null) + text = CompileNullBinaryExpression(bin, leftr); + else + text = "(" + leftr.CommandText + " " + GetSqlName(bin) + " " + rightr.CommandText + ")"; + return new CompileResult { CommandText = text }; + } + else if (expr.NodeType == ExpressionType.Not) + { + var operandExpr = ((UnaryExpression)expr).Operand; + var opr = CompileExpr(operandExpr, queryArgs); + object val = opr.Value; + if (val is bool) + val = !((bool)val); + return new CompileResult + { + CommandText = "NOT(" + opr.CommandText + ")", + Value = val + }; + } + else if (expr.NodeType == ExpressionType.Call) + { + + var call = (MethodCallExpression)expr; + var args = new CompileResult[call.Arguments.Count]; + var obj = call.Object != null ? CompileExpr(call.Object, queryArgs) : null; + + for (var i = 0; i < args.Length; i++) + { + args[i] = CompileExpr(call.Arguments[i], queryArgs); + } + + var sqlCall = ""; + + if (call.Method.Name == "Like" && args.Length == 2) + { + sqlCall = "(" + args[0].CommandText + " like " + args[1].CommandText + ")"; + } + else if (call.Method.Name == "Contains" && args.Length == 2) + { + sqlCall = "(" + args[1].CommandText + " in " + args[0].CommandText + ")"; + } + else if (call.Method.Name == "Contains" && args.Length == 1) + { + if (call.Object != null && call.Object.Type == typeof(string)) + { + sqlCall = "( instr(" + obj.CommandText + "," + args[0].CommandText + ") >0 )"; + } + else + { + sqlCall = "(" + args[0].CommandText + " in " + obj.CommandText + ")"; + } + } + else if (call.Method.Name == "StartsWith" && args.Length >= 1) + { + var startsWithCmpOp = StringComparison.CurrentCulture; + if (args.Length == 2) + { + startsWithCmpOp = (StringComparison)args[1].Value; + } + switch (startsWithCmpOp) + { + case StringComparison.Ordinal: + case StringComparison.CurrentCulture: + sqlCall = "( substr(" + obj.CommandText + ", 1, " + args[0].Value.ToString().Length + ") = " + args[0].CommandText + ")"; + break; + case StringComparison.OrdinalIgnoreCase: + case StringComparison.CurrentCultureIgnoreCase: + sqlCall = "(" + obj.CommandText + " like (" + args[0].CommandText + " || '%'))"; + break; + } + + } + else if (call.Method.Name == "EndsWith" && args.Length >= 1) + { + var endsWithCmpOp = StringComparison.CurrentCulture; + if (args.Length == 2) + { + endsWithCmpOp = (StringComparison)args[1].Value; + } + switch (endsWithCmpOp) + { + case StringComparison.Ordinal: + case StringComparison.CurrentCulture: + sqlCall = "( substr(" + obj.CommandText + ", length(" + obj.CommandText + ") - " + args[0].Value.ToString().Length + "+1, " + args[0].Value.ToString().Length + ") = " + args[0].CommandText + ")"; + break; + case StringComparison.OrdinalIgnoreCase: + case StringComparison.CurrentCultureIgnoreCase: + sqlCall = "(" + obj.CommandText + " like ('%' || " + args[0].CommandText + "))"; + break; + } + } + else if (call.Method.Name == "Equals" && args.Length == 1) + { + sqlCall = "(" + obj.CommandText + " = (" + args[0].CommandText + "))"; + } + else if (call.Method.Name == "ToLower") + { + sqlCall = "(lower(" + obj.CommandText + "))"; + } + else if (call.Method.Name == "ToUpper") + { + sqlCall = "(upper(" + obj.CommandText + "))"; + } + else if (call.Method.Name == "Replace" && args.Length == 2) + { + sqlCall = "(replace(" + obj.CommandText + "," + args[0].CommandText + "," + args[1].CommandText + "))"; + } + else if (call.Method.Name == "IsNullOrEmpty" && args.Length == 1) + { + sqlCall = "(" + args[0].CommandText + " is null or" + args[0].CommandText + " ='' )"; + } + else + { + sqlCall = call.Method.Name.ToLower() + "(" + string.Join(",", args.Select(a => a.CommandText).ToArray()) + ")"; + } + return new CompileResult { CommandText = sqlCall }; + + } + else if (expr.NodeType == ExpressionType.Constant) + { + var c = (ConstantExpression)expr; + queryArgs.Add(c.Value); + return new CompileResult + { + CommandText = "?", + Value = c.Value + }; + } + else if (expr.NodeType == ExpressionType.Convert) + { + var u = (UnaryExpression)expr; + var ty = u.Type; + var valr = CompileExpr(u.Operand, queryArgs); + return new CompileResult + { + CommandText = valr.CommandText, + Value = valr.Value != null ? ConvertTo(valr.Value, ty) : null + }; + } + else if (expr.NodeType == ExpressionType.MemberAccess) + { + var mem = (MemberExpression)expr; + + var paramExpr = mem.Expression as ParameterExpression; + if (paramExpr == null) + { + var convert = mem.Expression as UnaryExpression; + if (convert != null && convert.NodeType == ExpressionType.Convert) + { + paramExpr = convert.Operand as ParameterExpression; + } + } + + if (paramExpr != null) + { + // + // This is a column of our table, output just the column name + // Need to translate it if that column name is mapped + // + var columnName = Table.FindColumnWithPropertyName(mem.Member.Name).Name; + return new CompileResult { CommandText = "\"" + columnName + "\"" }; + } + else + { + object obj = null; + if (mem.Expression != null) + { + var r = CompileExpr(mem.Expression, queryArgs); + if (r.Value == null) + { + throw new NotSupportedException("Member access failed to compile expression"); + } + if (r.CommandText == "?") + { + queryArgs.RemoveAt(queryArgs.Count - 1); + } + obj = r.Value; + } + + // + // Get the member value + // + object val = null; + + if (mem.Member is PropertyInfo) + { + var m = (PropertyInfo)mem.Member; + val = m.GetValue(obj, null); + } + else if (mem.Member is FieldInfo) + { + var m = (FieldInfo)mem.Member; + val = m.GetValue(obj); + } + else + { + throw new NotSupportedException("MemberExpr: " + mem.Member.GetType()); + } + + // + // Work special magic for enumerables + // + if (val != null && val is System.Collections.IEnumerable && !(val is string) && !(val is System.Collections.Generic.IEnumerable)) + { + var sb = new System.Text.StringBuilder(); + sb.Append("("); + var head = ""; + foreach (var a in (System.Collections.IEnumerable)val) + { + queryArgs.Add(a); + sb.Append(head); + sb.Append("?"); + head = ","; + } + sb.Append(")"); + return new CompileResult + { + CommandText = sb.ToString(), + Value = val + }; + } + else + { + queryArgs.Add(val); + return new CompileResult + { + CommandText = "?", + Value = val + }; + } + } + } + throw new NotSupportedException("Cannot compile: " + expr.NodeType.ToString()); + } + + static object ConvertTo(object obj, Type t) + { + Type nut = Nullable.GetUnderlyingType(t); + + if (nut != null) + { + if (obj == null) + return null; + return Convert.ChangeType(obj, nut); + } + else + { + return Convert.ChangeType(obj, t); + } + } + + /// + /// Compiles a BinaryExpression where one of the parameters is null. + /// + /// The expression to compile + /// The non-null parameter + private string CompileNullBinaryExpression(BinaryExpression expression, CompileResult parameter) + { + if (expression.NodeType == ExpressionType.Equal) + return "(" + parameter.CommandText + " is ?)"; + else if (expression.NodeType == ExpressionType.NotEqual) + return "(" + parameter.CommandText + " is not ?)"; + else if (expression.NodeType == ExpressionType.GreaterThan + || expression.NodeType == ExpressionType.GreaterThanOrEqual + || expression.NodeType == ExpressionType.LessThan + || expression.NodeType == ExpressionType.LessThanOrEqual) + return "(" + parameter.CommandText + " < ?)"; // always false + else + throw new NotSupportedException("Cannot compile Null-BinaryExpression with type " + expression.NodeType.ToString()); + } + + string GetSqlName(Expression expr) + { + var n = expr.NodeType; + if (n == ExpressionType.GreaterThan) + return ">"; + else if (n == ExpressionType.GreaterThanOrEqual) + { + return ">="; + } + else if (n == ExpressionType.LessThan) + { + return "<"; + } + else if (n == ExpressionType.LessThanOrEqual) + { + return "<="; + } + else if (n == ExpressionType.And) + { + return "&"; + } + else if (n == ExpressionType.AndAlso) + { + return "and"; + } + else if (n == ExpressionType.Or) + { + return "|"; + } + else if (n == ExpressionType.OrElse) + { + return "or"; + } + else if (n == ExpressionType.Equal) + { + return "="; + } + else if (n == ExpressionType.NotEqual) + { + return "!="; + } + else + { + throw new NotSupportedException("Cannot get SQL for: " + n); + } + } + + /// + /// Execute SELECT COUNT(*) on the query + /// + public int Count() + { + return GenerateCommand("count(*)").ExecuteScalar(); + } + + /// + /// Execute SELECT COUNT(*) on the query with an additional WHERE clause. + /// + public int Count(Expression> predExpr) + { + return Where(predExpr).Count(); + } + + public IEnumerator GetEnumerator() + { + if (!_deferred) + return GenerateCommand("*").ExecuteQuery().GetEnumerator(); + + return GenerateCommand("*").ExecuteDeferredQuery().GetEnumerator(); + } + + System.Collections.IEnumerator System.Collections.IEnumerable.GetEnumerator() + { + return GetEnumerator(); + } + + /// + /// Queries the database and returns the results as a List. + /// + public List ToList() + { + return GenerateCommand("*").ExecuteQuery(); + } + + /// + /// Queries the database and returns the results as an array. + /// + public T[] ToArray() + { + return GenerateCommand("*").ExecuteQuery().ToArray(); + } + + /// + /// Returns the first element of this query. + /// + public T First() + { + var query = Take(1); + return query.ToList().First(); + } + + /// + /// Returns the first element of this query, or null if no element is found. + /// + public T FirstOrDefault() + { + var query = Take(1); + return query.ToList().FirstOrDefault(); + } + + /// + /// Returns the first element of this query that matches the predicate. + /// + public T First(Expression> predExpr) + { + return Where(predExpr).First(); + } + + /// + /// Returns the first element of this query that matches the predicate, or null + /// if no element is found. + /// + public T FirstOrDefault(Expression> predExpr) + { + return Where(predExpr).FirstOrDefault(); + } + } + + public static class SQLite3 + { + public enum Result : int + { + OK = 0, + Error = 1, + Internal = 2, + Perm = 3, + Abort = 4, + Busy = 5, + Locked = 6, + NoMem = 7, + ReadOnly = 8, + Interrupt = 9, + IOError = 10, + Corrupt = 11, + NotFound = 12, + Full = 13, + CannotOpen = 14, + LockErr = 15, + Empty = 16, + SchemaChngd = 17, + TooBig = 18, + Constraint = 19, + Mismatch = 20, + Misuse = 21, + NotImplementedLFS = 22, + AccessDenied = 23, + Format = 24, + Range = 25, + NonDBFile = 26, + Notice = 27, + Warning = 28, + Row = 100, + Done = 101 + } + + public enum ExtendedResult : int + { + IOErrorRead = (Result.IOError | (1 << 8)), + IOErrorShortRead = (Result.IOError | (2 << 8)), + IOErrorWrite = (Result.IOError | (3 << 8)), + IOErrorFsync = (Result.IOError | (4 << 8)), + IOErrorDirFSync = (Result.IOError | (5 << 8)), + IOErrorTruncate = (Result.IOError | (6 << 8)), + IOErrorFStat = (Result.IOError | (7 << 8)), + IOErrorUnlock = (Result.IOError | (8 << 8)), + IOErrorRdlock = (Result.IOError | (9 << 8)), + IOErrorDelete = (Result.IOError | (10 << 8)), + IOErrorBlocked = (Result.IOError | (11 << 8)), + IOErrorNoMem = (Result.IOError | (12 << 8)), + IOErrorAccess = (Result.IOError | (13 << 8)), + IOErrorCheckReservedLock = (Result.IOError | (14 << 8)), + IOErrorLock = (Result.IOError | (15 << 8)), + IOErrorClose = (Result.IOError | (16 << 8)), + IOErrorDirClose = (Result.IOError | (17 << 8)), + IOErrorSHMOpen = (Result.IOError | (18 << 8)), + IOErrorSHMSize = (Result.IOError | (19 << 8)), + IOErrorSHMLock = (Result.IOError | (20 << 8)), + IOErrorSHMMap = (Result.IOError | (21 << 8)), + IOErrorSeek = (Result.IOError | (22 << 8)), + IOErrorDeleteNoEnt = (Result.IOError | (23 << 8)), + IOErrorMMap = (Result.IOError | (24 << 8)), + LockedSharedcache = (Result.Locked | (1 << 8)), + BusyRecovery = (Result.Busy | (1 << 8)), + CannottOpenNoTempDir = (Result.CannotOpen | (1 << 8)), + CannotOpenIsDir = (Result.CannotOpen | (2 << 8)), + CannotOpenFullPath = (Result.CannotOpen | (3 << 8)), + CorruptVTab = (Result.Corrupt | (1 << 8)), + ReadonlyRecovery = (Result.ReadOnly | (1 << 8)), + ReadonlyCannotLock = (Result.ReadOnly | (2 << 8)), + ReadonlyRollback = (Result.ReadOnly | (3 << 8)), + AbortRollback = (Result.Abort | (2 << 8)), + ConstraintCheck = (Result.Constraint | (1 << 8)), + ConstraintCommitHook = (Result.Constraint | (2 << 8)), + ConstraintForeignKey = (Result.Constraint | (3 << 8)), + ConstraintFunction = (Result.Constraint | (4 << 8)), + ConstraintNotNull = (Result.Constraint | (5 << 8)), + ConstraintPrimaryKey = (Result.Constraint | (6 << 8)), + ConstraintTrigger = (Result.Constraint | (7 << 8)), + ConstraintUnique = (Result.Constraint | (8 << 8)), + ConstraintVTab = (Result.Constraint | (9 << 8)), + NoticeRecoverWAL = (Result.Notice | (1 << 8)), + NoticeRecoverRollback = (Result.Notice | (2 << 8)) + } + + + public enum ConfigOption : int + { + SingleThread = 1, + MultiThread = 2, + Serialized = 3 + } + + const string LibraryPath = "sqlite3"; + +#if !USE_CSHARP_SQLITE && !USE_WP8_NATIVE_SQLITE && !USE_SQLITEPCL_RAW + [DllImport(LibraryPath, EntryPoint = "sqlite3_threadsafe", CallingConvention = CallingConvention.Cdecl)] + public static extern int Threadsafe(); + + [DllImport(LibraryPath, EntryPoint = "sqlite3_open", CallingConvention = CallingConvention.Cdecl)] + public static extern Result Open([MarshalAs(UnmanagedType.LPStr)] string filename, out IntPtr db); + + [DllImport(LibraryPath, EntryPoint = "sqlite3_open_v2", CallingConvention = CallingConvention.Cdecl)] + public static extern Result Open([MarshalAs(UnmanagedType.LPStr)] string filename, out IntPtr db, int flags, [MarshalAs(UnmanagedType.LPStr)] string zvfs); + + [DllImport(LibraryPath, EntryPoint = "sqlite3_open_v2", CallingConvention = CallingConvention.Cdecl)] + public static extern Result Open(byte[] filename, out IntPtr db, int flags, [MarshalAs(UnmanagedType.LPStr)] string zvfs); + + [DllImport(LibraryPath, EntryPoint = "sqlite3_open16", CallingConvention = CallingConvention.Cdecl)] + public static extern Result Open16([MarshalAs(UnmanagedType.LPWStr)] string filename, out IntPtr db); + + [DllImport(LibraryPath, EntryPoint = "sqlite3_enable_load_extension", CallingConvention = CallingConvention.Cdecl)] + public static extern Result EnableLoadExtension(IntPtr db, int onoff); + + [DllImport(LibraryPath, EntryPoint = "sqlite3_close", CallingConvention = CallingConvention.Cdecl)] + public static extern Result Close(IntPtr db); + + [DllImport(LibraryPath, EntryPoint = "sqlite3_close_v2", CallingConvention = CallingConvention.Cdecl)] + public static extern Result Close2(IntPtr db); + + [DllImport(LibraryPath, EntryPoint = "sqlite3_initialize", CallingConvention = CallingConvention.Cdecl)] + public static extern Result Initialize(); + + [DllImport(LibraryPath, EntryPoint = "sqlite3_shutdown", CallingConvention = CallingConvention.Cdecl)] + public static extern Result Shutdown(); + + [DllImport(LibraryPath, EntryPoint = "sqlite3_config", CallingConvention = CallingConvention.Cdecl)] + public static extern Result Config(ConfigOption option); + + [DllImport(LibraryPath, EntryPoint = "sqlite3_win32_set_directory", CallingConvention = CallingConvention.Cdecl, CharSet = CharSet.Unicode)] + public static extern int SetDirectory(uint directoryType, string directoryPath); + + [DllImport(LibraryPath, EntryPoint = "sqlite3_busy_timeout", CallingConvention = CallingConvention.Cdecl)] + public static extern Result BusyTimeout(IntPtr db, int milliseconds); + + [DllImport(LibraryPath, EntryPoint = "sqlite3_changes", CallingConvention = CallingConvention.Cdecl)] + public static extern int Changes(IntPtr db); + + [DllImport(LibraryPath, EntryPoint = "sqlite3_prepare_v2", CallingConvention = CallingConvention.Cdecl)] + public static extern Result Prepare2(IntPtr db, [MarshalAs(UnmanagedType.LPStr)] string sql, int numBytes, out IntPtr stmt, IntPtr pzTail); + +#if NETFX_CORE + [DllImport (LibraryPath, EntryPoint = "sqlite3_prepare_v2", CallingConvention = CallingConvention.Cdecl)] + public static extern Result Prepare2 (IntPtr db, byte[] queryBytes, int numBytes, out IntPtr stmt, IntPtr pzTail); +#endif + + public static IntPtr Prepare2(IntPtr db, string query) + { + IntPtr stmt; +#if NETFX_CORE + byte[] queryBytes = System.Text.UTF8Encoding.UTF8.GetBytes (query); + var r = Prepare2 (db, queryBytes, queryBytes.Length, out stmt, IntPtr.Zero); +#else + var r = Prepare2(db, query, System.Text.UTF8Encoding.UTF8.GetByteCount(query), out stmt, IntPtr.Zero); +#endif + if (r != Result.OK) + { + throw SQLiteException.New(r, GetErrmsg(db)); + } + return stmt; + } + + [DllImport(LibraryPath, EntryPoint = "sqlite3_step", CallingConvention = CallingConvention.Cdecl)] + public static extern Result Step(IntPtr stmt); + + [DllImport(LibraryPath, EntryPoint = "sqlite3_reset", CallingConvention = CallingConvention.Cdecl)] + public static extern Result Reset(IntPtr stmt); + + [DllImport(LibraryPath, EntryPoint = "sqlite3_finalize", CallingConvention = CallingConvention.Cdecl)] + public static extern Result Finalize(IntPtr stmt); + + [DllImport(LibraryPath, EntryPoint = "sqlite3_last_insert_rowid", CallingConvention = CallingConvention.Cdecl)] + public static extern long LastInsertRowid(IntPtr db); + + [DllImport(LibraryPath, EntryPoint = "sqlite3_errmsg16", CallingConvention = CallingConvention.Cdecl)] + public static extern IntPtr Errmsg(IntPtr db); + + public static string GetErrmsg(IntPtr db) + { + return Marshal.PtrToStringUni(Errmsg(db)); + } + + [DllImport(LibraryPath, EntryPoint = "sqlite3_bind_parameter_index", CallingConvention = CallingConvention.Cdecl)] + public static extern int BindParameterIndex(IntPtr stmt, [MarshalAs(UnmanagedType.LPStr)] string name); + + [DllImport(LibraryPath, EntryPoint = "sqlite3_bind_null", CallingConvention = CallingConvention.Cdecl)] + public static extern int BindNull(IntPtr stmt, int index); + + [DllImport(LibraryPath, EntryPoint = "sqlite3_bind_int", CallingConvention = CallingConvention.Cdecl)] + public static extern int BindInt(IntPtr stmt, int index, int val); + + [DllImport(LibraryPath, EntryPoint = "sqlite3_bind_int64", CallingConvention = CallingConvention.Cdecl)] + public static extern int BindInt64(IntPtr stmt, int index, long val); + + [DllImport(LibraryPath, EntryPoint = "sqlite3_bind_double", CallingConvention = CallingConvention.Cdecl)] + public static extern int BindDouble(IntPtr stmt, int index, double val); + + [DllImport(LibraryPath, EntryPoint = "sqlite3_bind_text16", CallingConvention = CallingConvention.Cdecl, CharSet = CharSet.Unicode)] + public static extern int BindText(IntPtr stmt, int index, [MarshalAs(UnmanagedType.LPWStr)] string val, int n, IntPtr free); + + [DllImport(LibraryPath, EntryPoint = "sqlite3_bind_blob", CallingConvention = CallingConvention.Cdecl)] + public static extern int BindBlob(IntPtr stmt, int index, byte[] val, int n, IntPtr free); + + [DllImport(LibraryPath, EntryPoint = "sqlite3_column_count", CallingConvention = CallingConvention.Cdecl)] + public static extern int ColumnCount(IntPtr stmt); + + [DllImport(LibraryPath, EntryPoint = "sqlite3_column_name", CallingConvention = CallingConvention.Cdecl)] + public static extern IntPtr ColumnName(IntPtr stmt, int index); + + [DllImport(LibraryPath, EntryPoint = "sqlite3_column_name16", CallingConvention = CallingConvention.Cdecl)] + static extern IntPtr ColumnName16Internal(IntPtr stmt, int index); + public static string ColumnName16(IntPtr stmt, int index) + { + return Marshal.PtrToStringUni(ColumnName16Internal(stmt, index)); + } + + [DllImport(LibraryPath, EntryPoint = "sqlite3_column_type", CallingConvention = CallingConvention.Cdecl)] + public static extern ColType ColumnType(IntPtr stmt, int index); + + [DllImport(LibraryPath, EntryPoint = "sqlite3_column_int", CallingConvention = CallingConvention.Cdecl)] + public static extern int ColumnInt(IntPtr stmt, int index); + + [DllImport(LibraryPath, EntryPoint = "sqlite3_column_int64", CallingConvention = CallingConvention.Cdecl)] + public static extern long ColumnInt64(IntPtr stmt, int index); + + [DllImport(LibraryPath, EntryPoint = "sqlite3_column_double", CallingConvention = CallingConvention.Cdecl)] + public static extern double ColumnDouble(IntPtr stmt, int index); + + [DllImport(LibraryPath, EntryPoint = "sqlite3_column_text", CallingConvention = CallingConvention.Cdecl)] + public static extern IntPtr ColumnText(IntPtr stmt, int index); + + [DllImport(LibraryPath, EntryPoint = "sqlite3_column_text16", CallingConvention = CallingConvention.Cdecl)] + public static extern IntPtr ColumnText16(IntPtr stmt, int index); + + [DllImport(LibraryPath, EntryPoint = "sqlite3_column_blob", CallingConvention = CallingConvention.Cdecl)] + public static extern IntPtr ColumnBlob(IntPtr stmt, int index); + + [DllImport(LibraryPath, EntryPoint = "sqlite3_column_bytes", CallingConvention = CallingConvention.Cdecl)] + public static extern int ColumnBytes(IntPtr stmt, int index); + + public static string ColumnString(IntPtr stmt, int index) + { + return Marshal.PtrToStringUni(SQLite3.ColumnText16(stmt, index)); + } + + public static byte[] ColumnByteArray(IntPtr stmt, int index) + { + int length = ColumnBytes(stmt, index); + var result = new byte[length]; + if (length > 0) + Marshal.Copy(ColumnBlob(stmt, index), result, 0, length); + return result; + } + + [DllImport(LibraryPath, EntryPoint = "sqlite3_errcode", CallingConvention = CallingConvention.Cdecl)] + public static extern Result GetResult(Sqlite3DatabaseHandle db); + + [DllImport(LibraryPath, EntryPoint = "sqlite3_extended_errcode", CallingConvention = CallingConvention.Cdecl)] + public static extern ExtendedResult ExtendedErrCode(IntPtr db); + + [DllImport(LibraryPath, EntryPoint = "sqlite3_libversion_number", CallingConvention = CallingConvention.Cdecl)] + public static extern int LibVersionNumber(); + + [DllImport(LibraryPath, EntryPoint = "sqlite3_backup_init", CallingConvention = CallingConvention.Cdecl)] + public static extern Sqlite3BackupHandle BackupInit(Sqlite3DatabaseHandle destDb, [MarshalAs(UnmanagedType.LPStr)] string destName, Sqlite3DatabaseHandle sourceDb, [MarshalAs(UnmanagedType.LPStr)] string sourceName); + + [DllImport(LibraryPath, EntryPoint = "sqlite3_backup_step", CallingConvention = CallingConvention.Cdecl)] + public static extern Result BackupStep(Sqlite3BackupHandle backup, int numPages); + + [DllImport(LibraryPath, EntryPoint = "sqlite3_backup_finish", CallingConvention = CallingConvention.Cdecl)] + public static extern Result BackupFinish(Sqlite3BackupHandle backup); +#else + public static Result Open (string filename, out Sqlite3DatabaseHandle db) + { + return (Result)Sqlite3.sqlite3_open (filename, out db); + } + + public static Result Open (string filename, out Sqlite3DatabaseHandle db, int flags, string vfsName) + { +#if USE_WP8_NATIVE_SQLITE + return (Result)Sqlite3.sqlite3_open_v2(filename, out db, flags, vfsName ?? ""); +#else + return (Result)Sqlite3.sqlite3_open_v2 (filename, out db, flags, vfsName); +#endif + } + + public static Result Close (Sqlite3DatabaseHandle db) + { + return (Result)Sqlite3.sqlite3_close (db); + } + + public static Result Close2 (Sqlite3DatabaseHandle db) + { + return (Result)Sqlite3.sqlite3_close_v2 (db); + } + + public static Result BusyTimeout (Sqlite3DatabaseHandle db, int milliseconds) + { + return (Result)Sqlite3.sqlite3_busy_timeout (db, milliseconds); + } + + public static int Changes (Sqlite3DatabaseHandle db) + { + return Sqlite3.sqlite3_changes (db); + } + + public static Sqlite3Statement Prepare2 (Sqlite3DatabaseHandle db, string query) + { + Sqlite3Statement stmt = default (Sqlite3Statement); +#if USE_WP8_NATIVE_SQLITE || USE_SQLITEPCL_RAW + var r = Sqlite3.sqlite3_prepare_v2 (db, query, out stmt); +#else + stmt = new Sqlite3Statement(); + var r = Sqlite3.sqlite3_prepare_v2(db, query, -1, ref stmt, 0); +#endif + if (r != 0) { + throw SQLiteException.New ((Result)r, GetErrmsg (db)); + } + return stmt; + } + + public static Result Step (Sqlite3Statement stmt) + { + return (Result)Sqlite3.sqlite3_step (stmt); + } + + public static Result Reset (Sqlite3Statement stmt) + { + return (Result)Sqlite3.sqlite3_reset (stmt); + } + + public static Result Finalize (Sqlite3Statement stmt) + { + return (Result)Sqlite3.sqlite3_finalize (stmt); + } + + public static long LastInsertRowid (Sqlite3DatabaseHandle db) + { + return Sqlite3.sqlite3_last_insert_rowid (db); + } + + public static string GetErrmsg (Sqlite3DatabaseHandle db) + { + return Sqlite3.sqlite3_errmsg (db).utf8_to_string (); + } + + public static int BindParameterIndex (Sqlite3Statement stmt, string name) + { + return Sqlite3.sqlite3_bind_parameter_index (stmt, name); + } + + public static int BindNull (Sqlite3Statement stmt, int index) + { + return Sqlite3.sqlite3_bind_null (stmt, index); + } + + public static int BindInt (Sqlite3Statement stmt, int index, int val) + { + return Sqlite3.sqlite3_bind_int (stmt, index, val); + } + + public static int BindInt64 (Sqlite3Statement stmt, int index, long val) + { + return Sqlite3.sqlite3_bind_int64 (stmt, index, val); + } + + public static int BindDouble (Sqlite3Statement stmt, int index, double val) + { + return Sqlite3.sqlite3_bind_double (stmt, index, val); + } + + public static int BindText (Sqlite3Statement stmt, int index, string val, int n, IntPtr free) + { +#if USE_WP8_NATIVE_SQLITE + return Sqlite3.sqlite3_bind_text(stmt, index, val, n); +#elif USE_SQLITEPCL_RAW + return Sqlite3.sqlite3_bind_text (stmt, index, val); +#else + return Sqlite3.sqlite3_bind_text(stmt, index, val, n, null); +#endif + } + + public static int BindBlob (Sqlite3Statement stmt, int index, byte[] val, int n, IntPtr free) + { +#if USE_WP8_NATIVE_SQLITE + return Sqlite3.sqlite3_bind_blob(stmt, index, val, n); +#elif USE_SQLITEPCL_RAW + return Sqlite3.sqlite3_bind_blob (stmt, index, val); +#else + return Sqlite3.sqlite3_bind_blob(stmt, index, val, n, null); +#endif + } + + public static int ColumnCount (Sqlite3Statement stmt) + { + return Sqlite3.sqlite3_column_count (stmt); + } + + public static string ColumnName (Sqlite3Statement stmt, int index) + { + return Sqlite3.sqlite3_column_name (stmt, index).utf8_to_string (); + } + + public static string ColumnName16 (Sqlite3Statement stmt, int index) + { + return Sqlite3.sqlite3_column_name (stmt, index).utf8_to_string (); + } + + public static ColType ColumnType (Sqlite3Statement stmt, int index) + { + return (ColType)Sqlite3.sqlite3_column_type (stmt, index); + } + + public static int ColumnInt (Sqlite3Statement stmt, int index) + { + return Sqlite3.sqlite3_column_int (stmt, index); + } + + public static long ColumnInt64 (Sqlite3Statement stmt, int index) + { + return Sqlite3.sqlite3_column_int64 (stmt, index); + } + + public static double ColumnDouble (Sqlite3Statement stmt, int index) + { + return Sqlite3.sqlite3_column_double (stmt, index); + } + + public static string ColumnText (Sqlite3Statement stmt, int index) + { + return Sqlite3.sqlite3_column_text (stmt, index).utf8_to_string (); + } + + public static string ColumnText16 (Sqlite3Statement stmt, int index) + { + return Sqlite3.sqlite3_column_text (stmt, index).utf8_to_string (); + } + + public static byte[] ColumnBlob (Sqlite3Statement stmt, int index) + { + return Sqlite3.sqlite3_column_blob (stmt, index).ToArray (); + } + + public static int ColumnBytes (Sqlite3Statement stmt, int index) + { + return Sqlite3.sqlite3_column_bytes (stmt, index); + } + + public static string ColumnString (Sqlite3Statement stmt, int index) + { + return Sqlite3.sqlite3_column_text (stmt, index).utf8_to_string (); + } + + public static byte[] ColumnByteArray (Sqlite3Statement stmt, int index) + { + int length = ColumnBytes (stmt, index); + if (length > 0) { + return ColumnBlob (stmt, index); + } + return new byte[0]; + } + + public static Result EnableLoadExtension (Sqlite3DatabaseHandle db, int onoff) + { + return (Result)Sqlite3.sqlite3_enable_load_extension (db, onoff); + } + + public static int LibVersionNumber () + { + return Sqlite3.sqlite3_libversion_number (); + } + + public static Result GetResult (Sqlite3DatabaseHandle db) + { + return (Result)Sqlite3.sqlite3_errcode (db); + } + + public static ExtendedResult ExtendedErrCode (Sqlite3DatabaseHandle db) + { + return (ExtendedResult)Sqlite3.sqlite3_extended_errcode (db); + } + + public static Sqlite3BackupHandle BackupInit (Sqlite3DatabaseHandle destDb, string destName, Sqlite3DatabaseHandle sourceDb, string sourceName) + { + return Sqlite3.sqlite3_backup_init (destDb, destName, sourceDb, sourceName); + } + + public static Result BackupStep (Sqlite3BackupHandle backup, int numPages) + { + return (Result)Sqlite3.sqlite3_backup_step (backup, numPages); + } + + public static Result BackupFinish (Sqlite3BackupHandle backup) + { + return (Result)Sqlite3.sqlite3_backup_finish (backup); + } +#endif + + public enum ColType : int + { + Integer = 1, + Float = 2, + Text = 3, + Blob = 4, + Null = 5 + } + } +} diff --git a/Assets/Scripts/Infrastructure/EQ/SQLite/SQLite.cs.meta b/Assets/Scripts/Infrastructure/EQ/SQLite/SQLite.cs.meta new file mode 100644 index 0000000..37e60f4 --- /dev/null +++ b/Assets/Scripts/Infrastructure/EQ/SQLite/SQLite.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 61becd27b6066874b88d9cf9f8d87e18 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Scripts/Infrastructure/EQ/SQLite/SQLiteExt.cs b/Assets/Scripts/Infrastructure/EQ/SQLite/SQLiteExt.cs new file mode 100644 index 0000000..2078a6b --- /dev/null +++ b/Assets/Scripts/Infrastructure/EQ/SQLite/SQLiteExt.cs @@ -0,0 +1,14 @@ +namespace Infrastructure.Lantern.SQLite +{ + public static class SqlMethods + { + // Stub to allow sqlite-net `like` queries when using linq syntax. + // https://github.com/guillaume86/data-linq/blob/master/Mindbox.Data.Linq/SqlClient/SqlMethods.cs#L567 + // Usage: + // Table().Where(x => SQLiteExt.SqlMethods.Like(x.Name, name)) + public static bool Like(string matchExpression, string pattern) + { + throw new System.NotImplementedException(); + } + } +} diff --git a/Assets/Scripts/Infrastructure/EQ/SQLite/SQLiteExt.cs.meta b/Assets/Scripts/Infrastructure/EQ/SQLite/SQLiteExt.cs.meta new file mode 100644 index 0000000..6591898 --- /dev/null +++ b/Assets/Scripts/Infrastructure/EQ/SQLite/SQLiteExt.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 30fc8127ef7e76c44bd594fecb97cd82 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Scripts/Infrastructure/EQ/SavWav.cs b/Assets/Scripts/Infrastructure/EQ/SavWav.cs new file mode 100644 index 0000000..4deae3f --- /dev/null +++ b/Assets/Scripts/Infrastructure/EQ/SavWav.cs @@ -0,0 +1,165 @@ +// Copyright (c) 2012 Calvin Rien +// http://the.darktable.com +// +// This software is provided 'as-is', without any express or implied warranty. In +// no event will the authors be held liable for any damages arising from the use +// of this software. +// +// Permission is granted to anyone to use this software for any purpose, +// including commercial applications, and to alter it and redistribute it freely, +// subject to the following restrictions: +// +// 1. The origin of this software must not be misrepresented; you must not claim +// that you wrote the original software. If you use this software in a product, +// an acknowledgment in the product documentation would be appreciated but is not +// required. +// +// 2. Altered source versions must be plainly marked as such, and must not be +// misrepresented as being the original software. +// +// 3. This notice may not be removed or altered from any source distribution. +// +// ============================================================================= +// +// derived from Gregorio Zanon's script +// http://forum.unity3d.com/threads/119295-Writing-AudioListener.GetOutputData-to-wav-problem?p=806734&viewfull=1#post806734 + +// https://gist.github.com/Vanlalhriata/dc5b703cca6f2e885557e1c2f54c376a +// Forked by Vanlalhriata (github.com/vanlalhriata). Changes: +// - Fix incomplete writes for multichannel audio +// - Change Save to use filepath +// - Handle errors in Save +// - Remove TrimSilence +// - Minor refactors + +using System; +using System.IO; +using UnityEngine; + +namespace Infrastructure.Lantern +{ + + public static class SavWav + { + + const int HEADER_SIZE = 44; + + public static bool Save(string filepath, AudioClip clip) + { + try + { + // Make sure directory exists if user is saving to sub dir. + Directory.CreateDirectory(Path.GetDirectoryName(filepath)); + + using (var fileStream = CreateEmpty(filepath)) + { + + ConvertAndWrite(fileStream, clip); + + WriteHeader(fileStream, clip); + } + + return true; + } + catch (Exception exception) + { + Debug.LogException(exception); + return false; + } + } + + static FileStream CreateEmpty(string filepath) + { + var fileStream = new FileStream(filepath, FileMode.Create); + byte emptyByte = new byte(); + + for (int i = 0; i < HEADER_SIZE; i++) //preparing the header + { + fileStream.WriteByte(emptyByte); + } + + return fileStream; + } + + static void ConvertAndWrite(FileStream fileStream, AudioClip clip) + { + + var samples = new float[clip.samples * clip.channels]; + + clip.GetData(samples, 0); + + Int16[] intData = new Int16[samples.Length]; + //converting in 2 float[] steps to Int16[], //then Int16[] to Byte[] + + Byte[] bytesData = new Byte[samples.Length * 2]; + //bytesData array is twice the size of + //dataSource array because a float converted in Int16 is 2 bytes. + + int rescaleFactor = 32767; //to convert float to Int16 + + for (int i = 0; i < samples.Length; i++) + { + intData[i] = (short)(samples[i] * rescaleFactor); + var byteArr = BitConverter.GetBytes(intData[i]); + byteArr.CopyTo(bytesData, i * 2); + } + + fileStream.Write(bytesData, 0, bytesData.Length); + } + + static void WriteHeader(FileStream fileStream, AudioClip clip) + { + + var hz = clip.frequency; + var channels = clip.channels; + var samples = clip.samples; + + fileStream.Seek(0, SeekOrigin.Begin); + + Byte[] riff = System.Text.Encoding.UTF8.GetBytes("RIFF"); + fileStream.Write(riff, 0, 4); + + Byte[] chunkSize = BitConverter.GetBytes(fileStream.Length - 8); + fileStream.Write(chunkSize, 0, 4); + + Byte[] wave = System.Text.Encoding.UTF8.GetBytes("WAVE"); + fileStream.Write(wave, 0, 4); + + Byte[] fmt = System.Text.Encoding.UTF8.GetBytes("fmt "); + fileStream.Write(fmt, 0, 4); + + Byte[] subChunk1 = BitConverter.GetBytes(16); + fileStream.Write(subChunk1, 0, 4); + + //UInt16 two = 2; + UInt16 one = 1; + + Byte[] audioFormat = BitConverter.GetBytes(one); + fileStream.Write(audioFormat, 0, 2); + + Byte[] numChannels = BitConverter.GetBytes(channels); + fileStream.Write(numChannels, 0, 2); + + Byte[] sampleRate = BitConverter.GetBytes(hz); + fileStream.Write(sampleRate, 0, 4); + + Byte[] byteRate = BitConverter.GetBytes(hz * channels * 2); // sampleRate * bytesPerSample*number of channels, here 44100*2*2 + fileStream.Write(byteRate, 0, 4); + + UInt16 blockAlign = (ushort)(channels * 2); + fileStream.Write(BitConverter.GetBytes(blockAlign), 0, 2); + + UInt16 bps = 16; + Byte[] bitsPerSample = BitConverter.GetBytes(bps); + fileStream.Write(bitsPerSample, 0, 2); + + Byte[] datastring = System.Text.Encoding.UTF8.GetBytes("data"); + fileStream.Write(datastring, 0, 4); + + Byte[] subChunk2 = BitConverter.GetBytes(samples * channels * 2); + fileStream.Write(subChunk2, 0, 4); + + // fileStream.Close(); + } + } +} diff --git a/Assets/Scripts/Infrastructure/EQ/SavWav.cs.meta b/Assets/Scripts/Infrastructure/EQ/SavWav.cs.meta new file mode 100644 index 0000000..af69b13 --- /dev/null +++ b/Assets/Scripts/Infrastructure/EQ/SavWav.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 9927e0523e1c9b84fbeb9b0d619e0c67 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Scripts/Infrastructure/EQ/TextParser/TextParser.cs b/Assets/Scripts/Infrastructure/EQ/TextParser/TextParser.cs index 59fd5bb..32b08b9 100644 --- a/Assets/Scripts/Infrastructure/EQ/TextParser/TextParser.cs +++ b/Assets/Scripts/Infrastructure/EQ/TextParser/TextParser.cs @@ -16,7 +16,7 @@ public static class TextParser /// The text file to be parsed /// The character that denotes a comment line /// A list of parsed lines - public static List ParseTextByNewline(string text, char commentChar = '#') + public static List ParseTextByNewline(string text, bool removeComments = true, char commentChar = '#') { if (string.IsNullOrEmpty(text)) { @@ -28,8 +28,13 @@ public static List ParseTextByNewline(string text, char commentChar = '# StringSplitOptions.None ); - return textLines.Where(line => !string.IsNullOrEmpty(line)) - .Where(line => !line.StartsWith(commentChar.ToString())).ToList(); + if (removeComments) + { + return textLines.Where(line => !string.IsNullOrEmpty(line)) + .Where(line => !line.StartsWith(commentChar.ToString())).ToList(); + } + + return textLines.ToList(); } /// @@ -71,7 +76,7 @@ public static List> ParseTextByDelimitedLines(string text, char del return null; } - List parsedLines = ParseTextByNewline(text, commentChar); + List parsedLines = ParseTextByNewline(text, true, commentChar); var parsedOutput = new List>(); @@ -111,7 +116,7 @@ public static Dictionary ParseTextToDictionary(string text, char return null; } - List parsedLines = ParseTextByNewline(text, commentChar); + List parsedLines = ParseTextByNewline(text, false); var parsedOutput = new Dictionary(); @@ -135,7 +140,33 @@ public static Dictionary ParseTextToDictionary(string text, char return parsedOutput; } - public static List ParseStringToList(string text) + public static Dictionary> ParseTextToDictionaryOfStringList(string text) + { + var dictionary = new Dictionary>(); + if (string.IsNullOrEmpty(text)) + { + return dictionary; + } + + var lines = TextParser.ParseTextByDelimitedLines(text, ','); + + for (int i = 0; i < lines.Count; i++) + { + var mtl = lines[i]; + if (mtl == null || mtl.Count == 0) + { + continue; + } + + string first = mtl[0]; + mtl.RemoveAt(0); + dictionary[first] = new List(mtl); + } + + return dictionary; + } + + public static List ParseTextToList(string text) { List returnList = new List(); diff --git a/Assets/Scripts/Lantern/EQ/Animation/AnimationHelper.cs b/Assets/Scripts/Lantern/EQ/Animation/AnimationHelper.cs index a2629f1..6f9d7b7 100644 --- a/Assets/Scripts/Lantern/EQ/Animation/AnimationHelper.cs +++ b/Assets/Scripts/Lantern/EQ/Animation/AnimationHelper.cs @@ -1,5 +1,7 @@ -using System.Collections.Generic; +using System; +using System.Collections.Generic; using Lantern.EQ.Data; +using Lantern.EQ.Sound; using UnityEngine; namespace Lantern.EQ.Animation @@ -331,5 +333,202 @@ public static string GetDebugName(AnimationType animationType) return animationType + $" ({animName})"; } + + public static bool IsAttackAnimation(AnimationType animationType) + { + switch(animationType) + { + case AnimationType.CombatPiercing: + case AnimationType.Combat2HSlash: + case AnimationType.Combat2HBlunt: + case AnimationType.Combat1HSlash: + case AnimationType.Combat1HSlashOffhand: + case AnimationType.CombatBash: + case AnimationType.CombatHandToHand: + case AnimationType.CombatSwimAttack: + return true; + } + + return false; + } + + public static CharacterSoundType GetSoundFromType(AnimationType animationType) + { + switch (animationType) + { + case AnimationType.CombatKick: // 1 + return CharacterSoundType.Kick; + case AnimationType.CombatPiercing: // 2 + // TODO: checks model override + return CharacterSoundType.Pierce; + case AnimationType.Combat2HSlash: // 3 + // TODO: checks model override + return CharacterSoundType.TwoHandSlash; + case AnimationType.Combat2HBlunt: // 4 + return CharacterSoundType.TwoHandBlunt; + case AnimationType.Combat1HSlash: // TODO: + // TODO: checks model override + return CharacterSoundType.Attack; + case AnimationType.Combat1HSlashOffhand: // TODO: + // TODO: checks model override + break; + case AnimationType.CombatBash: // 7 + // TODO: checks model override + return CharacterSoundType.Bash; + case AnimationType.CombatHandToHand: // TODO: + // TODO: checks model override + return CharacterSoundType.Attack; + case AnimationType.CombatArchery: // 9 + return CharacterSoundType.Archery; + case AnimationType.CombatSwimAttack: // TODO: + // TODO: checks model override + break; + case AnimationType.CombatRoundKick: // 11 + return CharacterSoundType.RoundKick; + case AnimationType.Damage1: + case AnimationType.Damage2: + return CharacterSoundType.GetHit; + case AnimationType.DamageTrap: // TODO: + break; + case AnimationType.DamageDrowningBurning: // 15 + return CharacterSoundType.Drown; + case AnimationType.DamageDeath: // 16 + return CharacterSoundType.Death; + case AnimationType.LocomotionWalk: // 17 + case AnimationType.LocomotionWalkReverse: + return CharacterSoundType.Walking; + case AnimationType.LocomotionRun: // 18 + case AnimationType.LocomotionRunReverse: + return CharacterSoundType.Running; + case AnimationType.LocomotionJumpRun: // 19 + case AnimationType.LocomotionJumpStand: // 20 + return CharacterSoundType.Jump; + case AnimationType.LocomotionFall: // 21 + break; + case AnimationType.LocomotionDuckWalk: // 22 + return CharacterSoundType.Crouch; + case AnimationType.LocomotionClimb: // 23 + return CharacterSoundType.Climb; + case AnimationType.Duck: // 24 + case AnimationType.DuckReverse: + break; + case AnimationType.LocomotionSwimTread: // 25 + return CharacterSoundType.Treading; + case AnimationType.IdleStand: // 26 + return CharacterSoundType.Idle; + case AnimationType.SocialCheer: + break; + case AnimationType.SocialMourn: + break; + case AnimationType.SocialWave: + break; + case AnimationType.SocialRude: + break; + case AnimationType.SocialYawn: + break; + case AnimationType.PassiveStand: + break; + case AnimationType.PassiveSitStand: // 33 + case AnimationType.PassiveStandSit: + return CharacterSoundType.Sit; + case AnimationType.PassiveRotating: + case AnimationType.PassiveRotatingReverse: + break; + case AnimationType.Loot: // 36 + case AnimationType.LootReverse: + return CharacterSoundType.Kneel; + case AnimationType.LocomotionSwimMove: // 37 + return CharacterSoundType.Swim; + case AnimationType.PassiveSitting: + break; + case AnimationType.InstrumentDrum: + break; + case AnimationType.InstrumentString: + break; + case AnimationType.InstrumentWind: + break; + case AnimationType.SpellCastDefense: // 42 + case AnimationType.SpellCastGeneral: // 43 + case AnimationType.SpellCastMissile: // 44 + return CharacterSoundType.TAttack; + case AnimationType.CombatFlyingKick: // 45 + return CharacterSoundType.FlyingKick; + case AnimationType.CombatRapidPunch: // 46 + return CharacterSoundType.RapidPunch; + case AnimationType.CombatHeavyPunch: // 47 + return CharacterSoundType.LargePunch; + case AnimationType.SocialNod: + break; + case AnimationType.SocialAmazed: + break; + case AnimationType.SocialPlead: + break; + case AnimationType.SocialClap: + break; + case AnimationType.SocialDistress: + break; + case AnimationType.SocialBlush: + break; + case AnimationType.SocialChuckle: + break; + case AnimationType.SocialBurp: + break; + case AnimationType.SocialDuck: + break; + case AnimationType.SocialLookAround: + break; + case AnimationType.SocialDance: + break; + case AnimationType.SocialBlink: + break; + case AnimationType.SocialGlare: + break; + case AnimationType.SocialDrool: + break; + case AnimationType.SocialKneel: + break; + case AnimationType.SocialLaugh: + break; + case AnimationType.SocialPoint: + break; + case AnimationType.SocialPonder: + break; + case AnimationType.SocialReady: + break; + case AnimationType.SocialSalute: + break; + case AnimationType.SocialShiver: + break; + case AnimationType.SocialTapFoot: + break; + case AnimationType.SocialBow: + break; + case AnimationType.PassiveStandArmsAtSides: + break; + case AnimationType.IdleStandArmsAtSides: + break; + case AnimationType.IdleSit: + break; + } + + return CharacterSoundType.None; + } + + public static bool IsWalkRunInterrupt(AnimationType animationType) + { + switch (animationType) + { + case AnimationType.PassiveStandSit: + case AnimationType.PassiveSitStand: + case AnimationType.IdleSit: + case AnimationType.IdleStand: + case AnimationType.PassiveStandArmsAtSides: + case AnimationType.PassiveStand: + case AnimationType.PassiveSitting: + return true; + default: + return false; + } + } } } diff --git a/Assets/Scripts/Lantern/EQ/Animation/CharacterAnimationController.cs b/Assets/Scripts/Lantern/EQ/Animation/CharacterAnimationController.cs index 3f75fc4..e1d8097 100644 --- a/Assets/Scripts/Lantern/EQ/Animation/CharacterAnimationController.cs +++ b/Assets/Scripts/Lantern/EQ/Animation/CharacterAnimationController.cs @@ -37,6 +37,7 @@ public class CharacterAnimationController : MonoBehaviour private float _oneShotSpeed; private Action _animationDebugCallback; + private Action _animationFiredCallback; public void Initialize(AnimationType initialAnimation) { @@ -51,6 +52,11 @@ public void SetAnimationDebug(Action callback) _animationDebugCallback = callback; } + public void SetAnimationFiredCallback(Action callback) + { + _animationFiredCallback = callback; + } + public void SetNewConstantStateIfNot(AnimationType animationType, AnimationType ifNotAnimation, int priority, float speed = 1f) { @@ -122,6 +128,7 @@ public void SetNewConstantState(AnimationType animationType, int priority, float } _currentConstantState = animationType; + _animationFiredCallback?.Invoke(animationType); } private void StopOneShot() @@ -273,6 +280,7 @@ public void PlayOneShotAnimation(AnimationType animationType, float speed = 1f, _resolveOneShot = StartCoroutine(ReturnToDefault(returnTime)); _animationDebugCallback?.Invoke("Returning to constant state in : " + returnTime); + _animationFiredCallback?.Invoke(animationType); } private void AbortReturnToConstantState() diff --git a/Assets/Scripts/Lantern/EQ/Animation/CharacterAnimationLogic.cs b/Assets/Scripts/Lantern/EQ/Animation/CharacterAnimationLogic.cs index 6a17685..f4bb397 100644 --- a/Assets/Scripts/Lantern/EQ/Animation/CharacterAnimationLogic.cs +++ b/Assets/Scripts/Lantern/EQ/Animation/CharacterAnimationLogic.cs @@ -41,6 +41,11 @@ public void Initialize(CharacterAnimationState state) _isInitialized = true; } + public void SetAnimationFiredCallback(Action callback) + { + _animationController.SetAnimationFiredCallback(callback); + } + #if UNITY_EDITOR public void InitializeImport() { @@ -321,6 +326,7 @@ private void Update() if (_state.IsInCombat) { + ResetIdleTimer(false); return; } @@ -429,14 +435,7 @@ public bool GetIsPlaying() public void SetAnimationDebug(Action callback) { _animationDebugCallback = callback; - _animationController.SetAnimationDebug(_animationDebugCallback); - } - - // TODO: Deprecate this. Should all go through something else. - public void PlayOneShotAnimation(AnimationType animationType, float speed = 1f, int importance = 0, AnimationType? newConstantState = null) - { - - _animationController.PlayOneShotAnimation(animationType, speed, importance, true, newConstantState); + _animationController.SetAnimationDebug(_animationDebugCallback); } } } diff --git a/Assets/Scripts/Lantern/EQ/Animation/EquipmentAnimation.cs b/Assets/Scripts/Lantern/EQ/Animation/EquipmentAnimation.cs new file mode 100644 index 0000000..73e765d --- /dev/null +++ b/Assets/Scripts/Lantern/EQ/Animation/EquipmentAnimation.cs @@ -0,0 +1,82 @@ +using UnityEngine; + +namespace Lantern.EQ.Animation +{ + public class EquipmentAnimation : MonoBehaviour + { + [SerializeField] + private UnityEngine.Animation _animation; + [SerializeField] + private AnimationClipMapping _clips; + + public void Play(AnimationType animationType) + { + if (_animation == null || _clips == null) + { + return; + } + + if (!_clips.TryGetValue(animationType, out var clipName)) + { + return; + } + + if (_animation[clipName] != null) + { + _animation.CrossFade(clipName); + if (_animation.clip != null) + { + _animation.CrossFadeQueued(_animation.clip.name); + } + } + } + +#if UNITY_EDITOR + public void InitializeImport() + { + if (!TryGetComponent(out _animation)) + { + return; + } + + BuildClipList(); + } + + private void BuildClipList() + { + _clips = new AnimationClipMapping(); + + foreach (AnimationState animationClip in _animation) + { + var animationSuffix = animationClip.name.Split('_')[1]; + AnimationType? animationType = AnimationHelper.GetAnimationType(animationSuffix); + + // use pos as the default animation. + // it198 for example animates independent of the character's animation + if (animationSuffix == "pos") + { + _animation.clip = animationClip.clip; + } + + if (!animationType.HasValue || _clips.ContainsKey(animationType.Value)) + { + continue; + } + + _clips[animationType.Value] = animationClip.name; + } + + // does equipment need fallbacks? + /* + foreach (var animationFallback in AnimationHelper.AnimationFallbacks) + { + if (!_animationClips.ContainsKey(animationFallback.Key) && _animationClips.ContainsKey(animationFallback.Value)) + { + _animationClips[animationFallback.Key] = _animationClips[animationFallback.Value]; + } + } + */ + } +#endif + } +} diff --git a/Assets/Scripts/Lantern/EQ/Animation/EquipmentAnimation.cs.meta b/Assets/Scripts/Lantern/EQ/Animation/EquipmentAnimation.cs.meta new file mode 100644 index 0000000..53e353c --- /dev/null +++ b/Assets/Scripts/Lantern/EQ/Animation/EquipmentAnimation.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 71ab627a43d8a5b4e8762a87c8419904 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Scripts/Lantern/EQ/AssetBundles/AssetBundleVersions.cs b/Assets/Scripts/Lantern/EQ/AssetBundles/AssetBundleVersions.cs index 2788a05..109cac0 100644 --- a/Assets/Scripts/Lantern/EQ/AssetBundles/AssetBundleVersions.cs +++ b/Assets/Scripts/Lantern/EQ/AssetBundles/AssetBundleVersions.cs @@ -13,25 +13,26 @@ public static class AssetBundleVersions public static Dictionary Versions = new Dictionary() { - {LanternAssetBundleId.Characters, new Version(0, 1, 6)}, - {LanternAssetBundleId.Construct, new Version(0, 1, 6)}, - {LanternAssetBundleId.UI_Lantern, new Version(0, 1, 6)}, - {LanternAssetBundleId.ClientData, new Version(0, 1, 6)}, - {LanternAssetBundleId.Zones, new Version(0, 1, 5)}, - {LanternAssetBundleId.Equipment, new Version(0, 1, 5)}, - {LanternAssetBundleId.Sprites, new Version(0, 1, 5)}, - {LanternAssetBundleId.Startup, new Version(0, 1, 5)}, - {LanternAssetBundleId.Sky, new Version(0, 1, 5)}, - {LanternAssetBundleId.CharacterSelect_Classic, new Version(0, 1, 5)}, - {LanternAssetBundleId.Defaults, new Version(0, 1, 5)}, - {LanternAssetBundleId.Sound, new Version(0, 1, 0)}, - {LanternAssetBundleId.Music, new Version(0, 1, 0)}, + {LanternAssetBundleId.Music_Audio, new Version(0, 1, 7)}, + {LanternAssetBundleId.Music_Midi, new Version(0, 1, 7)}, + {LanternAssetBundleId.Characters, new Version(0, 1, 7)}, + {LanternAssetBundleId.Construct, new Version(0, 1, 7)}, + {LanternAssetBundleId.UI_Lantern, new Version(0, 1, 7)}, + {LanternAssetBundleId.ClientData, new Version(0, 1, 7)}, + {LanternAssetBundleId.Zones, new Version(0, 1, 7)}, + {LanternAssetBundleId.Equipment, new Version(0, 1, 7)}, + {LanternAssetBundleId.Sprites, new Version(0, 1, 7)}, + {LanternAssetBundleId.Startup, new Version(0, 1, 7)}, + {LanternAssetBundleId.Sky, new Version(0, 1, 7)}, + {LanternAssetBundleId.CharacterSelect_Classic, new Version(0, 1, 7)}, + {LanternAssetBundleId.Defaults, new Version(0, 1, 7)}, + {LanternAssetBundleId.Sound, new Version(0, 1, 7)}, // Unused/old - {LanternAssetBundleId.UI_Titanium, new Version(0, 1, 4)}, - {LanternAssetBundleId.UI_Debug, new Version(0, 1, 4)}, - {LanternAssetBundleId.Login_Dev, new Version(0, 1, 5)}, - {LanternAssetBundleId.Login_Classic, new Version(0, 1, 5)}, + {LanternAssetBundleId.UI_Titanium, new Version(0, 1, 7)}, + {LanternAssetBundleId.UI_Debug, new Version(0, 1, 7)}, + {LanternAssetBundleId.Login_Dev, new Version(0, 1, 7)}, + {LanternAssetBundleId.Login_Classic, new Version(0, 1, 7)}, // Removed //{LanternAssetBundleId.CharacterSelect_Dev, new Version(0, 1, 5)}, diff --git a/Assets/Scripts/Lantern/EQ/AssetBundles/LanternAssetBundleId.cs b/Assets/Scripts/Lantern/EQ/AssetBundles/LanternAssetBundleId.cs index 02df9f6..9805aef 100644 --- a/Assets/Scripts/Lantern/EQ/AssetBundles/LanternAssetBundleId.cs +++ b/Assets/Scripts/Lantern/EQ/AssetBundles/LanternAssetBundleId.cs @@ -6,7 +6,7 @@ /// public enum LanternAssetBundleId { - Music = 0, + Music_Audio = 0, //Shaders = 1, Sound = 2, Characters = 3, @@ -27,5 +27,6 @@ public enum LanternAssetBundleId Login_Classic = 18, Defaults = 19, ClientData = 20, + Music_Midi = 21, } } diff --git a/Assets/Scripts/Lantern/EQ/Audio.meta b/Assets/Scripts/Lantern/EQ/Audio.meta new file mode 100644 index 0000000..d04de73 --- /dev/null +++ b/Assets/Scripts/Lantern/EQ/Audio.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: aa4c133ce6a79924dac930ebe1710934 +folderAsset: yes +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Scripts/Lantern/EQ/Audio/AudioHelper.cs b/Assets/Scripts/Lantern/EQ/Audio/AudioHelper.cs new file mode 100644 index 0000000..b3a2337 --- /dev/null +++ b/Assets/Scripts/Lantern/EQ/Audio/AudioHelper.cs @@ -0,0 +1,162 @@ +using Lantern.EQ.Data; +using Lantern.EQ.Lantern; +using Lantern.EQ.Sound; +using UnityEngine; + +namespace Lantern.EQ.Audio +{ + public static class AudioHelper + { + public static AudioSource AddCharacterAudioSource(GameObject gameObject, int size) + { + var audioSource = Create3dAudioSource(gameObject); + audioSource.volume = EqConstants.AudioVolumeCharacter; + audioSource.maxDistance = GetNpcAudioDistance(size); + var curve = GetFalloff(EqConstants.AudioVolumeCharacter); + audioSource.SetCustomCurve(AudioSourceCurveType.CustomRolloff, curve); + return audioSource; + } + + public static AudioSource AddDoorAudioSource(GameObject gameObject) + { + var audioSource = Create3dAudioSource(gameObject); + audioSource.volume = EqConstants.AudioVolumeCharacter; + audioSource.maxDistance = 50f; + var curve = GetFalloff(EqConstants.AudioVolumeDoor); + audioSource.SetCustomCurve(AudioSourceCurveType.CustomRolloff, curve); + return audioSource; + } + + public static AudioSource AddTemporaryAudioSource(GameObject gameObject) + { + var audioSource = Create3dAudioSource(gameObject); + audioSource.volume = 0.75f; // guess + audioSource.maxDistance = 100f; // guess + var curve = GetFalloff(EqConstants.AudioVolumeCharacter); + audioSource.SetCustomCurve(AudioSourceCurveType.CustomRolloff, curve); + return audioSource; + } + + public static AnimationCurve GetSound3DFalloff(int multiplier) + { + // Yes, this is correct + if (multiplier < 3) + { + multiplier = 1; + } + + var curve = GetFalloff(EqConstants.AudioVolumeSound3d, multiplier); + return curve; + } + + private static float GetNpcAudioDistance(int size) + { + int units = 0; + switch (size) + { + case 1: + units = 48; + break; + case 2: + units = 95; + break; + case 3: + units = 143; + break; + case 4: + units = 192; + break; + case 5: + units = 240; + break; + case 6: + units = 287; + break; + case 7: + units = 167; + break; + case 8: + units = 192; + break; + case 9: + units = 215; + break; + case 10: + units = 240; + break; + } + + return units * LanternConstants.WorldScale; + } + + private static AnimationCurve GetFalloff(float maxVolume, int multiplier = 1) + { + var ac = new AnimationCurve(); + AddCurveKey(ref ac, 0f, 1f, maxVolume, multiplier); + AddCurveKey(ref ac, 0.03472222222f, 1f, maxVolume, multiplier); + AddCurveKey(ref ac, 0.04166666667f, 1f, maxVolume, multiplier); + AddCurveKey(ref ac, 0.04305555556f, 0.9730769231f, maxVolume, multiplier); + AddCurveKey(ref ac, 0.04444444444f, 0.9384615385f, maxVolume, multiplier); + AddCurveKey(ref ac, 0.04583333333f, 0.9184615385f, maxVolume, multiplier); + AddCurveKey(ref ac, 0.04722222222f, 0.8873076923f, maxVolume, multiplier); + AddCurveKey(ref ac, 0.04861111111f, 0.8673076923f, maxVolume, multiplier); + AddCurveKey(ref ac, 0.05555555556f, 0.7692307692f, maxVolume, multiplier); + AddCurveKey(ref ac, 0.0625f, 0.6923076923f, maxVolume, multiplier); + AddCurveKey(ref ac, 0.06944444444f, 0.6153846154f, maxVolume, multiplier); + AddCurveKey(ref ac, 0.08333333333f, 0.5f, maxVolume, multiplier); + AddCurveKey(ref ac, 0.09722222222f, 0.4230769231f, maxVolume, multiplier); + AddCurveKey(ref ac, 0.1111111111f, 0.3846153846f, maxVolume, multiplier); + AddCurveKey(ref ac, 0.125f, 0.3461538462f, maxVolume, multiplier); + AddCurveKey(ref ac, 0.1388888889f, 0.3076923077f, maxVolume, multiplier); + AddCurveKey(ref ac, 0.1736111111f, 0.2307692308f, maxVolume, multiplier); + AddCurveKey(ref ac, 0.2083333333f, 0.1923076923f, maxVolume, multiplier); + AddCurveKey(ref ac, 0.2430555556f, 0.1730769231f, maxVolume, multiplier); + AddCurveKey(ref ac, 0.2777777778f, 0.1507692308f, maxVolume, multiplier); + AddCurveKey(ref ac, 0.3472222222f, 0.1192307692f, maxVolume, multiplier); + AddCurveKey(ref ac, 0.4166666667f, 0.1f, maxVolume, multiplier); + AddCurveKey(ref ac, 0.5555555556f, 0.07307692308f, maxVolume, multiplier); + AddCurveKey(ref ac, 0.6944444444f, 0.06076923077f, maxVolume, multiplier); + AddCurveKey(ref ac, 0.8333333333f, 0.05f, maxVolume, multiplier); + AddCurveKey(ref ac, 0.9722222222f, 0.04307692308f, maxVolume, multiplier); + AddCurveKey(ref ac, 1f, 0f, maxVolume, multiplier); // it's really 0.04192307692f, but we need it to be 0f + return ac; + } + + private static void AddCurveKey(ref AnimationCurve curve, float time, float value, float maxVolume, int multiplier) + { + curve.AddKey(time, Mathf.Min(value * maxVolume, value * maxVolume * multiplier)); + } + + private static AudioSource Create3dAudioSource(GameObject gameObject) + { + var audioSource = gameObject.AddComponent(); + audioSource.rolloffMode = AudioRolloffMode.Custom; + audioSource.spatialBlend = 1f; + audioSource.dopplerLevel = 0f; + return audioSource; + } + + public static bool IsWalkOrRunSound(CharacterSoundType type) + { + return type == CharacterSoundType.Walking || type == CharacterSoundType.Running; + } + + public static bool IsWalkOrRunInterrupt(CharacterSoundType type) + { + return type == CharacterSoundType.Sit || type == CharacterSoundType.Idle || + type == CharacterSoundType.Passive; + } + + public static bool IsSilentPlayerSound(CharacterSoundType type) + { + switch (type) + { + case CharacterSoundType.Swim: + case CharacterSoundType.Treading: + return true; + default: + return false; + } + } + } +} diff --git a/Assets/Scripts/Lantern/EQ/Audio/AudioHelper.cs.meta b/Assets/Scripts/Lantern/EQ/Audio/AudioHelper.cs.meta new file mode 100644 index 0000000..8b079dd --- /dev/null +++ b/Assets/Scripts/Lantern/EQ/Audio/AudioHelper.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: a50fb21c18efe9d4ab9277c819768fd0 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Scripts/Lantern/EQ/Audio/ChannelFlag.cs b/Assets/Scripts/Lantern/EQ/Audio/ChannelFlag.cs new file mode 100644 index 0000000..9f70a41 --- /dev/null +++ b/Assets/Scripts/Lantern/EQ/Audio/ChannelFlag.cs @@ -0,0 +1,40 @@ +using System; + +namespace Lantern.EQ.Audio +{ + [Flags] + public enum ChannelFlag + { + Unset = 0, + Channel1 = 1 << 0, + Channel2 = 1 << 1, + Channel3 = 1 << 2, + Channel4 = 1 << 3, + Channel5 = 1 << 4, + Channel6 = 1 << 5, + Channel7 = 1 << 6, + Channel8 = 1 << 7, + Channel9 = 1 << 8, + Channel10 = 1 << 9, + Channel11 = 1 << 10, + Channel12 = 1 << 11, + Channel13 = 1 << 12, + Channel14 = 1 << 13, + Channel15 = 1 << 14, + Channel16 = 1 << 15, + + Barbarian = Channel2 | Channel3 | Channel6 | Channel8 | Channel9 | Channel10 | Channel11 | Channel13 | Channel14, + Dark_Elf = Channel6 | Channel7 | Channel10 | Channel11 | Channel14, + Dwarf = Channel4 | Channel5 | Channel10 | Channel11 | Channel13 | Channel15, + Erudite = Channel1 | Channel4 | Channel5 | Channel6 | Channel8 | Channel11, + Gnome = Channel2 | Channel3 | Channel6 | Channel10 | Channel13 | Channel15, + Half_Elf = Channel2 | Channel3 | Channel6 | Channel7 | Channel9 | Channel11, + Halfling = Channel1 | Channel2 | Channel3 | Channel6 | Channel15, + High_Elf = Channel2 | Channel3 | Channel4 | Channel5 | Channel10 | Channel13 | Channel14, + Human = Channel1 | Channel6 | Channel11, + Iksar = Channel2 | Channel3 | Channel6 | Channel8 | Channel9 | Channel10 | Channel14, + Ogre = Channel11 | Channel12 | Channel13, + Troll = Channel2 | Channel3 | Channel4 | Channel5 | Channel10 | Channel12, + Wood_Elf = Channel1 | Channel2 | Channel3 | Channel7 | Channel11 | Channel13 + } +} diff --git a/Assets/Scripts/Lantern/EQ/Audio/ChannelFlag.cs.meta b/Assets/Scripts/Lantern/EQ/Audio/ChannelFlag.cs.meta new file mode 100644 index 0000000..316cbf0 --- /dev/null +++ b/Assets/Scripts/Lantern/EQ/Audio/ChannelFlag.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: f718cc4c93076b94a82157214dfb3d70 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Scripts/Lantern/EQ/Audio/CharacterSoundData.cs b/Assets/Scripts/Lantern/EQ/Audio/CharacterSoundData.cs new file mode 100644 index 0000000..cd7c4b3 --- /dev/null +++ b/Assets/Scripts/Lantern/EQ/Audio/CharacterSoundData.cs @@ -0,0 +1,16 @@ +using System.Collections.Generic; +using Lantern.EQ.Sound; +using UnityEngine; + +namespace Lantern.EQ.Audio +{ + public class CharacterSoundData + { + public Dictionary> Sounds; + + public CharacterSoundData() + { + Sounds = new Dictionary>(); + } + } +} diff --git a/Assets/Scripts/Lantern/EQ/Audio/CharacterSoundData.cs.meta b/Assets/Scripts/Lantern/EQ/Audio/CharacterSoundData.cs.meta new file mode 100644 index 0000000..2e6b90b --- /dev/null +++ b/Assets/Scripts/Lantern/EQ/Audio/CharacterSoundData.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 0aa61508dd845a1489e8c82333a7fe52 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Scripts/Lantern/EQ/Audio/DoorSoundData.cs b/Assets/Scripts/Lantern/EQ/Audio/DoorSoundData.cs new file mode 100644 index 0000000..bb345b4 --- /dev/null +++ b/Assets/Scripts/Lantern/EQ/Audio/DoorSoundData.cs @@ -0,0 +1,10 @@ +using UnityEngine; + +namespace Lantern.EQ.Audio +{ + public class DoorSoundData + { + public AudioClip SoundOpen; + public AudioClip SoundClose; + } +} diff --git a/Assets/Scripts/Lantern/EQ/Audio/DoorSoundData.cs.meta b/Assets/Scripts/Lantern/EQ/Audio/DoorSoundData.cs.meta new file mode 100644 index 0000000..8d3d32a --- /dev/null +++ b/Assets/Scripts/Lantern/EQ/Audio/DoorSoundData.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: b011f60a41440584bae9e4b686504abc +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Scripts/Lantern/EQ/Audio/EquipmentSound.cs b/Assets/Scripts/Lantern/EQ/Audio/EquipmentSound.cs new file mode 100644 index 0000000..b950a86 --- /dev/null +++ b/Assets/Scripts/Lantern/EQ/Audio/EquipmentSound.cs @@ -0,0 +1,12 @@ +namespace Lantern.EQ.Audio +{ + public enum EquipmentSound + { + HandToHand = 0, + Blunt = 1, + Slashing = 2, + Bash = 3, + Bow = 4, + Whip = 5, + } +} diff --git a/Assets/Scripts/Lantern/EQ/Audio/EquipmentSound.cs.meta b/Assets/Scripts/Lantern/EQ/Audio/EquipmentSound.cs.meta new file mode 100644 index 0000000..a551146 --- /dev/null +++ b/Assets/Scripts/Lantern/EQ/Audio/EquipmentSound.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: d9e2dd9919ddc30418bdb17e23847807 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Scripts/Lantern/EQ/Audio/IAudioClipLocator.cs b/Assets/Scripts/Lantern/EQ/Audio/IAudioClipLocator.cs new file mode 100644 index 0000000..703a66a --- /dev/null +++ b/Assets/Scripts/Lantern/EQ/Audio/IAudioClipLocator.cs @@ -0,0 +1,9 @@ +using UnityEngine; + +namespace Lantern.EQ.Audio +{ + public interface IAudioClipLocator + { + public AudioClip GetAudioClip(string clipName); + } +} diff --git a/Assets/Scripts/Lantern/EQ/Audio/IAudioClipLocator.cs.meta b/Assets/Scripts/Lantern/EQ/Audio/IAudioClipLocator.cs.meta new file mode 100644 index 0000000..484c6c2 --- /dev/null +++ b/Assets/Scripts/Lantern/EQ/Audio/IAudioClipLocator.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: db69cc7f1316cee42bb46c1634dde079 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Scripts/Lantern/EQ/Audio/ICharacterSoundLocator.cs b/Assets/Scripts/Lantern/EQ/Audio/ICharacterSoundLocator.cs new file mode 100644 index 0000000..6bfd1b0 --- /dev/null +++ b/Assets/Scripts/Lantern/EQ/Audio/ICharacterSoundLocator.cs @@ -0,0 +1,7 @@ +namespace Lantern.EQ.Audio +{ + public interface ICharacterSoundLocator + { + public CharacterSoundData GetCharacterSounds(int raceId, int classId, int textureId, float scaleFactor = 6f); + } +} diff --git a/Assets/Scripts/Lantern/EQ/Audio/ICharacterSoundLocator.cs.meta b/Assets/Scripts/Lantern/EQ/Audio/ICharacterSoundLocator.cs.meta new file mode 100644 index 0000000..78bad2d --- /dev/null +++ b/Assets/Scripts/Lantern/EQ/Audio/ICharacterSoundLocator.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 29a8b838e751b474496b4c8940364586 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Scripts/Lantern/EQ/Audio/IDoorSoundLocator.cs b/Assets/Scripts/Lantern/EQ/Audio/IDoorSoundLocator.cs new file mode 100644 index 0000000..75894ab --- /dev/null +++ b/Assets/Scripts/Lantern/EQ/Audio/IDoorSoundLocator.cs @@ -0,0 +1,7 @@ +namespace Lantern.EQ.Audio +{ + public interface IDoorSoundLocator + { + public DoorSoundData GetDoorSounds(int openType); + } +} diff --git a/Assets/Scripts/Lantern/EQ/Audio/IDoorSoundLocator.cs.meta b/Assets/Scripts/Lantern/EQ/Audio/IDoorSoundLocator.cs.meta new file mode 100644 index 0000000..7e71b47 --- /dev/null +++ b/Assets/Scripts/Lantern/EQ/Audio/IDoorSoundLocator.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: e9c0c9cd1ccb1dc4ebe835315ed62769 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Scripts/Lantern/EQ/Audio/IMusicInvoker.cs b/Assets/Scripts/Lantern/EQ/Audio/IMusicInvoker.cs new file mode 100644 index 0000000..a747216 --- /dev/null +++ b/Assets/Scripts/Lantern/EQ/Audio/IMusicInvoker.cs @@ -0,0 +1,9 @@ +namespace Lantern.EQ.Audio +{ + public interface IMusicInvoker + { + void Initialize(MusicData musicData); + void Play(); + void Stop(); + } +} diff --git a/Assets/Scripts/Lantern/EQ/Audio/IMusicInvoker.cs.meta b/Assets/Scripts/Lantern/EQ/Audio/IMusicInvoker.cs.meta new file mode 100644 index 0000000..5d11d7a --- /dev/null +++ b/Assets/Scripts/Lantern/EQ/Audio/IMusicInvoker.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 63bca04b1b3c4504b9d62abedc599de5 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Scripts/Lantern/EQ/Audio/MidiTrack.cs b/Assets/Scripts/Lantern/EQ/Audio/MidiTrack.cs new file mode 100644 index 0000000..20a8ce9 --- /dev/null +++ b/Assets/Scripts/Lantern/EQ/Audio/MidiTrack.cs @@ -0,0 +1,21 @@ +using UnityEngine; + +namespace Lantern.EQ.Audio +{ + [PreferBinarySerialization] + public class MidiTrack : ScriptableObject + { + public string Name; + public int TrackNumber; + public byte[] Bytes; + + public static MidiTrack CreateMidiTrack(string name, int trackNumber, byte[] bytes) + { + var midiTrack = CreateInstance(); + midiTrack.Name = name; + midiTrack.TrackNumber = trackNumber; + midiTrack.Bytes = bytes; + return midiTrack; + } + } +} diff --git a/Assets/Scripts/Lantern/EQ/Audio/MidiTrack.cs.meta b/Assets/Scripts/Lantern/EQ/Audio/MidiTrack.cs.meta new file mode 100644 index 0000000..55a23ad --- /dev/null +++ b/Assets/Scripts/Lantern/EQ/Audio/MidiTrack.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: f5dc763fee7cae846887d1148e1bf6f1 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Scripts/Lantern/EQ/Audio/MidiTrackCollection.cs b/Assets/Scripts/Lantern/EQ/Audio/MidiTrackCollection.cs new file mode 100644 index 0000000..da96d81 --- /dev/null +++ b/Assets/Scripts/Lantern/EQ/Audio/MidiTrackCollection.cs @@ -0,0 +1,10 @@ +using System.Collections.Generic; +using UnityEngine; + +namespace Lantern.EQ.Audio +{ + public class MidiTrackCollection : ScriptableObject + { + public List MidiTracks = new List(); + } +} diff --git a/Assets/Scripts/Lantern/EQ/Audio/MidiTrackCollection.cs.meta b/Assets/Scripts/Lantern/EQ/Audio/MidiTrackCollection.cs.meta new file mode 100644 index 0000000..8252b63 --- /dev/null +++ b/Assets/Scripts/Lantern/EQ/Audio/MidiTrackCollection.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: cbbfb99e6957813428bed04cf154a366 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Scripts/Lantern/EQ/Audio/MusicData.cs b/Assets/Scripts/Lantern/EQ/Audio/MusicData.cs new file mode 100644 index 0000000..6fd735c --- /dev/null +++ b/Assets/Scripts/Lantern/EQ/Audio/MusicData.cs @@ -0,0 +1,15 @@ +using System; + +namespace Lantern.EQ.Audio +{ + [Serializable] + public class MusicData + { + public int TrackIndexDay; + public int TrackIndexNight; + public int PlayCountDay; + public int PlayCountNight; + public int FadeOutMsDay; // EQ only has a single fade for both day/night + public int FadeOutMsNight; // EQ only has a single fade for both day/night + } +} diff --git a/Assets/Scripts/Lantern/EQ/Audio/MusicData.cs.meta b/Assets/Scripts/Lantern/EQ/Audio/MusicData.cs.meta new file mode 100644 index 0000000..494a606 --- /dev/null +++ b/Assets/Scripts/Lantern/EQ/Audio/MusicData.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: c2015aeef29d6b84a87df350f00d406c +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Scripts/Lantern/EQ/Audio/MusicTrigger.cs b/Assets/Scripts/Lantern/EQ/Audio/MusicTrigger.cs new file mode 100644 index 0000000..43ffe4c --- /dev/null +++ b/Assets/Scripts/Lantern/EQ/Audio/MusicTrigger.cs @@ -0,0 +1,44 @@ +using Lantern.EQ.Trigger; +using UnityEngine; + +namespace Lantern.EQ.Audio +{ + public class MusicTrigger : SphereTrigger + { + [SerializeField] + private MusicData _musicData; + + private IMusicInvoker _invoker; + +#if UNITY_EDITOR + public void SetData(string tag, float radius, MusicData musicData) + { + _tag = tag; + _collider.radius = radius; + _musicData = musicData; + } +#endif + + private void Awake() + { + _collider.enabled = false; + } + + public void SetInvoker(IMusicInvoker musicInvoker) + { + _invoker = musicInvoker; + _invoker.Initialize(_musicData); + _collider.enabled = true; + } + + protected override void OnEnter() + { + _invoker?.Play(); + } + + protected override void OnExit() + { + _invoker?.Stop(); + } + } +} diff --git a/Assets/Scripts/Lantern/EQ/Audio/MusicTrigger.cs.meta b/Assets/Scripts/Lantern/EQ/Audio/MusicTrigger.cs.meta new file mode 100644 index 0000000..d03d7fc --- /dev/null +++ b/Assets/Scripts/Lantern/EQ/Audio/MusicTrigger.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 95022e660d665a54c8e03a9bee961edb +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Scripts/Lantern/EQ/Audio/Sound2dData.cs b/Assets/Scripts/Lantern/EQ/Audio/Sound2dData.cs new file mode 100644 index 0000000..1bac4e7 --- /dev/null +++ b/Assets/Scripts/Lantern/EQ/Audio/Sound2dData.cs @@ -0,0 +1,16 @@ +using System; + +namespace Lantern.EQ.Audio +{ + [Serializable] + public class Sound2dData + { + public string ClipNameDay; + public string ClipNameNight; + public float VolumeDay; + public float VolumeNight; + public float CooldownDay; + public float CooldownNight; + public float CooldownRandom; + } +} diff --git a/Assets/Scripts/Lantern/EQ/Audio/Sound2dData.cs.meta b/Assets/Scripts/Lantern/EQ/Audio/Sound2dData.cs.meta new file mode 100644 index 0000000..517ea17 --- /dev/null +++ b/Assets/Scripts/Lantern/EQ/Audio/Sound2dData.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: b943cb86bcc107744a66198b05fbf261 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Scripts/Lantern/EQ/Audio/Sound3dData.cs b/Assets/Scripts/Lantern/EQ/Audio/Sound3dData.cs new file mode 100644 index 0000000..79fbe92 --- /dev/null +++ b/Assets/Scripts/Lantern/EQ/Audio/Sound3dData.cs @@ -0,0 +1,14 @@ +using System; + +namespace Lantern.EQ.Audio +{ + [Serializable] + public class Sound3dData + { + public string ClipName; + public float Volume; + public float Cooldown; + public float CooldownRandom; + public int Multiplier; + } +} diff --git a/Assets/Scripts/Lantern/EQ/Audio/Sound3dData.cs.meta b/Assets/Scripts/Lantern/EQ/Audio/Sound3dData.cs.meta new file mode 100644 index 0000000..c2d5e80 --- /dev/null +++ b/Assets/Scripts/Lantern/EQ/Audio/Sound3dData.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 979d8a81a6b12314da4f567c27731d34 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Scripts/Lantern/EQ/Audio/SoundTrigger.cs b/Assets/Scripts/Lantern/EQ/Audio/SoundTrigger.cs new file mode 100644 index 0000000..4dea29b --- /dev/null +++ b/Assets/Scripts/Lantern/EQ/Audio/SoundTrigger.cs @@ -0,0 +1,44 @@ +using System.Collections; +using Lantern.EQ.Trigger; +using UnityEngine; + +namespace Lantern.EQ.Audio +{ + [RequireComponent(typeof(AudioSource))] + + public abstract class SoundTrigger : SphereTrigger + { + [SerializeField] + protected AudioSource AudioSource; + + protected Coroutine LoopCoroutine; + protected bool IsPlayerInside; + + public abstract void FindAudioClips(IAudioClipLocator locator); + + protected abstract bool HasCooldown(); + protected abstract IEnumerator DoLoop(); + + protected override void OnEnter() + { + IsPlayerInside = true; + + if (HasCooldown()) + { + AudioSource.loop = false; + LoopCoroutine = StartCoroutine(DoLoop()); + } + else + { + AudioSource.loop = true; + AudioSource.Play(); + } + } + + protected override void OnExit() + { + IsPlayerInside = false; + AudioSource.Stop(); + } + } +} diff --git a/Assets/Scripts/Lantern/EQ/Audio/SoundTrigger.cs.meta b/Assets/Scripts/Lantern/EQ/Audio/SoundTrigger.cs.meta new file mode 100644 index 0000000..9c82742 --- /dev/null +++ b/Assets/Scripts/Lantern/EQ/Audio/SoundTrigger.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 1db0d5d443ed7d04aa8e67761ecb52f5 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Scripts/Lantern/EQ/Audio/SoundTrigger2d.cs b/Assets/Scripts/Lantern/EQ/Audio/SoundTrigger2d.cs new file mode 100644 index 0000000..47ab262 --- /dev/null +++ b/Assets/Scripts/Lantern/EQ/Audio/SoundTrigger2d.cs @@ -0,0 +1,81 @@ +using System.Collections; +using Lantern.EQ.Data; +using Lantern.EQ.Lantern; +using UnityEngine; + +namespace Lantern.EQ.Audio +{ + public class SoundTrigger2d : SoundTrigger + { + [SerializeField] + private Sound2dData _soundData; + + private AudioClip _clipDay; + private AudioClip _clipNight; + private bool _isDay; + + private void Awake() + { + _collider.enabled = false; + } + + public override void FindAudioClips(IAudioClipLocator locator) + { + _clipDay = locator.GetAudioClip(_soundData.ClipNameDay); + _clipNight = locator.GetAudioClip(_soundData.ClipNameNight); + _collider.enabled = true; + } + + protected override bool HasCooldown() + { + var clip = AudioSource.clip; + return clip == _isDay && _soundData.CooldownDay > 0 || + clip == !_isDay && _soundData.CooldownNight > 0; + } + + protected override IEnumerator DoLoop() + { + // If audio source is playing, ignore + if (!AudioSource.isPlaying) + { + SetClipAndVolume(); + AudioSource.Play(); + } + + // TODO: Random + yield return new WaitForSeconds(_isDay ? _soundData.CooldownDay : _soundData.CooldownNight); + LoopCoroutine = IsPlayerInside ? StartCoroutine(DoLoop()) : null; + } + + private void SetClipAndVolume() + { + AudioSource.clip = _isDay ? _clipDay : _clipNight; + AudioSource.volume = (_isDay ? _soundData.VolumeDay : _soundData.VolumeNight) * EqConstants.AudioVolumeSound2d; + } + + public void SetDayNight(bool isDayTime) + { + _isDay = isDayTime; + SetClipAndVolume(); + + if (IsPlayerInside && AudioSource.clip != null) + { + AudioSource.Play(); + } + else + { + AudioSource.Stop(); + } + } + +#if UNITY_EDITOR + public void SetData(Sound2dData sound2dData, string tag, float radius) + { + _soundData = sound2dData; + _tag = tag; + AudioSource.maxDistance = radius * LanternConstants.WorldScale; // 2d sounds don't have a radius, but it shows the 3d radius anyway + _collider.radius = radius; // colliders scale with world automatically + } +#endif + } +} diff --git a/Assets/Scripts/Lantern/EQ/Audio/SoundTrigger2d.cs.meta b/Assets/Scripts/Lantern/EQ/Audio/SoundTrigger2d.cs.meta new file mode 100644 index 0000000..173f24d --- /dev/null +++ b/Assets/Scripts/Lantern/EQ/Audio/SoundTrigger2d.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 5b1e9899421fcfd4ab2a379e1a553bb9 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Scripts/Lantern/EQ/Audio/SoundTrigger3d.cs b/Assets/Scripts/Lantern/EQ/Audio/SoundTrigger3d.cs new file mode 100644 index 0000000..e347ac3 --- /dev/null +++ b/Assets/Scripts/Lantern/EQ/Audio/SoundTrigger3d.cs @@ -0,0 +1,50 @@ +using System.Collections; +using Lantern.EQ.Data; +using Lantern.EQ.Lantern; +using UnityEngine; + +namespace Lantern.EQ.Audio +{ + public class SoundTrigger3d : SoundTrigger + { + [SerializeField] + private Sound3dData _soundData; + + public override void FindAudioClips(IAudioClipLocator locator) + { + AudioSource.clip = locator.GetAudioClip(_soundData.ClipName); + AudioSource.volume = _soundData.Volume * EqConstants.AudioVolumeSound3d; + } + + protected override bool HasCooldown() + { + return _soundData.Cooldown > 0; + } + + protected override IEnumerator DoLoop() + { + // If audio source is playing, ignore + if (!AudioSource.isPlaying) + { + AudioSource.Play(); + } + + // TODO: Random + yield return new WaitForSeconds(_soundData.Cooldown); + LoopCoroutine = IsPlayerInside ? StartCoroutine(DoLoop()) : null; + } + +#if UNITY_EDITOR + public void SetData(Sound3dData soundData, string tag, float radius) + { + _tag = tag; + _soundData = soundData; + AudioSource.maxDistance = radius * LanternConstants.WorldScale; + AudioSource.rolloffMode = AudioRolloffMode.Custom; + AudioSource.SetCustomCurve(AudioSourceCurveType.CustomRolloff, + AudioHelper.GetSound3DFalloff(soundData.Multiplier)); + _collider.radius = radius; // colliders scale with world automatically + } +#endif + } +} diff --git a/Assets/Scripts/Lantern/EQ/Audio/SoundTrigger3d.cs.meta b/Assets/Scripts/Lantern/EQ/Audio/SoundTrigger3d.cs.meta new file mode 100644 index 0000000..496497f --- /dev/null +++ b/Assets/Scripts/Lantern/EQ/Audio/SoundTrigger3d.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 295fb91b0638a4b4886c849a70dd8439 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Scripts/Lantern/EQ/Audio/Xmi.meta b/Assets/Scripts/Lantern/EQ/Audio/Xmi.meta new file mode 100644 index 0000000..a4040d1 --- /dev/null +++ b/Assets/Scripts/Lantern/EQ/Audio/Xmi.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: 9eb8fc7af5067284c9f7c4f56f30bfc3 +folderAsset: yes +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Scripts/Lantern/EQ/Audio/Xmi/BranchChunk.cs b/Assets/Scripts/Lantern/EQ/Audio/Xmi/BranchChunk.cs new file mode 100644 index 0000000..654b8a2 --- /dev/null +++ b/Assets/Scripts/Lantern/EQ/Audio/Xmi/BranchChunk.cs @@ -0,0 +1,54 @@ +using System.Collections.Generic; +using System.IO; + +namespace Lantern.EQ.Audio.Xmi +{ + public class BranchChunk : Chunk + { + //--Fields + private Branch[] branches; + + //--Properties + public IList BranchPoints + { + get { return branches; } + } + + //--Methods + public BranchChunk(string id, int size, BinaryReader reader) + : base(id, size) + { + int branchCount = reader.ReadUInt16(); + branches = new Branch[branchCount]; + for (int x = 0; x < branches.Length; x++) + { + branches[x] = new Branch(reader); + } + } + + //--Internal classes and structs + public class Branch + { + //--Fields + private int branchControllerIndex; + private int branchControllerOffset; + + //--Properties + public int ControllerIndex + { + get { return branchControllerIndex; } + } + public int ControllerOffset + { + get { return branchControllerOffset; } + } + + //--Methods + public Branch(BinaryReader reader) + { + branchControllerIndex = reader.ReadUInt16(); + branchControllerOffset = reader.ReadInt32(); + } + } + } +} diff --git a/Assets/Scripts/Lantern/EQ/Audio/Xmi/BranchChunk.cs.meta b/Assets/Scripts/Lantern/EQ/Audio/Xmi/BranchChunk.cs.meta new file mode 100644 index 0000000..a97b5db --- /dev/null +++ b/Assets/Scripts/Lantern/EQ/Audio/Xmi/BranchChunk.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 2c2a6201aa6219e42b996b832cbc3bf5 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Scripts/Lantern/EQ/Audio/Xmi/CatChunk.cs b/Assets/Scripts/Lantern/EQ/Audio/Xmi/CatChunk.cs new file mode 100644 index 0000000..5fbb111 --- /dev/null +++ b/Assets/Scripts/Lantern/EQ/Audio/Xmi/CatChunk.cs @@ -0,0 +1,36 @@ +using System; +using System.Collections.Generic; +using System.IO; + +namespace Lantern.EQ.Audio.Xmi +{ + public class CatChunk : Chunk + { + //--Fields + private string catTypeId; + private Chunk[] catSubChunks; + //--Properties + public string TypeId + { + get { return catTypeId; } + } + public Chunk[] SubChunks + { + get { return catSubChunks; } + } + //--Methods + public CatChunk(string id, int size, BinaryReader reader, Func catCallback) + : base(id, size) + { + long readTo = reader.BaseStream.Position + size; + catTypeId = new string(XmiHelper.Read8BitChars(reader, 4)); + List chunkList = new List(); + while (reader.BaseStream.Position < readTo) + { + Chunk chk = catCallback.Invoke(reader); + chunkList.Add(chk); + } + catSubChunks = chunkList.ToArray(); + } + } +} diff --git a/Assets/Scripts/Lantern/EQ/Audio/Xmi/CatChunk.cs.meta b/Assets/Scripts/Lantern/EQ/Audio/Xmi/CatChunk.cs.meta new file mode 100644 index 0000000..e17751a --- /dev/null +++ b/Assets/Scripts/Lantern/EQ/Audio/Xmi/CatChunk.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: bb1ef428c1cb27d4fb61594716e7ba08 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Scripts/Lantern/EQ/Audio/Xmi/Chunk.cs b/Assets/Scripts/Lantern/EQ/Audio/Xmi/Chunk.cs new file mode 100644 index 0000000..cd3f418 --- /dev/null +++ b/Assets/Scripts/Lantern/EQ/Audio/Xmi/Chunk.cs @@ -0,0 +1,24 @@ +namespace Lantern.EQ.Audio.Xmi +{ + public abstract class Chunk + { + //--Fields + protected string id; + protected int size; + //--Properties + public string ChunkId + { + get { return id; } + } + public int ChunkSize + { + get { return size; } + } + //--Methods + public Chunk(string id, int size) + { + this.id = id; + this.size = size; + } + } +} diff --git a/Assets/Scripts/Lantern/EQ/Audio/Xmi/Chunk.cs.meta b/Assets/Scripts/Lantern/EQ/Audio/Xmi/Chunk.cs.meta new file mode 100644 index 0000000..da6adf0 --- /dev/null +++ b/Assets/Scripts/Lantern/EQ/Audio/Xmi/Chunk.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: f24e1c97b049f094791af837f1d84c7a +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Scripts/Lantern/EQ/Audio/Xmi/EventChunk.cs b/Assets/Scripts/Lantern/EQ/Audio/Xmi/EventChunk.cs new file mode 100644 index 0000000..fe1302d --- /dev/null +++ b/Assets/Scripts/Lantern/EQ/Audio/Xmi/EventChunk.cs @@ -0,0 +1,25 @@ +using System.IO; + +namespace Lantern.EQ.Audio.Xmi +{ + public class EventChunk : Chunk + { + //--Fields + private byte[] eventData; + + //--Properties + public byte[] Data + { + get { return eventData; } + } + + //--Methods + public EventChunk(string id, int size, BinaryReader reader) + : base(id, size) + { + eventData = reader.ReadBytes(size); + // if (size % 2 == 1 && reader.PeekChar() == 0) + // reader.ReadByte(); + } + } +} diff --git a/Assets/Scripts/Lantern/EQ/Audio/Xmi/EventChunk.cs.meta b/Assets/Scripts/Lantern/EQ/Audio/Xmi/EventChunk.cs.meta new file mode 100644 index 0000000..f7a8eab --- /dev/null +++ b/Assets/Scripts/Lantern/EQ/Audio/Xmi/EventChunk.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: e1cbbd67490a5da47a592090b2330068 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Scripts/Lantern/EQ/Audio/Xmi/Extensions.cs b/Assets/Scripts/Lantern/EQ/Audio/Xmi/Extensions.cs new file mode 100644 index 0000000..481c060 --- /dev/null +++ b/Assets/Scripts/Lantern/EQ/Audio/Xmi/Extensions.cs @@ -0,0 +1,33 @@ +using Melanchall.DryWetMidi.Core; + +namespace Lantern.EQ.Audio.Xmi +{ + public static class Extensions + { + // Special xmidi VLQ + public static long ReadXmiVlqLongNumber(this MidiReader reader) + { + long result = 0; + byte b; + bool validXmiVlqByte; + + do + { + b = reader.ReadByte(); + validXmiVlqByte = (b & 0x80) == 0; + + if (validXmiVlqByte) + result += b; + } + while (validXmiVlqByte); + reader.Position--; + + return result; + } + + public static int ReadXmiVlqNumber(this MidiReader reader) + { + return (int)reader.ReadXmiVlqLongNumber(); + } + } +} diff --git a/Assets/Scripts/Lantern/EQ/Audio/Xmi/Extensions.cs.meta b/Assets/Scripts/Lantern/EQ/Audio/Xmi/Extensions.cs.meta new file mode 100644 index 0000000..3af853f --- /dev/null +++ b/Assets/Scripts/Lantern/EQ/Audio/Xmi/Extensions.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 6d643538223abf34aaff98b9c8ccaa94 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Scripts/Lantern/EQ/Audio/Xmi/FormChunk.cs b/Assets/Scripts/Lantern/EQ/Audio/Xmi/FormChunk.cs new file mode 100644 index 0000000..f82943f --- /dev/null +++ b/Assets/Scripts/Lantern/EQ/Audio/Xmi/FormChunk.cs @@ -0,0 +1,59 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; + +namespace Lantern.EQ.Audio.Xmi +{ + public class FormChunk : Chunk + { + //--Fields + private string formTypeId; + private Chunk[] formSubChunks; + private Dictionary branchLocations; + //--Properties + public string TypeId + { + get { return formTypeId; } + } + public Chunk[] SubChunks + { + get { return formSubChunks; } + } + public Dictionary BranchLocations + { + get { return branchLocations; } + } + //--Methods + public FormChunk(string id, int size, BinaryReader reader, Func formCallback) + : base(id, size) + { + long readTo = reader.BaseStream.Position + size; + formTypeId = new string(XmiHelper.Read8BitChars(reader, 4)); + List chunkList = new List(); + while (reader.BaseStream.Position < readTo) + { + Chunk chk = formCallback.Invoke(reader); + chunkList.Add(chk); + } + formSubChunks = chunkList.ToArray(); + branchLocations = GetBranchLocations(); + } + + // map byte offset => branch index + private Dictionary GetBranchLocations() + { + var branchLocations = new Dictionary(); + var branchChunk = SubChunks.OfType().FirstOrDefault(); + if (branchChunk != null) + { + foreach (var branch in branchChunk.BranchPoints) + { + branchLocations.Add(branch.ControllerOffset, branch.ControllerIndex); + } + } + + return branchLocations; + } + } +} diff --git a/Assets/Scripts/Lantern/EQ/Audio/Xmi/FormChunk.cs.meta b/Assets/Scripts/Lantern/EQ/Audio/Xmi/FormChunk.cs.meta new file mode 100644 index 0000000..ca707f1 --- /dev/null +++ b/Assets/Scripts/Lantern/EQ/Audio/Xmi/FormChunk.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 3e588c386dca06341997426f0d5ff173 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Scripts/Lantern/EQ/Audio/Xmi/InfoChunk.cs b/Assets/Scripts/Lantern/EQ/Audio/Xmi/InfoChunk.cs new file mode 100644 index 0000000..2d4b04b --- /dev/null +++ b/Assets/Scripts/Lantern/EQ/Audio/Xmi/InfoChunk.cs @@ -0,0 +1,22 @@ +using System.IO; + +namespace Lantern.EQ.Audio.Xmi +{ + public class InfoChunk : Chunk + { + //--Fields + private int xmidCount; // UWORD + //--Properties + public int XmidCount + { + get { return xmidCount; } + } + + //--Methods + public InfoChunk(string id, int size, BinaryReader reader) + : base(id, size) + { + xmidCount = reader.ReadUInt16(); + } + } +} diff --git a/Assets/Scripts/Lantern/EQ/Audio/Xmi/InfoChunk.cs.meta b/Assets/Scripts/Lantern/EQ/Audio/Xmi/InfoChunk.cs.meta new file mode 100644 index 0000000..52492d1 --- /dev/null +++ b/Assets/Scripts/Lantern/EQ/Audio/Xmi/InfoChunk.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: d1eeead9d294de84eaeffc22b65b09fa +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Scripts/Lantern/EQ/Audio/Xmi/TimbreChunk.cs b/Assets/Scripts/Lantern/EQ/Audio/Xmi/TimbreChunk.cs new file mode 100644 index 0000000..764c086 --- /dev/null +++ b/Assets/Scripts/Lantern/EQ/Audio/Xmi/TimbreChunk.cs @@ -0,0 +1,54 @@ +using System.Collections.Generic; +using System.IO; + +namespace Lantern.EQ.Audio.Xmi +{ + public class TimbreChunk : Chunk + { + //--Fields + private Timbre[] timbres; + + //--Properties + public IList TimbreList + { + get { return timbres; } + } + + //--Methods + public TimbreChunk(string id, int size, BinaryReader reader) + : base(id, size) + { + int timbreCount = reader.ReadUInt16(); + timbres = new Timbre[timbreCount]; + for (int x = 0; x < timbres.Length; x++) + { + timbres[x] = new Timbre(reader); + } + } + + //--Internal classes and structs + public class Timbre + { + //--Fields + private int timbrePatchNumber; + private int timbreBank; + + //--Properties + public int PatchNumber + { + get { return timbrePatchNumber; } + } + public int Bank + { + get { return timbreBank; } + } + + //--Methods + public Timbre(BinaryReader reader) + { + timbrePatchNumber = reader.ReadByte(); + timbreBank = reader.ReadByte(); + } + } + } +} diff --git a/Assets/Scripts/Lantern/EQ/Audio/Xmi/TimbreChunk.cs.meta b/Assets/Scripts/Lantern/EQ/Audio/Xmi/TimbreChunk.cs.meta new file mode 100644 index 0000000..3b4c171 --- /dev/null +++ b/Assets/Scripts/Lantern/EQ/Audio/Xmi/TimbreChunk.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 712dcae66a5b24b429d7d1c7bf525c21 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Scripts/Lantern/EQ/Audio/Xmi/UnknownChunk.cs b/Assets/Scripts/Lantern/EQ/Audio/Xmi/UnknownChunk.cs new file mode 100644 index 0000000..0baa0de --- /dev/null +++ b/Assets/Scripts/Lantern/EQ/Audio/Xmi/UnknownChunk.cs @@ -0,0 +1,23 @@ +using System.IO; + +namespace Lantern.EQ.Audio.Xmi +{ + public class UnknownChunk : Chunk + { + //--Fields + private byte[] data; + //--Properties + public byte[] Data + { + get { return data; } + } + //--Methods + public UnknownChunk(string id, int size, BinaryReader reader) + : base(id, size) + { + data = reader.ReadBytes(size); + if (size % 2 == 1 && reader.PeekChar() == 0) + reader.ReadByte(); + } + } +} diff --git a/Assets/Scripts/Lantern/EQ/Audio/Xmi/UnknownChunk.cs.meta b/Assets/Scripts/Lantern/EQ/Audio/Xmi/UnknownChunk.cs.meta new file mode 100644 index 0000000..303f05c --- /dev/null +++ b/Assets/Scripts/Lantern/EQ/Audio/Xmi/UnknownChunk.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: cf35907d0cc3ffb46b1b94826c32b2d9 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Scripts/Lantern/EQ/Audio/Xmi/XmiFile.cs b/Assets/Scripts/Lantern/EQ/Audio/Xmi/XmiFile.cs new file mode 100644 index 0000000..c968cc2 --- /dev/null +++ b/Assets/Scripts/Lantern/EQ/Audio/Xmi/XmiFile.cs @@ -0,0 +1,256 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using Melanchall.DryWetMidi.Common; +using Melanchall.DryWetMidi.Core; +using Melanchall.DryWetMidi.Interaction; +using Melanchall.DryWetMidi.Tools; + +namespace Lantern.EQ.Audio.Xmi +{ + public class XmiFile + { + //--Fields + private Chunk[] chks; + private CatChunk catChk; + private FormChunk[] xmidChks; + + //--Properties + public CatChunk Cat + { + get { return catChk; } + } + public FormChunk[] XmidiTracks + { + get { return xmidChks; } + } + public Chunk[] Chunks + { + get { return chks; } + } + + //--Methods + public XmiFile(Chunk[] chunks) + { + chks = chunks; + catChk = FindChunk(); + if (catChk != null) + { + xmidChks = catChk.SubChunks.OfType().Where(fc => fc.TypeId == "XMID").ToArray(); + } + } + public T FindChunk(int startIndex = 0) where T : Chunk + { + for (int x = startIndex; x < chks.Length; x++) + { + if (chks[x] is T) + return (T)chks[x]; + } + return default(T); + } + + public Stream WriteMidiTrack(int trackNumber) + { + if (trackNumber >= XmidiTracks.Length || XmidiTracks[trackNumber] == null) + { + throw new ArgumentException("Invalid track number.", nameof(trackNumber)); + } + + var stream = new MemoryStream(); + var xmidiTrack = XmidiTracks[trackNumber]; + var events = TranslateXmi(xmidiTrack, out var ppqn) + .OrderBy(e => e.Time).ToList(); + var timeDivision = new TicksPerQuarterNoteTimeDivision((short)ppqn); + + // xmi2mid will do a MultiSequence (type 2) if the original file had multiple tracks + // which meltysynth will complain about + using (var tokensWriter = MidiFile.WriteLazy(stream, settings: null, MidiFileFormat.SingleTrack, timeDivision)) + using (var objectsWriter = new TimedObjectsWriter(tokensWriter)) + { + tokensWriter.StartTrackChunk(); + objectsWriter.WriteObjects(events); + } + + stream.Position = 0; + + return stream; + } + + public Stream WriteMidiTrackSequence(int trackNumber, int sequenceNumber, ChannelFlag channelMask = ChannelFlag.Unset) + { + var stream = WriteMidiTrack(trackNumber); + + var xmidiTrack = XmidiTracks[trackNumber]; + var branchLocations = xmidiTrack.BranchLocations; + + if (sequenceNumber == -1 || sequenceNumber >= branchLocations.Count || branchLocations.Count == 0) + { + return stream; + } + + var midiFile = MidiFile.Read(stream); + var markerEvents = midiFile.GetTimedEvents().Where(e => e.Event is MarkerEvent).ToList(); + + var startTime = new MidiTimeSpan(); + var endTime = new MidiTimeSpan(markerEvents[sequenceNumber].Time); + + if (sequenceNumber > 0) + { + startTime = new MidiTimeSpan(markerEvents[sequenceNumber - 1].Time); + } + + var partLength = endTime.Subtract(startTime, TimeSpanMode.TimeTime); + + var midiSequence = midiFile.TakePart(startTime, partLength); + + // support for filtering out channels + // used primarily for character select loop section. + if (channelMask != ChannelFlag.Unset) + { + // there shouldn't be more than one track chunk. + foreach (var trackChunk in midiSequence.GetTrackChunks()) + { + using (var notesManager = trackChunk.ManageNotes()) + { + notesManager.Objects.RemoveAll(note => { + var bitflagChannel = (ChannelFlag)(1 << (int)note.Channel); + return (channelMask & bitflagChannel) == 0; + }); + } + } + } + + stream = new MemoryStream(); + midiSequence.Write(stream); + stream.Position = 0; + + return stream; + } + + ICollection TranslateXmi(FormChunk xmidiTrack, out long ppqn) + { + var result = new List(); + byte? channelEventStatusByte = null; + ppqn = 0; + + long time = 0; + long tempo = SetTempoEvent.DefaultMicrosecondsPerQuarterNote; + var tempoSet = false; + + var eventChunk = xmidiTrack.SubChunks.OfType().FirstOrDefault(); + if (eventChunk == null) + { + return result; + } + + var xmiData = eventChunk.Data; + var stream = new MemoryStream(xmiData); + var reader = new MidiReader(stream, new ReaderSettings()); + + var dwm = typeof(MidiEvent).Assembly; + var eventReaderInterface = dwm.GetType("Melanchall.DryWetMidi.Core.IEventReader"); + var eventReaderType = dwm.GetType("Melanchall.DryWetMidi.Core.EventReaderFactory"); + var readerInfo = eventReaderType.GetMethod("GetReader", System.Reflection.BindingFlags.NonPublic | System.Reflection.BindingFlags.Static); + var readMethodInfo = eventReaderInterface.GetMethod("Read"); + + while (reader.Position < reader.Length) + { + MidiEvent midiEvent; + TimedEvent timedEvent; + + if (xmidiTrack.BranchLocations.TryGetValue(reader.Position, out var branchIndex)) + { + midiEvent = new MarkerEvent($":XBRN:{branchIndex:X2}"); + timedEvent = new TimedEvent(midiEvent, time); + result.Add(timedEvent); + } + + long deltaTime = reader.ReadXmiVlqLongNumber(); + if (deltaTime < 0) + deltaTime = 0; + + var statusByte = reader.ReadByte(); + if (statusByte <= SevenBitNumber.MaxValue) + { + if (channelEventStatusByte == null) + { + // TODO: log warning + // throw new UnexpectedRunningStatusException(); + continue; + } + + statusByte = channelEventStatusByte.Value; + reader.Position--; + } + + // midiEvent = ReadEvent(reader, statusByte); + var eventReader = readerInfo.Invoke(null, new object[] { statusByte, true }); + midiEvent = (MidiEvent)readMethodInfo.Invoke(eventReader, new object[] { reader, new ReadingSettings(), statusByte }); + + if (midiEvent == null) + { + // TODO: log warning + continue; + } + + if (midiEvent is EndOfTrackEvent) + { + break; + } + + if (midiEvent is ChannelEvent) + { + channelEventStatusByte = statusByte; + } + + midiEvent.DeltaTime = deltaTime * 3; + time += midiEvent.DeltaTime; + + // needs to happen after the time is advanced. + if (midiEvent is SetTempoEvent tempoEvent) + { + // Skip any other tempo changes (but still advance the time) + if (tempoSet) + { + continue; + } + + tempo = tempoEvent.MicrosecondsPerQuarterNote; + tempoSet = true; + } + + timedEvent = new TimedEvent(midiEvent, time); + result.Add(timedEvent); + + // Add note off event + if (midiEvent is NoteOnEvent noteOnEvent) + { + var durationInIntervals = reader.ReadVlqLongNumber(); + + // xmi2mid appears to always use note off velocity 0, not the note on velocity. + var noteOffEvent = new NoteOffEvent(noteOnEvent.NoteNumber, velocity: (SevenBitNumber)0) + { + Channel = noteOnEvent.Channel + }; + timedEvent = new TimedEvent(noteOffEvent, time + (durationInIntervals * 3)); + result.Add(timedEvent); + } + } + + var ppqnFloat = tempo * 3f / 25000f * 3f; + ppqn = Convert.ToInt32(ppqnFloat); + + return result; + } + + // Replaced with reflection. + // private MidiEvent ReadEvent(MidiReader midiReader, byte statusByte) + // { + // var settings = new ReadingSettings(); + + // var eventReader = EventReaderFactory.GetReader(statusByte, smfOnly: true); + // return eventReader.Read(midiReader, settings, statusByte); + // } + } +} diff --git a/Assets/Scripts/Lantern/EQ/Audio/Xmi/XmiFile.cs.meta b/Assets/Scripts/Lantern/EQ/Audio/Xmi/XmiFile.cs.meta new file mode 100644 index 0000000..8805391 --- /dev/null +++ b/Assets/Scripts/Lantern/EQ/Audio/Xmi/XmiFile.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 3019b5ea3d8644a4ea4919def0a36192 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Scripts/Lantern/EQ/Audio/Xmi/XmiFileReader.cs b/Assets/Scripts/Lantern/EQ/Audio/Xmi/XmiFileReader.cs new file mode 100644 index 0000000..6e82384 --- /dev/null +++ b/Assets/Scripts/Lantern/EQ/Audio/Xmi/XmiFileReader.cs @@ -0,0 +1,93 @@ +using System; +using System.Collections.Generic; +using System.IO; + +namespace Lantern.EQ.Audio.Xmi +{ + public sealed class XmiFileReader : IDisposable + { + //--Fields + private BinaryReader reader; + //--Properties + + //--Methods + public XmiFileReader(string fileName) + { + // if (!Path.GetExtension(fileName).ToLower().Equals(".xmi") || !CrossPlatformHelper.ResourceExists(fileName)) + // throw new InvalidDataException("Invalid xmi file : " + fileName); + // reader = new BinaryReader(CrossPlatformHelper.OpenResource(fileName)); + + var stream = File.Open(fileName, FileMode.Open); + reader = new BinaryReader(stream); + } + public XmiFileReader(Stream stream) + { + reader = new BinaryReader(stream); + } + + public XmiFile ReadXmiFile() + { + return new XmiFile(XmiFileReader.ReadAllChunks(reader)); + } + public Chunk[] ReadAllChunks() + { + return XmiFileReader.ReadAllChunks(reader); + } + public Chunk ReadNextChunk() + { + return XmiFileReader.ReadNextChunk(reader); + } + public void Close() + { + Dispose(); + } + public void Dispose() + { + if (reader == null) + return; + reader.Dispose(); + reader = null; + } + + internal static Chunk[] ReadAllChunks(BinaryReader reader) + { + List chunks = new List(); + + while(reader.BaseStream.Position < reader.BaseStream.Length) + { + Chunk chunk = ReadNextChunk(reader); + if (chunk != null) + chunks.Add(chunk); + } + + return chunks.ToArray(); + } + internal static Chunk ReadNextChunk(BinaryReader reader) + { + string id = new string(XmiHelper.Read8BitChars(reader, 4)); + int size = XmiHelper.ReadInt32BE(reader); + switch (id.ToLower()) + { + case "form": + return new FormChunk(id, size, reader, new Func(ReadNextChunk)); + case "info": + return new InfoChunk(id, size, reader); + case "cat ": + return new CatChunk(id, size, reader, new Func(ReadNextChunk)); + case "timb": + return new TimbreChunk(id, size, reader); + case "rbrn": + return new BranchChunk(id, size, reader); + case "evnt": + return new EventChunk(id, size, reader); + + default: + return new UnknownChunk(id, size, reader); + } + } + internal static XmiFile ReadXmiFile(BinaryReader reader) + { + return new XmiFile(ReadAllChunks(reader)); + } + } +} diff --git a/Assets/Scripts/Lantern/EQ/Audio/Xmi/XmiFileReader.cs.meta b/Assets/Scripts/Lantern/EQ/Audio/Xmi/XmiFileReader.cs.meta new file mode 100644 index 0000000..4ced75e --- /dev/null +++ b/Assets/Scripts/Lantern/EQ/Audio/Xmi/XmiFileReader.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: e445f0fb3f8884442990eb2f343811cf +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Scripts/Lantern/EQ/Audio/Xmi/XmiHelper.cs b/Assets/Scripts/Lantern/EQ/Audio/Xmi/XmiHelper.cs new file mode 100644 index 0000000..291531c --- /dev/null +++ b/Assets/Scripts/Lantern/EQ/Audio/Xmi/XmiHelper.cs @@ -0,0 +1,32 @@ +using System; +using System.IO; + +namespace Lantern.EQ.Audio.Xmi +{ + public static class XmiHelper + { + public static char[] Read8BitChars(BinaryReader reader, int length) + { + char[] chars = new char[length]; + for (int x = 0; x < chars.Length; x++) + chars[x] = (char)reader.ReadByte(); + return chars; + } + + public static int ReadInt32(byte[] input, int index) + { + if (BitConverter.IsLittleEndian) + return input[index] | (input[index + 1] << 8) | (input[index + 2] << 16) | (input[index + 3] << 24); + return (input[index] << 24) | (input[index + 1] << 16) | (input[index + 2] << 8) | input[index + 3]; + } + + public static int ReadInt32BE(BinaryReader reader) + { + var bytes = reader.ReadBytes(4); + if (BitConverter.IsLittleEndian) + Array.Reverse(bytes); + + return ReadInt32(bytes, 0); + } + } +} diff --git a/Assets/Scripts/Lantern/EQ/Audio/Xmi/XmiHelper.cs.meta b/Assets/Scripts/Lantern/EQ/Audio/Xmi/XmiHelper.cs.meta new file mode 100644 index 0000000..f697064 --- /dev/null +++ b/Assets/Scripts/Lantern/EQ/Audio/Xmi/XmiHelper.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 8d1747fb6ce128744ba23fb923c29c72 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Scripts/Lantern/EQ/Characters/CharacterModel.cs b/Assets/Scripts/Lantern/EQ/Characters/CharacterModel.cs index f62d2df..c717113 100644 --- a/Assets/Scripts/Lantern/EQ/Characters/CharacterModel.cs +++ b/Assets/Scripts/Lantern/EQ/Characters/CharacterModel.cs @@ -3,7 +3,6 @@ using Lantern.EQ.Animation; using Lantern.EQ.Equipment; using Lantern.EQ.Lighting; -using Lantern.EQ.Sound; using UnityEngine; namespace Lantern.EQ.Characters @@ -21,8 +20,46 @@ public class CharacterModel : MonoBehaviour public Equipment2dHandler Equipment2dHandler; public Equipment3dHandler Equipment3dHandler; public AmbientLightSetterDynamic AmbientLightSetterDynamic; - public CharacterSoundsBase CharacterSounds; public CharacterAnimationLogic CharacterAnimationLogic; + public CharacterSoundLogic CharacterSoundLogic; + + private void Awake() + { + if (CharacterAnimationLogic != null) + { + CharacterAnimationLogic.SetAnimationFiredCallback(OnAnimationPlayed); + } + } + + private void OnAnimationPlayed(AnimationType animationType) + { + // Propagate to animated equipment + if (Equipment3dHandler != null) + { + Equipment3dHandler.PlayAnimation(animationType); + } + + if (CharacterSoundLogic == null) + { + return; + } + + // Sound is a combination of animation and equipment sound override + if (AnimationHelper.IsAttackAnimation(animationType)) + { + if (animationType == AnimationType.Combat1HSlashOffhand) + { + //CharacterSoundLogic.PlaySound(EquipmentHelper.GetSoundForEquipment(Equipment3dHandler.EquipmentSoundPrimary)); + } + } + + if (AnimationHelper.IsWalkRunInterrupt(animationType)) + { + CharacterSoundLogic.InterruptWalkRunSound(); + } + + CharacterSoundLogic.PlaySound(AnimationHelper.GetSoundFromType(animationType)); + } public void SetLayer(int layer) { @@ -40,15 +77,15 @@ public void SetLayer(int layer) #if UNITY_EDITOR public void SetReferences(SkeletonAttachPoints skeletonAttachPoints, Equipment2dHandler equipment2dHandler, Equipment3dHandler equipment3dHandler, AmbientLightSetterDynamic - ambientLightSetterDynamic, CharacterSoundsBase characterSounds, + ambientLightSetterDynamic, CharacterSoundLogic characterSounds, CharacterAnimationLogic characterAnimationLogic) { SkeletonAttachPoints = skeletonAttachPoints; Equipment2dHandler = equipment2dHandler; Equipment3dHandler = equipment3dHandler; AmbientLightSetterDynamic = ambientLightSetterDynamic; - CharacterSounds = characterSounds; CharacterAnimationLogic = characterAnimationLogic; + CharacterSoundLogic = characterSounds; Renderers = GetComponentsInChildren(true).ToList(); } #endif diff --git a/Assets/Scripts/Lantern/EQ/Characters/CharacterSoundLogic.cs b/Assets/Scripts/Lantern/EQ/Characters/CharacterSoundLogic.cs new file mode 100644 index 0000000..c3c316e --- /dev/null +++ b/Assets/Scripts/Lantern/EQ/Characters/CharacterSoundLogic.cs @@ -0,0 +1,145 @@ +using Lantern.EQ.Audio; +using Lantern.EQ.Sound; +using UnityEngine; + +namespace Lantern.EQ.Characters +{ + public class CharacterSoundLogic : MonoBehaviour + { + private CharacterSoundData _sounds; + private AudioSource _audioSource; + private AudioSource _audioSourceLoop; + private AudioSource _audioSourceWalkRun; + + // TODO: Maybe it's best if for the player, we just disable this script + private bool _isPlayer; + + public void FindAudioClips(ICharacterSoundLocator locator, IAudioSourceProcessor audioSourceProcessor, int raceId, int genderId, int skin, bool isPlayer) + { + _sounds = locator.GetCharacterSounds(raceId, genderId, skin); + CreateAudioSources(isPlayer, audioSourceProcessor); + } + + private void CreateAudioSources(bool isPlayer, IAudioSourceProcessor audioSourceProcessor) + { + if (_sounds == null) + { + return; + } + + _isPlayer = isPlayer; + _audioSource = AudioHelper.AddCharacterAudioSource(gameObject, 6); + _audioSource.spatialBlend = _isPlayer ? 0f : 1f; + audioSourceProcessor?.Process(_audioSource); + + if (_sounds.Sounds.ContainsKey(CharacterSoundType.Loop)) + { + _audioSourceLoop = AudioHelper.AddCharacterAudioSource(gameObject, 6); + _audioSourceLoop.spatialBlend = _isPlayer ? 0f : 1f; + _audioSourceLoop.loop = true; + audioSourceProcessor?.Process(_audioSourceLoop); + PlaySound(CharacterSoundType.Loop); + } + + bool hasWalkRunSounds = _sounds.Sounds.ContainsKey(CharacterSoundType.Walking) || + _sounds.Sounds.ContainsKey(CharacterSoundType.Running); + if (hasWalkRunSounds)// or is an npc type)) + { + _audioSourceWalkRun = AudioHelper.AddCharacterAudioSource(gameObject, 6); + _audioSourceWalkRun.spatialBlend = _isPlayer ? 0f : 1f; + _audioSourceWalkRun.loop = true; + audioSourceProcessor?.Process(_audioSourceWalkRun); + } + } + + private AudioSource GetAudioSourceForType(CharacterSoundType type) + { + if (type == CharacterSoundType.Loop) + { + return _audioSourceLoop; + } + + if (AudioHelper.IsWalkOrRunSound(type)) + { + return _audioSourceWalkRun; + } + + return _audioSource; + } + + public void InterruptWalkRunSound() + { + if (_audioSourceWalkRun != null) + { + _audioSourceWalkRun.clip = null; + _audioSourceWalkRun.Stop(); + } + } + + public void PlaySound(CharacterSoundType type) + { + if (_sounds == null) + { + return; + } + + if (_isPlayer && AudioHelper.IsSilentPlayerSound(type)) + { + return; + } + + if (type == CharacterSoundType.Death) + { + if (_audioSourceLoop != null) + { + _audioSourceLoop.Stop(); + _audioSourceLoop.clip = null; + } + + if (_audioSourceWalkRun != null) + { + _audioSourceWalkRun.Stop(); + _audioSourceWalkRun.clip = null; + } + } + + if (!_sounds.Sounds.TryGetValue(type, out var clips)) + { + return; + } + + if (clips.Count == 0) + { + return; + } + + var clip = clips[Random.Range(0, clips.Count)]; + var source = GetAudioSourceForType(type); + + if (source == null) + { + Debug.LogError($"Invalid audio source for type: {type}"); + return; + } + + if (source.isPlaying) + { + source.Stop(); + } + + if (AudioHelper.IsWalkOrRunSound(type)) + { + if (_isPlayer) + { + return; + } + + _audioSourceWalkRun.clip = clip; + _audioSourceWalkRun.Play(); + } + + source.clip = clip; + source.Play(); + } + } +} diff --git a/Assets/Scripts/Lantern/EQ/Characters/CharacterSoundLogic.cs.meta b/Assets/Scripts/Lantern/EQ/Characters/CharacterSoundLogic.cs.meta new file mode 100644 index 0000000..38e8f23 --- /dev/null +++ b/Assets/Scripts/Lantern/EQ/Characters/CharacterSoundLogic.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 0c3fa8070aae7da49a3e588459be463e +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Scripts/Lantern/EQ/Characters/IAudioSourceProcessor.cs b/Assets/Scripts/Lantern/EQ/Characters/IAudioSourceProcessor.cs new file mode 100644 index 0000000..4ddcfec --- /dev/null +++ b/Assets/Scripts/Lantern/EQ/Characters/IAudioSourceProcessor.cs @@ -0,0 +1,9 @@ +using UnityEngine; + +namespace Lantern.EQ.Characters +{ + public interface IAudioSourceProcessor + { + void Process(AudioSource source); + } +} diff --git a/Assets/Scripts/Lantern/EQ/Characters/IAudioSourceProcessor.cs.meta b/Assets/Scripts/Lantern/EQ/Characters/IAudioSourceProcessor.cs.meta new file mode 100644 index 0000000..b9190fa --- /dev/null +++ b/Assets/Scripts/Lantern/EQ/Characters/IAudioSourceProcessor.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: bd4e16a892fe3ae44895701daa20f06a +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Scripts/Lantern/EQ/Data/EqConstants.cs b/Assets/Scripts/Lantern/EQ/Data/EqConstants.cs index 307d4fa..5f5505e 100644 --- a/Assets/Scripts/Lantern/EQ/Data/EqConstants.cs +++ b/Assets/Scripts/Lantern/EQ/Data/EqConstants.cs @@ -8,7 +8,7 @@ public static class EqConstants public static float DayStartTime = 0.25f; public static float NightStartTime = 0.75f; public static int MaxCharacterCount = 8; - public static int RaceCount = 13; + public static int PlayableRaceCount = 13; public static int ClassCount = 14; public static int FaceCount = 8; public static int GenderCount = 2; @@ -21,6 +21,8 @@ public static class EqConstants public static float TickInterval = 6.0f; public static int SpellEffectCount = 12; public static int SpellLevelLimit = 61; + public static int RaceCount = 198; + public static int SkyCount = 5; // Movement public static float Velocity = 42.65f; @@ -43,7 +45,7 @@ public static class EqConstants public static float AnimationSpeedMultiplier = 1.066666126f; // Camera - public const float CameraPivotIncrement = 1.24f; + public const float CameraHeightIncrement = 1.24f; public const float MainCameraFov = 110f; public const float InventoryCameraFov = 86f; @@ -51,14 +53,41 @@ public static class EqConstants public static float NpcInteractionRange = 20f; public static float ObjectInteractionRange = 20f; public static float DoorInteractionRange = 20f; + public static float ObjectPickupRange = 30f; public static float BeggingRange = 15f; public static float ExperienceRange = 500f; + public static float GroundItemDropHeight = 0.5f; // Models public static string DefaultModelMale = "hum"; public static string DefaultModelFemale = "huf"; + public static string DefaultItemModel = "IT63"; public static int DefaultModelSize = 6; - public static int ZonelineMagicNumber = 999999; + + // Music + public static int MusicFadeQuickMs = 100; + public static int MusicFadeMinMs = 2000; + + // Audio + public static float AudioVolumeCharacter = 0.25f; + public static float AudioVolumeDoor = 0.45f; + public static float AudioVolumeSound2d = 0.1156f; + public static float AudioVolumeSound3d = 0.2275f; + public static double VelocityMinSound = -20f; // TODO: Placeholder + public static double VelocityMinDamage = -100f; // TODO: Placeholder + // Skills + public static int SkillLevelMax = 252; + public static int SkillCannotLearn = 255; + public static int SkillNotYetLearned = 254; + public static int SkillIdMax = 73; + + // Money + public static int CopperPerPlatinum = 1000; + public static int CopperPerGold = 100; + public static int CopperPerSilver = 10; + + // Doors + public static int DoorOpenTypeInvisible = 54; } } diff --git a/Assets/Scripts/Lantern/EQ/Editor/AssetBundles/AssetBundleBuilder.cs b/Assets/Scripts/Lantern/EQ/Editor/AssetBundles/AssetBundleBuilder.cs index 517736a..247263b 100644 --- a/Assets/Scripts/Lantern/EQ/Editor/AssetBundles/AssetBundleBuilder.cs +++ b/Assets/Scripts/Lantern/EQ/Editor/AssetBundles/AssetBundleBuilder.cs @@ -15,11 +15,7 @@ namespace Lantern.EQ.Editor.AssetBundles /// public static class BuildAssetBundles { -#if LANTERN_CLIENT - [MenuItem("Lantern/General/Build Asset Bundles", false, 0)] -#else - [MenuItem("EQ/Build Asset Bundles", false, 100)] -#endif + [MenuItem("EQ/Assets/Build Asset Bundles", false, 75)] public static void BuildAllAssetBundles() { BuildAllAssetBundles(true); diff --git a/Assets/Scripts/Lantern/EQ/Editor/EqAssetCopy/AssetImportType.cs b/Assets/Scripts/Lantern/EQ/Editor/EqAssetCopy/AssetImportType.cs index a077add..7d2c801 100644 --- a/Assets/Scripts/Lantern/EQ/Editor/EqAssetCopy/AssetImportType.cs +++ b/Assets/Scripts/Lantern/EQ/Editor/EqAssetCopy/AssetImportType.cs @@ -6,5 +6,7 @@ public enum AssetImportType Sprite = 1, SpriteSheet = 2, ClientData = 3, + Xmi = 4, + Sound = 5, } } diff --git a/Assets/Scripts/Lantern/EQ/Editor/EqAssetCopy/AssetList.cs b/Assets/Scripts/Lantern/EQ/Editor/EqAssetCopy/AssetList.cs index a0dd80f..b6a2ebd 100644 --- a/Assets/Scripts/Lantern/EQ/Editor/EqAssetCopy/AssetList.cs +++ b/Assets/Scripts/Lantern/EQ/Editor/EqAssetCopy/AssetList.cs @@ -6,7 +6,7 @@ namespace Lantern.EQ.Editor.EqAssetCopy { public static class AssetList { - public static List AssetsToCopy = new List() + public static List AssetsToCopy = new() { new AssetToCopy { @@ -85,13 +85,31 @@ public static class AssetList { EqFolder = "clientdata", AssetBundle = LanternAssetBundleId.ClientData, AssetsToCopy = new List { - "spells.eff", - }, - AssetImportType = AssetImportType.ClientData + "spells.eff" + }, AssetImportType = AssetImportType.ClientData + }, + new AssetToCopy + { + EqFolder = "equipment/textures", AssetBundle = LanternAssetBundleId.Equipment, AssetsToCopy = new List + { + "icopper.png", + "isilver.png", + "igold.png", + "iplat.png", + }, AssetImportType = AssetImportType.Sprite + }, + new AssetToCopy + { + EqFolder = "music", AssetBundle = LanternAssetBundleId.Music_Midi, + AssetImportType = AssetImportType.Xmi + }, + new AssetToCopy() + { + EqFolder = "sounds", AssetBundle = LanternAssetBundleId.Sound, AssetImportType = AssetImportType.Sound }, }; - public static List SpriteSheetsToSplice = new List + public static List SpriteSheetsToSplice = new() { new SpritesSheetsToSplice { @@ -130,7 +148,7 @@ public static class AssetList } }; - public static List SpriteSheetsToCreate = new List + public static List SpriteSheetsToCreate = new() { new SpriteSheetsToCreate { diff --git a/Assets/Scripts/Lantern/EQ/Editor/EqAssetCopy/EqAssetsCopy.cs b/Assets/Scripts/Lantern/EQ/Editor/EqAssetCopy/EqAssetsCopy.cs index 621b8d4..9fff131 100644 --- a/Assets/Scripts/Lantern/EQ/Editor/EqAssetCopy/EqAssetsCopy.cs +++ b/Assets/Scripts/Lantern/EQ/Editor/EqAssetCopy/EqAssetsCopy.cs @@ -6,10 +6,45 @@ namespace Lantern.EQ.Editor.EqAssetCopy { - public static class EqAssetCopier + public class EqAssetCopier : LanternEditorWindow { - [MenuItem("EQ/Copy EQ Assets", false, 30)] - public static void Copy() + private static readonly List Text1 = new() + { + "This process copies and prepares exported EverQuest assets for LanternEQ runtime use. Created bundles include:", + "\fCharacterSelect_Classic", + "\fClientData", + "\fMusic_Midi", + "\fSound", + "\fSprites", + "\fStartup", + "This usually takes around 5-10 minutes." + }; + + private static readonly List Text2 = new() + { + "All EverQuest assets must be located in:", + "\fAssets/EQAssets/", + }; + + [MenuItem("EQ/Assets/Copy Assets", false, 20)] + public static void ShowImportDialog() + { + GetWindow("Copy Assets", typeof(EditorWindow)); + } + + private void OnGUI() + { + DrawInfoBox(Text1, "d_console.infoicon"); + DrawInfoBox(Text2, "d_Collab.FolderConflict"); + DrawHorizontalLine(); + + if (DrawButton("Start Copy")) + { + Copy(); + } + } + + private void Copy() { if (Application.isPlaying) { @@ -17,6 +52,7 @@ public static void Copy() return; } + StartImport(); var startTime = EditorApplication.timeSinceStartup; var sourceRootFolder = PathHelper.GetSystemPathFromUnity(PathHelper.GetEqAssetPath()); @@ -28,11 +64,20 @@ public static void Copy() { var sourcePath = Path.Combine(sourceRootFolder, atc.EqFolder); var destPath = Path.Combine(destRootFolder, atc.AssetBundle.ToString()); - foreach (var file in atc.AssetsToCopy) + + // If no assets are specified, copy the whole folder + if (atc.AssetsToCopy == null || atc.AssetsToCopy.Count == 0) { - if (!CopyAssetToBundle(sourcePath, destPath, file, atc.AssetImportType)) + CopyFolderToBundle(sourcePath, destPath, atc.AssetImportType); + } + else + { + foreach (var file in atc.AssetsToCopy) { - failedToCopy.Add(file); + if (!CopyAssetToBundle(sourcePath, destPath, file, atc.AssetImportType)) + { + failedToCopy.Add(file); + } } } } @@ -58,7 +103,7 @@ public static void Copy() if (ss.AssetIndices != null) { ss.AssetsToPack ??= new List(); - foreach(var i in ss.AssetIndices) + foreach (var i in ss.AssetIndices) { ss.AssetsToPack.Add($"{ss.AssetBase}{i:00}.png"); } @@ -77,17 +122,41 @@ public static void Copy() } AssetDatabase.Refresh(); - ImportHelper.TagAllAssetsForBundles(PathHelper.GetAssetBundleContentPath()+ "Sprites", "sprites"); - ImportHelper.TagAllAssetsForBundles(PathHelper.GetAssetBundleContentPath()+ "Startup", "startup"); - ImportHelper.TagAllAssetsForBundles(PathHelper.GetAssetBundleContentPath()+ "ClientData", "clientdata"); - ImportHelper.TagAllAssetsForBundles(PathHelper.GetAssetBundleContentPath()+ "CharacterSelect_Classic", "characterselect_classic"); + ImportHelper.TagAllAssetsForBundles(PathHelper.GetAssetBundleContentPath() + "CharacterSelect_Classic", + "characterselect_classic"); + ImportHelper.TagAllAssetsForBundles(PathHelper.GetAssetBundleContentPath() + "ClientData", "clientdata"); + ImportHelper.TagAllAssetsForBundles(PathHelper.GetAssetBundleContentPath() + "Music_Midi", "music_midi"); + ImportHelper.TagAllAssetsForBundles(PathHelper.GetAssetBundleContentPath() + "Sound", "sound"); + ImportHelper.TagAllAssetsForBundles(PathHelper.GetAssetBundleContentPath() + "Sprites", "sprites"); + ImportHelper.TagAllAssetsForBundles(PathHelper.GetAssetBundleContentPath() + "Startup", "startup"); AssetDatabase.Refresh(); + var importTime = FinishImport(); EditorUtility.DisplayDialog("EQAssetsCopy", - $"EQ asset copy finished in {(int) (EditorApplication.timeSinceStartup - startTime)} seconds", "OK"); + $"EQ asset copy finished in {importTime} seconds", "OK"); + } + + private static bool CopyFolderToBundle(string sourcePath, string destPath, AssetImportType assetImportType) + { + if (!Directory.Exists(sourcePath)) + { + return false; + } + + var files = Directory.GetFiles(sourcePath); + + foreach (var file in files) + { + if (!CopyAssetToBundle(sourcePath, destPath, Path.GetFileName(file), assetImportType, true)) + { + return false; + } + } + + return true; } private static bool CopyAssetToBundle(string sourcePath, string destPath, string file, - AssetImportType assetImportType) + AssetImportType assetImportType, bool overwriteExisting = false) { if (!Directory.Exists(sourcePath)) { @@ -110,12 +179,18 @@ private static bool CopyAssetToBundle(string sourcePath, string destPath, string if (File.Exists(destinationFile)) { - return true; + if (!overwriteExisting) + { + return true; + } + + File.Delete(destinationFile); } File.Copy(sourceFile, destinationFile); var unityPath = PathHelper.GetUnityPathFromSystem(destPath); + // Move into postprocess switch (assetImportType) { case AssetImportType.Texture2d: @@ -186,6 +261,7 @@ private static bool CopyAssetToBundle(string sourcePath, string destPath, string importer.SaveAndReimport(); } } + break; } } diff --git a/Assets/Scripts/Lantern/EQ/Editor/Helpers/ImportHelper.cs b/Assets/Scripts/Lantern/EQ/Editor/Helpers/ImportHelper.cs index 6415d0a..6c51ee0 100644 --- a/Assets/Scripts/Lantern/EQ/Editor/Helpers/ImportHelper.cs +++ b/Assets/Scripts/Lantern/EQ/Editor/Helpers/ImportHelper.cs @@ -1,4 +1,7 @@ -using System.IO; +using System.Collections.Generic; +using System.IO; +using Infrastructure.EQ.TextParser; +using Lantern.EQ.Editor.Importers; using UnityEditor; using UnityEngine; @@ -98,5 +101,19 @@ public static void TagAllAssetsForBundles(string folderPath, string bundleTag) importer.SetAssetBundleNameAndVariant(bundleTag, string.Empty); importer.SaveAndReimport(); } + + public static List GetBatchZoneNames(ZoneBatchType zoneBatchType) + { + var type = zoneBatchType.ToString().ToLower(); + if (LoadTextAsset($"Assets/Content/ClientData/zonelist_{type}.txt", + out var allShortnames)) + { + return TextParser.ParseTextByNewline(allShortnames); + } + + Debug.LogError($"ZoneImporter: Unable to load zone list for specifier: {type}"); + return new List(); + + } } } diff --git a/Assets/Scripts/Lantern/EQ/Editor/Helpers/PathHelper.cs b/Assets/Scripts/Lantern/EQ/Editor/Helpers/PathHelper.cs index 6f1236c..f3708b7 100644 --- a/Assets/Scripts/Lantern/EQ/Editor/Helpers/PathHelper.cs +++ b/Assets/Scripts/Lantern/EQ/Editor/Helpers/PathHelper.cs @@ -1,5 +1,6 @@ using System; -using Lantern.Editor.Importers; +using System.IO; +using Lantern.EQ.Editor.Importers; using UnityEngine; namespace Lantern.EQ.Editor.Helpers @@ -109,6 +110,11 @@ public static string GetClientDataPath() return "Assets/Content/ClientData/"; } + public static string GetRuntimeClientDataPath() + { + return Path.Combine(Application.streamingAssetsPath, "ClientData"); + } + public static string GetSystemPathFromUnity(string unityPath) { // Trims the Assets prefix as it's included in the data path as well diff --git a/Assets/Scripts/Lantern/EQ/Editor/Helpers/TextureHelper.cs b/Assets/Scripts/Lantern/EQ/Editor/Helpers/TextureHelper.cs index 816f200..27ade0e 100644 --- a/Assets/Scripts/Lantern/EQ/Editor/Helpers/TextureHelper.cs +++ b/Assets/Scripts/Lantern/EQ/Editor/Helpers/TextureHelper.cs @@ -1,7 +1,7 @@ using System.IO; using System.Linq; using System.Text; -using Lantern.Editor.Importers; +using Lantern.EQ.Editor.Importers; using UnityEditor; using UnityEngine; @@ -50,7 +50,7 @@ public static Texture GetTexture(string shortname, AssetImportType importType, s // Check in global texture folder string globalPath = Path.Combine(PathHelper.GetEqAssetPath(), "equipment/Textures/") + textureName + ".png"; globalPath = PathHelper.GetSystemPathFromUnity(globalPath); - + if (File.Exists(globalPath)) { CopyAndRefresh(destination, globalPath, unityDestinationPath, isMasked); @@ -100,7 +100,7 @@ public static Texture FindFaceVariant(string modelName, Texture texture, int ind // Edge case: Iksar females use the Iksar Citizen Female base texture. // Looking for additional textures with the ICF prefix won't find the - // additional faces. Instead, we must look for the IKF prefix. + // additional variants. Instead, we must look for the IKF prefix. if (modelName == "ikf" && texture.name == "icfhe0001") { textureName = "ikfhe0001"; @@ -112,7 +112,7 @@ public static Texture FindFaceVariant(string modelName, Texture texture, int ind return faceTexture; } - public static Texture FindEquipmentVariant(Texture texture, int index, string requiredString) + public static Texture FindEquipmentVariant(string modelName, Texture texture, int index, string requiredString) { if (texture == null) { @@ -120,14 +120,25 @@ public static Texture FindEquipmentVariant(Texture texture, int index, string re return null; } - if (texture.name.StartsWith("clkerf") || texture.name.StartsWith("clkerm")) + string textureName = texture.name; + StringBuilder variantName = new StringBuilder(textureName); + + // Edge case: Iksar females use the Iksar Citizen Female base texture. + // Looking for additional textures with the ICF prefix won't find the + // additional variants. Instead, we must look for the IKF prefix. + if (modelName == "ikf" && textureName.StartsWith("icf")) + { + variantName[1] = 'k'; + } + + if (textureName.StartsWith("clkerf") || textureName.StartsWith("clkerm")) { if (index == 0) { return texture; } - if (!string.IsNullOrEmpty(requiredString) && !texture.name.Contains(requiredString)) + if (!string.IsNullOrEmpty(requiredString) && !textureName.Contains(requiredString)) { return null; } @@ -135,8 +146,6 @@ public static Texture FindEquipmentVariant(Texture texture, int index, string re return GetTexture("all", AssetImportType.Characters, "clk" + index.ToString("00") + "06", false, false); } - string textureName = texture.name; - StringBuilder variantName = new StringBuilder(textureName); string index10 = index.ToString("00"); variantName[variantName.Length - 4] = index10.FirstOrDefault(); variantName[variantName.Length - 3] = index10.LastOrDefault(); diff --git a/Assets/Scripts/Lantern/EQ/Editor/Importers/ActorSkeletalImporter.cs b/Assets/Scripts/Lantern/EQ/Editor/Importers/ActorSkeletalImporter.cs index fe1310f..98d6e90 100644 --- a/Assets/Scripts/Lantern/EQ/Editor/Importers/ActorSkeletalImporter.cs +++ b/Assets/Scripts/Lantern/EQ/Editor/Importers/ActorSkeletalImporter.cs @@ -4,7 +4,7 @@ using UnityEditor; using UnityEngine; -namespace Lantern.Editor.Importers +namespace Lantern.EQ.Editor.Importers { public static class ActorSkeletalImporter { diff --git a/Assets/Scripts/Lantern/EQ/Editor/Importers/ActorStaticImporter.cs b/Assets/Scripts/Lantern/EQ/Editor/Importers/ActorStaticImporter.cs index 1748f5d..737483b 100644 --- a/Assets/Scripts/Lantern/EQ/Editor/Importers/ActorStaticImporter.cs +++ b/Assets/Scripts/Lantern/EQ/Editor/Importers/ActorStaticImporter.cs @@ -4,7 +4,7 @@ using UnityEditor; using UnityEngine; -namespace Lantern.Editor.Importers +namespace Lantern.EQ.Editor.Importers { public static class ActorStaticImporter { diff --git a/Assets/Scripts/Lantern/EQ/Editor/Importers/AnimationImporter.cs b/Assets/Scripts/Lantern/EQ/Editor/Importers/AnimationImporter.cs index 9d661ce..a1d6336 100644 --- a/Assets/Scripts/Lantern/EQ/Editor/Importers/AnimationImporter.cs +++ b/Assets/Scripts/Lantern/EQ/Editor/Importers/AnimationImporter.cs @@ -2,7 +2,6 @@ using System.Collections.Generic; using System.IO; using Infrastructure.EQ.TextParser; -using Lantern.Editor.Importers; using Lantern.EQ.Animation; using Lantern.EQ.Editor.Helpers; using UnityEditor; diff --git a/Assets/Scripts/Lantern/EQ/Editor/Importers/AssetImportType.cs b/Assets/Scripts/Lantern/EQ/Editor/Importers/AssetImportType.cs index 189349a..f7d1411 100644 --- a/Assets/Scripts/Lantern/EQ/Editor/Importers/AssetImportType.cs +++ b/Assets/Scripts/Lantern/EQ/Editor/Importers/AssetImportType.cs @@ -1,4 +1,4 @@ -namespace Lantern.Editor.Importers +namespace Lantern.EQ.Editor.Importers { public enum AssetImportType { diff --git a/Assets/Scripts/Lantern/EQ/Editor/Importers/CharacterImporter.cs b/Assets/Scripts/Lantern/EQ/Editor/Importers/CharacterImporter.cs index 8663671..96f26d0 100644 --- a/Assets/Scripts/Lantern/EQ/Editor/Importers/CharacterImporter.cs +++ b/Assets/Scripts/Lantern/EQ/Editor/Importers/CharacterImporter.cs @@ -4,9 +4,7 @@ using Infrastructure.EQ.TextParser; using Lantern.EQ.Animation; using Lantern.EQ.Characters; -using Lantern.EQ.Data; using Lantern.EQ.Editor.Helpers; -using Lantern.EQ.Editor.Importers; using Lantern.EQ.Equipment; using Lantern.EQ.Helpers; using Lantern.EQ.Lighting; @@ -14,9 +12,9 @@ using UnityEditor; using UnityEngine; -namespace Lantern.Editor.Importers +namespace Lantern.EQ.Editor.Importers { - public class CharacterImporter : EditorWindow + public class CharacterImporter : LanternEditorWindow { /// /// Zone for which characters will be imported @@ -44,10 +42,39 @@ public class CharacterImporter : EditorWindow /// private static List _animationPaths; - [MenuItem("EQ/Import/Characters", false, 50)] + private bool _importAllCharacters = true; + + private static readonly List Lines1 = new() + { + "This process creates character prefabs from intermediate EverQuest data.", + "Importing all characters takes around an hour and a half.", + "You can choose to import all characters or just characters from a specific zone.", + }; + + private static readonly List Lines2A = new() + { + "EverQuest character data must be located in:", + "\fAssets/EQAssets/Characters/", + }; + + private static readonly List Lines2B = new() + { + "EverQuest character data (one character folder per zone) must be located in:", + "\fAssets/EQAssets/", + }; + + private static readonly List Lines3 = new() + { + "Character prefabs will be created in:", + "\fAssets/Content/AssetBundleContent/Characters/" + }; + + private static readonly int BaseMap = Shader.PropertyToID("_BaseMap"); + + [MenuItem("EQ/Assets/Import Characters &c", false, 2)] public static void ShowImportDialog() { - GetWindow(typeof(CharacterImporter), true, "Import Characters"); + GetWindow("Import Characters", typeof(EditorWindow)); } /// @@ -55,34 +82,34 @@ public static void ShowImportDialog() /// private void OnGUI() { - int minHeight = 60; - minSize = maxSize = new Vector2(225, minHeight); - EditorGUIUtility.labelWidth = 100; - _zoneShortname = EditorGUILayout.TextField("Zone Shortname", _zoneShortname); - EditorGUILayout.Space(); - - Rect r = EditorGUILayout.BeginHorizontal("Button"); - if (GUI.Button(r, GUIContent.none)) + DrawInfoBox(Lines1, "d_console.infoicon"); + DrawInfoBox(_importAllCharacters ? Lines2A : Lines2B, "d_Collab.FolderConflict"); + DrawInfoBox(Lines3, "d_Collab.FolderMoved"); + DrawHorizontalLine(); + DrawToggle("Import All Characters", ref _importAllCharacters); + + if (!_importAllCharacters) { - Close(); - Import(); + DrawTextField("Zone Name:", ref _zoneShortname); } - GUILayout.Label("Import"); - EditorGUILayout.EndHorizontal(); - EditorGUILayout.Space(); + if (DrawButton("Import")) + { + ImportCharacters(); + } } - private static void Import() + private void ImportCharacters() { + StartImport(); LoadData(); - var startTime = EditorApplication.timeSinceStartup; TextureHelper.CopyTextures(_zoneShortname, AssetImportType.Characters); ActorStaticImporter.ImportList("characters", AssetImportType.Characters, PostProcess); ActorSkeletalImporter.ImportList("characters", AssetImportType.Characters, PostProcessSkeletal); ImportHelper.TagAllAssetsForBundles(PathHelper.GetAssetBundleContentPath()+ "Characters", "characters"); + var importTime = FinishImport(); EditorUtility.DisplayDialog("CharacterImport", - $"Character import finished in {(int)(EditorApplication.timeSinceStartup - startTime)} seconds", "OK"); + $"Character import finished in {importTime} seconds", "OK"); Cleanup(); } @@ -102,42 +129,35 @@ private static void Cleanup() _animationPaths.Clear(); } - private static void PostProcess(GameObject obj) + private static void PostProcess(GameObject character) { - string modelAsset = obj.name; - var characterModel = obj.AddComponent(); - - var lightSetter = obj.AddComponent(); + var characterModel = character.AddComponent(); + var lightSetter = character.AddComponent(); lightSetter.FindRenderers(); - - // Disabled in 0.1.5 - //LoadCharacterSounds(obj, _modelSounds); - characterModel.SetReferences(null, null, null, lightSetter, - obj.GetComponent(), - obj.GetComponent()); + character.GetComponent(), + character.GetComponent()); } - private static void PostProcessSkeletal(GameObject skeleton) + private static void PostProcessSkeletal(GameObject character) { - string modelAsset = skeleton.name; - var characterModel = skeleton.AddComponent(); - - VariantHandler variantHandler = null; + string modelAsset = character.name; + var characterModel = character.AddComponent(); - var cac = skeleton.AddComponent(); - var cal = skeleton.AddComponent(); + var cac = character.AddComponent(); + var cal = character.AddComponent(); cal.InitializeImport(); - var ap = skeleton.AddComponent(); + var ap = character.AddComponent(); ap.FindAttachPoints(); - var e3d = skeleton.AddComponent(); + var e3d = character.AddComponent(); e3d.SetSkeletonAttachPoints(ap); + character.AddComponent(); // TODO: Is this still needed? - var animatedObject = skeleton.GetComponent(); + var animatedObject = character.GetComponent(); DestroyImmediate(animatedObject); - var animation = skeleton.GetComponent(); + var animation = character.GetComponent(); // Find all meshes for this model string path = _zoneShortname == "characters" @@ -157,16 +177,16 @@ private static void PostProcessSkeletal(GameObject skeleton) { LoadModelAnimations(animation, modelAsset, AssetImportType.Characters); - if (_animationModelSources.ContainsKey(modelAsset)) + if (_animationModelSources.TryGetValue(modelAsset, out var source)) { - LoadModelAnimations(animation, _animationModelSources[modelAsset], AssetImportType.Characters); + LoadModelAnimations(animation, source, AssetImportType.Characters); } } // Initialize the animation controller cac.InitializeImport(); - variantHandler = skeleton.GetComponent(); + var variantHandler = character.GetComponent(); if (RaceHelper.IsPlayableRaceModel(modelAsset)) { @@ -174,17 +194,15 @@ private static void PostProcessSkeletal(GameObject skeleton) FindAdditionalFaces(modelAsset, variantHandler.GetLastPrimaryMesh(), variantHandler); } - var vertexColorDebug = skeleton.AddComponent(); + var vertexColorDebug = character.AddComponent(); vertexColorDebug.FindRenderers(); - LoadCharacterSounds(skeleton, _modelSounds); - characterModel.SetReferences(ap, variantHandler as Equipment2dHandler, e3d, vertexColorDebug, - skeleton.GetComponent(), - skeleton.GetComponent()); + character.GetComponent(), + character.GetComponent()); } - private static void LoadModelAnimations(Animation animation, string animationBase, AssetImportType type) + private static void LoadModelAnimations(UnityEngine.Animation animation, string animationBase, AssetImportType type) { string prefix = animationBase + "_"; @@ -248,18 +266,18 @@ private static void FindEquipmentTextures(string modelAsset, VariantHandler vari return; } - GetEquipmentVariantsForIndex(0, 0, mainMesh, pvh); - GetEquipmentVariantsForIndex(1, 1, mainMesh, pvh); - GetEquipmentVariantsForIndex(2, 2, mainMesh, pvh); - GetEquipmentVariantsForIndex(3, 3, mainMesh, pvh); - GetEquipmentVariantsForIndex(4, 4, mainMesh, pvh); - GetEquipmentVariantsForIndex(17, 17, mainMesh, pvh); - GetEquipmentVariantsForIndex(18, 18, mainMesh, pvh); - GetEquipmentVariantsForIndex(19, 19, mainMesh, pvh); - GetEquipmentVariantsForIndex(20, 20, mainMesh, pvh); - GetEquipmentVariantsForIndex(21, 21, mainMesh, pvh); - GetEquipmentVariantsForIndex(22, 22, mainMesh, pvh); - GetEquipmentVariantsForIndex(23, 23, mainMesh, pvh); + GetEquipmentVariantsForIndex(modelAsset, 0, 0, mainMesh, pvh); + GetEquipmentVariantsForIndex(modelAsset, 1, 1, mainMesh, pvh); + GetEquipmentVariantsForIndex(modelAsset, 2, 2, mainMesh, pvh); + GetEquipmentVariantsForIndex(modelAsset, 3, 3, mainMesh, pvh); + GetEquipmentVariantsForIndex(modelAsset, 4, 4, mainMesh, pvh); + GetEquipmentVariantsForIndex(modelAsset, 17, 17, mainMesh, pvh); + GetEquipmentVariantsForIndex(modelAsset, 18, 18, mainMesh, pvh); + GetEquipmentVariantsForIndex(modelAsset, 19, 19, mainMesh, pvh); + GetEquipmentVariantsForIndex(modelAsset, 20, 20, mainMesh, pvh); + GetEquipmentVariantsForIndex(modelAsset, 21, 21, mainMesh, pvh); + GetEquipmentVariantsForIndex(modelAsset, 22, 22, mainMesh, pvh); + GetEquipmentVariantsForIndex(modelAsset, 23, 23, mainMesh, pvh); var robeMesh = pvh.GetRobeMesh(); @@ -269,29 +287,30 @@ private static void FindEquipmentTextures(string modelAsset, VariantHandler vari } // First skin has all materials (hands + feet) - GetEquipmentVariantsForIndex(10, 4, robeMesh, pvh); - GetEquipmentVariantsForIndex(11, 5, robeMesh, pvh, "clk"); - GetEquipmentVariantsForIndex(12, 6, robeMesh, pvh, "clk"); - GetEquipmentVariantsForIndex(13, 7, robeMesh, pvh, "clk"); - GetEquipmentVariantsForIndex(14, 8, robeMesh, pvh, "clk"); - GetEquipmentVariantsForIndex(15, 9, robeMesh, pvh, "clk"); - GetEquipmentVariantsForIndex(16, 10, robeMesh, pvh, "clk"); - + GetEquipmentVariantsForIndex(modelAsset, 10, 4, robeMesh, pvh); + GetEquipmentVariantsForIndex(modelAsset, 11, 5, robeMesh, pvh, "clk"); + GetEquipmentVariantsForIndex(modelAsset, 12, 6, robeMesh, pvh, "clk"); + GetEquipmentVariantsForIndex(modelAsset, 13, 7, robeMesh, pvh, "clk"); + GetEquipmentVariantsForIndex(modelAsset, 14, 8, robeMesh, pvh, "clk"); + GetEquipmentVariantsForIndex(modelAsset, 15, 9, robeMesh, pvh, "clk"); + GetEquipmentVariantsForIndex(modelAsset, 16, 10, robeMesh, pvh, "clk"); + + // Erudite edge case if (modelAsset == "erm" || modelAsset == "erf") { var headMesh = pvh.GetHeadMesh(0); - GetEquipmentVariantsForIndex(0, 0, headMesh, pvh); - GetEquipmentVariantsForIndex(10, 4, headMesh, pvh, "clk"); - GetEquipmentVariantsForIndex(11, 5, headMesh, pvh, "clk"); - GetEquipmentVariantsForIndex(12, 6, headMesh, pvh, "clk"); - GetEquipmentVariantsForIndex(13, 7, headMesh, pvh, "clk"); - GetEquipmentVariantsForIndex(14, 8, headMesh, pvh, "clk"); - GetEquipmentVariantsForIndex(15, 9, headMesh, pvh, "clk"); - GetEquipmentVariantsForIndex(16, 10, headMesh, pvh, "clk"); + GetEquipmentVariantsForIndex(modelAsset, 0, 0, headMesh, pvh); + GetEquipmentVariantsForIndex(modelAsset, 10, 4, headMesh, pvh, "clk"); + GetEquipmentVariantsForIndex(modelAsset, 11, 5, headMesh, pvh, "clk"); + GetEquipmentVariantsForIndex(modelAsset, 12, 6, headMesh, pvh, "clk"); + GetEquipmentVariantsForIndex(modelAsset, 13, 7, headMesh, pvh, "clk"); + GetEquipmentVariantsForIndex(modelAsset, 14, 8, headMesh, pvh, "clk"); + GetEquipmentVariantsForIndex(modelAsset, 15, 9, headMesh, pvh, "clk"); + GetEquipmentVariantsForIndex(modelAsset, 16, 10, headMesh, pvh, "clk"); } } - private static void GetEquipmentVariantsForIndex(int armorIndex, int textureIndex, SkinnedMeshRenderer mesh, + private static void GetEquipmentVariantsForIndex(string modelAsset, int armorIndex, int textureIndex, SkinnedMeshRenderer mesh, Equipment2dHandler handler, string requiredString = "") { var materials = mesh.sharedMaterials; @@ -299,12 +318,13 @@ private static void GetEquipmentVariantsForIndex(int armorIndex, int textureInde for (int i = 0; i < materials.Length; i++) { - if (materials[i] == null) + var material = materials[i]; + if (material == null) { continue; } - textures[i] = TextureHelper.FindEquipmentVariant(materials[i].GetTexture("_BaseMap"), textureIndex, + textures[i] = TextureHelper.FindEquipmentVariant(modelAsset, material.GetTexture(BaseMap), textureIndex, requiredString); } @@ -344,7 +364,7 @@ private static void FindAdditionalFaces(string modelName, GameObject lastPrimary continue; } - firstMaterials[i] = sharedMaterials[i].GetTexture("_BaseMap"); + firstMaterials[i] = sharedMaterials[i].GetTexture(BaseMap); } faces.Add(firstMaterials); @@ -379,244 +399,6 @@ private static void FindAdditionalFaces(string modelName, GameObject lastPrimary (nonPlayableVariantHandler as Equipment2dHandler)?.SetAdditionalFaces(faces); } - private static void LoadCharacterSounds(GameObject skeleton, List soundData) - { - // Sound script is added regardless - /* string modelName = skeleton.name; - int raceId = ServiceFa RaceHelper.GetRaceIdFromModelName(skeleton.name); - - if (raceId == 0) - { - return; - } - - CharacterSoundsBase soundBaseScript = null; - - List validSounds = GetAllSoundsForRace(raceId.Value, soundData); - - if (validSounds.Count == 0) - { - soundBaseScript = skeleton.AddComponent(); - return; - } - else - { - soundBaseScript = skeleton.AddComponent(); - } - - // Handle gender variants - if (validSounds.Count > 1) - { - GenderId? gender = RaceHelper.GetGenderFromModel(modelName.ToUpper()); - - if (!gender.HasValue) - { - Debug.LogError("No gender found for model: " + modelName); - return; - } - - List genderSounds = new List(); - - foreach (var soundEntry in validSounds) - { - if (soundEntry.GenderId == gender.Value) - { - genderSounds.Add(soundEntry); - } - } - - validSounds = genderSounds; - - if (validSounds.Count == 0) - { - Debug.LogError($"CharacterImport: No sounds found for race and gender: {modelName} {gender.Value}"); - return; - } - } - - foreach (var validSound in validSounds) - { - AddSoundToCharacterSounds(validSound.Attack, CharacterSoundType.Attack, soundBaseScript, - validSound.VariantId); - AddSoundToCharacterSounds(validSound.GetHit1, CharacterSoundType.GetHit, soundBaseScript, - validSound.VariantId); - AddSoundToCharacterSounds(validSound.GetHit2, CharacterSoundType.GetHit, soundBaseScript, - validSound.VariantId); - AddSoundToCharacterSounds(validSound.GetHit3, CharacterSoundType.GetHit, soundBaseScript, - validSound.VariantId); - AddSoundToCharacterSounds(validSound.GetHit4, CharacterSoundType.GetHit, soundBaseScript, - validSound.VariantId); - AddSoundToCharacterSounds(validSound.Death, CharacterSoundType.Death, soundBaseScript, - validSound.VariantId); - AddSoundToCharacterSounds(validSound.Drown, CharacterSoundType.Drown, soundBaseScript, - validSound.VariantId); - AddSoundToCharacterSounds(validSound.Idle1, CharacterSoundType.Idle, soundBaseScript, - validSound.VariantId); - AddSoundToCharacterSounds(validSound.Idle2, CharacterSoundType.Idle, soundBaseScript, - validSound.VariantId); - AddSoundToCharacterSounds(validSound.Loop, CharacterSoundType.Loop, soundBaseScript, - validSound.VariantId); - AddSoundToCharacterSounds(validSound.Walking, CharacterSoundType.Walking, soundBaseScript, - validSound.VariantId); - AddSoundToCharacterSounds(validSound.Running, CharacterSoundType.Running, soundBaseScript, - validSound.VariantId); - AddSoundToCharacterSounds(validSound.Jump, CharacterSoundType.Jump, soundBaseScript, - validSound.VariantId); - AddSoundToCharacterSounds(validSound.SAttack, CharacterSoundType.SAttack, soundBaseScript, - validSound.VariantId); - AddSoundToCharacterSounds(validSound.TAttack, CharacterSoundType.TAttack, soundBaseScript, - validSound.VariantId); - - // Sitting test - CharacterAnimationController animationsController = - skeleton.GetComponent(); - - if (animationsController == null) - { - return; - } - - if (animationsController.HasAnimation("p02")) - { - string sittingSound = modelName.ToLower() == "ske" ? "Skel_Std.wav" : "StepCrch.WAV"; - AddSoundToCharacterSounds(sittingSound, CharacterSoundType.Sit, soundBaseScript, - validSound.VariantId); - } - - // Crouch walk - if (animationsController.HasAnimation("l06")) - { - AddSoundToCharacterSounds("StepCrch.WAV", CharacterSoundType.Crouch, soundBaseScript, - validSound.VariantId); - } - - // Treading swim - if (animationsController.HasAnimation("l09")) - { - AddSoundToCharacterSounds("WatTrd_1.WAV", CharacterSoundType.Treading, soundBaseScript, - validSound.VariantId); - } - - // Moving swim - if (animationsController.HasAnimation("p06")) - { - AddSoundToCharacterSounds("WatTrd_2.WAV", CharacterSoundType.Swim, soundBaseScript, - validSound.VariantId); - } - - // Kneel - if (animationsController.HasAnimation("p05")) - { - AddSoundToCharacterSounds("StepCrch.WAV", CharacterSoundType.Kneel, soundBaseScript, - validSound.VariantId); - } - - // Kick - if (animationsController.HasAnimation("c01")) - { - AddSoundToCharacterSounds("Kick1.WAV", CharacterSoundType.Kick, soundBaseScript, - validSound.VariantId); - } - - // Pierce - if (animationsController.HasAnimation("c02")) - { - AddSoundToCharacterSounds("Stab.WAV", CharacterSoundType.Pierce, soundBaseScript, - validSound.VariantId); - } - - // 2H slash - if (animationsController.HasAnimation("c03")) - { - AddSoundToCharacterSounds("SwingBig.WAV", CharacterSoundType.TwoHandSlash, soundBaseScript, - validSound.VariantId); - } - - // 2H blunt - if (animationsController.HasAnimation("c04")) - { - AddSoundToCharacterSounds("Impale.WAV", CharacterSoundType.TwoHandBlunt, soundBaseScript, - validSound.VariantId); - } - - // Bash? - if (animationsController.HasAnimation("c07")) - { - AddSoundToCharacterSounds("BashShld.WAV", CharacterSoundType.Bash, soundBaseScript, - validSound.VariantId); - } - - // Archery - if (animationsController.HasAnimation("c09")) - { - AddSoundToCharacterSounds("BowDraw.WAV", CharacterSoundType.Archery, soundBaseScript, - validSound.VariantId); - } - - // Flying kick - if (animationsController.HasAnimation("t07")) - { - AddSoundToCharacterSounds("RndKick.WAV", CharacterSoundType.FlyingKick, soundBaseScript, - validSound.VariantId); - } - - // Rapid punch - if (animationsController.HasAnimation("t08")) - { - AddSoundToCharacterSounds("Punch1.WAV", CharacterSoundType.RapidPunch, soundBaseScript, - validSound.VariantId); - } - - // Large punch - if (animationsController.HasAnimation("t09")) - { - AddSoundToCharacterSounds("Punch1.WAV", CharacterSoundType.LargePunch, soundBaseScript, - validSound.VariantId); - } - }*/ - } - - // Move this to the parser - private static List GetAllSoundsForRace(int raceId, - List soundData) - { - List raceSounds = new List(); - - foreach (var entry in soundData) - { - if (entry.RaceId == raceId) - { - raceSounds.Add(entry); - } - } - - return raceSounds; - } - - private static void AddSoundToCharacterSounds(string soundName, CharacterSoundType characterSoundType, - CharacterSoundsBase soundBaseScript, int variant) - { - if (string.IsNullOrEmpty(soundName)) - { - return; - } - - if (soundName.ToLower().Contains("null")) - { - return; - } - - string realSoundId = soundName.ToLower().Substring(0, soundName.Length - 3) + "ogg"; - - AudioClip soundClip = - AssetDatabase.LoadAssetAtPath(PathHelper.GetAssetBundleContentPath()+ "Sound/" + realSoundId); - - if (soundClip != null) - { - soundBaseScript.AddSoundClip(characterSoundType, soundClip, variant); - } - } - private static void CreateModelAnimationLink() { _animationModelSources = new Dictionary(); diff --git a/Assets/Scripts/Lantern/EQ/Editor/Importers/DoorImporter.cs b/Assets/Scripts/Lantern/EQ/Editor/Importers/DoorImporter.cs index 2b45b28..a258485 100644 --- a/Assets/Scripts/Lantern/EQ/Editor/Importers/DoorImporter.cs +++ b/Assets/Scripts/Lantern/EQ/Editor/Importers/DoorImporter.cs @@ -1,104 +1,41 @@ -namespace Lantern.Editor.Importers +using System.IO; +using Lantern.EQ.Data; +using Lantern.EQ.Editor.Helpers; +using Lantern.EQ.Helpers; +using Lantern.EQ.Lantern; +using Lantern.EQ.Viewers; +using UnityEditor; +using UnityEngine; + +namespace Lantern.EQ.Editor.Importers { public static class DoorImporter - {/* - public static void CreateDoorInstances(string shortname, ZoneMeshSunlightValues sunlightValues, Transform doorRoot) + { + public static void CreateDoorInstances(string shortname, Transform doorRoot) { - if (sunlightValues == null) - { - return; - } - - var doorService = ServiceFactory.Get(); + var databaseLoader = new DatabaseLoader(Path.Combine(Application.streamingAssetsPath, "Database")); + var doors = databaseLoader.GetDatabase() + ?.Table().Where(x => x.zone == shortname && x.opentype != EqConstants.DoorOpenTypeInvisible); - if (doorService == null) + foreach (var d in doors) { - var bootstrapper = new ServiceBootstrapper(false, null); - - doorService = ServiceFactory.Get(); - - if (doorService == null) - { - Debug.LogError("ZoneImporter: Unable to load zone doors. Cannot bootstrap services!"); - return; - } - } - - IEnumerable doors = doorService.GetDoorsForZone(shortname); - - Dictionary spawnedClickableDoors = new Dictionary(); - - foreach (Doors door in doors) - { - // Skip invisible doors - if (door.opentype == 54) - { - continue; - } - - string prefabLoadPath = PathHelper.GetSavePath(shortname, AssetImportType.Objects) + door.name + ".prefab"; - - // Get the door mesh - GameObject loadedDoor = AssetDatabase.LoadAssetAtPath(prefabLoadPath); + string prefabLoadPath = PathHelper.GetSavePath(shortname, AssetImportType.Objects) + d.name + ".prefab"; + var loadedPrefab = AssetDatabase.LoadAssetAtPath(prefabLoadPath); - if (loadedDoor == null) + if (loadedPrefab == null) { - Debug.LogError("ZoneImporter: Unable to find the door object asset: " + - door.name + ".obj"); + Debug.LogError($"Unable to load door prefab: {d.name}"); continue; } - GameObject spawnedDoor = (GameObject) PrefabUtility.InstantiatePrefab(loadedDoor, doorRoot.transform); - - spawnedDoor.layer = LanternLayers.Door; - - spawnedDoor.transform.position = new Vector3(door.pos_y, - door.pos_z, door.pos_x); - - spawnedDoor = ImportHelper.FixModelParent(spawnedDoor, doorRoot.transform); - - ImportHelper.FixCloneNameAppend(spawnedDoor); - - spawnedDoor.transform.rotation = - Quaternion.Euler(0.0f, RotationHelper.GetEqToLanternRotation(-door.heading), 0.0f); - - DoorId doorScript = spawnedDoor.AddComponent(); - doorScript.Id = door.id; - - spawnedDoor.tag = "Clickable"; - - ClickableDoor cd = spawnedDoor.AddComponent(); - cd.SetLockPick(door.lockpick); - cd.SetKeyItem(door.keyitem); - cd.SetTriggerDoor(door.triggerdoor); - cd.SetParameter(door.door_param); - cd.SetWidth(door.width); - cd.SetHeading(door.heading); - cd.SetOpenType(door.opentype); // Perform Last - - spawnedClickableDoors.Add(door.doorid, cd); - - var vcsn = spawnedDoor.GetComponent(); - List colors = new List(); - var zoneValues = Object.FindObjectOfType(); - RaycastHelper.TryGetSunlightValueRuntime(spawnedDoor.transform.position, zoneValues, out var sunlightA); - colors.Add(new Color(0, 0, 0, sunlightA)); - vcsn.SetColorData(colors); - } - - // Loop Thru Spawned Doors and Link Any Triggered Doors - foreach (ClickableDoor cd in spawnedClickableDoors.Values) - { - int triggeredDoor = cd.GetTriggerDoor(); - - if (triggeredDoor != 0) - { - if (spawnedClickableDoors.ContainsKey(triggeredDoor)) - { - cd.SetLinkedDoor(spawnedClickableDoors[triggeredDoor]); - } - } + var spawnedObject = Object.Instantiate(loadedPrefab); + spawnedObject.transform.position = + new Vector3(d.pos_y, d.pos_z, d.pos_x); + spawnedObject.transform.rotation = + Quaternion.Euler(0.0f, RotationHelper.GetEqToLanternRotation(-d.heading), 0.0f); + spawnedObject.transform.localScale = Vector3.one; + spawnedObject.transform.parent = doorRoot.transform; } - }*/ + } } } diff --git a/Assets/Scripts/Lantern/EQ/Editor/Importers/EquipmentImporter.cs b/Assets/Scripts/Lantern/EQ/Editor/Importers/EquipmentImporter.cs index 0b8872c..10b009f 100644 --- a/Assets/Scripts/Lantern/EQ/Editor/Importers/EquipmentImporter.cs +++ b/Assets/Scripts/Lantern/EQ/Editor/Importers/EquipmentImporter.cs @@ -3,30 +3,61 @@ using System.Linq; using Lantern.EQ.Animation; using Lantern.EQ.Editor.Helpers; -using Lantern.EQ.Editor.Importers; +using Lantern.EQ.Equipment; using UnityEditor; using UnityEngine; -namespace Lantern.Editor.Importers +namespace Lantern.EQ.Editor.Importers { - public static class EquipmentImporter + public class EquipmentImporter : LanternEditorWindow { private static Dictionary _animations; + private static readonly List Text1 = new() + { + "This process creates equipment prefabs from intermediate EverQuest data.", + "Importing all equipment takes around fifteen minutes.", + }; + + private static readonly List Text2 = new() + { + "EverQuest equipment data must be located in:", + "\fAssets/EQAssets/equipment/", + }; + + private static readonly List Text3 = new() + { + "Equipment prefabs will be output to:", + "\fAssets/Content/AssetBundleContent/Equipment/" + }; + /// /// Unity relative paths to animation text files /// private static List _animationPaths; - [MenuItem("EQ/Import/Equipment", false, 50)] - public static void ImportEquipment() + [MenuItem("EQ/Assets/Import Equipment &e", false, 3)] + public static void ShowImportDialog() { - if (!EditorUtility.DisplayDialog("Import Equipment", - "Are you sure you want to import equipment?", "Yes", "No")) + GetWindow("Import Equipment", typeof(EditorWindow)); + } + + private void OnGUI() + { + DrawInfoBox(Text1, "d_console.infoicon"); + DrawInfoBox(Text2, "d_Collab.FolderConflict"); + DrawInfoBox(Text3, "d_Collab.FolderMoved"); + DrawHorizontalLine(); + + if (DrawButton("Import")) { - return; + ImportEquipment(); } + } + private void ImportEquipment() + { + StartImport(); _animations = new Dictionary(); _animationPaths = AnimationImporter.LoadAnimationPaths("equipment", AssetImportType.Equipment); @@ -37,15 +68,16 @@ public static void ImportEquipment() return; } - var startTime = EditorApplication.timeSinceStartup; ActorStaticImporter.ImportList("equipment", AssetImportType.Equipment, PostProcessStatic); ActorSkeletalImporter.ImportList("equipment", AssetImportType.Equipment, PostProcessSkeletal); ImportHelper.TagAllAssetsForBundles(PathHelper.GetAssetBundleContentPath()+ "Equipment", "equipment"); - EditorUtility.DisplayDialog("EquipmentImport", - $"Equipment import finished in {(int) (EditorApplication.timeSinceStartup - startTime)} seconds", "OK"); _animations.Clear(); _animationPaths.Clear(); + + var importTime = FinishImport(); + EditorUtility.DisplayDialog("EquipmentImport", + $"Equipment import finished in {importTime} seconds", "OK"); } private static void PostProcessStatic(GameObject go) @@ -61,8 +93,11 @@ private static void PostProcessStatic(GameObject go) private static void AddModelScript(GameObject go) { + var ea = go.AddComponent(); + ea.InitializeImport(); + var em = go.AddComponent(); - em.FindRenderers(); + em.SetReferences(ea); } private static void CreateHdVariant(GameObject go) @@ -145,30 +180,30 @@ private static void CreateHdVariant(GameObject go) mf.sharedMesh = newMesh; savePath = PathHelper.GetSavePath("equipment", AssetImportType.Equipment) + $"{hdAssetName}.prefab"; PrefabUtility.SaveAsPrefabAsset(newPrefab, savePath); - Object.DestroyImmediate(newPrefab); + DestroyImmediate(newPrefab); } private static void PostProcessSkeletal(GameObject go) { string modelAsset = go.name; - var animation = go.GetComponent(); + var animation = go.GetComponent(); if (animation == null) { return; } - AddModelScript(go); - // Find animations if (animation != null) { LoadEquipmentAnimations(animation, modelAsset, AssetImportType.Equipment); } + + AddModelScript(go); } - private static void LoadEquipmentAnimations(Animation animation, string animationBase, AssetImportType type) + private static void LoadEquipmentAnimations(UnityEngine.Animation animation, string animationBase, AssetImportType type) { string prefix = animationBase + "_"; diff --git a/Assets/Scripts/Lantern/EQ/Editor/Importers/ExportBundles.cs b/Assets/Scripts/Lantern/EQ/Editor/Importers/ExportBundles.cs new file mode 100644 index 0000000..268cabf --- /dev/null +++ b/Assets/Scripts/Lantern/EQ/Editor/Importers/ExportBundles.cs @@ -0,0 +1,148 @@ +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.IO; +using System.Linq; +using Lantern.EQ.Viewers; +using UnityEditor; +using UnityEngine; +using Debug = UnityEngine.Debug; + +namespace Lantern.EQ.Editor.Importers +{ + public class ExportBundles : LanternEditorWindow + { + private bool _organizeInSubfolders = true; + + private static readonly List Text1 = new() + { + "Copies all built asset bundles to a separate folder one level up from the project in:", + "\f../AssetBundles/", + "Optionally, the bundles can be organized into separate folders.", + }; + + private static readonly List Text2 = new() + { + "This process will delete all existing assets in the destination folder!", + }; + + [MenuItem("EQ/Assets/Export Bundles", false, 101)] + public static void ExportBundles2() + { + GetWindow("Export Bundles", typeof(EditorWindow)); + } + + private void OnGUI() + { + DrawInfoBox(Text1, "d_console.infoicon"); + DrawInfoBox(Text2, "d_console.warnicon"); + _organizeInSubfolders = GUILayout.Toggle(_organizeInSubfolders, "Organize In Subfolders"); + + if (GUILayout.Button("Start Export")) + { + Export(); + } + + if (GUILayout.Button("Open Export Folder")) + { + OpenFolderExplorer(); + } + } + + private void Export() + { + DeleteExistingBundles(); + var assetBundlePath = AssetBundleHelper.GetAssetBundlePath(); + var files = Directory.GetFiles(assetBundlePath, "*.*", SearchOption.AllDirectories) + .Where(f => !f.EndsWith(".meta")).ToList(); + + string rootFolderPath = GetBundlePath(); + string globalsFolderPath = Path.Combine(rootFolderPath, "Global"); + string zonesFolderPath = Path.Combine(rootFolderPath, "Zones"); + + if (_organizeInSubfolders) + { + Directory.CreateDirectory(globalsFolderPath); + Directory.CreateDirectory(zonesFolderPath); + } + + foreach (string file in files) + { + string fileName = Path.GetFileName(file); + + // Skip the manifest as there are no cross bundle dependencies + if (fileName.StartsWith("AssetBundles")) + { + continue; + } + + bool isGlobal = AssetBundleHelper.IsGlobalBundle(fileName); + string destinationFolder = isGlobal ? globalsFolderPath : zonesFolderPath; + File.Copy(file, Path.Combine(destinationFolder, fileName)); + } + + OpenFolderExplorer(); + } + + private static string GetProjectRootPath() + { + return Directory.GetParent(Application.dataPath)?.FullName; + } + + private static string GetBundlePath() + { + return Path.Combine(GetProjectRootPath(), "Builds", "AssetBundles"); + } + + private static void DeleteExistingBundles() + { + var path = GetBundlePath(); + + if (!Directory.Exists(path)) + { + return; + } + + DirectoryInfo di = new DirectoryInfo(path); + + foreach (FileInfo file in di.GetFiles()) + { + file.Delete(); + } + + foreach (DirectoryInfo dir in di.GetDirectories()) + { + dir.Delete(true); + } + } + + private static void OpenFolderExplorer() + { + var bundlePath = GetBundlePath(); + if (!Directory.Exists(bundlePath)) + { + Directory.CreateDirectory(bundlePath); + } + + if (Directory.Exists(bundlePath)) + { + if (System.Environment.OSVersion.Platform == PlatformID.Win32NT) + { + Process.Start("explorer.exe", bundlePath); + } + else if (System.Environment.OSVersion.Platform == PlatformID.Unix) + { + Process.Start("open", bundlePath); + } + else + { + Debug.Log("Unsupported operating system."); + } + } + else + { + Debug.Log("Project root folder does not exist."); + } + } + } +} diff --git a/Assets/Scripts/Lantern/EQ/Editor/Importers/ExportBundles.cs.meta b/Assets/Scripts/Lantern/EQ/Editor/Importers/ExportBundles.cs.meta new file mode 100644 index 0000000..711654c --- /dev/null +++ b/Assets/Scripts/Lantern/EQ/Editor/Importers/ExportBundles.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 24caf04683acf7445ae6987e19646448 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Scripts/Lantern/EQ/Editor/Importers/LightImporter.cs b/Assets/Scripts/Lantern/EQ/Editor/Importers/LightImporter.cs index 29c2601..0fe7844 100644 --- a/Assets/Scripts/Lantern/EQ/Editor/Importers/LightImporter.cs +++ b/Assets/Scripts/Lantern/EQ/Editor/Importers/LightImporter.cs @@ -4,7 +4,7 @@ using Lantern.EQ.Lantern; using UnityEngine; -namespace Lantern.Editor.Importers +namespace Lantern.EQ.Editor.Importers { public static class LightImporter { diff --git a/Assets/Scripts/Lantern/EQ/Editor/Importers/MaterialImporter.cs b/Assets/Scripts/Lantern/EQ/Editor/Importers/MaterialImporter.cs index 502e8d4..0392835 100644 --- a/Assets/Scripts/Lantern/EQ/Editor/Importers/MaterialImporter.cs +++ b/Assets/Scripts/Lantern/EQ/Editor/Importers/MaterialImporter.cs @@ -9,7 +9,7 @@ using UnityEditor; using UnityEngine; -namespace Lantern.Editor.Importers +namespace Lantern.EQ.Editor.Importers { public static class MaterialImporter { @@ -71,7 +71,7 @@ public static void CreateMaterials(string shortname, AssetImportType importType, } else { - List textureNames = TextParser.ParseStringToList(line[3]); + List textureNames = TextParser.ParseTextToList(line[3]); List textures = new List(); diff --git a/Assets/Scripts/Lantern/EQ/Editor/Importers/MeshImporter.cs b/Assets/Scripts/Lantern/EQ/Editor/Importers/MeshImporter.cs index 2427691..e65073d 100644 --- a/Assets/Scripts/Lantern/EQ/Editor/Importers/MeshImporter.cs +++ b/Assets/Scripts/Lantern/EQ/Editor/Importers/MeshImporter.cs @@ -12,7 +12,7 @@ using UnityEngine.Rendering; using Object = UnityEngine.Object; -namespace Lantern.Editor.Importers +namespace Lantern.EQ.Editor.Importers { public static class MeshImporter { diff --git a/Assets/Scripts/Lantern/EQ/Editor/Importers/MusicImporter.cs b/Assets/Scripts/Lantern/EQ/Editor/Importers/MusicImporter.cs index 9511f28..a0ed34b 100644 --- a/Assets/Scripts/Lantern/EQ/Editor/Importers/MusicImporter.cs +++ b/Assets/Scripts/Lantern/EQ/Editor/Importers/MusicImporter.cs @@ -1,8 +1,20 @@ -namespace Lantern.Editor.Importers +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Text; +using Infrastructure.EQ.TextParser; +using Lantern.EQ.Audio; +using Lantern.EQ.Editor.Helpers; +using Lantern.EQ.Lantern; +using UnityEditor; +using UnityEngine; + +namespace Lantern.EQ.Editor.Importers { public static class MusicImporter { - /*public static void CreateMusicInstances(string musicTextAssetPath, Transform musicRoot) + public static void CreateMusicInstances(string musicTextAssetPath, Transform musicRoot, string shortName) { string musicInstanceList = string.Empty; @@ -14,55 +26,79 @@ public static class MusicImporter } GameObject musicTriggerPrefab = GetMusicTriggerPrefab(); - + if (musicTriggerPrefab == null) { Debug.LogError("MusicImporter: Cannot get music trigger prefab."); return; } + var trackNames = GetTrackNamesForZone(shortName); var parsedMusicLines = TextParser.ParseTextByDelimitedLines(musicInstanceList, ','); - + foreach (var instance in parsedMusicLines) { - CreateMusicInstance(musicTriggerPrefab, instance, musicRoot); + CreateMusicInstance(musicTriggerPrefab, instance, trackNames, musicRoot); } } - private static void CreateMusicInstance(GameObject musicTriggerPrefab, List musicData, Transform parent) + private static void CreateMusicInstance(GameObject musicTriggerPrefab, List musicLines, List trackNames, Transform parent) { - if (musicData.Count != 9) + if (musicLines.Count != 9) { Debug.LogError("MusicImporter: Unable to parse music line. Unexpected item count"); return; } - - MusicTrigger musicTrigger = + + var musicTrigger = ((GameObject) PrefabUtility.InstantiatePrefab(musicTriggerPrefab)).GetComponent(); musicTrigger.transform.parent = parent; musicTrigger.gameObject.layer = 2; // Ignore Raycast Layer - - float x = Convert.ToSingle(musicData[0]); - float y = Convert.ToSingle(musicData[1]); - float z = Convert.ToSingle(musicData[2]); + float x = Convert.ToSingle(musicLines[0]); + float y = Convert.ToSingle(musicLines[1]); + float z = Convert.ToSingle(musicLines[2]); musicTrigger.transform.position = new Vector3(x, y, z); - float radius = Convert.ToSingle(musicData[3]); - string trackNameDay = musicData[4]; - string trackNameNight = musicData[5]; - int loopCountDay = Convert.ToInt32(musicData[6]); - int loopCountNight = Convert.ToInt32(musicData[7]); - int fadeOutTimeMs = Convert.ToInt32(musicData[8]); + float radius = Convert.ToSingle(musicLines[3]); + int trackIndexDay = Convert.ToInt32(musicLines[4]); + int trackIndexNight = Convert.ToInt32(musicLines[5]); + int playCountDay = Convert.ToInt32(musicLines[6]); + int playCountNight = Convert.ToInt32(musicLines[7]); + int fadeOutMs = Convert.ToInt32(musicLines[8]); - musicTrigger.SetData(radius, trackNameDay, trackNameNight, loopCountDay, loopCountNight, fadeOutTimeMs); + var musicData = new MusicData() + { + TrackIndexDay = trackIndexDay, + TrackIndexNight = trackIndexNight, + PlayCountDay = playCountDay, + PlayCountNight = playCountNight, + FadeOutMsDay = fadeOutMs, + FadeOutMsNight = fadeOutMs, + }; + + musicTrigger.SetData(LanternTags.Player, radius, musicData); + } + + private static List GetTrackNamesForZone(string shortname) + { + string assetPath = PathHelper.GetClientDataPath() + "music_tracks.txt"; + if (!ImportHelper.LoadTextAsset(assetPath, out var musicTrackFile)) + { + return null; + } + + var musicTrackLines = TextParser.ParseTextByDelimitedLines(musicTrackFile, ','); + var trackNames = musicTrackLines.FirstOrDefault(x => x[0] == shortname); + trackNames?.RemoveAt(0); + return trackNames ?? new List(); } private static GameObject GetMusicTriggerPrefab() { - var musicTriggerPrefabPath = "Assets/Content/Prefabs/MusicTrigger.Prefab"; + var musicTriggerPrefabPath = "Assets/Content/Features/Game/MusicTrigger.Prefab"; return AssetDatabase.LoadAssetAtPath(musicTriggerPrefabPath); - }*/ + } } } diff --git a/Assets/Scripts/Lantern/EQ/Editor/Importers/ObjectImporter.cs b/Assets/Scripts/Lantern/EQ/Editor/Importers/ObjectImporter.cs index c086adf..b314b21 100644 --- a/Assets/Scripts/Lantern/EQ/Editor/Importers/ObjectImporter.cs +++ b/Assets/Scripts/Lantern/EQ/Editor/Importers/ObjectImporter.cs @@ -7,7 +7,7 @@ using UnityEditor; using UnityEngine; -namespace Lantern.Editor.Importers +namespace Lantern.EQ.Editor.Importers { public static class ObjectImporter { diff --git a/Assets/Scripts/Lantern/EQ/Editor/Importers/SkeletonImporter.cs b/Assets/Scripts/Lantern/EQ/Editor/Importers/SkeletonImporter.cs index 686c51f..41e5267 100644 --- a/Assets/Scripts/Lantern/EQ/Editor/Importers/SkeletonImporter.cs +++ b/Assets/Scripts/Lantern/EQ/Editor/Importers/SkeletonImporter.cs @@ -3,14 +3,13 @@ using Infrastructure.EQ.TextParser; using Lantern.EQ.Animation; using Lantern.EQ.Editor.Helpers; -using Lantern.EQ.Editor.Importers; using Lantern.EQ.Equipment; using Lantern.EQ.Helpers; using Lantern.EQ.Lighting; using UnityEditor; using UnityEngine; -namespace Lantern.Editor.Importers +namespace Lantern.EQ.Editor.Importers { public static class SkeletonImporter { @@ -35,18 +34,35 @@ public static void Import(string assetName, string shortName, AssetImportType im return; } + var primaryMeshes = new List(); + var secondaryMeshes = new List(); + if (skeletonLines[0][0] == "meshes") { for (int i = 1; i < skeletonLines[0].Count; ++i) { - meshesToCreate.Add(skeletonLines[0][i]); + var mesh = skeletonLines[0][i]; + primaryMeshes.Add(mesh); + meshesToCreate.Add(mesh); + } + + skeletonLines.RemoveAt(0); + } + + if (skeletonLines[0][0] == "secondary_meshes") + { + for (int i = 1; i < skeletonLines[0].Count; ++i) + { + var mesh = skeletonLines[0][i]; + secondaryMeshes.Add(mesh); + meshesToCreate.Add(mesh); } skeletonLines.RemoveAt(0); } var skeletonRoot = new GameObject(assetName); - var baseAnimation = skeletonRoot.AddComponent(); + var baseAnimation = skeletonRoot.AddComponent(); baseAnimation.cullingType = AnimationCullingType.BasedOnRenderers; var bones = new Transform[skeletonLines.Count]; @@ -122,7 +138,7 @@ public static void Import(string assetName, string shortName, AssetImportType im // LANTERN ONLY START if(handler != null) { - if (meshName == assetName || meshName.EndsWith("00")) + if (primaryMeshes.Contains(meshName)) { handler.AddPrimaryMesh(go); } @@ -158,7 +174,7 @@ private static void CreateSkeletonBone(int boneIndex, List> skeleto // Spawn bones with attached meshes if (boneData.Count != 2) { - var meshName = boneData[2]; + var meshName = string.IsNullOrEmpty(boneData[2]) ? boneData[3] : boneData[2]; if (!string.IsNullOrEmpty(meshName)) { @@ -206,7 +222,7 @@ private static void CreateSkeletonBone(int boneIndex, List> skeleto return; } - List childBones = TextParser.ParseStringToList(boneData[1]); + List childBones = TextParser.ParseTextToList(boneData[1]); foreach (var childBoneIndex in childBones) { diff --git a/Assets/Scripts/Lantern/EQ/Editor/Importers/SkyImporter.cs b/Assets/Scripts/Lantern/EQ/Editor/Importers/SkyImporter.cs index 8b7b72c..867be50 100644 --- a/Assets/Scripts/Lantern/EQ/Editor/Importers/SkyImporter.cs +++ b/Assets/Scripts/Lantern/EQ/Editor/Importers/SkyImporter.cs @@ -1,6 +1,5 @@ using System.Collections.Generic; using System.IO; -using Lantern.Editor.Importers; using Lantern.EQ.Editor.Helpers; using Lantern.EQ.Environment; using Lantern.EQ.Helpers; @@ -9,18 +8,51 @@ namespace Lantern.EQ.Editor.Importers { - public static class SkyImporter + public class SkyImporter : LanternEditorWindow { - [MenuItem("EQ/Import/Sky", false, 50)] - public static void ImportSky() + private static readonly List Text1 = new() { - if (!EditorUtility.DisplayDialog("Import Sky", - "Are you sure you want to import the sky?", "Yes", "No")) + "This process creates the sky prefab from intermediate EverQuest data.", + "Importing the sky takes around one minute.", + }; + + private static readonly List Text2 = new() + { + "EverQuest sky data must be located in:", + "\fAssets/EQAssets/sky/", + }; + + private static readonly List Text3 = new() + { + "Sky prefab will be output to:", + "\fAssets/Content/AssetBundleContent/Sky/" + }; + + [MenuItem("EQ/Assets/Import Sky &s", false, 4)] + public static void ShowImportDialog() + { + GetWindow("Import Sky", typeof(EditorWindow)); + } + + /// + /// Draws the settings window for the character importer + /// + private void OnGUI() + { + DrawInfoBox(Text1, "d_console.infoicon"); + DrawInfoBox(Text2, "d_Collab.FolderConflict"); + DrawInfoBox(Text3, "d_Collab.FolderMoved"); + DrawHorizontalLine(); + + if (DrawButton("Import")) { - return; + ImportSky(); } + } - var startTime = EditorApplication.timeSinceStartup; + private void ImportSky() + { + StartImport(); var meshesToCreate = new List { @@ -116,11 +148,13 @@ public static void ImportSky() var savePath = Path.Combine(PathHelper.GetAssetBundleContentPath(), "Sky/Sky.prefab"); PrefabUtility.SaveAsPrefabAsset(root, savePath); AssetDatabase.Refresh(); - Object.DestroyImmediate(root); + DestroyImmediate(root); ImportHelper.TagAllAssetsForBundles(PathHelper.GetAssetBundleContentPath()+ "Sky", "sky"); + + var importTime = FinishImport(); EditorUtility.DisplayDialog("SkyImport", - $"Sky import finished in {(int)(EditorApplication.timeSinceStartup - startTime)} seconds", "OK"); + $"Sky import finished in {importTime} seconds", "OK"); } private static GameObject InstantiateSkyPrefabAsChild(string prefabName, GameObject parent) diff --git a/Assets/Scripts/Lantern/EQ/Editor/Importers/SoundImporter.cs b/Assets/Scripts/Lantern/EQ/Editor/Importers/SoundImporter.cs index e809717..f073519 100644 --- a/Assets/Scripts/Lantern/EQ/Editor/Importers/SoundImporter.cs +++ b/Assets/Scripts/Lantern/EQ/Editor/Importers/SoundImporter.cs @@ -1,114 +1,146 @@ using System; using System.Collections.Generic; using Infrastructure.EQ.TextParser; +using Lantern.EQ.Audio; using Lantern.EQ.Editor.Helpers; +using Lantern.EQ.Lantern; using UnityEditor; using UnityEngine; -namespace Lantern.Editor.Importers +namespace Lantern.EQ.Editor.Importers { public static class SoundImporter { - public static void CreateSoundInstances(string soundTextAssetPath, Transform soundRoot) + public static void CreateSoundInstances(string sound2dTextAssetPath, string sound3dTextAssetPath, + Transform soundRoot) { - string soundInstanceList = string.Empty; + ImportHelper.LoadTextAsset(sound2dTextAssetPath, out var sound2dInstanceList); - ImportHelper.LoadTextAsset(soundTextAssetPath, out soundInstanceList); - - if (string.IsNullOrEmpty(soundInstanceList)) + if (!string.IsNullOrEmpty(sound2dInstanceList)) { - return; - } + GameObject sound2dTriggerPrefab = GetSound2dTriggerPrefab(); - GameObject sound2dTriggerPrefab = GetSound2dTriggerPrefab(); - GameObject sound3dTriggerPrefab = GetSound3dTriggerPrefab(); + if (sound2dTriggerPrefab == null) + { + Debug.LogError("Could not load sound 2d trigger!"); + } + else + { + var parsedSound2dLines = TextParser.ParseTextByDelimitedLines(sound2dInstanceList, ','); - if (sound2dTriggerPrefab == null || sound3dTriggerPrefab == null) - { - Debug.LogError("SoundImporter: Cannot get sound trigger prefabs."); - return; + foreach (var instance in parsedSound2dLines) + { + CreateSound2dInstance(sound2dTriggerPrefab, instance, soundRoot); + } + } } - var parsedSoundLines = TextParser.ParseTextByDelimitedLines(soundInstanceList, ','); + ImportHelper.LoadTextAsset(sound3dTextAssetPath, out var sound3dInstanceList); - foreach (var instance in parsedSoundLines) + if (!string.IsNullOrEmpty(sound3dInstanceList)) { - CreateSoundInstance(sound2dTriggerPrefab, - sound3dTriggerPrefab, instance, soundRoot); + GameObject sound3dTriggerPrefab = GetSound3dTriggerPrefab(); + + if (sound3dTriggerPrefab == null) + { + Debug.LogError("Could not load sound 2d trigger!"); + } + else + { + var parsedSound3dLines = TextParser.ParseTextByDelimitedLines(sound3dInstanceList, ','); + + foreach (var instance in parsedSound3dLines) + { + CreateSound3dInstance(sound3dTriggerPrefab, instance, soundRoot); + } + } } } - private static void CreateSoundInstance(GameObject sound2dTriggerPrefab, - GameObject sound3dTriggerPrefab, List soundData, Transform parent) + private static void CreateSound2dInstance(GameObject sound2dTriggerPrefab, List soundLines, Transform parent) { - if (soundData.Count != 10) + if (soundLines.Count != 11) { - Debug.LogError("SoundImporter: Unable to parse sound line. Unexpected item count"); + Debug.LogError("SoundImporter: Unable to parse sound2D. Unexpected argument count"); return; } - int soundType = Convert.ToInt32(soundData[0]); - float x = Convert.ToSingle(soundData[1]); - float y = Convert.ToSingle(soundData[2]); - float z = Convert.ToSingle(soundData[3]); - float radius = Convert.ToSingle(soundData[4]); - string clipNameDay = soundData[5]; - string clipNameNight = soundData[6]; - int cooldownDay = Convert.ToInt32(soundData[7]); - int cooldownNight = Convert.ToInt32(soundData[8]); - int cooldownRandom = Convert.ToInt32(soundData[9]); - - string dayClipPath = PathHelper.GetAssetBundleContentPath()+ "Sound/" + clipNameDay + ".ogg"; - AudioClip dayClip = (AudioClip) AssetDatabase.LoadAssetAtPath(dayClipPath, typeof(AudioClip)); - - string nightClipPath = PathHelper.GetAssetBundleContentPath()+ "Sound/" + clipNameNight + ".ogg"; - AudioClip nightClip = (AudioClip) AssetDatabase.LoadAssetAtPath(nightClipPath, typeof(AudioClip)); + float x = Convert.ToSingle(soundLines[0]); + float y = Convert.ToSingle(soundLines[1]); + float z = Convert.ToSingle(soundLines[2]); + float radius = Convert.ToSingle(soundLines[3]); + string clipNameDay = soundLines[4]; + string clipNameNight = soundLines[5]; + int cooldownDay = Convert.ToInt32(soundLines[6]); + int cooldownNight = Convert.ToInt32(soundLines[7]); + int cooldownRandom = Convert.ToInt32(soundLines[8]); + float volumeDay = Convert.ToSingle(soundLines[9]); + float volumeNight = Convert.ToSingle(soundLines[10]); + + GameObject soundTrigger = (GameObject)PrefabUtility.InstantiatePrefab(sound2dTriggerPrefab); + soundTrigger.transform.parent = parent; + soundTrigger.transform.position = new Vector3(x, y, z); - if (dayClip == null && nightClip == null) + var soundData = new Sound2dData { - if (clipNameDay != string.Empty) - { - Debug.LogError("SoundImporter: Unable to load day clip: " + clipNameDay); - } - - if (clipNameNight != string.Empty) - { - Debug.LogError("SoundImporter: Unable to load night clip: " + clipNameNight); - } + ClipNameDay = clipNameDay, + ClipNameNight = clipNameNight, + CooldownDay = cooldownDay, + CooldownNight = cooldownNight, + CooldownRandom = cooldownRandom, + VolumeDay = volumeDay, + VolumeNight = volumeNight + }; + + var script = soundTrigger.GetComponent(); + script.SetData(soundData, LanternTags.Player, radius); + } + private static void CreateSound3dInstance(GameObject sound3dTriggerPrefab, List soundLines, Transform parent) + { + if (soundLines.Count != 9) + { + Debug.LogError("SoundImporter: Unable to parse sound3D. Unexpected argument count"); return; } - GameObject soundTrigger = - (GameObject) PrefabUtility.InstantiatePrefab(soundType == 0 - ? sound2dTriggerPrefab - : sound3dTriggerPrefab); - + float x = Convert.ToSingle(soundLines[0]); + float y = Convert.ToSingle(soundLines[1]); + float z = Convert.ToSingle(soundLines[2]); + float radius = Convert.ToSingle(soundLines[3]); + string clipName = soundLines[4]; + int cooldown = Convert.ToInt32(soundLines[5]); + int cooldownRandom = Convert.ToInt32(soundLines[6]); + float volume = Convert.ToSingle(soundLines[7]); + int multiplier = Convert.ToInt32(soundLines[8]); + + GameObject soundTrigger = (GameObject)PrefabUtility.InstantiatePrefab(sound3dTriggerPrefab); soundTrigger.transform.parent = parent; soundTrigger.transform.position = new Vector3(x, y, z); - /*if (soundType == 0) - { - var sound2dScript = soundTrigger.GetComponent(); - sound2dScript.SetData(dayClip, nightClip, radius, cooldownDay, cooldownNight, cooldownRandom); - } - else + var soundData = new Sound3dData { - var sound3dScript = soundTrigger.GetComponent(); - sound3dScript.SetData(dayClip, radius, cooldownDay, cooldownNight, cooldownRandom); - }*/ + ClipName = clipName, + Cooldown = cooldown, + CooldownRandom = cooldownRandom, + Volume = volume, + Multiplier = multiplier + }; + + var script = soundTrigger.GetComponent(); + script.SetData(soundData, LanternTags.Player, radius); } private static GameObject GetSound2dTriggerPrefab() { - var soundTriggerPrefabPath = "Assets/Content/Prefabs/Sound2dTrigger.Prefab"; - return AssetDatabase.LoadAssetAtPath(soundTriggerPrefabPath); + var prefabPath = "Assets/Content/Features/Game/SoundTrigger2d.prefab"; + return AssetDatabase.LoadAssetAtPath(prefabPath); } private static GameObject GetSound3dTriggerPrefab() { - var soundTriggerPrefabPath = "Assets/Content/Prefabs/Sound3dTrigger.Prefab"; - return AssetDatabase.LoadAssetAtPath(soundTriggerPrefabPath); + var prefabPath = "Assets/Content/Features/Game/SoundTrigger3d.prefab"; + return AssetDatabase.LoadAssetAtPath(prefabPath); } } } diff --git a/Assets/Scripts/Lantern/EQ/Editor/Importers/ValidateBundles.cs b/Assets/Scripts/Lantern/EQ/Editor/Importers/ValidateBundles.cs new file mode 100644 index 0000000..81d4b61 --- /dev/null +++ b/Assets/Scripts/Lantern/EQ/Editor/Importers/ValidateBundles.cs @@ -0,0 +1,92 @@ +using System.Collections.Generic; +using Lantern.EQ.AssetBundles; +using Lantern.EQ.Editor.Helpers; +using Lantern.EQ.Viewers; +using UnityEditor; +using UnityEngine; + +namespace Lantern.EQ.Editor.Importers +{ + public class ValidateBundles : LanternEditorWindow + { + private const string FoundImage = "sv_icon_dot3_pix16_gizmo"; + private const string NotFoundImage = "sv_icon_dot6_pix16_gizmo"; + private const string NotFoundOptionalImage = "sv_icon_dot4_pix16_gizmo"; + + private Vector2 _scrollPosition = Vector2.zero; + + private static readonly List Lines1 = new() + { + "Validates that LanternEQ version compliant bundles exist on disk. If you do not see a bundle you have imported, rebuild bundles.", + "This process only validates that the bundle exists. It does not verify the content of the bundle.", + }; + + [MenuItem("EQ/Assets/Validate Bundles", false, 100)] + private static void Init() + { + GetWindow("Validate Bundles", typeof(EditorWindow)); + } + + private void OnGUI() + { + DrawInfoBox(Lines1, "d_console.infoicon"); + + _scrollPosition = EditorGUILayout.BeginScrollView(_scrollPosition); + DisplayGlobalBundleStatus(LanternAssetBundleId.Characters); + DisplayGlobalBundleStatus(LanternAssetBundleId.Equipment); + DisplayGlobalBundleStatus(LanternAssetBundleId.Sprites); + DisplayGlobalBundleStatus(LanternAssetBundleId.Sky); + DisplayGlobalBundleStatus(LanternAssetBundleId.Sound); + DisplayGlobalBundleStatus(LanternAssetBundleId.Music_Audio); + DisplayGlobalBundleStatus(LanternAssetBundleId.Music_Midi); + DisplayGlobalBundleStatus(LanternAssetBundleId.Startup); + DisplayGlobalBundleStatus(LanternAssetBundleId.ClientData); + DisplayZoneBundleStatus(ZoneBatchType.Antonica, false); + DisplayZoneBundleStatus(ZoneBatchType.Faydwer, false); + DisplayZoneBundleStatus(ZoneBatchType.Odus, false); + DisplayZoneBundleStatus(ZoneBatchType.Kunark, false); + DisplayZoneBundleStatus(ZoneBatchType.Velious, false); + DisplayZoneBundleStatus(ZoneBatchType.Planes, false); + DisplayZoneBundleStatus(ZoneBatchType.Misc, true); + EditorGUILayout.EndScrollView(); + } + + private void DisplayZoneBundleStatus(ZoneBatchType type, bool optional) + { + var zones = ImportHelper.GetBatchZoneNames(type); + zones.Sort(); + + for (int i = zones.Count - 1; i >= 0; i--) + { + if (AssetBundleHelper.DoesZoneBundleExist(zones[i])) + { + zones.RemoveAt(i); + } + } + + if (zones.Count == 0) + { + DrawInfoBox(new List { $"All {type} zone bundles found." }, FoundImage, true); + } + else + { + DrawInfoBox(new List { $"Missing {type} zone bundles: {string.Join(", ", zones)}" }, + optional ? NotFoundOptionalImage : NotFoundImage, true); + } + } + + private void DisplayGlobalBundleStatus(LanternAssetBundleId assetBundleId) + { + bool doesExist = AssetBundleHelper.DoesGlobalBundleExist(assetBundleId); + + if (doesExist) + { + DrawInfoBox(new List { $"Bundle {assetBundleId} found." }, FoundImage, true); + } + else + { + DrawInfoBox(new List { $"Bundle {assetBundleId} not found." }, NotFoundImage, true); + } + } + } +} diff --git a/Assets/Scripts/Lantern/EQ/Editor/Importers/ValidateBundles.cs.meta b/Assets/Scripts/Lantern/EQ/Editor/Importers/ValidateBundles.cs.meta new file mode 100644 index 0000000..3a76559 --- /dev/null +++ b/Assets/Scripts/Lantern/EQ/Editor/Importers/ValidateBundles.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 83c9aff37dc7a614c9ce778b8edc4675 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Scripts/Lantern/EQ/Editor/Importers/XmiImporter.cs b/Assets/Scripts/Lantern/EQ/Editor/Importers/XmiImporter.cs new file mode 100644 index 0000000..88ce734 --- /dev/null +++ b/Assets/Scripts/Lantern/EQ/Editor/Importers/XmiImporter.cs @@ -0,0 +1,42 @@ +using UnityEngine; +using UnityEditor.AssetImporters; +using System.IO; +using Lantern.EQ.Audio; +using Lantern.EQ.Audio.Xmi; + +namespace Lantern.EQ.Editor.Importers +{ + [ScriptedImporter(version: 1, ext: "xmi", AllowCaching = true)] + public class XmiImporter : ScriptedImporter + { + public override void OnImportAsset(AssetImportContext ctx) + { + var fileName = Path.GetFileNameWithoutExtension(ctx.assetPath); + var midiTrackCollection = ScriptableObject.CreateInstance(); + + ctx.AddObjectToAsset($"{fileName}.miditracks", midiTrackCollection); + ctx.SetMainObject(midiTrackCollection); + + var xmiStream = File.OpenRead(ctx.assetPath); + var xmiReader = new XmiFileReader(xmiStream); + + var xmiFile = xmiReader.ReadXmiFile(); + var trackCount = xmiFile.XmidiTracks.Length; + + for (int trackNumber = 0; trackNumber < trackCount; trackNumber++) + { + var midiStream = xmiFile.WriteMidiTrack(trackNumber); + if (midiStream is MemoryStream memoryStream) + { + var midiTrack = MidiTrack.CreateMidiTrack(fileName, trackNumber, memoryStream.ToArray()); + ctx.AddObjectToAsset($"{fileName}_{trackNumber}.miditrack", midiTrack); + midiTrackCollection.MidiTracks.Add(midiTrack); + } + else + { + ctx.LogImportError($"[XmiImporter] Failed to create midi for track {trackNumber}"); + } + } + } + } +} diff --git a/Assets/Scripts/Lantern/EQ/Editor/Importers/XmiImporter.cs.meta b/Assets/Scripts/Lantern/EQ/Editor/Importers/XmiImporter.cs.meta new file mode 100644 index 0000000..0b095f3 --- /dev/null +++ b/Assets/Scripts/Lantern/EQ/Editor/Importers/XmiImporter.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: fe48da4e2c1ff1b4baf5fb1eb7b3331a +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Scripts/Lantern/EQ/Editor/Importers/ZoneBatchType.cs b/Assets/Scripts/Lantern/EQ/Editor/Importers/ZoneBatchType.cs new file mode 100644 index 0000000..0bd1505 --- /dev/null +++ b/Assets/Scripts/Lantern/EQ/Editor/Importers/ZoneBatchType.cs @@ -0,0 +1,14 @@ +namespace Lantern.EQ.Editor.Importers +{ + public enum ZoneBatchType + { + All = 0, + Antonica = 1, + Faydwer = 2, + Kunark = 3, + Misc = 4, + Odus = 5, + Planes = 6, + Velious = 7, + } +} diff --git a/Assets/Scripts/Lantern/EQ/Editor/Importers/ZoneBatchType.cs.meta b/Assets/Scripts/Lantern/EQ/Editor/Importers/ZoneBatchType.cs.meta new file mode 100644 index 0000000..1701358 --- /dev/null +++ b/Assets/Scripts/Lantern/EQ/Editor/Importers/ZoneBatchType.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: a9cdff62b3b94ae46814c4b52976cb76 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Scripts/Lantern/EQ/Editor/Importers/ZoneImporter.cs b/Assets/Scripts/Lantern/EQ/Editor/Importers/ZoneImporter.cs index 000480a..68c975e 100644 --- a/Assets/Scripts/Lantern/EQ/Editor/Importers/ZoneImporter.cs +++ b/Assets/Scripts/Lantern/EQ/Editor/Importers/ZoneImporter.cs @@ -12,9 +12,9 @@ using UnityEditor; using UnityEngine; -namespace Lantern.Editor.Importers +namespace Lantern.EQ.Editor.Importers { - public class ZoneImporter : EditorWindow + public class ZoneImporter : LanternEditorWindow { /// /// The shortname of the zone that will be imported - e.g. arena, qeynos2, gfaydark @@ -31,39 +31,95 @@ public class ZoneImporter : EditorWindow private bool _preinstantiateObjects; private bool _preinstantiateDoors; - private bool _rebuildBundles; + private bool _rebuildBundles = true; + + private enum ZoneImportType + { + SingleZone, + Batch + } + + private ZoneImportType _importType = ZoneImportType.SingleZone; + private ZoneBatchType _zoneBatchType = ZoneBatchType.All; + + private static readonly List Text1 = new() + { + "This process creates zone prefabs from intermediate EverQuest data.", + "This usually takes 2-6 minutes per zone depending on complexity." + }; + + private static readonly List Text2 = new() + { + "EverQuest zone data (one folder per zone) must be located in:", + "\fAssets/EQAssets/", + }; + + private static readonly List Text3 = new() + { + "Zone prefabs will be output to:", + "\fAssets/Content/AssetBundleContent/Zones/" + }; + + private static readonly List Text4 = new() + { + "Importing all zones may takes upwards of five hours." + }; + + private static readonly List Text5 = new() + { + "Pre-instantiating objects and doors should only be done if you're not building for the LanternEQ client." + }; + + public ZoneImporter() + { + SetMinHeight(435f); + } /// /// Opens the zone importer settings window /// - [MenuItem("EQ/Import/Zone &z", false, 10)] + [MenuItem("EQ/Assets/Import Zone &z", false, 1)] public static void ShowImportDialog() { - GetWindow(typeof(ZoneImporter), true, "Import Zone"); + GetWindow("Import Zone", typeof(EditorWindow)); } - /// - /// Draws the settings window for the zone importer - /// private void OnGUI() { - // Force the window size - int minHeight = 100; - minSize = maxSize = new Vector2(225, minHeight); - EditorGUIUtility.labelWidth = 100; - _zoneShortname = EditorGUILayout.TextField("Zone Shortname", _zoneShortname); - _preinstantiateObjects = GUILayout.Toggle(_preinstantiateObjects, "Pre-instantiate Objects"); - _preinstantiateDoors = GUILayout.Toggle(_preinstantiateDoors, "Pre-instantiate Doors"); - _rebuildBundles = GUILayout.Toggle(_rebuildBundles, "Rebuild Bundles"); - Rect r = EditorGUILayout.BeginHorizontal("Button"); - if (GUI.Button(r, GUIContent.none)) + DrawInfoBox(Text1, "d_console.infoicon"); + DrawInfoBox(Text2, "d_Collab.FolderConflict"); + DrawInfoBox(Text3, "d_Collab.FolderMoved"); + DrawHorizontalLine(); + + DrawEnumPopup("Import Type", ref _importType); + + if (_importType == ZoneImportType.SingleZone) { - ImportZone(); + DrawTextField("Zone Shortname", ref _zoneShortname); + } + else if (_importType == ZoneImportType.Batch) + { + DrawEnumPopup("Batch Type", ref _zoneBatchType); + } + + if (_importType == ZoneImportType.Batch && _zoneBatchType == ZoneBatchType.All) + { + DrawInfoBox(Text4, "d_console.warnicon"); } - GUILayout.Label("Import"); - EditorGUILayout.EndHorizontal(); - EditorGUILayout.Space(); + if (_preinstantiateDoors || _preinstantiateObjects) + { + DrawInfoBox(Text5, "d_console.warnicon"); + } + + DrawToggle("Pre-instantiate Objects", ref _preinstantiateObjects); + DrawToggle("Pre-instantiate Doors", ref _preinstantiateDoors); + DrawToggle("Rebuild Bundles", ref _rebuildBundles); + + if (DrawButton("Start Import")) + { + ImportZone(); + } } /// @@ -84,28 +140,14 @@ private void ImportZone() return; } - var startTime = (float)EditorApplication.timeSinceStartup; + StartImport(); var splitNames = _zoneShortname.Split(';').ToList(); - if (splitNames.Count == 1 && (splitNames[0] == "all" || splitNames[0] == "antonica" - || splitNames[0] == "odus" || - splitNames[0] == "faydwer" || splitNames[0] == "other" - || splitNames[0] == "kunark" || - splitNames[0] == "velious" || - splitNames[0] == "planes")) + if (_importType == ZoneImportType.Batch) { - if (!ImportHelper.LoadTextAsset($"Assets/Content/ClientData/zonelist_{splitNames[0]}.txt", - out var allShortnames)) - { - Debug.LogError($"ZoneImporter: Unable to load zone list for specifier: {splitNames[0]}"); - return; - } - - splitNames = TextParser.ParseTextByNewline(allShortnames); + splitNames = ImportHelper.GetBatchZoneNames(_zoneBatchType); } - Close(); - List successful = new List(); List failed = new List(); @@ -126,10 +168,11 @@ private void ImportZone() BuildAssetBundles.BuildAllAssetBundles(false); } - string importResult = GetFormattedImportResult(startTime, successful, failed); + var importTime = FinishImport(); + string importResult = GetFormattedImportResult(importTime, successful, failed); EditorUtility.DisplayDialog("ZoneImport" + (_rebuildBundles ? "/BuildBundles" : string.Empty), - importResult.ToString(), + importResult, "OK"); // LANTERN ONLY @@ -269,9 +312,7 @@ private void CreateGlobalAmbientLightSetter(string shortname) var globalLightTextAssetPath = PathHelper.GetLoadPath(shortname, AssetImportType.Zone) + "ambient_light.txt"; - string globalLightText = string.Empty; - - ImportHelper.LoadTextAsset(globalLightTextAssetPath, out globalLightText); + ImportHelper.LoadTextAsset(globalLightTextAssetPath, out var globalLightText); if (string.IsNullOrEmpty(globalLightText)) { @@ -328,34 +369,28 @@ private void ImportLights(string shortname) private void ImportSounds(string shortname) { - var soundsTextAssetPath = PathHelper.GetLoadPath(shortname, AssetImportType.Zone) + "sound_instances.txt"; - SoundImporter.CreateSoundInstances(soundsTextAssetPath, _soundRoot.transform); + var sounds2dTextAssetPath = + PathHelper.GetLoadPath(shortname, AssetImportType.Zone) + "sound2d_instances.txt"; + var sounds3dTextAssetPath = + PathHelper.GetLoadPath(shortname, AssetImportType.Zone) + "sound3d_instances.txt"; + SoundImporter.CreateSoundInstances(sounds2dTextAssetPath, sounds3dTextAssetPath, _soundRoot.transform); } private void ImportMusic(string shortname) { var musicTextAssetPath = PathHelper.GetLoadPath(shortname, AssetImportType.Zone) + "music_instances.txt"; - //MusicImporter.CreateMusicInstances(musicTextAssetPath, _musicRoot.transform); + MusicImporter.CreateMusicInstances(musicTextAssetPath, _musicRoot.transform, shortname); } private void ImportDoors(string shortname) { - //DoorImporter.CreateDoorInstances(shortname, FindObjectOfType(), - // _doorsRoot.transform); + DoorImporter.CreateDoorInstances(shortname, _doorsRoot.transform); } private void ScalePrefab() { _prefabRoot.transform.localScale = new Vector3(LanternConstants.WorldScale, LanternConstants.WorldScale, LanternConstants.WorldScale); - - // TODO: Move this into the audio importer - AudioSource[] audio = FindObjectsOfType(); - - foreach (AudioSource audioSource in audio) - { - audioSource.maxDistance *= LanternConstants.WorldScale; - } } private void TagRoots() @@ -391,10 +426,11 @@ private void SaveZonePrefab(string shortname) PathHelper.GetRootSavePath(shortname) + shortname + ".prefab"); } - private string GetFormattedImportResult(float startTime, List successful, List failed) + private string GetFormattedImportResult(int importTime, List successful, List failed) { StringBuilder importResult = new StringBuilder(); - importResult.AppendLine($"Zone(s) import {(_rebuildBundles ? "and build bundles " : String.Empty)}finished in {(int)(EditorApplication.timeSinceStartup - startTime)} seconds."); + importResult.AppendLine( + $"Zone(s) import {(_rebuildBundles ? "and build bundles " : String.Empty)}finished in {importTime} seconds."); importResult.AppendLine(); if (successful.Count > 0) { @@ -405,6 +441,7 @@ private string GetFormattedImportResult(float startTime, List successful { importResult.Append(", "); } + importResult.Append(successful[i]); } @@ -421,6 +458,7 @@ private string GetFormattedImportResult(float startTime, List successful { importResult.Append(", "); } + importResult.Append(failed[i]); } } diff --git a/Assets/Scripts/Lantern/EQ/Editor/Lantern.EQ.Editor.asmdef b/Assets/Scripts/Lantern/EQ/Editor/Lantern.EQ.Editor.asmdef index 5d298de..df2a368 100644 --- a/Assets/Scripts/Lantern/EQ/Editor/Lantern.EQ.Editor.asmdef +++ b/Assets/Scripts/Lantern/EQ/Editor/Lantern.EQ.Editor.asmdef @@ -3,7 +3,8 @@ "rootNamespace": "", "references": [ "GUID:361b2a66dbd61934c9baf3551fb4f50e", - "GUID:d1d6e5262cee07546bdd2e2e867ec461" + "GUID:d1d6e5262cee07546bdd2e2e867ec461", + "GUID:bd7cb45341512ab4fb6b23ac415ab477" ], "includePlatforms": [ "Editor" diff --git a/Assets/Scripts/Lantern/EQ/Editor/LanternEditorWindow.cs b/Assets/Scripts/Lantern/EQ/Editor/LanternEditorWindow.cs new file mode 100644 index 0000000..1cfe561 --- /dev/null +++ b/Assets/Scripts/Lantern/EQ/Editor/LanternEditorWindow.cs @@ -0,0 +1,137 @@ +using System; +using System.Collections.Generic; +using UnityEditor; +using UnityEngine; + +namespace Lantern.EQ.Editor +{ + public abstract class LanternEditorWindow : EditorWindow + { + private static Dictionary _icons; + private static GUIStyle _folderStyle; + private static GUIStyle _normalTextStyle; + private float _startTime; + protected float MinHeight = 300; + + protected LanternEditorWindow() + { + _icons = new Dictionary(); + SetMinHeight(MinHeight); + } + + protected void SetMinHeight(float minHeight) + { + MinHeight = minHeight; + minSize = new Vector2(375, MinHeight); + } + + protected void OnEnable() + { + _folderStyle = new GUIStyle(EditorStyles.label); + _folderStyle.fontStyle = FontStyle.Bold; + _folderStyle.normal.textColor = Color.white; + _folderStyle.padding = new RectOffset(4, 4, 2, 2); + + _normalTextStyle = new GUIStyle(EditorStyles.label); + _normalTextStyle.wordWrap = true; + } + + protected static void DrawHorizontalLine() + { + GUILayout.Space(5); + float padding = 10f; + float lineHeight = 1f; + float lineWidth = EditorGUIUtility.currentViewWidth - (2 * padding); + Rect lineRect = GUILayoutUtility.GetRect(lineWidth, lineHeight); + lineRect.x += padding; + lineRect.width -= 2 * padding; + EditorGUI.DrawRect(lineRect, Color.grey); + GUILayout.Space(5); + } + + protected static void DrawToggle(string label, ref bool value) + { + EditorGUILayout.BeginHorizontal(); + value = EditorGUILayout.Toggle(value, GUILayout.Width(15)); + GUILayout.Label(label); + EditorGUILayout.EndHorizontal(); + } + + protected static void DrawTextField(string label, ref string value) + { + EditorGUILayout.BeginHorizontal(); + GUILayout.Label(label); + value = EditorGUILayout.TextField(value); + EditorGUILayout.EndHorizontal(); + } + + protected static void DrawInfoBox(List lines, string iconString = "", bool smallIcon = false) + { + if (lines == null || lines.Count == 0) + { + return; + } + + var infoBoxStyle = new GUIStyle(GUI.skin.box) + { + normal = + { + textColor = Color.white + } + }; + + int iconSize = smallIcon ? 20 : 40; + const int iconMargin = 5; + GUILayout.BeginVertical(infoBoxStyle); + EditorGUILayout.BeginHorizontal(); + GUILayout.Space(iconMargin); + + if (iconString != string.Empty) + { + var icon = GetIcon(iconString); + GUILayout.Box(icon, GUILayout.Width(iconSize), GUILayout.Height(iconSize)); + } + + EditorGUILayout.BeginVertical(); + GUILayout.Space(5); + + foreach (var t in lines) + { + GUILayout.Label(t, t.StartsWith("\f") ? _folderStyle : _normalTextStyle); + } + + EditorGUILayout.EndVertical(); + EditorGUILayout.EndHorizontal(); + GUILayout.EndVertical(); + } + + protected static bool DrawButton(string label) + { + return GUILayout.Button(label); + } + + protected void DrawEnumPopup(string label, ref T enumValue) where T : Enum + { + enumValue = (T)EditorGUILayout.EnumPopup(label, enumValue); + } + + private static Texture2D GetIcon(string iconString) + { + if (_icons.TryGetValue(iconString, out var icon)) return icon; + var texture2D = EditorGUIUtility.IconContent(iconString).image as Texture2D; + _icons[iconString] = texture2D; + return _icons[iconString]; + } + + protected void StartImport() + { + Close(); + _startTime = (int)EditorApplication.timeSinceStartup; + } + + protected int FinishImport() + { + return (int)(EditorApplication.timeSinceStartup - _startTime); + } + } +} diff --git a/Assets/Scripts/Lantern/EQ/Editor/LanternEditorWindow.cs.meta b/Assets/Scripts/Lantern/EQ/Editor/LanternEditorWindow.cs.meta new file mode 100644 index 0000000..0657caa --- /dev/null +++ b/Assets/Scripts/Lantern/EQ/Editor/LanternEditorWindow.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 7189875361d029849aba35a3bbf88d96 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Scripts/Lantern/EQ/Editor/MusicRecorder.meta b/Assets/Scripts/Lantern/EQ/Editor/MusicRecorder.meta new file mode 100644 index 0000000..18f533d --- /dev/null +++ b/Assets/Scripts/Lantern/EQ/Editor/MusicRecorder.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: d657528669670bb43a0859e7d1351747 +folderAsset: yes +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Scripts/Lantern/EQ/Editor/MusicRecorder/MusicRecorder.cs b/Assets/Scripts/Lantern/EQ/Editor/MusicRecorder/MusicRecorder.cs new file mode 100644 index 0000000..fc0012c --- /dev/null +++ b/Assets/Scripts/Lantern/EQ/Editor/MusicRecorder/MusicRecorder.cs @@ -0,0 +1,303 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Text; +using Infrastructure.EQ.MeltySynth; +using Infrastructure.EQ.TextParser; +using Infrastructure.Lantern; +using Lantern.EQ.AssetBundles; +using Lantern.EQ.Audio; +using Lantern.EQ.Audio.Xmi; +using Lantern.EQ.Data; +using Lantern.EQ.Editor.Helpers; +using Melanchall.DryWetMidi.Core; +using Melanchall.DryWetMidi.Interaction; +using UnityEditor; +using UnityEngine; +using DryWetMidiFile = Melanchall.DryWetMidi.Core.MidiFile; +using MidiFile = Infrastructure.EQ.MeltySynth.MidiFile; + +namespace Lantern.EQ.Editor.MusicRecorder +{ + public class MusicRecorder : LanternEditorWindow + { + private static int _channels = 2; + private static int _sampleRate = 48000; + private static float _masterVolume = 0.28f; + private static string _soundFontName = "synthusr_samplefix.sf2"; + + private static bool _recordOnlyRequiredTracks = true; + + private static List text1 = new() + { + "This process will convert EverQuest XMI files into WAV audio recordings. When recording for the LanternEQ client, you only need a subset of all tracks as there are duplicates.", + "The recording process takes around thirty minutes. Recording all audio will take much longer." + }; + + private static List text2 = new() + { + "EverQuest XMI files must be located in:", + "\fAssets/EQAssets/music/", + }; + + private static List text3 = new() + { + "Recorded audio will be output to:", + "\fAssets/Content/AssetBundleContent/Music_Audio/" + }; + + [MenuItem("EQ/Assets/Record Music", false, 21)] + public static void ShowWindow() + { + GetWindow("Record Music", typeof(EditorWindow)); + } + + private void OnGUI() + { + DrawInfoBox(text1, "d_console.infoicon"); + DrawInfoBox(text2, "d_Collab.FolderConflict"); + DrawInfoBox(text3, "d_Collab.FolderMoved"); + + DrawHorizontalLine(); + + EditorGUILayout.BeginHorizontal(); + GUILayout.Label("Record only required tracks"); + _recordOnlyRequiredTracks = EditorGUILayout.Toggle(_recordOnlyRequiredTracks); + EditorGUILayout.EndHorizontal(); + + if (GUILayout.Button("Start Recording")) + { + CreateAllAudioTracks(); + } + } + + private static void GetLoopPoints(string name, XmiFile xmiFile, int trackIndex, StringBuilder writer) + { + if (xmiFile == null) + { + return; + } + var midiStream = xmiFile.WriteMidiTrack(trackIndex); + var midiFile = DryWetMidiFile.Read(midiStream); + var loopStart = GetLoopStartTimeSeconds(midiFile); + var loopEnd = GetLoopEndTimeSeconds(midiFile); + if (loopStart > 0f || loopEnd > 0f) + { + writer.AppendLine($"{name},{loopStart},{loopEnd}"); + } + } + + private void CreateAllAudioTracks() + { + StartImport(); + var usedTracksPath = Path.Combine(Application.dataPath, "Content/ClientData/music_tracks_used.txt"); + var usedTracksText = File.ReadAllText(usedTracksPath); + + if (string.IsNullOrEmpty(usedTracksText)) + { + return; + } + + var usedTracks = TextParser.ParseTextByNewline(usedTracksText); + + var tracksPath = Path.Combine(Application.streamingAssetsPath, "ClientData/Music/music_tracks.txt"); + var tracksText = File.ReadAllText(tracksPath); + + if (string.IsNullOrEmpty(tracksText)) + { + return; + } + + var trackDictionary = TextParser.ParseTextToDictionaryOfStringList(tracksText); + var createdAudio = new List(); + var loopPointsSb = new StringBuilder(); + + foreach (var xmi in trackDictionary) + { + for (var i = 0; i < xmi.Value.Count; i++) + { + var name = xmi.Value[i]; + if (!usedTracks.Contains(name) || createdAudio.Contains(name)) + { + if (_recordOnlyRequiredTracks) + { + continue; + } + } + + var xmiFile = LoadXmi(xmi.Key); + RecordAudio(CreateSequencer(),xmiFile , i, -1, name, ChannelFlag.Unset); + GetLoopPoints(name, xmiFile, i, loopPointsSb); + createdAudio.Add(name); + } + } + + var openerXmi = LoadXmi("opener4"); + CreateCharacterSelectMusic(openerXmi); + + if (loopPointsSb.Length != 0) + { + var folderPath = Path.Combine(PathHelper.GetAssetBundleContentPath(), "Music_Audio"); + var filePath = Path.Combine(folderPath, "loop_points.txt"); + File.WriteAllText(filePath, loopPointsSb.ToString()); + } + + var importTime = FinishImport(); + EditorUtility.DisplayDialog("MusicRecorder", + $"Music recording finished in {importTime} seconds", "OK"); + } + + private static XmiFile LoadXmi(string name) + { + var filePath = Path.Combine(PathHelper.GetEqAssetPath(), "Music", $"{name}.xmi"); + var xmiFs = File.OpenRead(filePath); + var xmiReader = new XmiFileReader(xmiFs); + var xmiFile = xmiReader.ReadXmiFile(); + return xmiFile; + } + + private static void CreateCharacterSelectMusic(XmiFile openerXmi) + { + RecordAudio(CreateSequencer(), openerXmi, 1, 0, "character_select-intro", ChannelFlag.Unset); + RecordAudio(CreateSequencer(), openerXmi, 1, 2, "character_select-outro", ChannelFlag.Unset); + + var races = Enum.GetNames(typeof(PlayerRaceId)); + + foreach (var race in races) + { + if (!Enum.TryParse(race, out var channelMask)) + { + Debug.LogError($"Failed to find channel mask for {race}"); + return; + } + + + RecordAudio(CreateSequencer(), openerXmi, 1, 1, "character_select-" + race.ToLower(), channelMask); + } + } + + private static void RecordAudio(MidiFileSequencer sequencer, XmiFile xmiFile, int trackIndex, int sequenceIndex, string trackName, ChannelFlag channel) + { + var midiStream = xmiFile.WriteMidiTrackSequence(trackIndex, sequenceIndex, channel); + var midiFile = new MidiFile(midiStream); + sequencer.Play(midiFile, false); + + // The output buffer. + var lenSamples = (int)(_sampleRate * midiFile.Length.TotalSeconds / sequencer.Speed); + if (lenSamples == 0) + { + Debug.LogError("Track {trackName} has no length and cannot be created."); + return; + } + + var buffer = new float[lenSamples * _channels]; + + // Render the waveform. + sequencer.RenderInterleaved(buffer); + + Debug.Log($"Creating clip: {trackName}"); + var clip = AudioClip.Create(trackName, lenSamples, _channels, _sampleRate, false); + clip.SetData(buffer, 0); + + var folderPath = Path.Combine(PathHelper.GetAssetBundleContentPath(), "Music_Audio"); + var wavPath = Path.Combine(folderPath, trackName + ".wav"); + SavWav.Save(wavPath, clip); + AssetDatabase.ImportAsset(wavPath); + + AudioImporter importer = AssetImporter.GetAtPath(wavPath) as AudioImporter; + if (importer != null) + { + importer.defaultSampleSettings = new AudioImporterSampleSettings + { + loadType = AudioClipLoadType.Streaming, + compressionFormat = AudioCompressionFormat.Vorbis, + quality = 0.7f + }; + importer.loadInBackground = !trackName.Contains("character_select"); + importer.preloadAudioData = false; + AssetDatabase.ImportAsset(wavPath); + AssetDatabase.Refresh(); + } + + // This could be moved out + ImportHelper.TagAllAssetsForBundles(folderPath, LanternAssetBundleId.Music_Audio.ToString().ToLower()); + } + + private static MidiFileSequencer CreateSequencer() + { + var sfFilePath = Path.Combine(Application.streamingAssetsPath, "Soundfont", _soundFontName); + var settings = new SynthesizerSettings(_sampleRate) + { + MaximumPolyphony = 256, + EnableReverbAndChorus = true, + }; + + var synthesizer = new Synthesizer(sfFilePath, settings) + { + MasterVolume = _masterVolume, + }; + return new MidiFileSequencer(synthesizer); + } + + private static double GetLoopStartTimeSeconds(DryWetMidiFile midiFile) + { + var tempoMap = midiFile.GetTempoMap(); + var timedEvents = midiFile.GetTimedEvents(); + + var timedEvent = timedEvents.FirstOrDefault(e => + { + if (e.Event is not ControlChangeEvent ccEvent) + { + return false; + } + + // Loop start as SEQ_INDEX 0 + if (ccEvent.ControlNumber == 120 && ccEvent.ControlValue == 0) + { + return true; + } + + // Xmi Loop Start Message + if (ccEvent.ControlNumber == 116) + { + return true; + } + + return false; + }); + + return timedEvent?.TimeAs(tempoMap)?.TotalSeconds ?? 0; + } + + private static double GetLoopEndTimeSeconds(DryWetMidiFile midiFile) + { + var tempoMap = midiFile.GetTempoMap(); + var timedEvents = midiFile.GetTimedEvents(); + + var timedEvent = timedEvents.FirstOrDefault(e => + { + if (e.Event is not ControlChangeEvent ccEvent) + { + return false; + } + + // Loop end as CALLBACK_PFX + if (ccEvent.ControlNumber == 108 && ccEvent.Channel == 0 && ccEvent.ControlValue == 0) + { + return true; + } + + // Xmi Loop End Message + if (ccEvent.ControlNumber == 117) + { + return true; + } + + return false; + }); + + return timedEvent?.TimeAs(tempoMap)?.TotalSeconds ?? 0; + } + } +} diff --git a/Assets/Scripts/Lantern/EQ/Editor/MusicRecorder/MusicRecorder.cs.meta b/Assets/Scripts/Lantern/EQ/Editor/MusicRecorder/MusicRecorder.cs.meta new file mode 100644 index 0000000..81dd7fa --- /dev/null +++ b/Assets/Scripts/Lantern/EQ/Editor/MusicRecorder/MusicRecorder.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 1e25664d3500c7b4982264cb77de052a +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Scripts/Lantern/EQ/Environment/SkyController.cs b/Assets/Scripts/Lantern/EQ/Environment/SkyController.cs index 16c793e..6dcc94e 100644 --- a/Assets/Scripts/Lantern/EQ/Environment/SkyController.cs +++ b/Assets/Scripts/Lantern/EQ/Environment/SkyController.cs @@ -1,27 +1,28 @@ using System; using System.Collections.Generic; using System.Linq; -using Lantern.EQ.Data; using UnityEngine; +using UnityEngine.Serialization; namespace Lantern.EQ.Environment { public class SkyController : MonoBehaviour { [Serializable] - private class SkyGroup2 + private class SkyGroup { public List GroupObjects; public UnityEngine.Animation ObjectsAnimation; } - [SerializeField] private List _skyGroups2 = new List(); + [FormerlySerializedAs("_skyGroups2")] [SerializeField] private List _skyGroups = new List(); [SerializeField] private List _objectPool = new List(); [SerializeField] private Transform _cameraFollow; [SerializeField] private Vector3 _cameraFollowOffset; private List _layers; + private float _secondsPerDay; private int _currentSky; private Quaternion _fixedRotation; private UnityEngine.Animation _currentAnimation; @@ -52,9 +53,14 @@ public void SetActiveCameraTransform(Transform cameraTransform, bool instant = f } } + public void SetSecondsPerDay(float seconds) + { + _secondsPerDay = seconds; + } + private void SetNewSkyIndex(int index) { - if (index < 0 || index >= _skyGroups2.Count) + if (index < 0 || index >= _skyGroups.Count) { return; } @@ -64,19 +70,19 @@ private void SetNewSkyIndex(int index) go.SetActive(false); } - foreach (var skyObject in _skyGroups2[index].GroupObjects) + foreach (var skyObject in _skyGroups[index].GroupObjects) { skyObject.SetActive(true); } - _currentAnimation = _skyGroups2[index].ObjectsAnimation; + _currentAnimation = _skyGroups[index].ObjectsAnimation; SetSkyAnimationState(index); } #if UNITY_EDITOR public void AddSkyGroup(List activeObjects, UnityEngine.Animation animation) { - _skyGroups2.Add(new SkyGroup2 + _skyGroups.Add(new SkyGroup { GroupObjects = activeObjects, ObjectsAnimation = animation @@ -127,25 +133,26 @@ private void FixSkyAnimation(float time) public void SetEnabledSky(int skyIndex) { + _currentSky = skyIndex; SetNewSkyIndex(skyIndex); SetSkyAnimationState(1); } private void SetSkyAnimationState(int index) { - if (index < 0 || index >= _skyGroups2.Count) + if (index < 0 || index >= _skyGroups.Count) { return; } - var animation = _skyGroups2[index].ObjectsAnimation; + var animation = _skyGroups[index].ObjectsAnimation; if (animation == null) { return; } float animationLength = 4f; - float playSpeed = EqConstants.SecondsPerDay; + float playSpeed = _secondsPerDay; float multiplier = animationLength / playSpeed; var clipName = animation.clip.name; animation[clipName].speed = multiplier; @@ -163,5 +170,10 @@ public void UpdateSkyPosition() selfTransform.position = _cameraFollow.position + _cameraFollowOffset; selfTransform.rotation = _fixedRotation; } + + public int GetSkyObjectCount() + { + return _skyGroups[_currentSky].GroupObjects.Count; + } } } diff --git a/Assets/Scripts/Lantern/EQ/Environment/WorldLightColor.cs b/Assets/Scripts/Lantern/EQ/Environment/WorldLightColor.cs new file mode 100644 index 0000000..3538243 --- /dev/null +++ b/Assets/Scripts/Lantern/EQ/Environment/WorldLightColor.cs @@ -0,0 +1,60 @@ +using System.Collections.Generic; +using System.Linq; +using UnityEngine; + +namespace Lantern.EQ.Environment +{ + public static class WorldLightColor + { + private static int _hourCount = 24; + + private static List> _hourColors = new() + { + new KeyValuePair(0, new Color(0.0f, 0.0f, 0.3f)), + new KeyValuePair(1, new Color(0.0f, 0.0f, 0.3f)), + new KeyValuePair(2, new Color(0.0f, 0.0f, 0.3f)), + new KeyValuePair(3, new Color(0.0f, 0.0f, 0.3f)), + new KeyValuePair(4, new Color(0.1f, 0.1f, 0.3f)), + new KeyValuePair(5, new Color(0.35f, 0.2f, 0.3f)), + new KeyValuePair(6, new Color(0.6f, 0.3f, 0.3f)), + new KeyValuePair(7, new Color(0.6f, 0.4f, 0.4f)), + new KeyValuePair(8, new Color(0.6f, 0.5f, 0.5f)), + new KeyValuePair(9, new Color(0.6f, 0.6f, 0.6f)), + new KeyValuePair(10, new Color(0.7f, 0.7f, 0.7f)), + new KeyValuePair(11, new Color(0.8f, 0.8f, 0.8f)), + new KeyValuePair(12, new Color(0.9f, 0.9f, 0.9f)), + new KeyValuePair(13, new Color(0.9f, 0.9f, 0.9f)), + new KeyValuePair(14, new Color(0.8f, 0.8f, 0.8f)), + new KeyValuePair(15, new Color(0.7f, 0.7f, 0.7f)), + new KeyValuePair(16, new Color(0.6f, 0.6f, 0.6f)), + new KeyValuePair(17, new Color(0.6f, 0.5f, 0.5f)), + new KeyValuePair(18, new Color(0.6f, 0.4f, 0.4f)), + new KeyValuePair(19, new Color(0.6f, 0.3f, 0.3f)), + new KeyValuePair(20, new Color(0.35f, 0.2f, 0.3f)), + new KeyValuePair(21, new Color(0.1f, 0.1f, 0.3f)), + new KeyValuePair(22, new Color(0.0f, 0.0f, 0.3f)), + new KeyValuePair(23, new Color(0.0f, 0.0f, 0.3f)), + }; + + public static Color Evaluate(float time) + { + time = Mathf.Clamp01(time); + + if (time >= 1.0f) + { + return _hourColors.Last().Value; + } + + float adjustedValue = time * _hourCount; + + int floorValue = (int)adjustedValue % _hourCount; + int nextValue = (floorValue + 1) % _hourCount; + float interpolation = adjustedValue % 1.0f; + + Color color1 = _hourColors[floorValue].Value; + Color color2 = _hourColors[nextValue].Value; + + return Color.Lerp(color1, color2, interpolation); + } + } +} diff --git a/Assets/Scripts/Lantern/EQ/Environment/WorldLightColor.cs.meta b/Assets/Scripts/Lantern/EQ/Environment/WorldLightColor.cs.meta new file mode 100644 index 0000000..1567d3b --- /dev/null +++ b/Assets/Scripts/Lantern/EQ/Environment/WorldLightColor.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 4cb97ade8c041db44b91525abac5f514 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Scripts/Lantern/EQ/Equipment/Equipment3dHandler.cs b/Assets/Scripts/Lantern/EQ/Equipment/Equipment3dHandler.cs index c42d955..45e7581 100644 --- a/Assets/Scripts/Lantern/EQ/Equipment/Equipment3dHandler.cs +++ b/Assets/Scripts/Lantern/EQ/Equipment/Equipment3dHandler.cs @@ -2,6 +2,7 @@ using System.Collections.Generic; using System.Linq; using Lantern.EQ.Animation; +using Lantern.EQ.Audio; using Lantern.EQ.Helpers; using Lantern.EQ.Lighting; using UnityEngine; @@ -24,6 +25,9 @@ public class Equipment3dHandler : MonoBehaviour private int _renderLayer; private Action _updateModelCallback; + public EquipmentSound EquipmentSoundPrimary; + public EquipmentSound EquipmentSoundSecondary; + private void Awake() { _dynamicLightSetter = GetComponent(); @@ -109,6 +113,13 @@ public void SpawnItemInSlot(Equipment3dSlot point, GameObject item) } } + if (point == Equipment3dSlot.MainHand) + { + var equipmentName = item.name; + int number = Convert.ToInt32(equipmentName.Replace("it", string.Empty)); + EquipmentHelper.GetSoundForEquipment(number); + } + UpdateLayerValues(); } @@ -233,5 +244,23 @@ public void SetModelUpdateCallback(Action onNewActiveModel) { _updateModelCallback = onNewActiveModel; } + + public void PlayAnimation(AnimationType animationType) + { + if (_spawnedEquipment == null) + { + return; + } + + foreach (var equipment in _spawnedEquipment.Values) + { + if (equipment.EquipmentAnimation == null) + { + continue; + } + + equipment.EquipmentAnimation.Play(animationType); + } + } } } diff --git a/Assets/Scripts/Lantern/EQ/Equipment/EquipmentHelper.cs b/Assets/Scripts/Lantern/EQ/Equipment/EquipmentHelper.cs index 6ff1ccb..a6cdae1 100644 --- a/Assets/Scripts/Lantern/EQ/Equipment/EquipmentHelper.cs +++ b/Assets/Scripts/Lantern/EQ/Equipment/EquipmentHelper.cs @@ -1,6 +1,7 @@ using System; using System.Collections.Generic; using Lantern.EQ.Animation; +using Lantern.EQ.Audio; namespace Lantern.EQ.Equipment { @@ -62,5 +63,54 @@ public static string GetVeliousHelmet(string modelName) { "ikm", "IT635" }, { "ikf", "IT630" }, }; + + public static EquipmentSound GetSoundForEquipment(int itemNumber) + { + switch (itemNumber) + { + case 0: + return EquipmentSound.HandToHand; + case 4: + return EquipmentSound.Bow; + case 61: + return EquipmentSound.Whip; + case 1: + case 2: + case 3: + case 5: + case 7: + case 9: + case 16: + case 17: + case 19: + case 20: + case 23: + case 24: + case 25: + case 30: + case 34: + case 35: + case 37: + case 39: + case 40: + case 41: + case 42: + case 43: + case 44: + case 50: + case 51: + case 53: + case 57: + case 58: + case 59: + case 60: + case 62: + return EquipmentSound.Slashing; + case int x when x is >= 200 and <= 300: + return EquipmentSound.Bash; + default: + return EquipmentSound.Blunt; + } + } } } diff --git a/Assets/Scripts/Lantern/EQ/Equipment/EquipmentModel.cs b/Assets/Scripts/Lantern/EQ/Equipment/EquipmentModel.cs index fad57b0..6b11c70 100644 --- a/Assets/Scripts/Lantern/EQ/Equipment/EquipmentModel.cs +++ b/Assets/Scripts/Lantern/EQ/Equipment/EquipmentModel.cs @@ -1,23 +1,32 @@ using System.Collections.Generic; using System.Linq; +using Lantern.EQ.Animation; using UnityEngine; -public class EquipmentModel : MonoBehaviour +namespace Lantern.EQ.Equipment { - public List Renderers; - - public void SetLayer(int layer) + /// + /// A directory class for all equipment models to avoid runtime component querying + /// + public class EquipmentModel : MonoBehaviour { - foreach (var r in Renderers) + public List Renderers; + public EquipmentAnimation EquipmentAnimation; + + public void SetLayer(int layer) { - r.gameObject.layer = layer; + foreach (var r in Renderers) + { + r.gameObject.layer = layer; + } } - } - #if UNITY_EDITOR - public void FindRenderers() - { - Renderers = GetComponentsInChildren().ToList(); +#if UNITY_EDITOR + public void SetReferences(EquipmentAnimation equipmentAnimation) + { + EquipmentAnimation = equipmentAnimation; + Renderers = GetComponentsInChildren().ToList(); + } +#endif } - #endif } diff --git a/Assets/Scripts/Lantern/EQ/Equipment/NonPlayableVariantHandler.cs b/Assets/Scripts/Lantern/EQ/Equipment/NonPlayableVariantHandler.cs index 3dbcdc0..9cd9f6d 100644 --- a/Assets/Scripts/Lantern/EQ/Equipment/NonPlayableVariantHandler.cs +++ b/Assets/Scripts/Lantern/EQ/Equipment/NonPlayableVariantHandler.cs @@ -53,7 +53,6 @@ public void SetCurrentActiveVariant(int texture, int helmTexture) HandleMainMeshes(helmTexture); SetActiveMeshFromGroup(_secondaryMeshes, helmTexture); SetMeshMaterials(texture); - FixColdainKing(); if (CharacterSoundsBase != null) { @@ -67,17 +66,6 @@ private void SetMeshMaterials(int index) SetMeshMaterialsInGroup(_secondaryMeshes, index); } - private void FixColdainKing() - { - foreach (var mesh in _secondaryMeshes) - { - if(mesh.name == "cokhe01") - { - mesh.SetActive(true); - } - } - } - private void SetMeshMaterialsInGroup(List meshes, int textureId) { if (_alternateSkins.Count == 0) diff --git a/Assets/Scripts/Lantern/EQ/Helpers/MeshHelper.cs b/Assets/Scripts/Lantern/EQ/Helpers/MeshHelper.cs new file mode 100644 index 0000000..bb700dd --- /dev/null +++ b/Assets/Scripts/Lantern/EQ/Helpers/MeshHelper.cs @@ -0,0 +1,17 @@ +using UnityEngine; + +namespace Lantern.EQ.Helpers +{ + public static class MeshHelper + { + public static void RotateMesh(Mesh mesh, float rotationDegrees) + { + Vector3[] vertices = mesh.vertices; + for (int i = 0; i < vertices.Length; i++) + { + vertices[i] = Quaternion.Euler(0f, rotationDegrees, 0f) * vertices[i]; + } + mesh.SetVertices(vertices); + } + } +} diff --git a/Assets/Scripts/Lantern/EQ/Helpers/MeshHelper.cs.meta b/Assets/Scripts/Lantern/EQ/Helpers/MeshHelper.cs.meta new file mode 100644 index 0000000..d778e0e --- /dev/null +++ b/Assets/Scripts/Lantern/EQ/Helpers/MeshHelper.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 3892c15d9296a57408c2be2659fa3ab4 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Scripts/Lantern/EQ/Helpers/RotationHelper.cs b/Assets/Scripts/Lantern/EQ/Helpers/RotationHelper.cs new file mode 100644 index 0000000..17aef09 --- /dev/null +++ b/Assets/Scripts/Lantern/EQ/Helpers/RotationHelper.cs @@ -0,0 +1,71 @@ +using UnityEngine; + +namespace Lantern.EQ.Helpers +{ + public static class RotationHelper + { + public static float GetEqToLanternRotation(float rotation) + { + return rotation * 1.0f / 512.0f * 360.0f; + } + + public static float GetLanternToEqRotation(float rotation) + { + return rotation * 1.0f / 360f * 512f / 2f; + } + + public static float GetLanternToEqPlayerRotation(float rotation) + { + rotation %= 360f; + rotation -= 90f; + rotation /= 360f; + rotation = (1 - rotation) * 256f; + rotation %= 256f; + if (rotation < 0) + { + rotation += 256f; + } + return rotation; + } + + public static float GetEqToLanternPlayerRotation(float rotation) + { + if (rotation < 0) + { + rotation += 256f; + } + rotation %= 256f; + rotation = (256 - rotation) / 256f; + rotation *= 360f; + rotation += 90f; + rotation %= 360; + return rotation; + } + + public static float RotateWithWrap(float angle, float delta) + { + angle += delta; + while (angle < 0) + { + angle += 360; + } + + return angle % 360f; + } + + public static float GetRotationToTarget(Vector3 currentPosition, Vector3 targetPosition) + { + Vector3 direction = PositionHelper.GetEqToLanternPosition(targetPosition, true) - + PositionHelper.GetEqToLanternPosition(currentPosition, true); + direction.y = 0f; // Ensure rotation only around the y-axis + + if (direction != Vector3.zero) + { + Quaternion targetRotation = Quaternion.LookRotation(direction); + return targetRotation.eulerAngles.y; + } + + return 0f; + } + } +} diff --git a/Assets/Scripts/Lantern/EQ/Helpers/RotationHelper.cs.meta b/Assets/Scripts/Lantern/EQ/Helpers/RotationHelper.cs.meta new file mode 100644 index 0000000..395769c --- /dev/null +++ b/Assets/Scripts/Lantern/EQ/Helpers/RotationHelper.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: cd92f92702fed7348abcde9b533085ab +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Scripts/Lantern/EQ/Lantern.EQ.asmdef b/Assets/Scripts/Lantern/EQ/Lantern.EQ.asmdef index 7ddf089..81a05a6 100644 --- a/Assets/Scripts/Lantern/EQ/Lantern.EQ.asmdef +++ b/Assets/Scripts/Lantern/EQ/Lantern.EQ.asmdef @@ -2,7 +2,8 @@ "name": "Lantern.EQ", "rootNamespace": "", "references": [ - "GUID:d1d6e5262cee07546bdd2e2e867ec461" + "GUID:d1d6e5262cee07546bdd2e2e867ec461", + "GUID:6055be8ebefd69e48b49212b09b47b2f" ], "includePlatforms": [], "excludePlatforms": [], diff --git a/Assets/Scripts/Lantern/EQ/Lantern/LanternTags.cs b/Assets/Scripts/Lantern/EQ/Lantern/LanternTags.cs index d54f621..b2719fc 100644 --- a/Assets/Scripts/Lantern/EQ/Lantern/LanternTags.cs +++ b/Assets/Scripts/Lantern/EQ/Lantern/LanternTags.cs @@ -5,6 +5,7 @@ /// public static class LanternTags { + public static string Player = "Player"; public static string PrefabRoot = "PrefabRoot"; public static string ZoneRoot = "ZoneRoot"; public static string ObjectRoot = "ObjectRoot"; diff --git a/Assets/Scripts/Lantern/EQ/Lighting/AmbientLightSetterDynamic.cs b/Assets/Scripts/Lantern/EQ/Lighting/AmbientLightSetterDynamic.cs index 12bf292..c368ae1 100644 --- a/Assets/Scripts/Lantern/EQ/Lighting/AmbientLightSetterDynamic.cs +++ b/Assets/Scripts/Lantern/EQ/Lighting/AmbientLightSetterDynamic.cs @@ -34,6 +34,8 @@ public class AmbientLightSetterDynamic : MonoBehaviour // TODO: Replace as property blocks no longer with with URP private MaterialPropertyBlock _block; + private List _sharedMaterials = new List(); + private int _dynamicSunlightID = Shader.PropertyToID("_DynamicSunlight"); private Vector3 _captureHeight; private bool _isInstantSunlight; @@ -52,9 +54,6 @@ public void Initialize(float captureHeight, ZoneAmbientLightValues sunlightValue _sunlightRecaptureCurrent = Random.Range(0f, 0.5f); ForceUpdate(); - - // Deprecate post 0.1.5 - MPB do not work with URP - _block = new MaterialPropertyBlock(); } /// @@ -110,6 +109,8 @@ private void UpdateLight() } } + _block ??= new MaterialPropertyBlock(); + foreach (var renderer in _renderers) { if (!renderer.gameObject.activeSelf) @@ -117,11 +118,12 @@ private void UpdateLight() continue; } - for (int i = 0; i < renderer.sharedMaterials.Length; ++i) + renderer.GetSharedMaterials(_sharedMaterials); + + for (int i = 0; i < _sharedMaterials.Count; ++i) { - _block = new MaterialPropertyBlock(); renderer.GetPropertyBlock(_block, i); - _block.SetFloat("_DynamicSunlight", _lastSunlight); + _block.SetFloat(_dynamicSunlightID, _lastSunlight); renderer.SetPropertyBlock(_block, i); } } diff --git a/Assets/Scripts/Lantern/EQ/Lighting/SunlightController.cs b/Assets/Scripts/Lantern/EQ/Lighting/SunlightController.cs new file mode 100644 index 0000000..fd1def4 --- /dev/null +++ b/Assets/Scripts/Lantern/EQ/Lighting/SunlightController.cs @@ -0,0 +1,27 @@ +using Lantern.EQ.Environment; +using UnityEngine; + +namespace Lantern.EQ.Lighting +{ + public class SunlightController : MonoBehaviour + { + [SerializeField] + private Transform _sunTransform; + + [SerializeField] + private Light _light; + + public void UpdateTime(float time) + { + float angle = CalculateSunAngle(time); + _sunTransform.localRotation = Quaternion.Euler(angle, 0f, 0f); + _light.color = WorldLightColor.Evaluate(time); + } + + private float CalculateSunAngle(float time) + { + float angle = Mathf.Lerp(-90f, 270f, time); + return angle; + } + } +} diff --git a/Assets/Scripts/Lantern/EQ/Lighting/SunlightController.cs.meta b/Assets/Scripts/Lantern/EQ/Lighting/SunlightController.cs.meta new file mode 100644 index 0000000..61b850e --- /dev/null +++ b/Assets/Scripts/Lantern/EQ/Lighting/SunlightController.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: d859f4d50b45c5849a77865b948909fa +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Scripts/Lantern/EQ/Sound/CharacterSoundType.cs b/Assets/Scripts/Lantern/EQ/Sound/CharacterSoundType.cs index afe1b34..a3e471d 100644 --- a/Assets/Scripts/Lantern/EQ/Sound/CharacterSoundType.cs +++ b/Assets/Scripts/Lantern/EQ/Sound/CharacterSoundType.cs @@ -2,32 +2,35 @@ { public enum CharacterSoundType { - Idle, - GetHit, - Jump, - Loop, - Gasp, - Death, - Drown, - Walking, - Running, - Attack, - SAttack, - TAttack, - Passive, - Sit, - Crouch, - Treading, - Swim, - Kneel, - Kick, - Pierce, - TwoHandSlash, - TwoHandBlunt, - Archery, - FlyingKick, - RapidPunch, - LargePunch, - Bash + None = 0, + Loop = 1, + Idle = 2, + GetHit = 3, + Jump = 4, + Gasp = 5, + Death = 6, + Drown = 7, + Walking = 8, + Running = 9, + Attack = 10, + SAttack = 11, + TAttack = 12, + Passive = 13, + Sit = 14, + Crouch = 15, + Treading = 16, + Swim = 17, + Kneel = 18, + Kick = 19, + Pierce = 20, + TwoHandSlash = 21, + TwoHandBlunt = 22, + Archery = 23, + FlyingKick = 24, + RapidPunch = 25, + LargePunch = 26, + Bash = 27, + RoundKick = 28, + Climb = 29 } } diff --git a/Assets/Scripts/Lantern/EQ/Trigger.meta b/Assets/Scripts/Lantern/EQ/Trigger.meta new file mode 100644 index 0000000..5096211 --- /dev/null +++ b/Assets/Scripts/Lantern/EQ/Trigger.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: f831320daf5f9884eb33a311c30bcaae +folderAsset: yes +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Scripts/Lantern/EQ/Trigger/SphereTrigger.cs b/Assets/Scripts/Lantern/EQ/Trigger/SphereTrigger.cs new file mode 100644 index 0000000..7ae437f --- /dev/null +++ b/Assets/Scripts/Lantern/EQ/Trigger/SphereTrigger.cs @@ -0,0 +1,32 @@ +using UnityEngine; + +namespace Lantern.EQ.Trigger +{ + public abstract class SphereTrigger : MonoBehaviour + { + [SerializeField] + protected SphereCollider _collider; + + [SerializeField] + protected string _tag; + + protected abstract void OnEnter(); + protected abstract void OnExit(); + + private void OnTriggerEnter(Collider col) + { + if (col.gameObject.CompareTag(_tag)) + { + OnEnter(); + } + } + + private void OnTriggerExit(Collider col) + { + if (col.gameObject.CompareTag(_tag)) + { + OnExit(); + } + } + } +} diff --git a/Assets/Scripts/Lantern/EQ/Trigger/SphereTrigger.cs.meta b/Assets/Scripts/Lantern/EQ/Trigger/SphereTrigger.cs.meta new file mode 100644 index 0000000..fdc5679 --- /dev/null +++ b/Assets/Scripts/Lantern/EQ/Trigger/SphereTrigger.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 6e164520971dae64b9e1275f111586e3 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Scripts/Lantern/EQ/Viewers.meta b/Assets/Scripts/Lantern/EQ/Viewers.meta new file mode 100644 index 0000000..4d57220 --- /dev/null +++ b/Assets/Scripts/Lantern/EQ/Viewers.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: 3cbab9319e38c0c4eadb1096993ab663 +folderAsset: yes +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Scripts/Lantern/EQ/Viewers/AssetBundleHelper.cs b/Assets/Scripts/Lantern/EQ/Viewers/AssetBundleHelper.cs new file mode 100644 index 0000000..f103243 --- /dev/null +++ b/Assets/Scripts/Lantern/EQ/Viewers/AssetBundleHelper.cs @@ -0,0 +1,70 @@ +using System; +using System.IO; +using Lantern.EQ.AssetBundles; +using UnityEngine; + +namespace Lantern.EQ.Viewers +{ + public static class AssetBundleHelper + { + /// + /// Takes an asset bundle ID and returns the versioned asset bundle name + /// + /// + /// + public static string GetAssetBundleName(LanternAssetBundleId assetBundleId) + { + var bundleName = assetBundleId.ToString().ToLower(); + var version = AssetBundleVersions.GetVersion(assetBundleId); + string versionString = version.ToString(); + bundleName += "-" + versionString.Replace(".", "_"); + return bundleName; + } + + /// + /// Takes an asset bundle name and a category and returns the versioned asset bundle name + /// + /// + /// + /// + public static string GetAssetBundleName(LanternAssetBundleId bundleCategory, string bundleName) + { + var version = AssetBundleVersions.GetVersion(bundleCategory); + string versionString = version.ToString(); + bundleName += "-" + versionString.Replace(".", "_"); + return bundleName; + } + + public static string GetAssetBundlePath() + { + return Path.Combine(Application.streamingAssetsPath, "AssetBundles"); + } + + public static bool DoesGlobalBundleExist(LanternAssetBundleId assetBundleId) + { + var bundleName = GetAssetBundleName(assetBundleId); + string path = GetAssetBundlePath(); + return File.Exists(Path.Combine(path, bundleName)); + } + + public static bool DoesZoneBundleExist(string shortname) + { + shortname = shortname.ToLower(); + var bundleName = GetAssetBundleName(LanternAssetBundleId.Zones, shortname); + string path = GetAssetBundlePath(); + return File.Exists(Path.Combine(path, bundleName)); + } + + public static bool IsGlobalBundle(string fileName) + { + foreach (var bundleId in Enum.GetNames(typeof(LanternAssetBundleId))) + { + if (fileName.ToLower().StartsWith(bundleId.ToLower())) + { + return true; + } + } + return false; + } + } +} diff --git a/Assets/Scripts/Lantern/EQ/Viewers/AssetBundleHelper.cs.meta b/Assets/Scripts/Lantern/EQ/Viewers/AssetBundleHelper.cs.meta new file mode 100644 index 0000000..528ac59 --- /dev/null +++ b/Assets/Scripts/Lantern/EQ/Viewers/AssetBundleHelper.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: e8b8c40ba64de2144bcef8b659ccf090 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Scripts/Lantern/EQ/Viewers/AssetBundleLoader.cs b/Assets/Scripts/Lantern/EQ/Viewers/AssetBundleLoader.cs new file mode 100644 index 0000000..3972408 --- /dev/null +++ b/Assets/Scripts/Lantern/EQ/Viewers/AssetBundleLoader.cs @@ -0,0 +1,62 @@ +using System.Collections.Generic; +using System.IO; +using Lantern.EQ.AssetBundles; +using UnityEngine; + +namespace Lantern.EQ.Viewers +{ + /// + /// A dirt simple asset bundle loader + /// Assumes asset bundles are located in Assets/StreamingAssets/AssetBundles + /// This classes uses the LanternEQ bundle names + /// Asset bundles can be loaded both by their ID and by raw name (e.g. zone bundles) + /// + public class AssetBundleLoader + { + private string _assetBundlePath = Path.Combine(Application.streamingAssetsPath, "AssetBundles"); + private Dictionary _loadedBundles = new(); + + public bool LoadAssetBundle(string assetBundleName) + { + var path = Path.Combine(_assetBundlePath, assetBundleName); + + if (_loadedBundles.ContainsKey(path)) + { + return true; + } + + var bundle = AssetBundle.LoadFromFile(path); + + if (bundle == null) + { + Debug.LogError($"Unable to load asset bundle at path: {path}"); + return false; + } + + _loadedBundles[assetBundleName] = bundle; + return true; + } + + public T LoadAsset(LanternAssetBundleId assetBundleId, string assetName) where T : Object + { + var bundleName = AssetBundleHelper.GetAssetBundleName(assetBundleId); + if (_loadedBundles.TryGetValue(bundleName, out var bundle)) + { + return bundle.LoadAsset(assetName); + } + + return !LoadAssetBundle(bundleName) ? default : _loadedBundles[bundleName].LoadAsset(assetName); + } + + public T LoadAsset(string assetBundleName, LanternAssetBundleId bundleType, string assetName) where T : Object + { + var path = AssetBundleHelper.GetAssetBundleName(bundleType, assetBundleName); + if (_loadedBundles.TryGetValue(path, out var bundle)) + { + return bundle.LoadAsset(assetName); + } + + return !LoadAssetBundle(path) ? default : _loadedBundles[path].LoadAsset(assetName); + } + } +} diff --git a/Assets/Scripts/Lantern/EQ/Viewers/AssetBundleLoader.cs.meta b/Assets/Scripts/Lantern/EQ/Viewers/AssetBundleLoader.cs.meta new file mode 100644 index 0000000..f1a63a1 --- /dev/null +++ b/Assets/Scripts/Lantern/EQ/Viewers/AssetBundleLoader.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 1ba879abd2e54944e949c750f753bbf3 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Scripts/Lantern/EQ/Viewers/Camera.meta b/Assets/Scripts/Lantern/EQ/Viewers/Camera.meta new file mode 100644 index 0000000..ac9a02c --- /dev/null +++ b/Assets/Scripts/Lantern/EQ/Viewers/Camera.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: 11fa48b9018d2614093810111a00dfc1 +folderAsset: yes +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Scripts/Lantern/EQ/Viewers/Camera/CameraBase.cs b/Assets/Scripts/Lantern/EQ/Viewers/Camera/CameraBase.cs new file mode 100644 index 0000000..b5d6618 --- /dev/null +++ b/Assets/Scripts/Lantern/EQ/Viewers/Camera/CameraBase.cs @@ -0,0 +1,15 @@ +using UnityEngine; + +namespace Lantern.EQ.Viewers +{ + public abstract class CameraBase : MonoBehaviour + { + [SerializeField] + protected Camera Camera; + + public Camera GetCamera() + { + return Camera; + } + } +} diff --git a/Assets/Scripts/Lantern/EQ/Viewers/Camera/CameraBase.cs.meta b/Assets/Scripts/Lantern/EQ/Viewers/Camera/CameraBase.cs.meta new file mode 100644 index 0000000..1d98f5f --- /dev/null +++ b/Assets/Scripts/Lantern/EQ/Viewers/Camera/CameraBase.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: f54d8389628ed61489201607d9a49e07 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Scripts/Lantern/EQ/Viewers/Camera/FreeCamera.cs b/Assets/Scripts/Lantern/EQ/Viewers/Camera/FreeCamera.cs new file mode 100644 index 0000000..ad6ee38 --- /dev/null +++ b/Assets/Scripts/Lantern/EQ/Viewers/Camera/FreeCamera.cs @@ -0,0 +1,56 @@ +using UnityEngine; + +namespace Lantern.EQ.Viewers +{ + public class FreeCamera : CameraBase + { + [SerializeField] + private float movementSpeed = 5f; + + [SerializeField, Range(1f, 1040f)] + private float rotationSpeed = 360f; + + [SerializeField, Range(-90f, 90f)] + private float minVerticalAngle = -90f; + + [SerializeField, Range(-90f, 90f)] + private float maxVerticalAngle = 90f; + + private Vector2 rotationAngles; + + private void Update() + { + HandleMovementInput(); + HandleRotationInput(); + ConstrainAngles(); + } + + private void HandleMovementInput() + { + float horizontal = Input.GetAxis("Horizontal"); + float vertical = Input.GetAxis("Vertical"); + + Vector3 movement = new Vector3(horizontal, 0f, vertical) * movementSpeed * Time.deltaTime; + transform.Translate(movement); + } + + private void HandleRotationInput() + { + Vector2 mouseInput = new Vector2( + -Input.GetAxis("Mouse Y"), + Input.GetAxis("Mouse X") + ); + + rotationAngles.x += rotationSpeed * Time.unscaledDeltaTime * mouseInput.x; + rotationAngles.y += rotationSpeed * Time.unscaledDeltaTime * mouseInput.y; + + transform.rotation = Quaternion.Euler(rotationAngles.x, rotationAngles.y, 0f); + } + + private void ConstrainAngles() + { + rotationAngles.x = Mathf.Clamp(rotationAngles.x, minVerticalAngle, maxVerticalAngle); + rotationAngles.y %= 360; + } + } +} diff --git a/Assets/Scripts/Lantern/EQ/Viewers/Camera/FreeCamera.cs.meta b/Assets/Scripts/Lantern/EQ/Viewers/Camera/FreeCamera.cs.meta new file mode 100644 index 0000000..de0221d --- /dev/null +++ b/Assets/Scripts/Lantern/EQ/Viewers/Camera/FreeCamera.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 5b9387143afd25f40815cbb8206f6479 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Scripts/Lantern/EQ/Viewers/Camera/RotateCamera.cs b/Assets/Scripts/Lantern/EQ/Viewers/Camera/RotateCamera.cs new file mode 100644 index 0000000..936186d --- /dev/null +++ b/Assets/Scripts/Lantern/EQ/Viewers/Camera/RotateCamera.cs @@ -0,0 +1,30 @@ +using UnityEngine; + +public class RotateCamera : MonoBehaviour +{ + public float rotationSpeed = 5.0f; + public float pitchRange = 80.0f; + + private float pitch = 0.0f; + private float yaw = 0.0f; + + public float smoothSpeed = 10.0f; + private Vector2 currentRotation = Vector2.zero; + + void Update() + { + float mouseX = Input.GetAxis("Mouse X"); + float mouseY = -Input.GetAxis("Mouse Y"); // Inverted for natural mouse movement + + yaw += mouseX * rotationSpeed; + pitch += mouseY * rotationSpeed; + + // Clamp the pitch to prevent looking straight up or down + pitch = Mathf.Clamp(pitch, -pitchRange, pitchRange); + + // Smooth the rotation + currentRotation = Vector2.Lerp(currentRotation, new Vector2(pitch, yaw), smoothSpeed * Time.deltaTime); + + transform.eulerAngles = new Vector3(currentRotation.x, currentRotation.y, 0.0f); + } +} diff --git a/Assets/Scripts/Lantern/EQ/Viewers/Camera/RotateCamera.cs.meta b/Assets/Scripts/Lantern/EQ/Viewers/Camera/RotateCamera.cs.meta new file mode 100644 index 0000000..3a157df --- /dev/null +++ b/Assets/Scripts/Lantern/EQ/Viewers/Camera/RotateCamera.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 3914d1570f7d1b14a85ca7bb1396a42a +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Scripts/Lantern/EQ/Viewers/CharacterViewer.meta b/Assets/Scripts/Lantern/EQ/Viewers/CharacterViewer.meta new file mode 100644 index 0000000..437d34d --- /dev/null +++ b/Assets/Scripts/Lantern/EQ/Viewers/CharacterViewer.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: 0bdcedabab8eea443aad8aa002d76a94 +folderAsset: yes +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Scripts/Lantern/EQ/Viewers/CharacterViewer/CharacterViewerRoot.cs b/Assets/Scripts/Lantern/EQ/Viewers/CharacterViewer/CharacterViewerRoot.cs new file mode 100644 index 0000000..62fadb0 --- /dev/null +++ b/Assets/Scripts/Lantern/EQ/Viewers/CharacterViewer/CharacterViewerRoot.cs @@ -0,0 +1,771 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using Infrastructure.EQ.TextParser; +using Lantern.EQ.Animation; +using Lantern.EQ.AssetBundles; +using Lantern.EQ.Equipment; +using Lantern.EQ.Helpers; +using Lantern.EQ.Viewers; +using TMPro; +using UnityEditor; +using UnityEngine; +using UnityEngine.UI; +#if UNITY_EDITOR +#endif + +namespace Lantern.Legacy.CharacterViewer +{ + public class CharacterViewerRoot : MonoBehaviour + { + [SerializeField] + private GameObject _uiCanvas; + + [SerializeField] + private TextMeshProUGUI _modelNameText; + + [SerializeField] + private TextMeshProUGUI _raceIdText; + + [SerializeField] + private TextMeshProUGUI _modelGenderText; + + [SerializeField] + private TextMeshProUGUI _modelVariantText; + + [SerializeField] + private TextMeshProUGUI _cameraZoomText; + + [SerializeField] + private TextMeshProUGUI _cameraHeightText; + + [SerializeField] + private TMP_InputField _raceInputField; + + private List _modelData = new List(); + + private Equipment2dHandler _e2dHandler; + + private int _currentModelId = 0; + private Gender _currentModelGender = Gender.Male; + private CharacterModelDefinition _currentModelData; + + private int _currentSkin; + private int _skinCountMax; + private int _currentFace; + private int _faceCountMax; + private int _currentHead; + private int _headCountMax; + private int _currentArmor; + private int[] _armorSkinIds = { 0, 1, 2, 3, 4, 17, 18, 19, 20, 21, 22, 23 }; + + private bool _isUiHidden = false; + + private enum Gender + { + Male = 0, + Female = 1, + Neutral = 2, + } + + [SerializeField] + private OrbitCamera _camera; + + private AssetBundleLoader _assetBundleLoader; + private GameObject _currentModel; + + [SerializeField] + private GameObject _gotoWindow; + + [SerializeField] + private List _backgrounds; + + private int _currentBackgroundIndex; + + [SerializeField] + private CharacterViewerTracks _tracks; + + private int _previousTouchCount = 0; + + [SerializeField] private Button _gotoButton; + + + private class CharacterModelDefinition + { + public int Id { get; set; } + public string Name { get; set; } + public string Code { get; set; } + public string Male { get; set; } + public string Female { get; set; } + public string Neutral { get; set; } + public float CameraDistance { get; set; } + public float YOffset { get; set; } + + public List SkinGroupsMale { get; set; } + public List SkinGroupsFemale { get; set; } + public List SkinGroupsNeutral { get; set; } + + public string DefaultAnimation { get; set; } + + } + + private int _raceIdInput; + + private void Start() + { + RenderSettings.skybox = _backgrounds[0]; + + _assetBundleLoader = new AssetBundleLoader(); + LoadModelList(); + SpawnNewModel(); + // ServiceFactory.Get().SetLoadingScreenVisibility(false); + } + + private void LoadModelList() + { + var text = Resources.Load("RaceData-CharacterViewer"); + + if (text == null) + { + return; + } + + var lines = TextParser.ParseTextByDelimitedLines(text.text, ','); + + for (int i = 0; i < lines.Count; ++i) + { + if (i == 0) + { + continue; + } + + List skinsMale = new List(); + ParseSkins(skinsMale, lines[i][8]); + + List skinsFemale = new List(); + ParseSkins(skinsFemale, lines[i][9]); + + List skinsNeutral = new List(); + ParseSkins(skinsNeutral, lines[i][10]); + + _modelData.Add(new CharacterModelDefinition + { + Id = Convert.ToInt32(lines[i][0]), + Name = lines[i][1], + Code = lines[i][2], + Male = lines[i][3], + Female = lines[i][4], + Neutral = lines[i][5], + CameraDistance = lines[i][6] == string.Empty ? 10.0f : Convert.ToSingle(lines[i][6]), + YOffset = lines[i][7] == string.Empty ? 0.0f : Convert.ToSingle(lines[i][7]), + SkinGroupsMale = skinsMale, + SkinGroupsFemale = skinsFemale, + SkinGroupsNeutral = skinsNeutral, + DefaultAnimation = lines[i][11] + }); + } + } + + private void ParseSkins(List skins, string skinString) + { + var skinGroups = skinString.Split('_'); + + if (skinString != string.Empty) + { + foreach (var skin in skinGroups) + { + var skinList = skin.Split(';'); + + if (skinList.Length != 2) + { + Debug.LogError("Error parsing skin list"); + continue; + } + + skins.Add(new Vector2Int {x = Convert.ToInt32(skinList[0]), y = Convert.ToInt32(skinList[1])}); + } + } + } + + public void ChangeVariantButton() + { + if (IsPlayableRace()) + { + ChangeModelFace(); + } + else + { + ChangeModelSkin(); + } + } + + private void Update() + { + CheckForInput(); + UpdateCameraUiInfo(); + } + + private void CheckForInput() + { + if (Input.GetKeyDown(KeyCode.Escape)) + { +#if UNITY_EDITOR + EditorApplication.ExitPlaymode(); +#else + Application.Quit(); +#endif + } + + if (Input.GetKeyDown(KeyCode.RightArrow)) + { + ChangeCurrentModel(true); + return; + } + + if (Input.GetKeyDown(KeyCode.LeftArrow)) + { + ChangeCurrentModel(false); + return; + } + + int currentTouch = Input.touchCount; + if (Input.GetKeyDown(KeyCode.V)) + { + ChangeModelSkin(); + } + + if (Input.GetKeyDown(KeyCode.F)) + { + ChangeModelFace(); + } + + if (Input.GetKeyDown(KeyCode.A)) + { + ChangeModelArmor(); + } + + if (Input.GetKeyDown(KeyCode.H)) + { + ChangeModelHead(); + } + + if (Input.GetKeyDown(KeyCode.BackQuote) || (_previousTouchCount != 3 && currentTouch == 3)) + { + bool isActive = !_gotoWindow.activeSelf; + _gotoWindow.SetActive(isActive); + + _camera._inputActive = !isActive; + } + + if (Input.GetKeyDown(KeyCode.Return)) + { + if (_gotoWindow.activeSelf) + { + GotoRaceId(); + } + } + + if (Input.GetKeyDown(KeyCode.F10)) + { + ToggleUiHidden(); + } + + _previousTouchCount = currentTouch; + } + + private void ToggleUiHidden() + { + _isUiHidden = !_isUiHidden; + + _uiCanvas.SetActive(!_isUiHidden); + } + + public void ChangeBackgroundClearColor(bool increment) + { + if (increment) + { + _currentBackgroundIndex++; + } + else + { + _currentBackgroundIndex--; + } + + if (_currentBackgroundIndex < 0) + { + _currentBackgroundIndex = _backgrounds.Count - 1; + } + else + { + _currentBackgroundIndex %= _backgrounds.Count; + } + + RenderSettings.skybox = _backgrounds[_currentBackgroundIndex]; + } + + private void ChangeModelSkin() + { + if (_currentModel == null) + { + return; + } + + List skinGroups = GetSkinGroupForCurrentGender(); + + if (skinGroups.Count < 1) + { + return; + } + + _currentSkin++; + + if (_currentSkin >= skinGroups.Count) + { + _currentSkin = 0; + } + + var variantSwapper = _currentModel.GetComponent(); + + if (variantSwapper) + { + variantSwapper.SetCurrentActiveVariant(skinGroups[_currentSkin].x, skinGroups[_currentSkin].y); + } + + UpdateVariantText(); + } + + private void ChangeModelFace() + { + if (_currentModel == null || _e2dHandler == null) + { + return; + } + + _currentFace++; + + if (_currentFace >= _faceCountMax) + { + _currentFace = 0; + } + + _e2dHandler.SetFaceId(_currentFace); + UpdateVariantText(); + } + + private void ChangeModelHead() + { + if (_currentModel == null || _e2dHandler == null) + { + return; + } + + _currentHead++; + if (_currentHead >= _headCountMax) + { + _currentHead = 0; + } + + _e2dHandler.SetArmorSetActive(GetArmorSkin(), _currentHead); + } + + private void ChangeModelArmor() + { + if (_currentModel == null || _e2dHandler == null) + { + return; + } + + _currentArmor++; + + if (IsMonkArmor() && !IsMonkRace()) + { + _currentArmor++; + } + + if (_currentArmor >= _armorSkinIds.Count()) + { + _currentArmor = 0; + } + + _e2dHandler.SetArmorSetActive(GetArmorSkin(), _currentHead); + } + + private List GetSkinGroupForCurrentGender() + { + switch (_currentModelGender) + { + case Gender.Male: + return _currentModelData.SkinGroupsMale; + case Gender.Female: + return _currentModelData.SkinGroupsFemale; + case Gender.Neutral: + return _currentModelData.SkinGroupsNeutral; + } + + return null; + } + + public void GotoRaceId() + { + if (_raceIdInput < 0 || _raceIdInput >= _modelData.Count) + { + _raceInputField.Select(); + _raceInputField.ActivateInputField(); + return; + } + + int targetRaceId = -1; + for (var i = 0; i < _modelData.Count; i++) + { + var raceEntry = _modelData[i]; + if (raceEntry.Id == _raceIdInput) + { + targetRaceId = i; + } + } + + if (targetRaceId == -1) + { + _raceInputField.Select(); + _raceInputField.ActivateInputField(); + return; + } + + Gender gender; + var targetModelData = _modelData[targetRaceId]; + if (!string.IsNullOrEmpty(targetModelData.Male)) + { + gender = Gender.Male; + } + else if (!string.IsNullOrEmpty(targetModelData.Female)) + { + gender = Gender.Female; + } + else if (!string.IsNullOrEmpty(targetModelData.Neutral)) + { + gender = Gender.Neutral; + } + else + { + return; + } + + _currentModelId = targetRaceId; + _currentModelGender = gender; + SpawnNewModel(); + _gotoWindow.SetActive(false); + _raceInputField.text = string.Empty; + _camera._inputActive = true; + } + + public void CloseGotoWindow() + { + if (_gotoWindow.activeSelf) + { + _gotoWindow.SetActive(false); + _camera._inputActive = true; + } + } + + public void NewRaceIdEntered(string newId) + { + if (newId == string.Empty) + { + _raceIdInput = -1; + _gotoButton.interactable = false; + return; + } + + _raceIdInput = Convert.ToInt32(newId); + _gotoButton.interactable = IsValidRaceId(_raceIdInput); + } + + private bool IsValidRaceId(int raceIdInput) + { + foreach (var entry in _modelData) + { + if (entry.Id == raceIdInput) + { + if (entry.Male != string.Empty || entry.Female != string.Empty || entry.Neutral != string.Empty) + { + return true; + } + } + } + + return false; + } + + private bool IsPlayableRace() + { + return RaceHelper.PlayableRaceIds.Contains(_currentModelData.Id); + } + + private bool IsMonkArmor() + { + var armorSkin = GetArmorSkin(); + return armorSkin == 4 || armorSkin == 23; + } + + private bool IsMonkRace() + { + return _currentModelData != null && (_currentModelData.Id == 1 || _currentModelData.Id == 128); + } + + private int GetArmorSkin() + { + return _currentArmor < _armorSkinIds.Count() ? _armorSkinIds[_currentArmor] : 0; + } + + private void UpdateCameraUiInfo() + { + _cameraZoomText.text = _camera.CurrentDistance.ToString("0.0"); + _cameraHeightText.text = _camera._targetPosition.y.ToString("0.0"); + } + + public void ChangeCurrentModel(bool up) + { + int newModelId = _currentModelId; + Gender newGenderId = _currentModelGender; + string newRaceString = string.Empty; + + do + { + switch (newGenderId) + { + case Gender.Male: + if (up) + { + newGenderId = Gender.Female; + newRaceString = _modelData[newModelId].Female; + } + else + { + newGenderId = Gender.Neutral; + newModelId -= 1; + + if (newModelId < 0) + { + newModelId = _modelData.Count - 1; + } + + newRaceString = _modelData[newModelId].Neutral; + } + + break; + case Gender.Female: + newGenderId = up ? Gender.Neutral : Gender.Male; + newRaceString = up ? _modelData[newModelId].Neutral : _modelData[newModelId].Male; + break; + case Gender.Neutral: + if (up) + { + newGenderId = Gender.Male; + newModelId += 1; + + if (newModelId >= _modelData.Count) + { + newModelId = 0; + } + newRaceString = _modelData[newModelId].Male; + } + else + { + newGenderId = Gender.Female; + newRaceString = _modelData[newModelId].Female; + } + + break; + } + } while (newRaceString == string.Empty); + + _currentModelId = newModelId; + _currentModelGender = newGenderId; + _currentSkin = 0; + _currentFace = 0; + _currentHead = 0; + _currentArmor = 0; + _faceCountMax = 0; + _headCountMax = 0; + + SpawnNewModel(); + } + + public void UpdateVariantText() + { + if (IsPlayableRace()) + { + + _modelVariantText.text = "Face " + (_currentFace + 1) + "/" + _faceCountMax; + } + else + { + _modelVariantText.text = "Variant " + (_currentSkin + 1) + "/" + _skinCountMax; + } + } + + private void SpawnNewModel() + { + if (_currentModel) + { + Destroy(_currentModel.gameObject); + _currentModel = null; + } + + _currentModelData = _modelData[_currentModelId]; + string newShortName = string.Empty; + string newFullName = _currentModelData.Name; + + switch (_currentModelGender) + { + case Gender.Male: + newShortName = _currentModelData.Male; + _skinCountMax = _currentModelData.SkinGroupsMale.Count; + break; + case Gender.Female: + newShortName = _currentModelData.Female; + _skinCountMax = _currentModelData.SkinGroupsFemale.Count; + break; + case Gender.Neutral: + newShortName = _currentModelData.Neutral; + _skinCountMax = _currentModelData.SkinGroupsNeutral.Count; + break; + } + + int realId = _currentModelData.Id; + float cameraDistance = _currentModelData.CameraDistance; + float yOffset = _currentModelData.YOffset; + + _modelNameText.text = newFullName; + _raceIdText.text = realId + $" - {newShortName}" + " (" + _currentModelGender.ToString().Substring(0, 1) + + ")"; + + var model = _assetBundleLoader.LoadAsset + (LanternAssetBundleId.Characters, newShortName + ".prefab"); + + if (model == null) + { + return; + } + + _currentModel = Instantiate(model); + + _e2dHandler = _currentModel.GetComponent(); + if (_e2dHandler != null) + { + _faceCountMax = _e2dHandler.GetFaceCount(); + _headCountMax = _e2dHandler.GetHeadCount(); + } + + UpdateVariantText(); + + if (newShortName == "EYE") + { + _currentModel.transform.rotation = Quaternion.Euler(0f, -90, 0f); + } + + var uac = _currentModel.GetComponent(); + + string defaultAnimation = string.Empty; + + if (uac != null) + { + defaultAnimation = _currentModelData.DefaultAnimation; + + if (string.IsNullOrEmpty(defaultAnimation)) + { + defaultAnimation = "p01"; + } + + var defaultAnimationType = AnimationHelper.GetAnimationType(defaultAnimation); + if (defaultAnimationType != null) + { + uac.SetNewConstantState((AnimationType)defaultAnimationType, 1); + } + } + + _camera.SetNewTarget(cameraDistance, yOffset); + + var skinGroup = GetGenderVariants(); + + if (skinGroup == null + || skinGroup.Count == 0) + { + Debug.LogError("Missing variant definitions for: " + newShortName); + return; + } + + var defaultSkin = skinGroup[0]; + + NonPlayableVariantHandler handler = _currentModel.GetComponent(); + + if (handler != null) + { + handler.SetCurrentActiveVariant(defaultSkin.x, defaultSkin.y); + } + + PopulateAnimationList(); + + if (defaultAnimation != string.Empty) + { + _tracks.HighlightItem(defaultAnimation); + } + } + + private List GetGenderVariants() + { + switch (_currentModelGender) + { + case Gender.Male: + return _currentModelData.SkinGroupsMale; + case Gender.Female: + return _currentModelData.SkinGroupsFemale; + case Gender.Neutral: + return _currentModelData.SkinGroupsNeutral; + } + + return null; + } + + private void PopulateAnimationList() + { + if (_currentModel == null) + { + _tracks.ClearList(); + return; + } + + Dictionary clips = new Dictionary(); + CharacterAnimationController controller = _currentModel.GetComponent(); + + Animation animation = _currentModel.GetComponent(); + + if (animation == null) + { + _tracks.ClearList(); + return; + } + + List clipNames = new List(); + + foreach (AnimationState clip in animation) + { + clipNames.Add(clip.name); + } + + clipNames = clipNames.OrderBy(str => str.Split('_').Last()).ToList(); + + foreach (string clipName in clipNames) + { + string animationType = clipName.Split('_')[1]; + string animationName = "" + AnimationHelper.GetAnimationName(animationType) + ""+ $"\n({clipName})"; + if (!clips.ContainsKey(animationType)) + { + clips.Add(animationType, animationName); + } + } + + _tracks.RefreshList(clips, (AnimationType animationType) => controller.PlayOneShotAnimation(animationType)); + } + } +} diff --git a/Assets/Scripts/Lantern/EQ/Viewers/CharacterViewer/CharacterViewerRoot.cs.meta b/Assets/Scripts/Lantern/EQ/Viewers/CharacterViewer/CharacterViewerRoot.cs.meta new file mode 100644 index 0000000..802aa88 --- /dev/null +++ b/Assets/Scripts/Lantern/EQ/Viewers/CharacterViewer/CharacterViewerRoot.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 221dc15f4992ffa47b9788a58a7fb247 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Scripts/Lantern/EQ/Viewers/CharacterViewer/CharacterViewerTrackItem.cs b/Assets/Scripts/Lantern/EQ/Viewers/CharacterViewer/CharacterViewerTrackItem.cs new file mode 100644 index 0000000..70c371a --- /dev/null +++ b/Assets/Scripts/Lantern/EQ/Viewers/CharacterViewer/CharacterViewerTrackItem.cs @@ -0,0 +1,21 @@ +using UnityEngine; +using UnityEngine.Events; +using UnityEngine.EventSystems; + +namespace Lantern.Legacy.CharacterViewer +{ + public class CharacterViewerTrackItem : MonoBehaviour, IPointerClickHandler + { + public string itemID = null; + + [System.Serializable] + public class TrackItemEvent : UnityEvent { } + + public TrackItemEvent onClick; + + public void OnPointerClick(PointerEventData pointerEventData) + { + onClick.Invoke(itemID); + } + } +} diff --git a/Assets/Scripts/Lantern/EQ/Viewers/CharacterViewer/CharacterViewerTrackItem.cs.meta b/Assets/Scripts/Lantern/EQ/Viewers/CharacterViewer/CharacterViewerTrackItem.cs.meta new file mode 100644 index 0000000..0cf37a6 --- /dev/null +++ b/Assets/Scripts/Lantern/EQ/Viewers/CharacterViewer/CharacterViewerTrackItem.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: a7de96dc83a8a4949a1928341fef3ccc +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Scripts/Lantern/EQ/Viewers/CharacterViewer/CharacterViewerTracks.cs b/Assets/Scripts/Lantern/EQ/Viewers/CharacterViewer/CharacterViewerTracks.cs new file mode 100644 index 0000000..5821d6b --- /dev/null +++ b/Assets/Scripts/Lantern/EQ/Viewers/CharacterViewer/CharacterViewerTracks.cs @@ -0,0 +1,110 @@ +using System.Collections.Generic; +using Lantern.EQ.Animation; +using TMPro; +using UnityEngine; +using UnityEngine.UI; + +namespace Lantern.Legacy.CharacterViewer +{ + public class CharacterViewerTracks : MonoBehaviour + { + public GameObject Content = null; + public GameObject ItemTemplate = null; + public delegate void PlayOneShot(AnimationType animationType); + + private RectTransform _contentRectTransform; + private RectTransform _templateRectTransform; + private Image _templateImage; + + Dictionary _items = new Dictionary(); + List _goItems = new List(); + + void Start() + { + _contentRectTransform = Content.GetComponent(); + _templateRectTransform = ItemTemplate.GetComponent(); + _templateImage = ItemTemplate.GetComponentInChildren(); + } + + public void HighlightItem(string selectedItem) + { + foreach (GameObject goItem in _goItems) + { + Text text = goItem.GetComponentInChildren(); + Image image = goItem.GetComponentInChildren(); + CharacterViewerTrackItem ti = goItem.GetComponentInChildren(); + + image.color = Color.clear; + if (ti.itemID == selectedItem) + image.color = _templateImage.color; + } + } + + public void ClearList() + { + foreach (GameObject go in _goItems) + { + Destroy(go); + } + + _goItems.Clear(); + } + + public void RefreshList(Dictionary items, PlayOneShot playOneShotAnimation) + { + // Keep Items Locally + _items = items; + + // Clear List of Items + ClearList(); + + float yPos = 0.0f; + int i = 0; + + foreach (KeyValuePair item in items) + { + // Clone Item from Template and add to items + GameObject goItem = Instantiate(ItemTemplate, ItemTemplate.transform.parent); + + goItem.transform.SetParent(ItemTemplate.transform.parent); + + // Fix Position and Scale + RectTransform rt = goItem.GetComponent(); + rt.localPosition = new Vector3(0.0f, yPos, 0.0f); + rt.localScale = Vector3.one; + + // Set Text + CharacterViewerTrackItem ti = goItem.GetComponentInChildren(); + ti.itemID = item.Key; + + ti.onClick.AddListener((string animationSuffix) => { + var animation = AnimationHelper.GetAnimationType(animationSuffix); + if (animation != null) + playOneShotAnimation((AnimationType) animation); + }); + + // Set Text + TextMeshProUGUI text = goItem.GetComponentInChildren(); + text.text = item.Value; + + // Set Background Color + Image image = goItem.GetComponentInChildren(); + image.color = Color.clear; + + // Set GameObject Name and Make Active + goItem.name = "Item " + i; + goItem.SetActive(true); + + // Add to Running List for Destroying Later + _goItems.Add(goItem); + + yPos -= _templateRectTransform.rect.height; + i++; + } + + // Resize Content Height to Accomodate New Item + Vector2 sizeDelta = new Vector2(0.0f, yPos * -1); + _contentRectTransform.sizeDelta = sizeDelta; + } + } +} diff --git a/Assets/Scripts/Lantern/EQ/Viewers/CharacterViewer/CharacterViewerTracks.cs.meta b/Assets/Scripts/Lantern/EQ/Viewers/CharacterViewer/CharacterViewerTracks.cs.meta new file mode 100644 index 0000000..3f3500d --- /dev/null +++ b/Assets/Scripts/Lantern/EQ/Viewers/CharacterViewer/CharacterViewerTracks.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: fa0daf482b2beda468919c827d1b2625 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Scripts/Lantern/EQ/Viewers/CharacterViewer/CharacterViewerUI.cs b/Assets/Scripts/Lantern/EQ/Viewers/CharacterViewer/CharacterViewerUI.cs new file mode 100644 index 0000000..bfdb23f --- /dev/null +++ b/Assets/Scripts/Lantern/EQ/Viewers/CharacterViewer/CharacterViewerUI.cs @@ -0,0 +1,18 @@ +using TMPro; +using UnityEngine; + +namespace Lantern.Legacy.CharacterViewer +{ + public class CharacterViewerUI : MonoBehaviour + { + [SerializeField] + private OrbitCamera cameraNew; + + [SerializeField] + private TextMeshProUGUI _text; + void Update() + { + _text.text = cameraNew.CurrentDistance.ToString("0.00"); + } + } +} diff --git a/Assets/Scripts/Lantern/EQ/Viewers/CharacterViewer/CharacterViewerUI.cs.meta b/Assets/Scripts/Lantern/EQ/Viewers/CharacterViewer/CharacterViewerUI.cs.meta new file mode 100644 index 0000000..bedf1ae --- /dev/null +++ b/Assets/Scripts/Lantern/EQ/Viewers/CharacterViewer/CharacterViewerUI.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: c8441f22d0421194f84e09c4d04c08e3 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Scripts/Lantern/EQ/Viewers/CharacterViewer/OrbitCamera.cs b/Assets/Scripts/Lantern/EQ/Viewers/CharacterViewer/OrbitCamera.cs new file mode 100644 index 0000000..13984ce --- /dev/null +++ b/Assets/Scripts/Lantern/EQ/Viewers/CharacterViewer/OrbitCamera.cs @@ -0,0 +1,215 @@ +using System.Collections; +using System.Collections.Generic; +using UnityEngine; +using UnityEngine.EventSystems; + +namespace Lantern.Legacy.CharacterViewer +{ + public class OrbitCamera : MonoBehaviour + { + public Vector3 _targetPosition; + public float CurrentDistance = 5.0f; + public float xSpeed = 5000.0f; + public float ySpeed = 5000.0f; + + [SerializeField] + private RectTransform _uiElement; + + [SerializeField] + private float yMinLimit = -30f; + + [SerializeField] + private float yMaxLimit = 90f; + + [SerializeField] + private float _distanceMin = 0.25f; + + [SerializeField] + private float _distanceMax = 5f; + public float maxSpeed = 20f; + + private float _zoomSpeed; + + private float _defaultDistance = 10f; + + + private Rigidbody rigidbody; + + private Vector2 _inputVelocity = new Vector2(); + + private Vector2 _rotation; + + [SerializeField] private float damping; + + private Vector2 _defaultRotation; + + public bool _inputActive = true; + + private float _previousPinchDistance = -1.0f; + + private bool _rotateTouchAvailable = true; + + private void Start() + { + Vector3 angles = transform.eulerAngles; + _rotation = new Vector2(angles.y, angles.x); + _defaultRotation = _rotation; + float ratio = _uiElement.sizeDelta.x * FindObjectOfType().scaleFactor / Screen.width; + var rect = GetComponent().rect; + rect.x = ratio; + GetComponent().rect = rect; + } + + public void SetNewTarget(float defaultDistance, float followOffset) + { + _targetPosition = new Vector3(0f, followOffset, 0f); + CurrentDistance = defaultDistance; + _defaultDistance = defaultDistance; + _zoomSpeed = defaultDistance / 10.0f; + } + + public void ResetCamera() + { + CurrentDistance = _defaultDistance; + _inputVelocity = Vector2.zero; + _rotation = _defaultRotation; + } + + void LateUpdate() + { + if (Input.GetKeyDown(KeyCode.R)) + { + ResetCamera(); + } + + if (Input.GetKeyDown(KeyCode.UpArrow)) + { + var speed = Input.GetKey(KeyCode.LeftShift) ? 1.0f : 0.1f; + var targetPos = _targetPosition; + targetPos.y += speed; + _targetPosition = targetPos; + } + + if (Input.GetKeyDown(KeyCode.DownArrow)) + { + var speed = Input.GetKey(KeyCode.LeftShift) ? 1.0f : 0.1f; + var targetPos = _targetPosition; + targetPos.y -= speed; + _targetPosition = targetPos; + } + + if (_inputActive && Input.touchCount == 2 && !IsPointerOverUiObject()) + { + float pinchDist = Vector2.Distance(Input.GetTouch(0).position, Input.GetTouch(1).position); + + if (_previousPinchDistance != -1.0f) + { + float newDistance = CurrentDistance - (pinchDist - _previousPinchDistance) * _zoomSpeed * Time.deltaTime; + CurrentDistance = + Mathf.Clamp(newDistance, _distanceMin * _defaultDistance, _distanceMax * _defaultDistance); + } + + _previousPinchDistance = pinchDist; + } + else + { + if (_previousPinchDistance != -1f) + { + StartCoroutine(SetPostPinchTimeout()); + } + + _previousPinchDistance = -1f; + } + + if (_inputActive && Input.touchCount == 1 && !IsTouchOverUiObject() && _rotateTouchAvailable) + { + var touch = Input.GetTouch(0); + _inputVelocity.x = touch.deltaPosition.x * 50f * Time.deltaTime; + _inputVelocity.y = -touch.deltaPosition.y * 50f * Time.deltaTime; + } + + /*if (Input.GetMouseButton(0) && !IsPointerOverUiObject()) + { + _inputVeloity.x = Input.GetAxis("Mouse X") * xSpeed * Time.deltaTime; + _inputVeloity.y = -Input.GetAxis("Mouse Y") * ySpeed * Time.deltaTime; + }*/ + + _rotation.x += _inputVelocity.x; + _rotation.y += _inputVelocity.y; + + _rotation.y = Mathf.Clamp(_rotation.y, yMinLimit, yMaxLimit); + + Quaternion rotation = Quaternion.Euler(_rotation.y, _rotation.x, 0); + + if (!IsPointerOverUiObject() && _inputActive) + { + float newDistance = CurrentDistance - Input.GetAxis("Mouse ScrollWheel") * _zoomSpeed; + CurrentDistance = + Mathf.Clamp(newDistance, _distanceMin * _defaultDistance, _distanceMax * _defaultDistance); + } + + PositionCamera(rotation, CurrentDistance); + + // Damping + _inputVelocity.x = Mathf.MoveTowards(_inputVelocity.x, 0.0f, damping * Time.deltaTime); + _inputVelocity.y = Mathf.MoveTowards(_inputVelocity.y, 0.0f, damping * Time.deltaTime); + + // Velocity limit + _inputVelocity.x = Mathf.Clamp(_inputVelocity.x, -maxSpeed, maxSpeed); + _inputVelocity.y = Mathf.Clamp(_inputVelocity.y, -maxSpeed, maxSpeed); + } + + private IEnumerator SetPostPinchTimeout() + { + _rotateTouchAvailable = false; + yield return new WaitForSeconds(0.1f); + _rotateTouchAvailable = true; + } + + private void PositionCamera(Quaternion rotation, float distance) + { + Vector3 negDistance = new Vector3(0.0f, 0.0f, -CurrentDistance); + Vector3 position = rotation * negDistance + _targetPosition; + + transform.rotation = rotation; + transform.position = position; + } + + private static bool IsPointerOverUiObject() + { + var eventDataCurrentPosition = new PointerEventData(EventSystem.current) + { + position = new Vector2(Input.mousePosition.x, Input.mousePosition.y) + }; + + List results = new List(); + EventSystem.current.RaycastAll(eventDataCurrentPosition, results); + return results.Count > 0; + } + + private static bool IsTouchOverUiObject() + { + if (Input.touchCount == 0) + { + return false; + } + + PointerEventData eventDataCurrentPosition = new PointerEventData(EventSystem.current); + for (int i = 0; i < Input.touchCount; ++i) + { + var touch = Input.GetTouch(i); + + eventDataCurrentPosition.position = new Vector2(touch.position.x, touch.position.y); + List results = new List(); + EventSystem.current.RaycastAll(eventDataCurrentPosition, results); + + if (results.Count > 0) + { + return true; + } + } + + return false; + } + } +} diff --git a/Assets/Scripts/Lantern/EQ/Viewers/CharacterViewer/OrbitCamera.cs.meta b/Assets/Scripts/Lantern/EQ/Viewers/CharacterViewer/OrbitCamera.cs.meta new file mode 100644 index 0000000..9a0410d --- /dev/null +++ b/Assets/Scripts/Lantern/EQ/Viewers/CharacterViewer/OrbitCamera.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 93e3db421ad94344b8d1a315f7b63dc0 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Scripts/Lantern/EQ/Viewers/DatabaseLoader.cs b/Assets/Scripts/Lantern/EQ/Viewers/DatabaseLoader.cs new file mode 100644 index 0000000..0145eb6 --- /dev/null +++ b/Assets/Scripts/Lantern/EQ/Viewers/DatabaseLoader.cs @@ -0,0 +1,48 @@ +using System.IO; +using Infrastructure.Lantern.SQLite; + +namespace Lantern.EQ.Viewers +{ + public class DatabaseLoader + { + private SQLiteConnection database; + public DatabaseLoader(string databasePath) + { + if (!LoadDatabase(Path.Combine(databasePath, "lantern_server.db"))) + { + return; + } + } + + private bool LoadDatabase(string databasePath) + { + try + { + database = new SQLiteConnection(databasePath, SQLiteOpenFlags.ReadOnly); + } + catch (SQLiteException e) + { + string message = string.Empty; + +#if UNITY_EDITOR + message = $"Unable to load database."; +#else + message = "Error loading databases."; +#endif + return false; + } + + return true; + } + + public SQLiteConnection GetDatabase() + { + return database; + } + + public void CloseConnections() + { + database.Close(); + } + } +} diff --git a/Assets/Scripts/Lantern/EQ/Viewers/DatabaseLoader.cs.meta b/Assets/Scripts/Lantern/EQ/Viewers/DatabaseLoader.cs.meta new file mode 100644 index 0000000..57c2db6 --- /dev/null +++ b/Assets/Scripts/Lantern/EQ/Viewers/DatabaseLoader.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: a5d38bda385337e45b711b09df1b876f +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Scripts/Lantern/EQ/Viewers/MusicPlayer.cs b/Assets/Scripts/Lantern/EQ/Viewers/MusicPlayer.cs new file mode 100644 index 0000000..55f48ad --- /dev/null +++ b/Assets/Scripts/Lantern/EQ/Viewers/MusicPlayer.cs @@ -0,0 +1,282 @@ +using System.Collections.Generic; +using System.IO; +using System.Linq; +using Infrastructure.EQ.MeltySynth; +using Lantern.EQ.AssetBundles; +using Lantern.EQ.Audio; +using TMPro; +using UnityEngine; + +namespace Scenes.EQ.MusicPlayer +{ + public class MusicPlayer : MonoBehaviour + { + public string SoundFontFile = "synthusr_samplefix.sf2"; + public string FileName; + public bool Loop; + public bool ReverbChorus; + public float MasterVolume = 0.28f; + public int MaximumPolyphony = 64; + public int TrackNumber; + public ChannelFlag ChannelMask; + + public bool IsPlaying => !_sequencer?.EndOfSequence ?? false; + + [SerializeField] + private TextMeshProUGUI _fileNameLabel; + [SerializeField] + private TextMeshProUGUI _trackLabel; + [SerializeField] + private TextMeshProUGUI _loopLabel; + [SerializeField] + private TextMeshProUGUI _sfLabel; + private AssetBundle _musicAssetBundle; + [SerializeField] + private List _midiFiles; + private int _midiFileIndex; + + private MidiTrackCollection _midiFile; + private MidiFile _currentPlaybackMidi; + private Synthesizer _synthesizer; + private MidiFileSequencer _sequencer; + private SoundFont _soundFont; + + private int _seqBranchZeroIndex; + private bool _playPending; + + void Awake() + { + Application.targetFrameRate = 60; + + if (!LoadMusicBundle()) + { + Debug.LogError("Failed to load Music_Midi bundle"); + return; + } + + _midiFiles = _musicAssetBundle.GetAllAssetNames() + .Select(filepath => Path.GetFileName(filepath)) + .ToList(); + + SelectFile(0); + ChangeLooping(true); + CreateSynth(); + _sfLabel.SetText(SoundFontFile); + } + + void Update() + { + if (Input.GetKey(KeyCode.Escape)) + { + Application.Quit(); + } + + if (Input.GetKeyUp(KeyCode.LeftArrow)) + { + ChangeFile(-1); + } + + if (Input.GetKeyUp(KeyCode.RightArrow)) + { + ChangeFile(1); + } + + if (Input.GetKeyUp(KeyCode.UpArrow)) + { + ChangeTrack(1); + } + + if (Input.GetKeyUp(KeyCode.DownArrow)) + { + ChangeTrack(-1); + } + + if (Input.GetKeyUp(KeyCode.L)) + { + ChangeLooping(!Loop); + } + + if (Input.GetKeyUp(KeyCode.Space)) + { + if (IsPlaying) + { + Stop(); + } + else + { + Play(); + } + } + } + + private bool LoadMusicBundle() + { + const LanternAssetBundleId bundleId = LanternAssetBundleId.Music_Midi; + var bundleVersion = AssetBundleVersions.GetVersion(bundleId); + + if (bundleVersion == null) + { + return false; + } + + var bundleName = bundleId.ToString().ToLower() + "-" + bundleVersion.ToString().Replace('.', '_'); + var bundleFilepath = Path.Combine(Application.streamingAssetsPath, "AssetBundles", bundleName); + _musicAssetBundle = AssetBundle.LoadFromFile(bundleFilepath); + + return _musicAssetBundle != null; + } + + public MidiTrackCollection LoadMidiTrackCollection(string fileName) + { + return _musicAssetBundle.LoadAsset(fileName); + } + + private MidiFile LoadPlaybackMidiFile() + { + var midiTrack = _midiFile.MidiTracks.ElementAtOrDefault(TrackNumber); + if (midiTrack == null) + { + return null; + } + + using var stream = new MemoryStream(midiTrack.Bytes); + return new MidiFile(stream, MidiFileLoopType.FinalFantasy); + } + + public void Play() + { + _playPending = true; + } + + private void PlayMidi() + { + if (_currentPlaybackMidi == null) + { + return; + } + + _playPending = false; + _sequencer.Play(_currentPlaybackMidi, Loop); + } + + public void Stop() + { + _sequencer.Stop(); + } + + private void SelectFile(int index) + { + FileName = _midiFiles[index]; + _midiFile = LoadMidiTrackCollection(FileName); + + SelectTrack(0); + + _fileNameLabel.SetText($"File: {FileName}"); + + if (IsPlaying) + { + Play(); + } + } + + private void SelectTrack(int index) + { + var trackCount = _midiFile.MidiTracks.Count; + TrackNumber = Mathf.Clamp(index, 0, trackCount); + _currentPlaybackMidi = LoadPlaybackMidiFile(); + + _trackLabel.SetText($"Track: {TrackNumber + 1}/{trackCount}"); + + if (IsPlaying) + { + Play(); + } + } + + private void ChangeFile(int change) + { + _midiFileIndex = (_midiFiles.Count + _midiFileIndex + change) % _midiFiles.Count; + SelectFile(_midiFileIndex); + } + + private void ChangeTrack(int change) + { + var trackIndex = (_midiFile.MidiTracks.Count + TrackNumber + change) % _midiFile.MidiTracks.Count; + SelectTrack(trackIndex); + } + + private void ChangeLooping(bool looping) + { + Loop = looping; + _loopLabel.SetText("Loop: " + (Loop ? "Yes" : "No")); + } + + private void CreateSynth() + { + var sfFilePath = Path.Combine(Application.streamingAssetsPath, "Soundfont", SoundFontFile); + _soundFont = new SoundFont(sfFilePath); + + var settings = new SynthesizerSettings(AudioSettings.outputSampleRate) + { + MaximumPolyphony = MaximumPolyphony, + EnableReverbAndChorus = ReverbChorus, + }; + + _synthesizer = new Synthesizer(_soundFont, settings) + { + MasterVolume = MasterVolume, + }; + _sequencer = new MidiFileSequencer(_synthesizer); + + _sequencer.OnSendMessage += HandleOnMidiMessage; + } + + private void HandleOnMidiMessage(Synthesizer synthesizer, int channel, int command, int data1, int data2) + { + // Debug.Log($"MidiMessageCallback: {channel}, {command}, {data1}, {data2}"); + + switch (command) + { + case 0x90: // NoteOn + var bitflagChannel = (ChannelFlag)(1 << channel); + if (ChannelMask != ChannelFlag.Unset && (ChannelMask & bitflagChannel) == 0) + { + return; + } + break; + case 0xb0: // CC + // SEQ_INDEX + if (data1 == 120 && data2 == 0) + { + _seqBranchZeroIndex = _sequencer.MessageIndex; + } + + // CALLBACK_PFX + if (data1 == 108 && channel == 0 && data2 == 0) + { + _sequencer.Seek(_seqBranchZeroIndex); + return; + } + break; + } + + synthesizer.ProcessMidiMessage(channel, command, data1, data2); + } + + private void OnAudioFilterRead(float[] data, int channels) + { + if (_playPending) + { + PlayMidi(); + return; + } + + if (!IsPlaying) + { + return; + } + + _sequencer.RenderInterleaved(data); + } + } +} diff --git a/Assets/Scripts/Lantern/EQ/Viewers/MusicPlayer.cs.meta b/Assets/Scripts/Lantern/EQ/Viewers/MusicPlayer.cs.meta new file mode 100644 index 0000000..a4900cb --- /dev/null +++ b/Assets/Scripts/Lantern/EQ/Viewers/MusicPlayer.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 26977947b51924c45bd93bde86bd531d +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Scripts/Lantern/EQ/Viewers/SelectionItemBase.cs b/Assets/Scripts/Lantern/EQ/Viewers/SelectionItemBase.cs new file mode 100644 index 0000000..76f26f2 --- /dev/null +++ b/Assets/Scripts/Lantern/EQ/Viewers/SelectionItemBase.cs @@ -0,0 +1,28 @@ +using System; +using UnityEngine; + +namespace Lantern.EQ.Viewers +{ + public abstract class SelectionItemBase : MonoBehaviour + { + protected Action SelectionAction; + private int _index; + + public abstract void Initialize(Action action, params string[] args); + + public void OnClick() + { + SelectionAction?.Invoke(_index); + } + + public void SetIndex(int index) + { + _index = index; + } + + public virtual void Clear() + { + SelectionAction = null; + } + } +} diff --git a/Assets/Scripts/Lantern/EQ/Viewers/SelectionItemBase.cs.meta b/Assets/Scripts/Lantern/EQ/Viewers/SelectionItemBase.cs.meta new file mode 100644 index 0000000..dc9ec4d --- /dev/null +++ b/Assets/Scripts/Lantern/EQ/Viewers/SelectionItemBase.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 3bfdd4efe76bfd54c89d766d4d5217e7 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Scripts/Lantern/EQ/Viewers/SelectionItemZone.cs b/Assets/Scripts/Lantern/EQ/Viewers/SelectionItemZone.cs new file mode 100644 index 0000000..521c5c1 --- /dev/null +++ b/Assets/Scripts/Lantern/EQ/Viewers/SelectionItemZone.cs @@ -0,0 +1,29 @@ +using System; +using TMPro; +using UnityEngine; + +namespace Lantern.EQ.Viewers +{ + public class SelectionItemZone : SelectionItemBase + { + [SerializeField] + private TextMeshProUGUI _longName; + + [SerializeField] + private TextMeshProUGUI _shortName; + + public override void Initialize(Action action, params string[] args) + { + SelectionAction = action; + _longName.text = args[0]; + _shortName.text = args[1]; + } + + public override void Clear() + { + base.Clear(); + _longName.text = string.Empty; + _shortName.text = string.Empty; + } + } +} diff --git a/Assets/Scripts/Lantern/EQ/Viewers/SelectionItemZone.cs.meta b/Assets/Scripts/Lantern/EQ/Viewers/SelectionItemZone.cs.meta new file mode 100644 index 0000000..172bf9f --- /dev/null +++ b/Assets/Scripts/Lantern/EQ/Viewers/SelectionItemZone.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 82e33815607465a4c9ebfb0a8c003332 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Scripts/Lantern/EQ/Viewers/SkyViewer.cs b/Assets/Scripts/Lantern/EQ/Viewers/SkyViewer.cs new file mode 100644 index 0000000..f634c2e --- /dev/null +++ b/Assets/Scripts/Lantern/EQ/Viewers/SkyViewer.cs @@ -0,0 +1,113 @@ +using Lantern.EQ.AssetBundles; +using Lantern.EQ.Environment; +using TMPro; +using UnityEngine; + +namespace Lantern.EQ.Viewers +{ + /// + /// A simple sky viewer demonstrating how to set up and manipulate EverQuest's sky + /// + public class SkyViewer : ViewerBase + { + [Header("Sky Viewer")] + [SerializeField] + private SkyViewerOptions _options; + + [SerializeField] + private Camera _camera; + + [SerializeField] + private TextMeshProUGUI _debugText; + + [SerializeField] + private GameObject _plane; + + private SkyController _sky; + private float _time; + private int _skyIndex; + + protected override void Awake() + { + base.Awake(); + InitializeOptions(); + LoadSky(); + } + + private void InitializeOptions() + { + _time = _options.StartingTime; + _skyIndex = _options.StartingSkyIndex; + _plane.SetActive(_options.ShowGroundPlane); + } + + private void LoadSky() + { + // Load sky bundle + var sky = AssetBundleLoader.LoadAsset(LanternAssetBundleId.Sky, "Sky"); + + if (sky == null) + { + ShowError("Unable to load sky prefab."); + return; + } + + // Instantiate the sky prefab and scale it up to make sure it's not clipped by the camera + // Uniform scaling does not make the sky appear larger + var skyGo = Instantiate(sky); + skyGo.transform.localScale = new Vector3(10f, 10f, 10f); + + // Get the SkyController component and initialize values + _sky = skyGo.GetComponent(); + _sky.SetSecondsPerDay(_options.SecondsPerDay); + _sky.SetEnabledSky(_options.StartingSkyIndex); + _sky.UpdateTime(_time); + } + + private void Update() + { + HandleInput(); + _sky.UpdateTime(_time); + UpdateText(); + UpdateTimeOfDay(); + } + + private void UpdateTimeOfDay() + { + _time += Time.deltaTime * 1f / _options.SecondsPerDay; + _time %= 1f; + _camera.backgroundColor = WorldLightColor.Evaluate(_time); + } + + private void UpdateText() + { + DebugTextBuilder.Clear(); + DebugTextBuilder.AppendLine("Sky Viewer"); + DebugTextBuilder.AppendLine($"Sky index: {_skyIndex}"); + DebugTextBuilder.AppendLine($"Time: {_time:0.00}"); + DebugTextBuilder.AppendLine($"Sky Objects: {_sky.GetSkyObjectCount()}"); + _debugText.text = DebugTextBuilder.ToString(); + } + + private void HandleInput() + { + if (Input.GetKeyDown(KeyCode.RightArrow)) + { + _skyIndex = Mathf.Min(_skyIndex + 1, 5); + _sky.SetEnabledSky(_skyIndex); + } + if(Input.GetKeyDown(KeyCode.LeftArrow)) + { + _skyIndex = Mathf.Max(_skyIndex - 1, 1); + _sky.SetEnabledSky(_skyIndex); + } + } + + private void LateUpdate() + { + _sky.UpdateTimeLate(Time.deltaTime, _time); + Shader.SetGlobalColor("_DayNightColor", WorldLightColor.Evaluate(_time)); + } + } +} + diff --git a/Assets/Scripts/Lantern/EQ/Viewers/SkyViewer.cs.meta b/Assets/Scripts/Lantern/EQ/Viewers/SkyViewer.cs.meta new file mode 100644 index 0000000..f19de55 --- /dev/null +++ b/Assets/Scripts/Lantern/EQ/Viewers/SkyViewer.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 156cf78843d2c8d43a6c06e8b0c7a365 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Scripts/Lantern/EQ/Viewers/SkyViewerOptions.cs b/Assets/Scripts/Lantern/EQ/Viewers/SkyViewerOptions.cs new file mode 100644 index 0000000..bb4791f --- /dev/null +++ b/Assets/Scripts/Lantern/EQ/Viewers/SkyViewerOptions.cs @@ -0,0 +1,13 @@ +using UnityEngine; + +namespace Lantern.EQ.Viewers +{ + public class SkyViewerOptions : ScriptableObject + { + public int StartingSkyIndex = 1; + public float StartingTime = 0.5f; + public bool TickTime = true; + public float SecondsPerDay = 60f; + public bool ShowGroundPlane; + } +} diff --git a/Assets/Scripts/Lantern/EQ/Viewers/SkyViewerOptions.cs.meta b/Assets/Scripts/Lantern/EQ/Viewers/SkyViewerOptions.cs.meta new file mode 100644 index 0000000..adca958 --- /dev/null +++ b/Assets/Scripts/Lantern/EQ/Viewers/SkyViewerOptions.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: ee97bf22f3b80ab4fac0070dbeeb2c34 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Scripts/Lantern/EQ/Viewers/ViewerBase.cs b/Assets/Scripts/Lantern/EQ/Viewers/ViewerBase.cs new file mode 100644 index 0000000..a81f046 --- /dev/null +++ b/Assets/Scripts/Lantern/EQ/Viewers/ViewerBase.cs @@ -0,0 +1,26 @@ +using System.Text; +using UnityEngine; + +namespace Lantern.EQ.Viewers +{ + public abstract class ViewerBase : MonoBehaviour + { + [Header("Viewer Base")] + [SerializeField] + private ViewerError _viewerError; + + protected AssetBundleLoader AssetBundleLoader; + protected StringBuilder DebugTextBuilder; + + protected virtual void Awake() + { + AssetBundleLoader = new AssetBundleLoader(); + DebugTextBuilder = new StringBuilder(); + } + + protected void ShowError(string text) + { + _viewerError.ShowError(text); + } + } +} diff --git a/Assets/Scripts/Lantern/EQ/Viewers/ViewerBase.cs.meta b/Assets/Scripts/Lantern/EQ/Viewers/ViewerBase.cs.meta new file mode 100644 index 0000000..eb0c925 --- /dev/null +++ b/Assets/Scripts/Lantern/EQ/Viewers/ViewerBase.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 8b22ff82dd3ef8f46838bc7346c6ffb3 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Scripts/Lantern/EQ/Viewers/ViewerError.cs b/Assets/Scripts/Lantern/EQ/Viewers/ViewerError.cs new file mode 100644 index 0000000..e90cccc --- /dev/null +++ b/Assets/Scripts/Lantern/EQ/Viewers/ViewerError.cs @@ -0,0 +1,17 @@ +using TMPro; +using UnityEngine; + +public class ViewerError : MonoBehaviour +{ + [SerializeField] + private GameObject _error; + + [SerializeField] + private TextMeshProUGUI _text; + + public void ShowError(string errorMessage) + { + _error.SetActive(true); + _text.text = errorMessage; + } +} diff --git a/Assets/Scripts/Lantern/EQ/Viewers/ViewerError.cs.meta b/Assets/Scripts/Lantern/EQ/Viewers/ViewerError.cs.meta new file mode 100644 index 0000000..d7b0378 --- /dev/null +++ b/Assets/Scripts/Lantern/EQ/Viewers/ViewerError.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 78cfaabdbd12be14ea581404f8f5afd3 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Scripts/Lantern/EQ/Viewers/ZoneViewer.cs b/Assets/Scripts/Lantern/EQ/Viewers/ZoneViewer.cs new file mode 100644 index 0000000..1c184d8 --- /dev/null +++ b/Assets/Scripts/Lantern/EQ/Viewers/ZoneViewer.cs @@ -0,0 +1,336 @@ +using System.Collections.Generic; +using System.IO; +using System.Linq; +using Infrastructure.Lantern.SQLite; +using Lantern.EQ.AssetBundles; +using Lantern.EQ.Environment; +using Lantern.EQ.Helpers; +using Lantern.EQ.Lantern; +using Lantern.EQ.Lighting; +using Lantern.EQ.Objects; +using UnityEngine; +using UnityEngine.Serialization; + +namespace Lantern.EQ.Viewers +{ + public class ZoneViewer : ViewerBase + { + [Header("Zone Viewer")] [SerializeField] + private ZoneViewerOptions _zoneViewerOptions; + + [SerializeField] private string _zoneToLoad; + + [SerializeField] private SunlightController _sunlightController; + + [SerializeField] private GameObject _selectionUi; + + private SkyController _sky; + + private float _time; + private GameObject _zone; + private Color _baseFogColor; + private ZoneViewerState _viewerState; + + private List _zonesInList; + private int _zoneScrollIndex = 0; + + [FormerlySerializedAs("_zones")] [SerializeField] private List _zoneButtons; + + [SerializeField] private FreeCamera _freeCamera; + + [SerializeField] private GameObject _scrollBar; + + private enum ZoneViewerState + { + SelectingZone = 0, + Viewer = 1 + } + + protected override void Awake() + { + base.Awake(); + _zonesInList = new List(); + InitializeOptions(); + if (!LoadSky()) + { + Debug.LogError("Unable to load sky"); + } + + SetZoneViewerState(_viewerState); + } + + private void SetZoneViewerState(ZoneViewerState viewerState) + { + _viewerState = viewerState; + _selectionUi.SetActive(viewerState == ZoneViewerState.SelectingZone); + + if (_viewerState == ZoneViewerState.SelectingZone) + { + if (_zone != null) + { + Destroy(_zone); + _zone = null; + } + + QueryZoneData(); + + //_freeCamera.SetEnableState(false); + Cursor.visible = true; + Cursor.lockState = CursorLockMode.None; + } + + if (_viewerState == ZoneViewerState.Viewer) + { + if (!LoadZone(_zoneToLoad, true)) + { + ShowError($"Unable to load zone: {_zoneToLoad}"); + return; + } + + //_freeCamera.SetEnableState(true); + _sunlightController.UpdateTime(_time); + Cursor.visible = false; + Cursor.lockState = CursorLockMode.Locked; + } + } + + public void SearchFieldUpdated(string filter) + { + QueryZoneData(filter.ToLower()); + } + + private void QueryZoneData(string filter = "") + { + var zoneLoader = new DatabaseLoader(Path.Combine(Application.streamingAssetsPath, "Database")); + var allZones = zoneLoader.GetDatabase() + ?.Table().ToList().OrderBy(x => x.long_name).ToList(); + + if (allZones == null || allZones.Count == 0) + { + Debug.LogError("No zones found."); + return; + } + + _zonesInList.Clear(); + + // Figure out which zones are valid + foreach (var z in allZones) + { + if (!AssetBundleHelper.DoesZoneBundleExist(z.short_name)) + { + continue; + } + + if (!string.IsNullOrEmpty(filter) && !z.long_name.ToLower().Contains(filter) && + !z.short_name.ToLower().Contains(filter)) + { + continue; + } + + _zonesInList.Add(z); + } + + UpdateZoneListVisual(); + _scrollBar.SetActive(_zonesInList.Count > _zoneButtons.Count); + } + + private void UpdateZoneListVisual() + { + for (int i = 0; i < _zoneButtons.Count; i++) + { + var button = _zoneButtons[i]; + if (_zonesInList.Count <= i) + { + button.Clear(); + } + else + { + var zone = _zonesInList[i]; + button.Initialize(Action, zone.long_name, zone.short_name); + button.SetIndex(i); + } + } + } + + private void Action(int index) + { + var zone = _zonesInList[index]; + _zoneToLoad = zone.short_name; + SetFog(zone.fogRed / 255f, zone.fogGreen / 255f, zone.fogBlue / 255f, zone.fogMinClip, zone.fogMaxClip, + true); + var safePoint = new Vector3(zone.safe_x, zone.safe_y, zone.safe_z); + safePoint = PositionHelper.GetEqDatabaseToEqPosition(safePoint) * LanternConstants.WorldScale; + safePoint.y += 3.75f; + //_freeCamera.Initialize(safePoint, Quaternion.identity); + SetZoneViewerState(ZoneViewerState.Viewer); + } + + private void InitializeOptions() + { + _time = _zoneViewerOptions.StartTime; + } + + private bool LoadSky() + { + // Load sky bundle + var sky = AssetBundleLoader.LoadAsset(LanternAssetBundleId.Sky, "Sky"); + + if (sky == null) + { + return false; + } + + // Instantiate the sky prefab and scale it up to make sure it's not clipped by the camera + // Uniform scaling does not make the sky appear larger + var skyGo = Instantiate(sky); + skyGo.transform.localScale = new Vector3(10f, 10f, 10f); + skyGo.transform.parent = _freeCamera.transform; + + // Get the SkyController component and initialize values + _sky = skyGo.GetComponent(); + _sky.SetSecondsPerDay(100); + _sky.SetEnabledSky(1); + _sky.UpdateTime(_time); + return true; + } + + public bool LoadZone(string zoneName, bool loadObjects) + { + var zone = AssetBundleLoader.LoadAsset(zoneName, LanternAssetBundleId.Zones, zoneName); + + if (zone == null) + { + return false; + } + + _zone = Instantiate(zone); + _zone.transform.localScale = new Vector3(LanternConstants.WorldScale, LanternConstants.WorldScale, + LanternConstants.WorldScale); + + // Spawn zone objects + var zoneObjects = _zone.GetComponent(); + var objects = zoneObjects.GetObjects(); + + if (loadObjects) + { + // This is a super rough way to load all zone objects + // The client will spawn/despawn objects as you walk around the zone + // and will never have everything loaded at once + foreach (var o in objects) + { + var obj = AssetBundleLoader.LoadAsset(zoneName, LanternAssetBundleId.Zones, o.Name); + + if (obj == null) + { + Debug.LogError($"Unable to load door prefab: {o.Name}"); + continue; + } + + var spawnedObject = Instantiate(obj); + spawnedObject.transform.position = o.Position * LanternConstants.WorldScale; + spawnedObject.transform.rotation = Quaternion.Euler(o.Rotation); + spawnedObject.transform.localScale = + new Vector3(o.Scale, o.Scale, o.Scale) * LanternConstants.WorldScale; + spawnedObject.transform.parent = _zone.transform; + } + } + + if (true) + { + var databaseLoader = new DatabaseLoader(Path.Combine(Application.streamingAssetsPath, "Database")); + var doors = databaseLoader.GetDatabase() + ?.Table().Where(x => x.zone == _zoneToLoad); + foreach (var d in doors) + { + var dr = AssetBundleLoader.LoadAsset(zoneName, LanternAssetBundleId.Zones, d.name); + + if (dr == null) + { + Debug.LogError($"Unable to load door prefab: {d.name}"); + continue; + } + + var spawnedObject = Instantiate(dr); + spawnedObject.transform.position = + PositionHelper.GetEqDatabaseToLanternPosition(d.pos_x, d.pos_y, d.pos_z); + spawnedObject.transform.rotation = + Quaternion.Euler(0.0f, RotationHelper.GetEqToLanternRotation(-d.heading), 0.0f); + spawnedObject.transform.localScale = Vector3.one * LanternConstants.WorldScale; + spawnedObject.transform.parent = _zone.transform; + } + } + + return true; + } + + private void Update() + { + //if (!_zoneViewerOptions.TickTime) + { + //return; + } + + HandleInput(); + + _freeCamera.GetCamera().backgroundColor = WorldLightColor.Evaluate(_time); + Shader.SetGlobalColor("_DayNightColor", WorldLightColor.Evaluate(_time)); + RenderSettings.fogColor = _baseFogColor; + _freeCamera.GetCamera().backgroundColor = _baseFogColor; + } + + private void HandleInput() + { + if (Input.GetKeyDown(KeyCode.Return)) + { + SetZoneViewerState(ZoneViewerState.Viewer); + } + else if (Input.GetKeyDown(KeyCode.Escape)) + { + SetZoneViewerState(ZoneViewerState.SelectingZone); + } + } + + public void SetFog(float colorR, float colorG, float colorB, float start, float end, bool isFogEnabled) + { + RenderSettings.fog = isFogEnabled; + _baseFogColor = new Color(colorR, colorG, colorB); + RenderSettings.fogStartDistance = start * LanternConstants.WorldScale; + RenderSettings.fogEndDistance = end * LanternConstants.WorldScale; + } + } + + + public class Doors + { + [PrimaryKey] public int id { get; set; } + public int doorid { get; set; } + + public string zone { get; set; } + + public float pos_x { get; set; } + public float pos_y { get; set; } + public float pos_z { get; set; } + public string name { get; set; } + public int opentype { get; set; } + public float heading { get; set; } + } + + public class Zone + { + [PrimaryKey] public int id { get; set; } + public string short_name { get; set; } + public string long_name { get; set; } + public float safe_x { get; set; } + public float safe_y { get; set; } + public float safe_z { get; set; } + public float safe_heading { get; set; } + public float minClip { get; set; } + public float maxClip { get; set; } + public float fogMinClip { get; set; } + public float fogMaxClip { get; set; } + public int fogBlue { get; set; } + public int fogRed { get; set; } + public int fogGreen { get; set; } + public int sky { get; set; } + } +} diff --git a/Assets/Scripts/Lantern/EQ/Viewers/ZoneViewer.cs.meta b/Assets/Scripts/Lantern/EQ/Viewers/ZoneViewer.cs.meta new file mode 100644 index 0000000..c8ef885 --- /dev/null +++ b/Assets/Scripts/Lantern/EQ/Viewers/ZoneViewer.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: ac7900ca13e845e49983c40275ffecd3 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Scripts/Lantern/EQ/Viewers/ZoneViewerOptions.cs b/Assets/Scripts/Lantern/EQ/Viewers/ZoneViewerOptions.cs new file mode 100644 index 0000000..eb4a17c --- /dev/null +++ b/Assets/Scripts/Lantern/EQ/Viewers/ZoneViewerOptions.cs @@ -0,0 +1,13 @@ +using UnityEngine; + +namespace Lantern.EQ.Viewers +{ + public class ZoneViewerOptions : ScriptableObject + { + public bool LoadAllObjects = true; + public bool LoadAllDoors = true; + public float StartTime = 0.5f; + public bool TickTime = false; + public float SecondsPerDay = 60f; + } +} diff --git a/Assets/Scripts/Lantern/EQ/Viewers/ZoneViewerOptions.cs.meta b/Assets/Scripts/Lantern/EQ/Viewers/ZoneViewerOptions.cs.meta new file mode 100644 index 0000000..a40770e --- /dev/null +++ b/Assets/Scripts/Lantern/EQ/Viewers/ZoneViewerOptions.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 4b64765d29282b44a9147b49505ef7d7 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Scripts/Lantern/EQ/Zone/ZoneHelper.cs b/Assets/Scripts/Lantern/EQ/Zone/ZoneHelper.cs index cafc0d2..2efbd58 100644 --- a/Assets/Scripts/Lantern/EQ/Zone/ZoneHelper.cs +++ b/Assets/Scripts/Lantern/EQ/Zone/ZoneHelper.cs @@ -77,8 +77,8 @@ public ZoneDesc(string ShortName, string LongName, string Continent) { "innothule", new ZoneDesc("innothule", "Innothule Swamp", "Antonica") }, { "kael", new ZoneDesc("kael", "Kael Drakael", "Velious") }, { "kaesora", new ZoneDesc("kaesora", "Kaesora", "Kunark") }, - { "kaladima", new ZoneDesc("kaladima", "North Kaladim", "Faydwer") }, - { "kaladimb", new ZoneDesc("kaladimb", "South Kaladim", "Faydwer") }, + { "kaladima", new ZoneDesc("kaladima", "South Kaladim", "Faydwer") }, + { "kaladimb", new ZoneDesc("kaladimb", "North Kaladim", "Faydwer") }, { "karnor", new ZoneDesc("karnor", "Karnor's Castle", "Kunark") }, { "kedge", new ZoneDesc("kedge", "Kedge Keep", "Faydwer") }, { "kerraridge", new ZoneDesc("kerraridge", "Kerra Isle", "Odus") }, diff --git a/Assets/StreamingAssets/ClientData/Music.meta b/Assets/StreamingAssets/ClientData/Music.meta new file mode 100644 index 0000000..bf2ddc2 --- /dev/null +++ b/Assets/StreamingAssets/ClientData/Music.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: a3b71927a4b72f84891e537dcaf5ee5d +folderAsset: yes +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/StreamingAssets/ClientData/Music/music_tracks.txt b/Assets/StreamingAssets/ClientData/Music/music_tracks.txt new file mode 100644 index 0000000..53c5ce8 --- /dev/null +++ b/Assets/StreamingAssets/ClientData/Music/music_tracks.txt @@ -0,0 +1,69 @@ +# Lantern Music Track Names +airplane,akanon-2,airplane_0,entrance_fanfare-1,arpeggiated_runs,airplane_1,heroism-1 +akanon,akanon,gfaydark-1,gfaydark-2,felwithe_2,felwithe_0,felwithe_1,arena,gypsies +befallen,eerie_0,eerie_1,eerie_2,eerie_3,eerie_4,eerie_5,eerie_6,eerie_7,eerie_8,eerie_9,eerie_10,eerie_11 +blackburrow,eerie_0,eerie_1,eerie_2,eerie_3,eerie_4,eerie_5,eerie_6,eerie_7,eerie_8,eerie_9,eerie_10,eerie_11 +butcher,entrance_fanfare,entrance_fanfare_1 +cauldron,entrance_fanfare,entrance_fanfare_1,entrance_fanfare_1-1,entrance_fanfare-1,sea +cobaltscar,cobaltscar,underwater +crushbone,gfaydark-4,airplane_0,entrance_fanfare-1,eerie_3,arpeggiated_runs,neriak_0-1 +crystal,erudsxing_0,gfaydark-1,cobaltscar,gfaydark +damage,damage_0,damage_1,damage_2,damage_3,damage_4 +damage1,damage_0,damage_1,damage_2,damage_3,damage_4 +damage2,damage_0,damage_1,damage_2,damage_3,damage_4 +eastkarana,cascade,eastkarana +eerie,eerie_0,eerie_1,eerie_2,eerie_3,eerie_4,eerie_5,eerie_6,eerie_7,eerie_8,eerie_9,eerie_10,eerie_11 +erudnext,felwithe_0,cascade,karana_river,entrance_fanfare_1-1,bard,freportn,karana_river-1,brass_harmonies +erudnint,cascade,karana_river,entrance_fanfare_1-1,bard,freportn,karana_river-1,brass_harmonies,felwithe_1 +erudsxing,erudsxing_0,erudsxing_1 +fearplane,eerie_8,eerie_9,eerie_0,eerie_1,eerie_2,eerie_3,eerie_4,eerie_5,eerie_7 +felwithea,felwithe_0,felwithe_1,felwithe_2 +freporte,freeport,lionsmane,fishsale,cottage,gypsies,death_variant,eerie_4,eerie_5 +freportn,harprun,gypsies-1,gypsies,eqtheme,arena,brass_harmonies,akanon-1,lionsmane,fishsale,bard,entrance_fanfare_1-1,cottage,freportn,nightchords,string_fanfare,brass_fanfare +freportw,heroism,hogcaller,militia,arena,entrance_fanfare_1-1,eastkarana,karana_river-1 +frozenshadow,eerie_7,eerie_0,lavastorm-1,templeoflife,qeynos_0-1,gfaydark +gfaydark,entrance_fanfare,entrance_fanfare_1,entrance_fanfare_1-1,entrance_fanfare-1,gfaydark,eerie_9 +gl,attack_0,attack_1,attack_2,death,airplane_0-1,gl_5,gl_6,crouching,underwater,gl_9,gl_10,gl_11,gl_12,bard_intro,bard_main,gl_15,gl_16,gl_17,gl_18,gl_19,sea,merchant,gfaydark-5,guildmaster +grobb,eerie_0,eerie_1,eerie_2,eerie_3,eerie_4,eerie_5,eerie_6,eerie_7,eerie_8,eerie_9,eerie_10,eerie_11 +guktop,eerie_0,eerie_1,eerie_5,eerie_7,arpeggiated_runs,neriak_0-1,eerie_3,maidensfancy,hogcaller,entrance_fanfare_1 +halas,felwithe_0,felwithe_1,felwithe_2,arena,hogcaller,maidensfancy,lionsmane,fishsale,cottage,eerie_4 +hateplane,felwithe_0,felwithe_2,felwithe_1,arpeggiated_runs,bard-2,neriak_0-1 +innothule,entrance_fanfare,entrance_fanfare_1,eerie_3,eerie_2 +kael,kael_0,kael_1,attack_1,arena +kaladima,entrance_fanfare_1,arena,nightchords,cascade,hogcaller,lionsmane,freportn,heroism,brass_harmonies-1 +lavastorm,lavastorm,eerie_1 +lfaydark,cascade,gfaydark-2,entrance_fanfare_1-1,gypsies-1,eerie_5,militia +mistmoore,cascade,eastkarana,eerie_1,entrance_fanfare,felwithe_1,felwithe_2,felwithe_0,neriak_0-1,karana_river,bard-2,brass_harmonies,string_fanfare,freportn-1,nightchords,gfaydark-3,heroism-1 +najena,eerie_0,eerie_1,eerie_2,eerie_3,eerie_4,eerie_5,eerie_6,eerie_7,eerie_8,eerie_9,eerie_10,eerie_11 +nektulos,lavastorm-1 +neriaka,eerie_0,eerie_1,eerie_2,eerie_3,eerie_4,eerie_5,eerie_6,eerie_7,eerie_8,eerie_9,eerie_10,eerie_11,neriak_0,neriak_1,neriak_2,arpeggiated_runs,fishsale,maidensfancy +neriakb,eerie_0,eerie_1,eerie_2,eerie_3,eerie_4,eerie_5,eerie_6,eerie_7,eerie_8,eerie_9,eerie_10,eerie_11,neriak_0,neriak_1,neriak_2,arpeggiated_runs,fishsale,maidensfancy +neriakc,eerie_0,eerie_1,eerie_2,eerie_3,eerie_4,eerie_5,eerie_6,eerie_7,eerie_8,eerie_9,eerie_10,eerie_11,neriak_0,neriak_1,neriak_2,arpeggiated_runs,fishsale,maidensfancy +northkarana,cascade,eastkarana +nro,nro,entrance_fanfare_1,freeport,gypsies,eerie_7 +opener,opener_0 +opener2,eastkarana,character_select +opener3,opener_3,character_select +opener4,eqtheme,character_select +paw,eerie_0,eerie_1,crouching +pickchar,character_select-1 +qcat,eerie_0,eerie_1,crouching +qey2hh1,cottage,maidensfancy,karana_river +qeynos,arena,qeynos_gates,eerie_0,eerie_1,templeoflife,bard,eqtheme,gypsies,lionsmane,fishsale,cottage,freeport,qeynos_0 +qeynos2,arena,qeynos_gates,eerie_0,eerie_1,templeoflife,bard-1 +qeytoqrg,arena,qeytoqrg,eerie_0,eerie_1,templeoflife,cottage,maidensfancy,entrance_fanfare +qrg,arena,qeytoqrg,templeoflife,qeynos_gates_brass +rathemtn,cascade,eastkarana,gypsies-2,entrance_fanfare_1,entrance_fanfare_1-1,eerie_2,eerie_3 +rivervale,rivervale +runnyeye,eerie_0,eerie_1,crouching +skyshrine,gl_12,bard_intro,skyshrine,beethoven6,nightchords,sea,gfaydark,gfaydark-2 +soldungb,lavastorm,eerie_10,eerie_11,eerie_0,eerie_1,eerie_2,eerie_3,eerie_6-1 +southkarana,cascade,eastkarana,eerie_2,entrance_fanfare,maidensfancy,akanon-1 +steamfont,gfaydark-4,entrance_fanfare,entrance_fanfare_1-1,akanon-1,lavastorm,eerie_3,eerie_5 +templeveeshan,templeveeshan,gfaydark-5,attack_1,attack_0 +thurgadina,templeveeshan,attack_1,attack_0,thurgadina_0,templeoflife,thurgadina_1 +thurgadinb,thurgadina_1,thurgadinb +tox,entrance_fanfare_1,karana_river,gypsies-1,cascade,karana_river-1 +unrest,eastkarana,eerie_2,eerie_1,entrance_fanfare,felwithe_1,felwithe_2,neriak_0-1,karana_river,brass_harmonies,string_fanfare,freportn-1,gfaydark-3,heroism-1 +velketor,velketor_0,velketor_1 +wakening,wakening,gfaydark \ No newline at end of file diff --git a/Assets/StreamingAssets/ClientData/Music/music_tracks.txt.meta b/Assets/StreamingAssets/ClientData/Music/music_tracks.txt.meta new file mode 100644 index 0000000..70c886c --- /dev/null +++ b/Assets/StreamingAssets/ClientData/Music/music_tracks.txt.meta @@ -0,0 +1,7 @@ +fileFormatVersion: 2 +guid: 09f70408b1795d14ab953e810071bdf3 +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/StreamingAssets/Database.meta b/Assets/StreamingAssets/Database.meta new file mode 100644 index 0000000..9ae7cbe --- /dev/null +++ b/Assets/StreamingAssets/Database.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: 92ca241729a36a242afcfaa956754694 +folderAsset: yes +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/StreamingAssets/Database/lantern_server.db b/Assets/StreamingAssets/Database/lantern_server.db new file mode 100644 index 0000000..0a2253b Binary files /dev/null and b/Assets/StreamingAssets/Database/lantern_server.db differ diff --git a/Assets/StreamingAssets/Database/lantern_server.db.meta b/Assets/StreamingAssets/Database/lantern_server.db.meta new file mode 100644 index 0000000..63d46d9 --- /dev/null +++ b/Assets/StreamingAssets/Database/lantern_server.db.meta @@ -0,0 +1,7 @@ +fileFormatVersion: 2 +guid: 50f30f12ed06f1340aba6f97a2612edf +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/StreamingAssets/Soundfont.meta b/Assets/StreamingAssets/Soundfont.meta new file mode 100644 index 0000000..695d2ff --- /dev/null +++ b/Assets/StreamingAssets/Soundfont.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: 35ec9c2eb7299d940b2138fc08bcc460 +folderAsset: yes +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/StreamingAssets/Soundfont/synthusr_samplefix.sf2 b/Assets/StreamingAssets/Soundfont/synthusr_samplefix.sf2 new file mode 100644 index 0000000..834866b Binary files /dev/null and b/Assets/StreamingAssets/Soundfont/synthusr_samplefix.sf2 differ diff --git a/Assets/StreamingAssets/Soundfont/synthusr_samplefix.sf2.meta b/Assets/StreamingAssets/Soundfont/synthusr_samplefix.sf2.meta new file mode 100644 index 0000000..8ec694d --- /dev/null +++ b/Assets/StreamingAssets/Soundfont/synthusr_samplefix.sf2.meta @@ -0,0 +1,7 @@ +fileFormatVersion: 2 +guid: 956f3de1286a46f4fa66dad7b81eb36e +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/UniversalRenderPipelineAsset_Renderer.asset b/Assets/UniversalRenderPipelineAsset_Renderer.asset index cd7074b..a5d508c 100644 --- a/Assets/UniversalRenderPipelineAsset_Renderer.asset +++ b/Assets/UniversalRenderPipelineAsset_Renderer.asset @@ -12,8 +12,12 @@ MonoBehaviour: m_Script: {fileID: 11500000, guid: de640fe3d0db1804a85f9fc8f5cadab6, type: 3} m_Name: UniversalRenderPipelineAsset_Renderer m_EditorClassIdentifier: + debugShaders: + debugReplacementPS: {fileID: 4800000, guid: cf852408f2e174538bcd9b7fda1c5ae7, + type: 3} m_RendererFeatures: [] m_RendererFeatureMap: + m_UseNativeRenderPass: 0 postProcessData: {fileID: 11400000, guid: 41439944d30ece34e96484bdb6645b55, type: 2} xrSystemData: {fileID: 11400000, guid: 60e1133243b97e347b653163a8c01b64, type: 2} shaders: @@ -22,11 +26,17 @@ MonoBehaviour: screenSpaceShadowPS: {fileID: 4800000, guid: 0f854b35a0cf61a429bd5dcfea30eddd, type: 3} samplingPS: {fileID: 4800000, guid: 04c410c9937594faa893a11dceb85f7e, type: 3} - tileDepthInfoPS: {fileID: 0} - tileDeferredPS: {fileID: 0} stencilDeferredPS: {fileID: 4800000, guid: e9155b26e1bc55942a41e518703fe304, type: 3} fallbackErrorPS: {fileID: 4800000, guid: e6e9a19c3678ded42a3bc431ebef7dbd, type: 3} materialErrorPS: {fileID: 4800000, guid: 5fd9a8feb75a4b5894c241777f519d4e, type: 3} + coreBlitPS: {fileID: 4800000, guid: 93446b5c5339d4f00b85c159e1159b7c, type: 3} + coreBlitColorAndDepthPS: {fileID: 4800000, guid: d104b2fc1ca6445babb8e90b0758136b, + type: 3} + cameraMotionVector: {fileID: 4800000, guid: c56b7e0d4c7cb484e959caeeedae9bbf, + type: 3} + objectMotionVector: {fileID: 4800000, guid: 7b3ede40266cd49a395def176e1bc486, + type: 3} + m_AssetVersion: 2 m_OpaqueLayerMask: serializedVersion: 2 m_Bits: 4294967295 @@ -42,4 +52,9 @@ MonoBehaviour: zFailOperation: 0 m_ShadowTransparentReceive: 1 m_RenderingMode: 0 + m_DepthPrimingMode: 0 + m_CopyDepthMode: 0 m_AccurateGbufferNormals: 0 + m_ClusteredRendering: 0 + m_TileSize: 32 + m_IntermediateTextureMode: 1 diff --git a/Assets/UniversalRenderPipelineGlobalSettings.asset b/Assets/UniversalRenderPipelineGlobalSettings.asset new file mode 100644 index 0000000..a996a2e --- /dev/null +++ b/Assets/UniversalRenderPipelineGlobalSettings.asset @@ -0,0 +1,27 @@ +%YAML 1.1 +%TAG !u! tag:unity3d.com,2011: +--- !u!114 &11400000 +MonoBehaviour: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 0} + m_Enabled: 1 + m_EditorHideFlags: 0 + m_Script: {fileID: 11500000, guid: 2ec995e51a6e251468d2a3fd8a686257, type: 3} + m_Name: UniversalRenderPipelineGlobalSettings + m_EditorClassIdentifier: + k_AssetVersion: 2 + lightLayerName0: Light Layer default + lightLayerName1: Light Layer 1 + lightLayerName2: Light Layer 2 + lightLayerName3: Light Layer 3 + lightLayerName4: Light Layer 4 + lightLayerName5: Light Layer 5 + lightLayerName6: Light Layer 6 + lightLayerName7: Light Layer 7 + m_StripDebugVariants: 1 + m_StripUnusedPostProcessingVariants: 0 + m_StripUnusedVariants: 1 + supportRuntimeDebugDisplay: 0 diff --git a/Assets/UniversalRenderPipelineGlobalSettings.asset.meta b/Assets/UniversalRenderPipelineGlobalSettings.asset.meta new file mode 100644 index 0000000..27fda10 --- /dev/null +++ b/Assets/UniversalRenderPipelineGlobalSettings.asset.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: 0566a65a5c9a8584ea8dce786491f02a +NativeFormatImporter: + externalObjects: {} + mainObjectFileID: 11400000 + userData: + assetBundleName: + assetBundleVariant: diff --git a/Packages/manifest.json b/Packages/manifest.json index 92f0bb5..a7f9c51 100644 --- a/Packages/manifest.json +++ b/Packages/manifest.json @@ -2,23 +2,22 @@ "dependencies": { "com.unity.2d.sprite": "1.0.0", "com.unity.2d.tilemap": "1.0.0", - "com.unity.collab-proxy": "1.15.4", + "com.unity.collab-proxy": "2.0.0", "com.unity.editorcoroutines": "1.0.0", "com.unity.ext.nunit": "1.0.6", - "com.unity.formats.fbx": "4.0.1", - "com.unity.ide.rider": "2.0.7", - "com.unity.ide.visualstudio": "2.0.12", - "com.unity.ide.vscode": "1.2.4", - "com.unity.inputsystem": "1.2.0", + "com.unity.formats.fbx": "4.1.3", + "com.unity.ide.rider": "3.0.18", + "com.unity.ide.visualstudio": "2.0.17", + "com.unity.ide.vscode": "1.2.5", + "com.unity.inputsystem": "1.4.4", "com.unity.memoryprofiler": "0.4.1-preview.1", - "com.unity.multiplayer-hlapi": "1.0.6", - "com.unity.probuilder": "4.2.3", - "com.unity.render-pipelines.universal": "10.7.0", - "com.unity.test-framework": "1.1.29", + "com.unity.probuilder": "5.0.3", + "com.unity.render-pipelines.universal": "12.1.10", + "com.unity.test-framework": "1.1.31", "com.unity.textmeshpro": "3.0.6", - "com.unity.timeline": "1.4.8", + "com.unity.timeline": "1.6.4", "com.unity.ugui": "1.0.0", - "com.unity.xr.legacyinputhelpers": "2.1.8", + "com.unity.xr.legacyinputhelpers": "2.1.10", "com.unity.modules.ai": "1.0.0", "com.unity.modules.androidjni": "1.0.0", "com.unity.modules.animation": "1.0.0", diff --git a/Packages/packages-lock.json b/Packages/packages-lock.json index 212a2dc..2320a22 100644 --- a/Packages/packages-lock.json +++ b/Packages/packages-lock.json @@ -1,7 +1,7 @@ { "dependencies": { "com.autodesk.fbx": { - "version": "4.0.1", + "version": "4.1.2", "depth": 1, "source": "registry", "dependencies": {}, @@ -19,16 +19,22 @@ "source": "builtin", "dependencies": {} }, - "com.unity.collab-proxy": { - "version": "1.15.4", - "depth": 0, + "com.unity.burst": { + "version": "1.8.2", + "depth": 1, "source": "registry", "dependencies": { - "com.unity.nuget.newtonsoft-json": "2.0.0", - "com.unity.services.core": "1.0.1" + "com.unity.mathematics": "1.2.1" }, "url": "https://packages.unity.com" }, + "com.unity.collab-proxy": { + "version": "2.0.0", + "depth": 0, + "source": "registry", + "dependencies": {}, + "url": "https://packages.unity.com" + }, "com.unity.editorcoroutines": { "version": "1.0.0", "depth": 0, @@ -44,26 +50,26 @@ "url": "https://packages.unity.com" }, "com.unity.formats.fbx": { - "version": "4.0.1", + "version": "4.1.3", "depth": 0, "source": "registry", "dependencies": { - "com.unity.timeline": "1.0.0", - "com.autodesk.fbx": "4.0.1" + "com.unity.timeline": "1.5.2", + "com.autodesk.fbx": "4.1.2" }, "url": "https://packages.unity.com" }, "com.unity.ide.rider": { - "version": "2.0.7", + "version": "3.0.18", "depth": 0, "source": "registry", "dependencies": { - "com.unity.test-framework": "1.1.1" + "com.unity.ext.nunit": "1.0.6" }, "url": "https://packages.unity.com" }, "com.unity.ide.visualstudio": { - "version": "2.0.12", + "version": "2.0.17", "depth": 0, "source": "registry", "dependencies": { @@ -72,14 +78,14 @@ "url": "https://packages.unity.com" }, "com.unity.ide.vscode": { - "version": "1.2.4", + "version": "1.2.5", "depth": 0, "source": "registry", "dependencies": {}, "url": "https://packages.unity.com" }, "com.unity.inputsystem": { - "version": "1.2.0", + "version": "1.4.4", "depth": 0, "source": "registry", "dependencies": { @@ -88,7 +94,7 @@ "url": "https://packages.unity.com" }, "com.unity.mathematics": { - "version": "1.1.0", + "version": "1.2.6", "depth": 1, "source": "registry", "dependencies": {}, @@ -103,88 +109,63 @@ }, "url": "https://packages.unity.com" }, - "com.unity.multiplayer-hlapi": { - "version": "1.0.6", - "depth": 0, - "source": "registry", - "dependencies": { - "nuget.mono-cecil": "0.1.6-preview" - }, - "url": "https://packages.unity.com" - }, - "com.unity.nuget.newtonsoft-json": { - "version": "2.0.0", - "depth": 1, - "source": "registry", - "dependencies": {}, - "url": "https://packages.unity.com" - }, "com.unity.probuilder": { - "version": "4.2.3", + "version": "5.0.3", "depth": 0, "source": "registry", "dependencies": { - "com.unity.settings-manager": "1.0.0" + "com.unity.settings-manager": "1.0.3", + "com.unity.modules.physics": "1.0.0", + "com.unity.modules.imgui": "1.0.0" }, "url": "https://packages.unity.com" }, "com.unity.render-pipelines.core": { - "version": "10.10.0", + "version": "12.1.10", "depth": 1, - "source": "registry", + "source": "builtin", "dependencies": { "com.unity.ugui": "1.0.0", "com.unity.modules.physics": "1.0.0", "com.unity.modules.jsonserialize": "1.0.0" - }, - "url": "https://packages.unity.com" + } }, "com.unity.render-pipelines.universal": { - "version": "10.7.0", + "version": "12.1.10", "depth": 0, - "source": "registry", + "source": "builtin", "dependencies": { - "com.unity.mathematics": "1.1.0", - "com.unity.render-pipelines.core": "10.7.0", - "com.unity.shadergraph": "10.7.0" - }, - "url": "https://packages.unity.com" + "com.unity.mathematics": "1.2.1", + "com.unity.burst": "1.8.2", + "com.unity.render-pipelines.core": "12.1.10", + "com.unity.shadergraph": "12.1.10" + } }, "com.unity.searcher": { - "version": "4.3.2", + "version": "4.9.1", "depth": 2, "source": "registry", "dependencies": {}, "url": "https://packages.unity.com" }, - "com.unity.services.core": { - "version": "1.0.1", - "depth": 1, - "source": "registry", - "dependencies": { - "com.unity.modules.unitywebrequest": "1.0.0" - }, - "url": "https://packages.unity.com" - }, "com.unity.settings-manager": { - "version": "1.0.0", + "version": "1.0.3", "depth": 1, "source": "registry", "dependencies": {}, "url": "https://packages.unity.com" }, "com.unity.shadergraph": { - "version": "10.10.0", + "version": "12.1.10", "depth": 1, - "source": "registry", + "source": "builtin", "dependencies": { - "com.unity.render-pipelines.core": "10.10.0", - "com.unity.searcher": "4.3.2" - }, - "url": "https://packages.unity.com" + "com.unity.render-pipelines.core": "12.1.10", + "com.unity.searcher": "4.9.1" + } }, "com.unity.test-framework": { - "version": "1.1.29", + "version": "1.1.31", "depth": 0, "source": "registry", "dependencies": { @@ -204,7 +185,7 @@ "url": "https://packages.unity.com" }, "com.unity.timeline": { - "version": "1.4.8", + "version": "1.6.4", "depth": 0, "source": "registry", "dependencies": { @@ -225,7 +206,7 @@ } }, "com.unity.xr.legacyinputhelpers": { - "version": "2.1.8", + "version": "2.1.10", "depth": 0, "source": "registry", "dependencies": { @@ -234,13 +215,6 @@ }, "url": "https://packages.unity.com" }, - "nuget.mono-cecil": { - "version": "0.1.6-preview", - "depth": 1, - "source": "registry", - "dependencies": {}, - "url": "https://packages.unity.com" - }, "com.unity.modules.ai": { "version": "1.0.0", "depth": 0, diff --git a/ProjectSettings/GraphicsSettings.asset b/ProjectSettings/GraphicsSettings.asset index a72fa8a..4aae85b 100644 --- a/ProjectSettings/GraphicsSettings.asset +++ b/ProjectSettings/GraphicsSettings.asset @@ -3,7 +3,7 @@ --- !u!30 &1 GraphicsSettings: m_ObjectHideFlags: 0 - serializedVersion: 13 + serializedVersion: 14 m_Deferred: m_Mode: 1 m_Shader: {fileID: 69, guid: 0000000000000000f000000000000000, type: 0} @@ -41,6 +41,7 @@ GraphicsSettings: - {fileID: 4800000, guid: ce27f0200716f7c449d0a38f3d83fb90, type: 3} - {fileID: 4800000, guid: 0c4dd978a96edc84f81f4a19c8a2080a, type: 3} m_PreloadedShaders: [] + m_PreloadShadersBatchTimeLimit: -1 m_SpritesDefaultMaterial: {fileID: 10754, guid: 0000000000000000f000000000000000, type: 0} m_CustomRenderPipeline: {fileID: 11400000, guid: c085866252ef6d64c8ba306eb99f8958, @@ -115,6 +116,11 @@ GraphicsSettings: m_FogKeepExp2: 0 m_AlbedoSwatchInfos: [] m_LightsUseLinearIntensity: 0 - m_LightsUseColorTemperature: 0 + m_LightsUseColorTemperature: 1 m_DefaultRenderingLayerMask: 1 m_LogWhenShaderIsCompiled: 0 + m_SRPDefaultSettings: + UnityEngine.Rendering.Universal.UniversalRenderPipeline: {fileID: 11400000, guid: 0566a65a5c9a8584ea8dce786491f02a, + type: 2} + m_CameraRelativeLightCulling: 0 + m_CameraRelativeShadowCulling: 0 diff --git a/ProjectSettings/MemorySettings.asset b/ProjectSettings/MemorySettings.asset new file mode 100644 index 0000000..5b5face --- /dev/null +++ b/ProjectSettings/MemorySettings.asset @@ -0,0 +1,35 @@ +%YAML 1.1 +%TAG !u! tag:unity3d.com,2011: +--- !u!387306366 &1 +MemorySettings: + m_ObjectHideFlags: 0 + m_EditorMemorySettings: + m_MainAllocatorBlockSize: -1 + m_ThreadAllocatorBlockSize: -1 + m_MainGfxBlockSize: -1 + m_ThreadGfxBlockSize: -1 + m_CacheBlockSize: -1 + m_TypetreeBlockSize: -1 + m_ProfilerBlockSize: -1 + m_ProfilerEditorBlockSize: -1 + m_BucketAllocatorGranularity: -1 + m_BucketAllocatorBucketsCount: -1 + m_BucketAllocatorBlockSize: -1 + m_BucketAllocatorBlockCount: -1 + m_ProfilerBucketAllocatorGranularity: -1 + m_ProfilerBucketAllocatorBucketsCount: -1 + m_ProfilerBucketAllocatorBlockSize: -1 + m_ProfilerBucketAllocatorBlockCount: -1 + m_TempAllocatorSizeMain: -1 + m_JobTempAllocatorBlockSize: -1 + m_BackgroundJobTempAllocatorBlockSize: -1 + m_JobTempAllocatorReducedBlockSize: -1 + m_TempAllocatorSizeGIBakingWorker: -1 + m_TempAllocatorSizeNavMeshWorker: -1 + m_TempAllocatorSizeAudioWorker: -1 + m_TempAllocatorSizeCloudWorker: -1 + m_TempAllocatorSizeGfx: -1 + m_TempAllocatorSizeJobWorker: -1 + m_TempAllocatorSizeBackgroundWorker: -1 + m_TempAllocatorSizePreloadManager: -1 + m_PlatformMemorySettings: {} diff --git a/ProjectSettings/Packages/com.unity.probuilder/Settings.json b/ProjectSettings/Packages/com.unity.probuilder/Settings.json index 92b556a..88c33df 100644 --- a/ProjectSettings/Packages/com.unity.probuilder/Settings.json +++ b/ProjectSettings/Packages/com.unity.probuilder/Settings.json @@ -21,12 +21,12 @@ { "type": "UnityEngine.ProBuilder.SemVer, Unity.ProBuilder, Version=0.0.0.0, Culture=neutral, PublicKeyToken=null", "key": "about.identifier", - "value": "{\"m_Value\":{\"m_Major\":4,\"m_Minor\":2,\"m_Patch\":3,\"m_Build\":-1,\"m_Type\":\"\",\"m_Metadata\":\"\",\"m_Date\":\"\"}}" + "value": "{\"m_Value\":{\"m_Major\":5,\"m_Minor\":0,\"m_Patch\":3,\"m_Build\":-1,\"m_Type\":\"\",\"m_Metadata\":\"\",\"m_Date\":\"\"}}" }, { "type": "UnityEngine.ProBuilder.SemVer, Unity.ProBuilder, Version=0.0.0.0, Culture=neutral, PublicKeyToken=null", "key": "preferences.version", - "value": "{\"m_Value\":{\"m_Major\":4,\"m_Minor\":2,\"m_Patch\":3,\"m_Build\":-1,\"m_Type\":\"\",\"m_Metadata\":\"\",\"m_Date\":\"\"}}" + "value": "{\"m_Value\":{\"m_Major\":5,\"m_Minor\":0,\"m_Patch\":3,\"m_Build\":-1,\"m_Type\":\"\",\"m_Metadata\":\"\",\"m_Date\":\"\"}}" }, { "type": "System.Boolean, mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089", @@ -93,6 +93,11 @@ "key": "UVEditor.showPreviewMaterial", "value": "{\"m_Value\":true}" }, + { + "type": "System.Boolean, mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089", + "key": "experimental.enabled", + "value": "{\"m_Value\":false}" + }, { "type": "UnityEngine.ProBuilder.SelectionModifierBehavior, Unity.ProBuilder, Version=0.0.0.0, Culture=neutral, PublicKeyToken=null", "key": "editor.rectSelectModifier", diff --git a/ProjectSettings/ProjectSettings.asset b/ProjectSettings/ProjectSettings.asset index 435a54b..deba821 100644 --- a/ProjectSettings/ProjectSettings.asset +++ b/ProjectSettings/ProjectSettings.asset @@ -3,7 +3,7 @@ --- !u!129 &1 PlayerSettings: m_ObjectHideFlags: 0 - serializedVersion: 22 + serializedVersion: 23 productGUID: 64ed21d951b90e448bc599c2c694ecd3 AndroidProfiler: 0 AndroidFilterTouchesWhenObscured: 0 @@ -12,8 +12,8 @@ PlayerSettings: targetDevice: 2 useOnDemandResources: 0 accelerometerFrequency: 60 - companyName: Lantern Team - productName: Lantern + companyName: LanternEQ Team + productName: LanternUnityTools defaultCursor: {fileID: 0} cursorHotspot: {x: 0, y: 0} m_SplashScreenBackgroundColor: {r: 0.13725491, g: 0.12156863, b: 0.1254902, a: 1} @@ -68,6 +68,12 @@ PlayerSettings: androidRenderOutsideSafeArea: 0 androidUseSwappy: 0 androidBlitType: 0 + androidResizableWindow: 0 + androidDefaultWindowWidth: 1920 + androidDefaultWindowHeight: 1080 + androidMinimumWindowWidth: 400 + androidMinimumWindowHeight: 300 + androidFullscreenMode: 1 defaultIsNativeResolution: 1 macRetinaSupport: 1 runInBackground: 1 @@ -121,13 +127,14 @@ PlayerSettings: vulkanEnableSetSRGBWrite: 0 vulkanEnablePreTransform: 0 vulkanEnableLateAcquireNextImage: 0 + vulkanEnableCommandBufferRecycling: 1 m_SupportedAspectRatios: 4:3: 1 5:4: 1 16:10: 1 16:9: 1 Others: 1 - bundleVersion: 1.0 + bundleVersion: 0.1.7 preloadedAssets: [] metroInputSource: 0 wsaTransparentSwapchain: 0 @@ -138,16 +145,18 @@ PlayerSettings: enable360StereoCapture: 0 isWsaHolographicRemotingEnabled: 0 enableFrameTimingStats: 0 + enableOpenGLProfilerGPURecorders: 1 useHDRDisplay: 0 D3DHDRBitDepth: 0 m_ColorGamuts: 00000000 targetPixelDensity: 30 resolutionScalingMode: 0 + resetResolutionOnWindowResize: 0 androidSupportedAspectRatio: 1 androidMaxAspectRatio: 2.1 applicationIdentifier: Android: com.LanternTeam.Lantern - Standalone: com.LanternTeam.Lantern + Standalone: com.LanternEQ-Team.LanternUnityTools Tizen: com.Company.ProductName iPhone: com.Company.ProductName tvOS: com.Company.ProductName @@ -157,7 +166,7 @@ PlayerSettings: tvOS: 0 overrideDefaultApplicationIdentifier: 0 AndroidBundleVersionCode: 1 - AndroidMinSdkVersion: 19 + AndroidMinSdkVersion: 22 AndroidTargetSdkVersion: 0 AndroidPreferredInstallLocation: 1 aotOptions: @@ -213,6 +222,7 @@ PlayerSettings: iOSLaunchScreeniPadCustomStoryboardPath: iOSDeviceRequirements: [] iOSURLSchemes: [] + macOSURLSchemes: [] iOSBackgroundModes: 0 iOSMetalForceHardShadows: 0 metalEditorSupport: 1 @@ -240,6 +250,7 @@ PlayerSettings: useCustomGradlePropertiesTemplate: 0 useCustomProguardFile: 0 AndroidTargetArchitectures: 5 + AndroidTargetDevices: 0 AndroidSplashScreenScale: 0 androidSplashScreen: {fileID: 0} AndroidKeystoreName: '{inproject}: ' @@ -256,6 +267,7 @@ PlayerSettings: height: 180 banner: {fileID: 0} androidGamepadSupportLevel: 0 + chromeosInputEmulation: 1 AndroidMinifyWithR8: 0 AndroidMinifyRelease: 0 AndroidMinifyDebug: 0 @@ -366,6 +378,7 @@ PlayerSettings: - m_BuildTarget: Standalone m_StaticBatching: 1 m_DynamicBatching: 1 + m_BuildTargetShaderSettings: [] m_BuildTargetGraphicsJobs: - m_BuildTarget: MacStandaloneSupport m_GraphicsJobs: 0 @@ -405,7 +418,12 @@ PlayerSettings: - m_BuildTarget: iOSSupport m_APIs: 10000000 m_Automatic: 1 + - m_BuildTarget: AndroidPlayer + m_APIs: 0b00000008000000 + m_Automatic: 0 m_BuildTargetVRSettings: [] + m_DefaultShaderChunkSizeInMB: 16 + m_DefaultShaderChunkCount: 0 openGLRequireES31: 0 openGLRequireES31AEP: 0 openGLRequireES32: 0 @@ -423,6 +441,7 @@ PlayerSettings: m_EncodingQuality: 1 m_BuildTargetGroupLightmapSettings: [] m_BuildTargetNormalMapEncoding: [] + m_BuildTargetDefaultTextureCompressionFormat: [] playModeTestRunnerEnabled: 0 runPlayModeTestAsEditModeTest: 0 actionOnDotNetUnhandledException: 1 @@ -432,6 +451,7 @@ PlayerSettings: cameraUsageDescription: locationUsageDescription: microphoneUsageDescription: + bluetoothUsageDescription: switchNMETAOverride: switchNetLibKey: switchSocketMemoryPoolSize: 6144 @@ -440,6 +460,7 @@ PlayerSettings: switchScreenResolutionBehavior: 2 switchUseCPUProfiler: 0 switchUseGOLDLinker: 0 + switchLTOSetting: 0 switchApplicationID: 0x0005000C10000001 switchNSODependencies: switchTitleNames_0: @@ -569,8 +590,11 @@ PlayerSettings: switchNetworkInterfaceManagerInitializeEnabled: 1 switchPlayerConnectionEnabled: 1 switchUseNewStyleFilepaths: 0 + switchUseLegacyFmodPriorities: 1 switchUseMicroSleepForYield: 1 + switchEnableRamDiskSupport: 0 switchMicroSleepForYieldTime: 25 + switchRamDiskSpaceSize: 12 ps4NPAgeRating: 12 ps4NPTitleSecret: ps4NPTrophyPackPath: @@ -666,24 +690,18 @@ PlayerSettings: webGLLinkerTarget: 1 webGLThreadsSupport: 0 webGLDecompressionFallback: 0 + webGLPowerPreference: 2 scriptingDefineSymbols: - 1: CROSS_PLATFORM_INPUT;UNITY_POST_PROCESSING_STACK_V2 - 2: CROSS_PLATFORM_INPUT - 4: CROSS_PLATFORM_INPUT;MOBILE_INPUT;UNITY_POST_PROCESSING_STACK_V2 - 7: CROSS_PLATFORM_INPUT;MOBILE_INPUT;UNITY_POST_PROCESSING_STACK_V2 - 13: UNITY_POST_PROCESSING_STACK_V2 - 14: MOBILE_INPUT - 15: CROSS_PLATFORM_INPUT;MOBILE_INPUT - 16: CROSS_PLATFORM_INPUT;MOBILE_INPUT - 17: MOBILE_INPUT - 18: UNITY_POST_PROCESSING_STACK_V2 - 19: UNITY_POST_PROCESSING_STACK_V2 - 20: MOBILE_INPUT - 21: UNITY_POST_PROCESSING_STACK_V2 - 23: UNITY_POST_PROCESSING_STACK_V2 - 25: UNITY_POST_PROCESSING_STACK_V2 - 26: UNITY_POST_PROCESSING_STACK_V2 - 27: UNITY_POST_PROCESSING_STACK_V2 + : UNITY_POST_PROCESSING_STACK_V2 + Android: CROSS_PLATFORM_INPUT;MOBILE_INPUT;UNITY_POST_PROCESSING_STACK_V2 + Nintendo Switch: UNITY_POST_PROCESSING_STACK_V2 + PS4: UNITY_POST_PROCESSING_STACK_V2 + Standalone: CROSS_PLATFORM_INPUT;UNITY_POST_PROCESSING_STACK_V2 + WebGL: UNITY_POST_PROCESSING_STACK_V2 + Windows Store Apps: MOBILE_INPUT + XboxOne: UNITY_POST_PROCESSING_STACK_V2 + iPhone: CROSS_PLATFORM_INPUT;MOBILE_INPUT;UNITY_POST_PROCESSING_STACK_V2 + tvOS: UNITY_POST_PROCESSING_STACK_V2 additionalCompilerArguments: {} platformArchitecture: {} scriptingBackend: @@ -695,8 +713,8 @@ PlayerSettings: suppressCommonWarnings: 1 allowUnsafeCode: 1 useDeterministicCompilation: 1 - useReferenceAssemblies: 1 enableRoslynAnalyzers: 1 + selectedPlatform: 0 additionalIl2CppArgs: scriptingRuntimeVersion: 1 gcIncremental: 0 @@ -733,6 +751,7 @@ PlayerSettings: metroFTAName: metroFTAFileTypes: [] metroProtocolName: + vcxProjDefaultLanguage: XboxOneProductId: XboxOneUpdateKey: XboxOneSandboxId: @@ -775,6 +794,7 @@ PlayerSettings: m_VersionName: apiCompatibilityLevel: 6 activeInputHandler: 2 + windowsGamepadBackendHint: 0 cloudProjectId: framebufferDepthMemorylessMode: 0 qualitySettingsNames: [] @@ -782,4 +802,6 @@ PlayerSettings: organizationId: cloudEnabled: 0 legacyClampBlendShapeWeights: 1 + playerDataPath: + forceSRGBBlit: 1 virtualTexturingSupportEnabled: 0 diff --git a/ProjectSettings/ProjectVersion.txt b/ProjectSettings/ProjectVersion.txt index 4c9401b..61bfe8a 100644 --- a/ProjectSettings/ProjectVersion.txt +++ b/ProjectSettings/ProjectVersion.txt @@ -1,2 +1,2 @@ -m_EditorVersion: 2020.3.25f1 -m_EditorVersionWithRevision: 2020.3.25f1 (9b9180224418) +m_EditorVersion: 2021.3.18f1 +m_EditorVersionWithRevision: 2021.3.18f1 (3129e69bc0c7) diff --git a/ProjectSettings/URPProjectSettings.asset b/ProjectSettings/URPProjectSettings.asset index 3077404..c1f118a 100644 --- a/ProjectSettings/URPProjectSettings.asset +++ b/ProjectSettings/URPProjectSettings.asset @@ -12,4 +12,4 @@ MonoBehaviour: m_Script: {fileID: 11500000, guid: 247994e1f5a72c2419c26a37e9334c01, type: 3} m_Name: m_EditorClassIdentifier: - m_LastMaterialVersion: 4 + m_LastMaterialVersion: 5 diff --git a/ProjectSettings/boot.config b/ProjectSettings/boot.config new file mode 100644 index 0000000..e69de29