diff --git a/addons/modplayer/GMP.gd b/addons/modplayer/GMP.gd
new file mode 100644
index 0000000..cc4aefc
--- /dev/null
+++ b/addons/modplayer/GMP.gd
@@ -0,0 +1,23 @@
+"""
+ Godot Mod Player Plugin by arlez80 (Yui Kinomoto)
+"""
+
+tool
+extends EditorPlugin
+
+func _enter_tree( ):
+ #self.add_custom_type( "GodotModPlayer", "Spatial", preload("ModPlayer.gd"), preload("icon.png") )
+ pass
+
+func _exit_tree( ):
+ #self.remove_custom_type( "GodotModPlayer" )
+ pass
+
+func has_main_screen():
+ return true
+
+func make_visible( visible:bool ):
+ pass
+
+func get_plugin_name( ):
+ return "Godot Mod Player"
diff --git a/addons/modplayer/Mod.gd b/addons/modplayer/Mod.gd
new file mode 100644
index 0000000..059a540
--- /dev/null
+++ b/addons/modplayer/Mod.gd
@@ -0,0 +1,290 @@
+"""
+ MOD reader by あるる(きのもと 結衣) @arlez80
+
+ MIT License
+"""
+
+class_name Mod
+
+const period_table:PoolIntArray = PoolIntArray([
+ 1712,1616,1525,1440,1357,1281,1209,1141,1077,1017, 961, 907,
+ 856, 808, 762, 720, 678, 640, 604, 570, 538, 508, 480, 453,
+ 428, 404, 381, 360, 339, 320, 302, 285, 269, 254, 240, 226,
+ 214, 202, 190, 180, 170, 160, 151, 143, 135, 127, 120, 113,
+ 107, 101, 95, 90, 85, 80, 76, 71, 67, 64, 60, 57,
+])
+
+enum ModFlags {
+ LINEAR_FREQUENCY_TABLE = 1,
+}
+
+enum ModWaveFormType {
+ SINE_WAVEFORM = 0,
+ SAW_WAVEFORM = 1,
+ SQUARE_WAVEFORM = 2,
+ RAMDOM_WAVEFORM = 3,
+ REV_SAW_WAVEFORM = 4,
+}
+
+enum ModLoopType {
+ NO_LOOP = 0,
+ FORWARD_LOOP = 1,
+ PING_PONG_LOOP = 2,
+}
+
+class ModData:
+ var module_name:String
+ var tracker_name:String
+ var version:int
+ var flags:int = 0
+
+ var song_length:int
+ var unknown_number:int
+ var channel_count:int
+ var song_positions:Array # int
+ var magic:String
+ var patterns:Array # ModPattern[][]
+ var instruments:Array # ModInstrument[]
+
+ var init_tick:int = 6
+ var init_bpm:int = 125
+ var restart_position:int = 0
+
+class ModPatternNote:
+ var key_number:int
+ var note:int
+ var instrument:int
+ var volume:int = 0
+ var effect_command:int
+ var effect_param:int
+
+class ModInstrument:
+ var name:String
+ var samples:Array # ModSample[96]
+ var volume_envelope:ModEnvelope
+ var panning_envelope:ModEnvelope
+ var vibrato_type:int = -1 # ModWaveFormType
+ var vibrato_speed:int = -1
+ var vibrato_depth:int = -1
+ var vibrato_depth_shift:int = -1
+ var volume_fadeout:int = -1
+
+class ModSample:
+ var data:PoolByteArray
+ var name:String
+ var length:int
+ var loop_start:int
+ var loop_length:int
+ var volume:int
+ var finetune:int
+ var loop_type:int = ModLoopType.FORWARD_LOOP # set(ModLoopType)
+ var bit:int = 8 # 8 or 16
+ var panning:int = -1
+ var relative_note:int = 0
+
+class ModEnvelope:
+ var points:Array # ModEnvelopePoint[12]
+ var point_count:int
+ var sustain_point:int
+ var loop_start_point:int
+ var loop_end_point:int
+
+ var enabled:bool
+ var sustain_enabled:bool
+ var loop_enabled:bool
+
+ func set_flag( f:int ):
+ self.enabled = ( f & 1 ) != 0
+ self.sustain_enabled = ( f & 2 ) != 0
+ self.loop_enabled = ( f & 4 ) != 0
+
+class ModEnvelopePoint:
+ var frame:int
+ var value:int
+
+class ModParseResult:
+ var error:int = OK
+ var data:ModData = null
+
+ func _init( ):
+ pass
+
+"""
+ ファイルから読み込み
+
+ @param path ファイルパス
+ @return 読んだ Modファイルデータ
+"""
+func read_file( path:String ) -> ModParseResult:
+ var f:File = File.new( )
+ var result: = ModParseResult.new( )
+
+ var err:int = f.open( path, f.READ )
+ if err != OK:
+ result.error = err
+ return result
+
+ var stream:StreamPeerBuffer = StreamPeerBuffer.new( )
+ stream.set_data_array( f.get_buffer( f.get_len( ) ) )
+ stream.big_endian = true
+ f.close( )
+
+ result.data = self._read( stream )
+ return result
+
+"""
+ 配列から読み込み
+
+ @param data データ
+ @return ModData
+"""
+func read_data( data:PoolByteArray ) -> ModParseResult:
+ var stream:StreamPeerBuffer = StreamPeerBuffer.new( )
+ stream.set_data_array( data )
+ stream.big_endian = true
+
+ var result: = ModParseResult.new( )
+ result.data = self._read( stream )
+ return result
+
+"""
+ ストリームから読み込み
+
+ @param stream ストリーム
+ @return 読んだ Modファイルデータ
+"""
+func _read( stream:StreamPeerBuffer ) -> ModData:
+ var mod: = ModData.new( )
+ mod.module_name = self._read_string( stream, 20 )
+ mod.tracker_name = ""
+ mod.instruments = self._read_instruments( stream )
+ mod.song_length = stream.get_u8( )
+ mod.unknown_number = stream.get_u8( )
+ mod.song_positions = stream.get_partial_data( 128 )[1]
+ var max_song_position:int = 0
+ for sp in mod.song_positions:
+ if max_song_position < sp:
+ max_song_position = sp
+
+ mod.magic = self._read_string( stream, 4 )
+ var channel_count:int = 4
+ match mod.magic:
+ "6CHN":
+ channel_count = 6
+ "FLT8", "8CHN", "CD81", "OKTA":
+ channel_count = 8
+ "16CN":
+ channel_count = 16
+ "32CN":
+ channel_count = 32
+ _:
+ if mod.magic.substr( 2 ) == "CH":
+ channel_count = int( mod.magic )
+ # print( "Unknown magic" )
+ # breakpoint
+ pass
+ mod.channel_count = channel_count
+
+ mod.patterns = self._read_patterns( stream, max_song_position + 1, channel_count )
+ self._read_sample_data( stream, mod.instruments )
+
+ return mod
+
+"""
+ 楽器データを読み込む
+
+ @param stream ストリーム
+ @return 楽器データ
+"""
+func _read_instruments( stream:StreamPeerBuffer ) -> Array: # of ModSample
+ var instruments:Array = []
+
+ for i in range( 31 ):
+ var sample: = ModSample.new( )
+ sample.name = self._read_string( stream, 22 )
+ sample.length = stream.get_u16( ) * 2
+ var finetune:int = stream.get_u8( ) & 0x0F
+ if 0x08 < finetune:
+ finetune = finetune - 0x10
+ sample.finetune = finetune * 16
+ sample.volume = stream.get_u8( )
+ sample.loop_start = stream.get_u16( ) * 2
+ sample.loop_length = stream.get_u16( ) * 2
+ if sample.loop_length < 8:
+ sample.loop_type = ModLoopType.NO_LOOP
+ else:
+ sample.loop_type = ModLoopType.FORWARD_LOOP
+
+ var instrument: = ModInstrument.new( )
+ instrument.name = sample.name
+ instrument.samples = []
+ for k in range( 96 ):
+ instrument.samples.append( sample )
+ instrument.volume_envelope = ModEnvelope.new( )
+ instrument.volume_envelope.enabled = false
+ instrument.panning_envelope = ModEnvelope.new( )
+ instrument.panning_envelope.enabled = false
+
+ instruments.append( instrument )
+
+ return instruments
+
+"""
+ パターンを読み込む
+
+ @param stream ストリームデータ
+ @param max_positions 最大パターン数
+ @param channels 最大チャンネル数
+ @return パターンデータ
+"""
+func _read_patterns( stream:StreamPeerBuffer, max_position:int, channels:int ) -> Array:
+ var patterns:Array = []
+
+ for position in range( 0, max_position ):
+ var pattern:Array = []
+ for i in range( 0, 64 ):
+ var line:Array = []
+ for ch in range( 0, channels ):
+ var v1:int = stream.get_u16( )
+ var v2:int = stream.get_u16( )
+ var mod_pattern_note: = ModPatternNote.new( )
+ mod_pattern_note.instrument = ( ( v1 >> 8 ) & 0xF0 ) | ( ( v2 >> 12 ) & 0x0F )
+ mod_pattern_note.key_number = v1 & 0x0FFF
+ mod_pattern_note.effect_command = ( v2 >> 8 ) & 0x0F
+ mod_pattern_note.effect_param = v2 & 0x0FF
+ # TODO: 遅かったら二分探索にでもしませんか
+ if 0 < mod_pattern_note.key_number:
+ mod_pattern_note.note = 23
+ for k in period_table:
+ mod_pattern_note.note += 1
+ if k <= mod_pattern_note.key_number:
+ break
+ else:
+ mod_pattern_note.note = 0
+ line.append( mod_pattern_note )
+ pattern.append( line )
+ patterns.append( pattern )
+
+ return patterns
+
+"""
+ 楽器のデータに基づき、波形を読み込む
+
+ @param stream ストリーム
+ @param instruments 波形を読み込む楽器データリスト
+"""
+func _read_sample_data( stream:StreamPeerBuffer, instruments:Array ) -> void:
+ for instrument in instruments:
+ var sample:Mod.ModSample = instrument.samples[0]
+ if 0 < sample.length:
+ sample.data = stream.get_partial_data( sample.length )[1]
+
+"""
+ 文字列の読み込み
+
+ @param stream ストリーム
+ @param size 文字列サイズ
+ @return 読み込んだ文字列を返す
+"""
+func _read_string( stream:StreamPeerBuffer, size:int ) -> String:
+ return stream.get_partial_data( size )[1].get_string_from_ascii( )
diff --git a/addons/modplayer/ModPlayer.gd b/addons/modplayer/ModPlayer.gd
new file mode 100644
index 0000000..55b049b
--- /dev/null
+++ b/addons/modplayer/ModPlayer.gd
@@ -0,0 +1,1053 @@
+extends Node
+
+"""
+ 100% pure GDScript software Mod Player [Godot Mod Player]
+ by あるる(きのもと 結衣) @arlez80
+
+ MIT License
+"""
+
+class_name ModPlayer, "icon.png"
+
+# -------------------------------------------------------
+# 定数
+const mod_master_bus_name:String = "arlez80_GModP_MASTER_BUS"
+const mod_channel_bus_name:String = "arlez80_GModP_CHANNEL_BUS%d"
+const default_mix_rate:int = 4144
+const default_linear_mix_rate:int = 8363
+const chip_speed:float = 50.0
+const volume_table:PoolRealArray = PoolRealArray([
+ -144.0,-36.1,-30.1,-26.6,-24.1,-22.1,-20.6,-19.2,-18.1,-17,-16.1,-15.3,-14.5,-13.8,-13.2,-12.6,-12,-11.5,-11,-10.5,-10.1,-9.7,-9.3,-8.9,-8.5,-8.2,-7.8,-7.5,-7.2,-6.9,-6.6,-6.3,-6,-5.8,-5.5,-5.2,-5,-4.8,-4.5,-4.3,-4.1,-3.9,-3.7,-3.5,-3.3,-3.1,-2.9,-2.7,-2.5,-2.3,-2.1,-2,-1.8,-1.6,-1.5,-1.3,-1.2,-1,-0.9,-0.7,-0.6,-0.4,-0.3,-0.1,0.0
+])
+
+# -----------------------------------------------------------------------------
+# Signals
+
+signal note_on( channel_number, note )
+signal looped
+
+# -----------------------------------------------------------------------------
+# Classes
+class GodotModPlayerChannelAudioEffect:
+ var ae_panner:AudioEffectPanner = null
+
+class GodotModPlayerInstrument:
+ var source:Object # Mod.ModInstrument
+ var array_ass:Array # AudioStreamSample[]
+
+class GodotModPlayerPitch:
+ const center_key_freq:float = 856.0
+
+ var value:int # 現在値
+ var dest:int # 変化目標(ポルタメント用)
+ var speed:int # 変化量(ポルタメント用)
+
+ var arpeggio:Array # アルペジオリスト
+ var arpeggio_count:int # アルペジオカウンタ
+ var arpeggio_enabled:bool
+
+ var linear_freq:bool = false
+
+ func _init( ):
+ self.value = 0
+ self.dest = 0
+ self.speed = 0
+ self.arpeggio = [0,0,0]
+ self.arpeggio_count = 0
+ self.arpeggio_enabled = false
+
+ func update( ):
+ if self.dest < self.value:
+ self.value -= self.speed
+ if self.value < self.dest:
+ self.value = self.dest
+ self.speed = 0
+ elif self.value < self.dest:
+ self.value += self.speed
+ if self.dest < self.value:
+ self.value = self.dest
+ self.speed = 0
+
+ """
+ 現在のピッチスケールを得る
+
+ @param with 加算値
+ @return 計算したピッチスケール
+ """
+ func get_pitch_scale( with:int = 0 ) -> float:
+ var v:int = self.value + with
+
+ if self.arpeggio_enabled:
+ v += self.arpeggio[self.arpeggio_count]
+ self.arpeggio_count = ( self.arpeggio_count + 1 ) % 3
+
+ if self.linear_freq:
+ return pow( 2.0, ( 4608 - v ) / 768.0 )
+
+ if v == 0:
+ return 0.0001
+
+ return center_key_freq / v
+
+ """
+ ファインピッチスケールを取得
+
+ @return 現在のファインピッチスケール
+ """
+ func get_fine_pitch_scale( ) -> float:
+ return pow( 2.0, ( self.value / 128.0 ) / 12.0 )
+
+class GodotModPlayerEffect:
+ var type:int = 0
+ var phase:int = 0
+ var speed:int = 0
+ var depth:int = 0
+ var depth_shift:int = 0
+
+ var value:int = 0
+
+ """
+ 更新
+ """
+ func update( ) -> void:
+ var v:int = 0
+ match self.type:
+ Mod.ModWaveFormType.SINE_WAVEFORM: # 正弦波
+ # TODO: そのうちPoolByteArrayのtableにでもしておく
+ v = int( sin( self.phase * ( PI / 32.0 ) ) * 255.0 )
+ Mod.ModWaveFormType.SAW_WAVEFORM: # のこぎり
+ v = 255 - ( ( self.phase + 0x20 ) & 0x3F ) * 8
+ Mod.ModWaveFormType.SQUARE_WAVEFORM: # 矩形波
+ v = 255 - ( self.phase & 0x20 ) * 16
+ Mod.ModWaveFormType.RAMDOM_WAVEFORM: # 乱数
+ v = self.rng.randi( 511 ) - 255
+ Mod.ModWaveFormType.REV_SAW_WAVEFORM: # 逆のこぎり
+ v = ( ( self.phase + 0x20 ) & 0x3F ) * 8
+
+ self.phase += self.speed
+
+ self.value = ( v * self.depth ) >> self.depth_shift
+
+class GodotModPlayerEnvelope:
+ const Mod = preload( "Mod.gd" )
+
+ var source:Mod.ModEnvelope
+
+ var frame:int = 0
+ var value:int = 0
+ var init_value:int = 0
+ var sustain:bool = false
+ var enabled:bool = false
+
+ """
+ ノートオン
+
+ @param _source エンベロープデータ
+ """
+ func note_on( _source:Mod.ModEnvelope ) -> void:
+ self.source = _source
+ self.sustain = true
+ self.frame = 0
+ if self.source != null and self.source.enabled:
+ self.value = self.source.points[0].value
+ else:
+ self.value = self.init_value
+
+ """
+ ノートオフ
+ """
+ func note_off( ) -> void:
+ self.sustain = false
+
+ """
+ 更新
+ """
+ func update( ) -> void:
+ if self.source == null:
+ self.value = self.init_value
+ return
+
+ self.enabled = self.source.enabled
+
+ if not self.enabled:
+ self.value = self.init_value
+ return
+
+ var current_point:int = 0
+ var prev_sum_frame:int = 0
+ var sum_frame:int = 0
+ var loop_start_frame:int = 0
+ for i in range( 1, self.source.point_count ):
+ sum_frame += self.source.points[i].frame
+ if self.source.loop_start_point == i:
+ loop_start_frame = sum_frame
+ if self.frame < sum_frame:
+ var t:float = float( self.frame - prev_sum_frame ) / float( sum_frame - prev_sum_frame )
+ var s:float = 1.0 - t
+ self.value = int( self.source.points[i-1].value * s + self.source.points[i].value * t )
+ break
+ prev_sum_frame = sum_frame
+ current_point = i
+
+ if self.source.sustain_enabled and self.sustain and self.source.sustain_point == current_point:
+ pass
+ elif self.source.loop_enabled and self.source.loop_end_point == current_point:
+ self.frame = loop_start_frame
+ elif current_point == self.source.point_count and sum_frame < self.frame:
+ self.frame = sum_frame
+ else:
+ self.frame += 1
+
+class GodotModPlayerChannelStatus:
+ const head_silent_second:float = 1.0
+ const gap_second:float = 44100.0 / 1024.0 / 1000.0
+
+ var source_inst:Object # Mod.ModInstrument
+ var source_sample:Object # Mod.ModSample
+ var asps:Array # AudioStreamPlayer[]
+ var asp_switcher:int
+
+ var channel_number:int
+ var last_instrument:int = -1
+ var last_key_number:int = -1
+ var mute:bool
+ var pitch:GodotModPlayerPitch
+ var fine_pitch:GodotModPlayerPitch
+ var relative_pitch:float = 1.0
+
+ var sample_number:int
+ var volume:int
+ var panning:int
+ var amplified:int
+ var fx_count:int
+ var vibrato:GodotModPlayerEffect
+ var tremolo:GodotModPlayerEffect
+
+ var volume_env:GodotModPlayerEnvelope
+ var panning_env:GodotModPlayerEnvelope
+
+ var force_note_off:bool = false
+
+ """
+ コンストラクタ
+
+ @param _channel_number チャンネル番号
+ @param linear_freq 周波数指定が線形か?
+ """
+ func _init( _channel_number:int, linear_freq:bool ):
+ self.mute = false
+ self.force_note_off = false
+
+ self.channel_number = _channel_number
+ self.panning = 128
+
+ for i in range( 2 ):
+ var asp: = AudioStreamPlayer.new( )
+ asp.bus = mod_channel_bus_name % self.channel_number
+ self.asps.append( asp )
+ self.asp_switcher = 0
+
+ self.pitch = GodotModPlayerPitch.new( )
+ self.fine_pitch = GodotModPlayerPitch.new( )
+ self.pitch.linear_freq = linear_freq
+ self.fine_pitch.linear_freq = linear_freq
+ self.vibrato = GodotModPlayerEffect.new( )
+ self.vibrato.depth_shift = 7
+ self.tremolo = GodotModPlayerEffect.new( )
+ self.tremolo.depth_shift = 6
+ self.volume_env = GodotModPlayerEnvelope.new( )
+ self.volume_env.init_value = 64
+ self.panning_env = GodotModPlayerEnvelope.new( )
+ self.panning_env.init_value = 32
+
+ """
+ Tick更新
+ """
+ func tick_update( ) -> void:
+ self.volume_env.update( )
+ self.panning_env.update( )
+
+ """
+ 更新
+ """
+ func update( ) -> void:
+ if self.mute:
+ for asp in self.asps:
+ if asp.is_playing( ):
+ asp.stop( )
+ return
+
+ for i in range( self.asps.size( ) ):
+ var asp:AudioStreamPlayer = self.asps[i]
+ if self.asp_switcher == i and ( not self.force_note_off ):
+ if asp.is_playing( ):
+ asp.volume_db = self.get_volume_db( )
+ asp.pitch_scale = self.get_pitch_scale( )
+ else:
+ if asp.is_playing( ):
+ asp.volume_db -= 4.0
+ if asp.volume_db < -100.0:
+ asp.stop( )
+
+ """
+ volume_dbを取得
+
+ @return 現在のvolume_dbを返す
+ """
+ func get_volume_db( ) -> float:
+ var v:float = ( clamp( self.volume + self.tremolo.value, 0.0, 64.0 ) / 64.0 ) * ( self.volume_env.value / 64.0 )
+ return volume_table[int(v * 64)]
+
+ """
+ ピッチスケールを取得
+
+ @return 現在のピッチスケールを返す
+ """
+ func get_pitch_scale( ) -> float:
+ return self.pitch.get_pitch_scale( self.vibrato.value ) * self.relative_pitch * self.fine_pitch.get_fine_pitch_scale( )
+
+ """
+ ノートオフ
+ """
+ func note_off( ) -> void:
+ self.volume_env.note_off( )
+ self.panning_env.note_off( )
+
+ """
+ ノートオン
+
+ @param inst 楽器データ
+ @param sample_number サンプル番号
+ @param effect_command エフェクトコマンド
+ @param key_number 周波数
+ @param note ノート番号
+ """
+ func note_on( inst:GodotModPlayerInstrument, sample_number:int, effect_command:int, key_number:int, note:int ) -> void:
+ self.last_key_number = key_number
+
+ if self.mute:
+ return
+
+ self.force_note_off = false
+ self.asp_switcher = ( self.asp_switcher + 1 ) % self.asps.size( )
+
+ self.source_inst = inst.source
+ self.source_sample = self.source_inst.samples[note-1]
+ if self.source_sample.panning != -1:
+ self.panning = self.source_sample.panning
+
+ var asp:AudioStreamPlayer = self.asps[self.asp_switcher]
+ asp.stop( )
+ asp.stream = inst.array_ass[note-1]
+ self.volume = self.source_sample.volume
+ self.sample_number = sample_number
+ if effect_command == 0x03 || effect_command == 0x05:
+ self.pitch.dest = key_number
+ else:
+ self.pitch.value = key_number
+ self.fine_pitch.value = self.source_sample.finetune
+ self.relative_pitch = pow( 2.0, self.source_sample.relative_note / 12.0 )
+ if self.source_inst.vibrato_type != -1:
+ self.vibrato.type = self.source_inst.vibrato_type
+ self.vibrato.speed = self.source_inst.vibrato_speed
+ self.vibrato.depth = self.source_inst.vibrato_depth
+ self.vibrato.depth_shift = self.source_inst.vibrato_depth_shift
+
+ self.volume_env.note_on( self.source_inst.volume_envelope )
+ self.panning_env.note_on( self.source_inst.panning_envelope )
+
+ asp.volume_db = self.get_volume_db( )
+ var pitch_scale:float = self.get_pitch_scale( )
+ asp.pitch_scale = pitch_scale
+ asp.play( max( 0.0, self.head_silent_second - clamp( self.gap_second - AudioServer.get_time_to_next_mix( ), 0.0, self.gap_second ) * pitch_scale ) )
+
+# -----------------------------------------------------------------------------
+# Export
+
+# ファイル
+export (String, FILE, "*.mod,*.xm") var file:String = "" setget set_file
+# 再生中か?
+export (bool) var playing:bool = false
+# 音量
+export (float, -144.0, 0.0) var volume_db:float = -20.0 setget set_volume_db
+# キーシフト(未実装)
+#export (int) var key_shift:int = 0
+# ループフラグ
+export (bool) var loop:bool = false
+# mix_target same as AudioStreamPlayer's one
+export (int, "MIX_TARGET_STEREO", "MIX_TARGET_SURROUND", "MIX_TARGET_CENTER") var mix_target:int = AudioStreamPlayer.MIX_TARGET_STEREO
+# bus same as AudioStreamPlayer's one
+export (String) var bus:String = "Master"
+# リリースタイムの発音軽減を与えてブツ切り回避する
+#export (bool) var append_release_time:bool = true
+# 1秒間処理する回数
+export (int, 60, 480) var sequence_per_seconds:int = 120
+# スレッドでの動作にするか
+export (bool) var no_thread_mode:bool = false
+
+# -----------------------------------------------------------------------------
+# 変数
+
+# Mod Playerスレッド
+var thread:Thread = null
+var mutex:Mutex = Mutex.new()
+var thread_delete:bool = false
+# Modデータ
+var mod_data:Mod.ModData = null setget set_mod_data
+# テンポ
+var tempo:float = 125.0 setget set_tempo
+# 行毎秒
+var row_per_second:float = 0.02
+# tick毎行
+var tick_per_row:int = 4
+# tick毎秒
+var tick_per_second:float = 1.0 / chip_speed
+# 次の行への秒数
+var next_row_remain_second:float = 0.0
+# 次のtickへの秒数
+var next_tick_remain_second:float = 0.0
+# 追加tick
+var extra_tick:int = 0
+# tick処理済み回数
+var processed_tick_count:int = 0
+# 位置(秒)
+var position:float = 0.0
+# 曲位置
+var song_position:int = 0
+# パターン内位置
+var pattern_position:int = 0
+# 次の行
+var pattern_position_on_next_row:int = 0
+# 楽器
+var instruments:Array = []
+# チャンネル
+var channel_status:Array = []
+# Modチャンネルエフェクト
+var channel_audio_effects:Array = []
+# パターンジャンプ先
+var pattern_position_jump_point:int = 0
+# パターンループカウンタ
+var pattern_loop_count:int = 0
+# パターンループ先
+var pattern_loop_origin:int = 0
+# 乱数
+var rng:RandomNumberGenerator
+# グローバル音量コマンドを有効にするか?
+var enable_global_volume_command:bool = true
+# グローバル音量 (mod/xm指定の数字)
+var global_volume:int = 64
+# グローバル音量 (計算用db)
+var global_volume_db:float = 0.0
+
+"""
+ 準備
+"""
+func _ready( ):
+ # HTML5 もしくは デバッグモード時には強制的にスレッド未使用モードに変更する
+ if OS.get_name( ) == "HTML5" or OS.is_debug_build( ):
+ self.no_thread_mode = true
+
+ self.rng = RandomNumberGenerator.new( )
+
+ if AudioServer.get_bus_index( self.mod_master_bus_name ) == -1:
+ AudioServer.add_bus( -1 )
+ var mod_master_bus_idx:int = AudioServer.get_bus_count( ) - 1
+ AudioServer.set_bus_name( mod_master_bus_idx, self.mod_master_bus_name )
+ AudioServer.set_bus_send( mod_master_bus_idx, self.bus )
+ AudioServer.set_bus_volume_db( AudioServer.get_bus_index( self.mod_master_bus_name ), self.volume_db )
+
+ for i in range( 32 ):
+ AudioServer.add_bus( -1 )
+ var mod_channel_bus_idx:int = AudioServer.get_bus_count( ) - 1
+ AudioServer.set_bus_name( mod_channel_bus_idx, self.mod_channel_bus_name % i )
+ AudioServer.set_bus_send( mod_channel_bus_idx, self.mod_master_bus_name )
+ AudioServer.set_bus_volume_db( mod_channel_bus_idx, 0.0 )
+
+ var cae: = GodotModPlayerChannelAudioEffect.new( )
+ cae.ae_panner = AudioEffectPanner.new( )
+ AudioServer.add_bus_effect( mod_channel_bus_idx, cae.ae_panner )
+ self.channel_audio_effects.append( cae )
+
+ if self.playing:
+ self.play( )
+
+"""
+ 通知
+
+ @param what 通知要因
+"""
+func _notification( what:int ):
+ # 破棄時
+ if what == NOTIFICATION_PREDELETE:
+ self.thread_delete = true
+ if self.thread != null:
+ self.thread.wait_to_finish( )
+ self.thread = null
+ #AudioServer.remove_bus( AudioServer.get_bus_index( self.mod_master_bus_name ) )
+ #for i in range( 0, 16 ):
+ # AudioServer.remove_bus( AudioServer.get_bus_index( self.midi_channel_bus_name % i ) )
+
+"""
+ Mutex Lock (for debug purpose)
+"""
+func _lock( callee:String ) -> void:
+ # print( "locked by %s" % callee )
+ self.mutex.lock( )
+
+"""
+ Mutex Unload (for debug purpose)
+"""
+func _unlock( callee:String ) -> void:
+ # print( "unlocked by %s" % callee )
+ self.mutex.unlock( )
+
+"""
+ 再生前の初期化
+"""
+func _prepare_to_play( ) -> void:
+ self._lock( "prepare_to_play" )
+
+ # ファイル読み込み
+ if self.mod_data == null:
+ match self.file.get_extension( ):
+ "mod":
+ var mod_reader: = Mod.new( )
+ self.mod_data = mod_reader.read_file( self.file ).data
+ "xm":
+ var xm_reader: = XM.new( )
+ var m:Object = xm_reader.read_file( self.file ).data
+ self.mod_data = m
+ _:
+ self.mod_data = null
+
+ if self.mod_data == null:
+ self._unlock( "prepare_to_play" )
+ self.stop( )
+ return
+
+ if self.channel_status != null:
+ for t in self.channel_status:
+ for asp in t.asps:
+ self.remove_child( asp )
+
+ self.set_volume_db( self.volume_db )
+
+ self.instruments = []
+ var temp_head_silent:Array = []
+ var head_silent_samples:int = default_mix_rate
+ if self.mod_data.flags & Mod.ModFlags.LINEAR_FREQUENCY_TABLE != 0:
+ head_silent_samples = default_linear_mix_rate
+ for i in range( head_silent_samples ):
+ temp_head_silent.append( 0 )
+ var head_silent:PoolByteArray = PoolByteArray( temp_head_silent )
+ var loaded:Dictionary = {}
+ for t in self.mod_data.instruments:
+ var inst:GodotModPlayerInstrument = GodotModPlayerInstrument.new( )
+
+ inst.source = t
+ inst.array_ass = []
+
+ for sample in t.samples:
+ var id:int = sample.get_instance_id( )
+ var ass:AudioStreamSample = null
+ if not( id in loaded ):
+ ass = AudioStreamSample.new( )
+ ass.stereo = false
+ if self.mod_data.flags & Mod.ModFlags.LINEAR_FREQUENCY_TABLE != 0:
+ ass.mix_rate = self.default_linear_mix_rate
+ else:
+ ass.mix_rate = self.default_mix_rate
+ if sample.bit == 16:
+ ass.data = head_silent + head_silent + sample.data
+ ass.format = AudioStreamSample.FORMAT_16_BITS
+ else:
+ ass.data = head_silent + sample.data
+ ass.format = AudioStreamSample.FORMAT_8_BITS
+ ass.loop_begin = sample.loop_start + head_silent_samples
+ ass.loop_end = ass.loop_begin + sample.loop_length
+ if sample.bit == 16:
+ ass.loop_begin /= 2
+ ass.loop_end /= 2
+ ass.loop_mode = AudioStreamSample.LOOP_DISABLED
+ if sample.loop_type & Mod.ModLoopType.FORWARD_LOOP != 0:
+ ass.loop_mode = AudioStreamSample.LOOP_FORWARD
+ elif sample.loop_type & Mod.ModLoopType.PING_PONG_LOOP != 0:
+ ass.loop_mode = AudioStreamSample.LOOP_PING_PONG
+ loaded[id] = ass
+ loaded[id] = ass
+ else:
+ ass = loaded[id]
+ inst.array_ass.append( ass )
+
+ self.instruments.append( inst )
+ for k in loaded.keys( ):
+ loaded.erase( k )
+
+ self.channel_status = []
+ for i in range( self.mod_data.channel_count ):
+ var cs: = GodotModPlayerChannelStatus.new( i, self.mod_data.flags & Mod.ModFlags.LINEAR_FREQUENCY_TABLE != 0 )
+ if 4 < self.mod_data.channel_count:
+ cs.panning = 128
+ else:
+ cs.panning = [64,192,192,64][i]
+ for asp in cs.asps:
+ self.add_child( asp )
+ self.channel_status.append( cs )
+
+ self._unlock( "prepare_to_play" )
+
+"""
+ 再生
+
+ @param from_position 再生開始位置(現在未実装)
+"""
+func play( from_position:float = 0.0 ):
+ self._prepare_to_play( )
+ if self.mod_data == null:
+ return
+
+ self.playing = true
+ if from_position == 0.0:
+ self.position = 0.0
+ self.song_position = 0
+ self.pattern_position_jump_point = 1
+ self.pattern_position = -1
+ self.pattern_position_on_next_row = -1
+ self.pattern_loop_count = 0
+ self.pattern_loop_origin = 0
+ self.tick_per_second = 1.0 / self.chip_speed
+ self.set_tempo( self.mod_data.init_bpm )
+ self.set_tick( self.mod_data.init_tick )
+ self.processed_tick_count = 10000
+ self.next_tick_remain_second = 0.0
+ self.next_row_remain_second = self.row_per_second
+ else:
+ self.seek( from_position )
+
+"""
+ シーク
+ TODO:未実装
+
+ @param to_position 再生位置
+"""
+func seek( to_position:float ) -> void:
+ self._lock( "seek" )
+ self._previous_time = 0.0
+ self._stop_all_notes( )
+ self._unlock( "seek" )
+
+"""
+ 停止
+"""
+func stop( ) -> void:
+ self._lock( "stop" )
+ self._stop_all_notes( )
+ self.playing = false
+ self._unlock( "stop" )
+
+"""
+ ファイル変更
+
+ @param path ファイルパス
+"""
+func set_file( path:String ) -> void:
+ file = path
+ self.mod_data = null
+ if self.playing:
+ self.play( )
+
+"""
+ Modデータ変更
+
+ @param md Modデータ
+"""
+func set_mod_data( md:Mod.ModData ) -> void:
+ mod_data = md
+
+"""
+ 音量設定
+
+ @param vdb 音量
+"""
+func set_volume_db( vdb:float ) -> void:
+ var master_bus_id:int = AudioServer.get_bus_index( self.mod_master_bus_name )
+ volume_db = vdb
+ if master_bus_id == -1:
+ return
+
+ var gvdb:float = self.global_volume_db if self.enable_global_volume_command else 0.0
+ AudioServer.set_bus_volume_db( master_bus_id, volume_db + gvdb )
+
+"""
+ 全音を止める
+"""
+func _stop_all_notes( ) -> void:
+ for t in self.channel_status:
+ t.note_off( )
+ t.force_note_off = true
+ for asp in t.asps:
+ if asp.is_playing( ):
+ asp.stop( )
+
+"""
+ テンポ設定
+
+ @param _tempo テンポ
+"""
+func set_tempo( _tempo:float ) -> void:
+ tempo = _tempo
+ self.tick_per_second = 1.0 / ( self.chip_speed * ( _tempo / 125.0 ) )
+ self.next_row_remain_second -= self.row_per_second
+ self.row_per_second = self.tick_per_row * self.tick_per_second
+ self.next_row_remain_second += self.row_per_second
+
+"""
+ tickからテンポ設定
+
+ @param _tick Tick数
+"""
+func set_tick( _tick:int ) -> void:
+ self.tick_per_row = _tick
+ self.next_row_remain_second -= self.row_per_second
+ self.row_per_second = self.tick_per_row * self.tick_per_second
+ self.next_row_remain_second += self.row_per_second
+
+"""
+ 1フレームでの処理
+
+ @param delta
+"""
+func _process( delta:float ):
+ if self.no_thread_mode:
+ self._sequence( delta )
+ else:
+ if self.thread == null or ( not self.thread.is_alive( ) ):
+ self._lock( "_process" )
+ if self.thread != null:
+ self.thread.wait_to_finish( )
+ self.thread = Thread.new( )
+ self.thread.start( self, "_thread_process" )
+ self._unlock( "_process" )
+
+"""
+ スレッド処理
+"""
+func _thread_process( ) -> void:
+ var last_time:int = OS.get_ticks_usec( )
+
+ while not self.thread_delete:
+ self._lock( "_thread_process" )
+
+ var current_time:int = OS.get_ticks_usec( )
+ var delta:float = ( current_time - last_time ) / 1000000.0
+ self._sequence( delta )
+
+ self._unlock( "_thread_process" )
+
+ last_time = current_time
+ var msec:int = int( 1000.0 / self.sequence_per_seconds )
+ OS.delay_msec( msec )
+
+"""
+ シーケンス処理を行う
+
+ @param delta 前回からの経過時間 sec
+"""
+func _sequence( delta:float ) -> void:
+ if delta < 0.0:
+ return
+
+ if self.mod_data != null:
+ if self.playing:
+ self.position += delta
+ self.next_row_remain_second -= delta
+ self.next_tick_remain_second -= delta
+ if self.next_row_remain_second <= 0.0:
+ self.next_tick_remain_second = -INF
+ while self.next_tick_remain_second <= 0.0 and self.processed_tick_count < self.tick_per_row + self.extra_tick:
+ self._process_tick( )
+ self.next_tick_remain_second += self.tick_per_second
+ self._process_row( )
+ self.processed_tick_count += 1
+ if self.next_row_remain_second <= 0.0:
+ self.extra_tick = 0
+ self._process_move_to_next_line( )
+ self.next_row_remain_second = self.row_per_second + self.next_row_remain_second
+ self.next_tick_remain_second = 0.0
+ self.processed_tick_count = 1
+ self._process_update_audio_effects( )
+
+"""
+ 次の行に移行する
+"""
+func _process_move_to_next_line( ) -> void:
+ if 0 <= self.pattern_position_on_next_row:
+ self.pattern_position = self.pattern_position_on_next_row
+ self.pattern_position_on_next_row = -1
+ self.song_position = self.pattern_position_jump_point
+ self.pattern_position_jump_point = self.song_position + 1
+ else:
+ self.pattern_position += 1
+ if len( self.mod_data.patterns[self.mod_data.song_positions[self.song_position]] ) <= self.pattern_position:
+ self.pattern_position = 0
+ self.song_position = self.pattern_position_jump_point
+ self.pattern_position_jump_point = self.song_position + 1
+
+ if self.mod_data.song_length <= self.song_position:
+ if self.loop:
+ self.song_position = self.mod_data.restart_position
+ self.pattern_position_jump_point = self.song_position + 1
+ self.emit_signal( "looped" )
+ else:
+ self.song_position = 0
+ self.stop( )
+ return
+
+"""
+ 1行処理
+"""
+func _process_row( ) -> void:
+ var pattern_line:Array = self.mod_data.patterns[self.mod_data.song_positions[self.song_position]][self.pattern_position]
+ for channel in self.channel_status:
+ self._process_row_for_channel( channel, pattern_line[channel.channel_number] )
+
+"""
+ チャンネルごとの1行処理
+
+ @param channel チャンネルデータ
+ @param note ノートデータ
+"""
+func _process_row_for_channel( channel:GodotModPlayerChannelStatus, note:Mod.ModPatternNote ) -> void:
+ #printt( channel.channel_number, pattern_node.sample_number, pattern_node.key_number, pattern_node.effect_command )
+
+ var note_on:bool = self.processed_tick_count == 1
+ # Note関係のエフェクトコマンド
+ if note.effect_command == 0x0E:
+ match ( note.effect_param >> 4 ):
+ 0x09: # Retrigger Note
+ note_on = ( ( self.processed_tick_count - 1 ) % ( note.effect_param & 0x0F ) ) == 0
+ 0x0D: # Note Delay
+ note_on = ( note.effect_param & 0x0F ) == self.processed_tick_count - 1
+
+ # 楽器設定
+ if note.instrument != 0:
+ channel.last_instrument = note.instrument - 1
+ # 発音
+ if note_on:
+ channel.pitch.arpeggio_count = 0
+ if note.note == 96:
+ channel.note_off( )
+ elif 0 < note.key_number:
+ if 0 <= channel.last_instrument and channel.last_instrument < self.instruments.size( ):
+ var inst:GodotModPlayerInstrument = self.instruments[channel.last_instrument]
+ channel.note_on( inst, note.instrument, note.effect_command, note.key_number, note.note )
+
+ self.emit_signal( "note_on", channel.channel_number, note )
+ elif note.instrument != 0:
+ if 0 <= channel.last_instrument and channel.last_instrument < self.instruments.size( ) and 0 <= channel.last_key_number:
+ var inst:GodotModPlayerInstrument = self.instruments[channel.last_instrument]
+ channel.note_on( inst, note.instrument, note.effect_command, channel.last_key_number, note.note )
+
+ self.emit_signal( "note_on", channel.channel_number, note )
+
+ if self.processed_tick_count == 1:
+ self._process_tick_for_channel( channel, note, true )
+
+"""
+ 1tick処理
+"""
+func _process_tick( ) -> void:
+ if self.pattern_position < 0:
+ return
+
+ var pattern_line:Array = self.mod_data.patterns[self.mod_data.song_positions[self.song_position]][self.pattern_position]
+ for channel in self.channel_status:
+ self._process_tick_for_channel( channel, pattern_line[channel.channel_number], false )
+
+"""
+ チャンネルごとの1tick処理
+
+ @param channel チャンネルデータ
+ @param note ノートデータ
+ @param disable_channel_row チャンネルイベントを無視する
+"""
+func _process_tick_for_channel( channel:GodotModPlayerChannelStatus, note:Mod.ModPatternNote, disable_channel_row:bool ) -> void:
+ #print( "%08x %08x" % [ note.effect_command, note.effect_param ] )
+
+ channel.pitch.arpeggio_enabled = false
+ channel.vibrato.value = 0
+ channel.tremolo.value = 0
+
+ channel.tick_update( )
+
+ if 0x10 <= note.volume and note.volume <= 0x50:
+ channel.volume = note.volume - 0x10
+ elif 0x60 <= note.volume:
+ match note.volume >> 4:
+ 0x06: # Volume slide down
+ if not disable_channel_row:
+ channel.volume = int( clamp( channel.volume - ( note.volume & 0x0F ), 0, 64 ) )
+ 0x07: # Volume slide up
+ if not disable_channel_row:
+ channel.volume = int( clamp( channel.volume + ( note.volume & 0x0F ), 0, 64 ) )
+ 0x08: # Fine volume slide down
+ if not disable_channel_row:
+ channel.volume = int( clamp( channel.volume - ( note.volume & 0x0F ), 0, 64 ) )
+ 0x09: # Fine volume slide up
+ if not disable_channel_row:
+ channel.volume = int( clamp( channel.volume + ( note.volume & 0x0F ), 0, 64 ) )
+ 0x0A: # Set vibrato speed
+ channel.vibrato.speed = (channel.vibrato.speed & 0x0F) | ( ( note.volume & 0x0F ) << 4 );
+ 0x0C: # Panning
+ var p:int = note.volume & 0x0F
+ p |= p << 4
+ channel.panning = p
+ 0x0D: # Panning slide left
+ if not disable_channel_row:
+ channel.panning = int( clamp( channel.panning - ( note.panning & 0x0F ), 0, 255 ) )
+ 0x0E: # Panning slide right
+ if not disable_channel_row:
+ channel.panning = int( clamp( channel.panning + ( note.panning & 0x0F ), 0, 255 ) )
+ 0x0F: # Tone portamento
+ if 0 < note.volume & 0x0F:
+ var p:int = note.volume & 0x0F
+ p |= p << 4
+ channel.pitch.speed = p
+ _:
+ printerr( "unknown volume effect command: %02x" % note.volume )
+
+ # エフェクトコマンド
+ match note.effect_command:
+ 0x00: # Arpeggio
+ channel.pitch.arpeggio[1] = int( channel.pitch.value / pow( 2.0, ( note.effect_param >> 4 ) / 12.0 ) ) - channel.pitch.value
+ channel.pitch.arpeggio[2] = int( channel.pitch.value / pow( 2.0, ( note.effect_param & 0x0F ) / 12.0 ) ) - channel.pitch.value
+ channel.pitch.arpeggio_enabled = true
+ 0x01: # Portament up
+ if not disable_channel_row:
+ channel.pitch.value = int( max( channel.pitch.value - note.effect_param, 0 ) )
+ 0x02: # Portament down
+ if not disable_channel_row:
+ channel.pitch.value = channel.pitch.value + note.effect_param
+ 0x03: # Portament speed
+ channel.pitch.speed = note.effect_param
+ channel.pitch.update( )
+ 0x04: # Vibrato
+ if 0 < note.effect_param & 0xF0:
+ channel.vibrato.speed = note.effect_param >> 4
+ if 0 < note.effect_param & 0x0F:
+ channel.vibrato.depth = note.effect_param & 0x0F
+ channel.vibrato.update( )
+ 0x05: # Portament + Volume slide
+ channel.pitch.update( )
+ channel.volume = int( clamp( channel.volume + ( note.effect_param >> 4 ) - ( note.effect_param & 0x0F ), 0, 64 ) )
+ 0x06: # Vibrato + Volume slide
+ channel.vibrato.update( )
+ channel.volume = int( clamp( channel.volume + ( note.effect_param >> 4 ) - ( note.effect_param & 0x0F ), 0, 64 ) )
+ 0x07: # Tremolo
+ if 0 < note.effect_param & 0xF0:
+ channel.tremolo.speed = note.effect_param >> 4
+ if 0 < note.effect_param & 0x0F:
+ channel.tremolo.depth = note.effect_param & 0x0F
+ channel.tremolo.update( )
+ 0x08: # Panning
+ channel.panning = int( clamp( note.effect_param * 2, 0, 255 ) )
+ 0x09: # Sample offset
+ printerr( "not implemented: 9xx Sample offset" )
+ 0x0A: # Volume slide
+ if not disable_channel_row:
+ channel.volume = int( clamp( channel.volume + ( note.effect_param >> 4 ) - ( note.effect_param & 0x0F ), 0, 64 ) )
+ 0x0B: # Pattern jump
+ if disable_channel_row:
+ self.pattern_position_jump_point = note.effect_param
+ self.pattern_position_on_next_row = 0
+ 0x0C: # Volume
+ channel.volume = int( clamp( note.effect_param, 0, 64 ) )
+ 0x0D: # Pattern break
+ if disable_channel_row:
+ self.pattern_position_jump_point = self.song_position + 1
+ self.pattern_position_on_next_row = ( note.effect_param >> 4 ) * 10 + ( note.effect_param & 0x0F )
+ if 64 <= self.pattern_position_on_next_row:
+ self.pattern_position_on_next_row = 0
+ 0x0E: # 拡張コマンド
+ match ( note.effect_param >> 4 ):
+ 0x01: # Fine portamento up
+ if 0 < note.effect_param & 0x0F:
+ channel.fine_pitch.speed = note.effect_param & 0x0F
+ channel.fine_pitch.update( )
+ 0x02: # Fine portamento down
+ if 0 < note.effect_param & 0x0F:
+ channel.fine_pitch.speed = - (note.effect_param & 0x0F)
+ channel.fine_pitch.update( )
+ 0x04: # Vibrato type
+ channel.vibrato.type = note.effect_param
+ channel.vibrato.update( )
+ 0x06: # Pattern loop
+ if disable_channel_row:
+ if 0 < ( note.effect_param & 0x0F ):
+ if ( note.effect_param & 0x0F ) == self.pattern_loop_count:
+ self.pattern_loop_count = 0
+ else:
+ self.pattern_loop_count += 1
+ self.pattern_position_jump_point = self.song_position
+ self.pattern_position_on_next_row = self.pattern_loop_origin
+ else:
+ self.pattern_loop_origin = self.pattern_position
+ 0x07: # Tremoro type
+ channel.tremolo.type = note.effect_param
+ channel.tremolo.update( )
+ 0x09: # Retrigger Note
+ pass
+ 0x0A: # Fine volume slide up
+ if not disable_channel_row:
+ channel.volume = int( clamp( channel.volume + ( note.volume & 0x0F ), 0, 64 ) )
+ 0x0B: # Fine volume slide down
+ if not disable_channel_row:
+ channel.volume = int( clamp( channel.volume - ( note.volume & 0x0F ), 0, 64 ) )
+ 0x0C: # Note cut
+ if self.processed_tick_count - 1 == note.effect_param & 0x0F:
+ channel.volume = 0
+ 0x0D: # Note delay (上で処理する)
+ pass
+ 0x0E: # Pattern delay
+ var r:int = ( note.effect_param & 0x0F )
+ self.extra_tick = r * self.tick_per_row
+ if disable_channel_row:
+ self.next_row_remain_second += self.row_per_second * r
+ _:
+ printerr( "unknown extended command: %04x" % [ note.effect_param ] )
+ 0x0F: # Tick / Tempo
+ if disable_channel_row:
+ if note.effect_param < 0x20:
+ self.set_tick( note.effect_param )
+ else:
+ self.set_tempo( note.effect_param )
+ 0x10: # Global volume
+ self.global_volume = int( clamp( note.effect_param, 0, 64 ) )
+ self.global_volume_db = self.volume_table[self.global_volume]
+ self.set_volume_db( self.volume_db )
+ 0x11: # Global volume slide
+ self.global_volume = int( clamp( self.global_volume + ( note.effect_param >> 4 ) - ( note.effect_param & 0x0F ), 0, 64 ) )
+ self.global_volume_db = self.volume_table[self.global_volume]
+ self.set_volume_db( self.volume_db )
+ 0x14: # Key off
+ printerr( "not implemented: Kxx Key off" )
+ 0x15: # Set envelope position
+ channel.tremolo.phase = note.effect_param
+ 0x19: # Pannning slide
+ if not disable_channel_row:
+ channel.panning = int( clamp( channel.panning + ( note.effect_param >> 4 ) - ( note.effect_param & 0x0F ), 0, 255 ) )
+ 0x1B: # Multi retrig note
+ printerr( "not implemented: Rxy Multi retrig note" )
+ 0x1D: # Tremor
+ printerr( "not implemented: Txy Tremor" )
+ _:
+ printerr( "unknown command: %02x : %04x" % [ note.effect_command, note.effect_param ] )
+
+"""
+ Godotのオーディオエフェクト更新
+"""
+func _process_update_audio_effects( ) -> void:
+ for channel in self.channel_status:
+ channel.update( )
+ var cae:GodotModPlayerChannelAudioEffect = self.channel_audio_effects[channel.channel_number]
+ cae.ae_panner.pan = clamp( ( ( channel.panning - 128 ) / 128.0 ) + ( ( channel.panning_env.value - 32 ) / 32.0 ), -1.0, 1.0 )
diff --git a/addons/modplayer/ModPlayer.tscn b/addons/modplayer/ModPlayer.tscn
new file mode 100644
index 0000000..ec4e2a5
--- /dev/null
+++ b/addons/modplayer/ModPlayer.tscn
@@ -0,0 +1,6 @@
+[gd_scene load_steps=2 format=2]
+
+[ext_resource path="res://addons/modplayer/ModPlayer.gd" type="Script" id=1]
+
+[node name="ModPlayer" type="Node"]
+script = ExtResource( 1 )
diff --git a/addons/modplayer/XM.gd b/addons/modplayer/XM.gd
new file mode 100644
index 0000000..28da330
--- /dev/null
+++ b/addons/modplayer/XM.gd
@@ -0,0 +1,287 @@
+"""
+ XM reader by あるる(きのもと 結衣) @arlez80
+
+ This script required Mod.gd for declaring the data structures.
+
+ MIT License
+"""
+
+class_name XM
+
+enum XMNoteFlags {
+ NOTE = 0x01,
+ INSTRUMENT = 0x02,
+ VOLUME = 0x04,
+ EFFECT_COMMAND = 0x08,
+ EFFECT_PARAM = 0x10,
+
+ FIRST_BYTE_AS_FLAGS = 0x80,
+}
+
+"""
+ ファイルから読み込む
+
+ @param path ファイルパス
+ @return xm
+"""
+func read_file( path:String ) -> Mod.ModParseResult:
+ var f:File = File.new( )
+ var result: = Mod.ModParseResult.new( )
+
+ var err:int = f.open( path, f.READ )
+ if err != OK:
+ result.error = err
+ return result
+
+ var stream:StreamPeerBuffer = StreamPeerBuffer.new( )
+ stream.set_data_array( f.get_buffer( f.get_len( ) ) )
+ f.close( )
+
+ result.data = self._read( stream )
+ if result.data == null:
+ result.error = ERR_PARSE_ERROR
+ return result
+
+"""
+ 配列から読み込む
+
+ @param data データ
+ @return xm
+"""
+func read_data( data:PoolByteArray ) -> Mod.ModParseResult:
+ var stream:StreamPeerBuffer = StreamPeerBuffer.new( )
+ stream.set_data_array( data )
+
+ var result: = Mod.ModParseResult.new( )
+ result.data = self._read( stream )
+ if result.data == null:
+ result.error = ERR_PARSE_ERROR
+ return result
+
+"""
+ ストリームから読み込み
+
+ @param stream ストリーム
+ @return xm
+"""
+func _read( stream:StreamPeerBuffer ) -> Mod.ModData:
+ var xm: = Mod.ModData.new( )
+
+ if self._read_string( stream, 17 ) != "Extended Module: ":
+ return null
+ xm.module_name = self._read_string( stream, 20 )
+ if stream.get_u8( ) != 0x1A:
+ return null
+ xm.tracker_name = self._read_string( stream, 20 )
+ xm.version = stream.get_u16( )
+
+ var header_size:int = stream.get_u32( )
+ xm.song_length = stream.get_u16( )
+ xm.restart_position = stream.get_u16( )
+ xm.channel_count = stream.get_u16( )
+
+ var pattern_count:int = stream.get_u16( )
+ var instrument_count:int = stream.get_u16( )
+ xm.flags = stream.get_u16( )
+ xm.init_tick = stream.get_u16( )
+ xm.init_bpm = stream.get_u16( )
+
+ xm.song_positions = stream.get_partial_data( header_size - 20 )[1]
+
+ xm.patterns = self._read_patterns( stream, xm.flags, pattern_count, xm.channel_count )
+ if len( xm.patterns ) == 0:
+ return null
+
+ xm.instruments = self._read_instruments( stream, instrument_count )
+ if len( xm.instruments ) == 0:
+ return null
+
+ return xm
+
+"""
+ パターン読み込み
+
+ @param stream ストリーム
+ @param xm_file_flags XMデータに関するフラグ
+ @param pattern_count パターン数
+ @param channles 最大チャンネル数
+ @return パターンデータ
+"""
+func _read_patterns( stream:StreamPeerBuffer, xm_file_flags:int, pattern_count:int, channels:int ) -> Array:
+ var patterns:Array = []
+
+ for i in range( pattern_count ):
+ var pattern:Array = []
+
+ if stream.get_u32( ) != 9:
+ return []
+ if stream.get_u8( ) != 0:
+ return []
+ var row_count:int = stream.get_u16( )
+ var size:int = stream.get_u16( )
+
+ for k in range( row_count ):
+ var line:Array = []
+ for ch in range( channels ):
+ var patr: = Mod.ModPatternNote.new( )
+ var first:int = stream.get_u8( )
+ if first & XMNoteFlags.FIRST_BYTE_AS_FLAGS != 0:
+ if first & XMNoteFlags.NOTE != 0:
+ patr.note = stream.get_u8( )
+ else:
+ patr.note = first
+ first = 0xFF
+ if 0 < patr.note:
+ patr.note -= 1
+ if first & XMNoteFlags.INSTRUMENT != 0:
+ patr.instrument = stream.get_u8( )
+ if first & XMNoteFlags.VOLUME != 0:
+ patr.volume = stream.get_u8( )
+ if first & XMNoteFlags.EFFECT_COMMAND != 0:
+ patr.effect_command = stream.get_u8( )
+ if first & XMNoteFlags.EFFECT_PARAM != 0:
+ patr.effect_param = stream.get_u8( )
+ if 0 < patr.note:
+ if xm_file_flags & Mod.ModFlags.LINEAR_FREQUENCY_TABLE != 0:
+ patr.key_number = self._conv_linear_freq( patr.note )
+ else:
+ patr.key_number = self._conv_amiga_freq( patr.note )
+ line.append( patr )
+ pattern.append( line )
+ patterns.append( pattern )
+
+ return patterns
+
+"""
+ ノート番号からAMIGA式で周波数計算する
+
+ @param note ノート番号
+ @return 周波数
+"""
+func _conv_amiga_freq( note:int ) -> int:
+ return int( 6848.0 / pow( 2.0, note / 12.0 ) )
+
+"""
+ ノート番号から線形で周波数計算する
+
+ @param note ノート番号
+ @return 周波数
+"""
+func _conv_linear_freq( note:int ) -> int:
+ return 7680 - note * 64
+
+"""
+ 楽器データを読み込む
+
+ @param stream ストリーム
+ @param instrument_count 楽器数
+ @return 楽器データ
+"""
+func _read_instruments( stream:StreamPeerBuffer, instrument_count:int ) -> Array:
+ var instruments:Array = []
+
+ for i in range( instrument_count ):
+ var inst: = Mod.ModInstrument.new( )
+ var size:int = stream.get_u32( )
+ var remain_size:int = size
+ inst.name = self._read_string( stream, 22 )
+ #print( "%d/%d %s" % [ i, instrument_count, inst.name] )
+ if stream.get_u8( ) != 0:
+ return []
+
+ var sample_count:int = stream.get_u16( )
+ var sample_numbers:Array = []
+ remain_size -= 4 + 22 + 1 + 2
+ if 0 < sample_count:
+ var sample_header_size:int = stream.get_u32( )
+ sample_numbers = stream.get_partial_data( 96 )[1]
+ inst.volume_envelope = Mod.ModEnvelope.new( )
+ for k in range( 12 ):
+ var ve: = Mod.ModEnvelopePoint.new( )
+ ve.frame = stream.get_u16( )
+ ve.value = stream.get_u16( )
+ inst.volume_envelope.points.append( ve )
+ inst.panning_envelope = Mod.ModEnvelope.new( )
+ for k in range( 12 ):
+ var pe: = Mod.ModEnvelopePoint.new( )
+ pe.frame = stream.get_u16( )
+ pe.value = stream.get_u16( )
+ inst.panning_envelope.points.append( pe )
+ remain_size -= 4 + 96 + 48 + 48
+ inst.volume_envelope.point_count = stream.get_u8( )
+ inst.panning_envelope.point_count = stream.get_u8( )
+ inst.volume_envelope.sustain_point = stream.get_u8( )
+ inst.volume_envelope.loop_start_point = stream.get_u8( )
+ inst.volume_envelope.loop_end_point = stream.get_u8( )
+ inst.panning_envelope.sustain_point = stream.get_u8( )
+ inst.panning_envelope.loop_start_point = stream.get_u8( )
+ inst.panning_envelope.loop_end_point = stream.get_u8( )
+ inst.volume_envelope.set_flag( stream.get_u8( ) )
+ inst.panning_envelope.set_flag( stream.get_u8( ) )
+ inst.vibrato_type = stream.get_u8( )
+ inst.vibrato_speed = stream.get_u8( )
+ inst.vibrato_depth = stream.get_u8( )
+ inst.vibrato_depth_shift = stream.get_u8( )
+ inst.volume_fadeout = stream.get_u16( )
+ remain_size -= 16
+
+ if 0 < remain_size:
+ stream.get_partial_data( remain_size ) # reserved
+
+ if 0 < sample_count:
+ var sounds:Array = []
+ # Sound Header
+ for k in range( sample_count ):
+ var xms: = Mod.ModSample.new( )
+ xms.length = stream.get_u32( )
+ xms.loop_start = stream.get_u32( )
+ xms.loop_length = stream.get_u32( )
+ xms.volume = stream.get_u8( )
+ xms.finetune = stream.get_8( )
+ xms.loop_type = stream.get_u8( )
+ if xms.loop_type & 16 != 0:
+ xms.bit = 16
+ xms.loop_type &= 3
+ xms.panning = stream.get_u8( )
+ xms.relative_note = stream.get_8( )
+ stream.get_u8( ) # Reserved
+ xms.name = self._read_string( stream, 22 )
+ sounds.append( xms )
+ # Sound Data
+ for xms in sounds:
+ var d:Array = []
+ var p:int = 0
+
+ if xms.bit == 16:
+ for k in range( xms.length >> 1 ):
+ p += stream.get_16( )
+ d.append( p & 0xFF )
+ d.append( ( p >> 8 ) & 0xFF )
+ else:
+ for k in range( xms.length ):
+ p += stream.get_8( )
+ d.append( p & 0xFF )
+
+ xms.data = PoolByteArray( d )
+
+ inst.samples = []
+ for k in sample_numbers:
+ if len( sounds ) <= k:
+ inst.samples.append( sounds[0] )
+ else:
+ inst.samples.append( sounds[k] )
+
+ instruments.append( inst )
+
+ return instruments
+
+"""
+ 文字列の読み込み
+
+ @param stream ストリーム
+ @param size 文字列サイズ
+ @return 読み込んだ文字列を返す
+"""
+func _read_string( stream:StreamPeerBuffer, size:int ) -> String:
+ return stream.get_partial_data( size )[1].get_string_from_ascii( )
+
diff --git a/addons/modplayer/icon.png b/addons/modplayer/icon.png
new file mode 100644
index 0000000..cfffaa4
Binary files /dev/null and b/addons/modplayer/icon.png differ
diff --git a/addons/modplayer/icon.png.import b/addons/modplayer/icon.png.import
new file mode 100644
index 0000000..11ab2fd
--- /dev/null
+++ b/addons/modplayer/icon.png.import
@@ -0,0 +1,35 @@
+[remap]
+
+importer="texture"
+type="StreamTexture"
+path="res://.import/icon.png-517e88a2da8173eba7cfa3edf0828c77.stex"
+metadata={
+"vram_texture": false
+}
+
+[deps]
+
+source_file="res://addons/modplayer/icon.png"
+dest_files=[ "res://.import/icon.png-517e88a2da8173eba7cfa3edf0828c77.stex" ]
+
+[params]
+
+compress/mode=0
+compress/lossy_quality=0.7
+compress/hdr_mode=0
+compress/bptc_ldr=0
+compress/normal_map=0
+flags/repeat=0
+flags/filter=false
+flags/mipmaps=false
+flags/anisotropic=false
+flags/srgb=2
+process/fix_alpha_border=true
+process/premult_alpha=false
+process/HDR_as_SRGB=false
+process/invert_color=false
+process/normal_map_invert_y=false
+stream=false
+size_limit=0
+detect_3d=false
+svg/scale=1.0
diff --git a/addons/modplayer/plugin.cfg b/addons/modplayer/plugin.cfg
new file mode 100644
index 0000000..04e3e4f
--- /dev/null
+++ b/addons/modplayer/plugin.cfg
@@ -0,0 +1,6 @@
+[plugin]
+name="Godot Mod Player"
+description="A plugin that playing Mod Files for Godot Engine."
+author="arlez80"
+version="1.4.4"
+script="GMP.gd"
\ No newline at end of file
diff --git a/addons/platformer_controller/._LICENSE b/addons/platformer_controller/._LICENSE
new file mode 100644
index 0000000..7f780e4
Binary files /dev/null and b/addons/platformer_controller/._LICENSE differ
diff --git a/addons/platformer_controller/DemoScene.tscn b/addons/platformer_controller/DemoScene.tscn
new file mode 100644
index 0000000..b29731e
--- /dev/null
+++ b/addons/platformer_controller/DemoScene.tscn
@@ -0,0 +1,73 @@
+[gd_scene load_steps=7 format=2]
+
+[ext_resource path="res://icon.png" type="Texture" id=1]
+[ext_resource path="res://addons/platformer_controller/platformer_controller.gd" type="Script" id=2]
+
+[sub_resource type="RectangleShape2D" id=1]
+extents = Vector2( 512, 56 )
+
+[sub_resource type="RectangleShape2D" id=2]
+extents = Vector2( 208, 56 )
+
+[sub_resource type="RectangleShape2D" id=3]
+extents = Vector2( 32, 304 )
+
+[sub_resource type="RectangleShape2D" id=4]
+extents = Vector2( 32, 32 )
+
+[node name="Main" type="Node2D"]
+
+[node name="StaticBody2D" type="StaticBody2D" parent="."]
+position = Vector2( 520, 544 )
+
+[node name="ColorRect" type="ColorRect" parent="StaticBody2D"]
+margin_left = -520.0
+margin_top = -56.0
+margin_right = 504.0
+margin_bottom = 56.0
+color = Color( 0, 0.443137, 0.027451, 1 )
+__meta__ = {
+"_edit_use_anchors_": false
+}
+
+[node name="CollisionShape2D" type="CollisionShape2D" parent="StaticBody2D"]
+position = Vector2( -8, 0 )
+shape = SubResource( 1 )
+
+[node name="StaticBody2D2" type="StaticBody2D" parent="."]
+position = Vector2( 520, 360 )
+
+[node name="ColorRect" type="ColorRect" parent="StaticBody2D2"]
+margin_left = -520.0
+margin_top = 16.0
+margin_right = -104.0
+margin_bottom = 128.0
+color = Color( 0, 0.443137, 0.027451, 1 )
+__meta__ = {
+"_edit_use_anchors_": false
+}
+
+[node name="CollisionShape2D" type="CollisionShape2D" parent="StaticBody2D2"]
+position = Vector2( -312, 72 )
+shape = SubResource( 2 )
+
+[node name="StaticBody2D3" type="StaticBody2D" parent="."]
+position = Vector2( 520, 360 )
+
+[node name="CollisionShape2D" type="CollisionShape2D" parent="StaticBody2D3"]
+position = Vector2( -552, -64 )
+shape = SubResource( 3 )
+
+[node name="CollisionShape2D2" type="CollisionShape2D" parent="StaticBody2D3"]
+position = Vector2( 536, -64 )
+shape = SubResource( 3 )
+
+[node name="PlatformerController2D" type="KinematicBody2D" parent="."]
+position = Vector2( 272, 167 )
+script = ExtResource( 2 )
+
+[node name="icon" type="Sprite" parent="PlatformerController2D"]
+texture = ExtResource( 1 )
+
+[node name="CollisionShape2D" type="CollisionShape2D" parent="PlatformerController2D"]
+shape = SubResource( 4 )
diff --git a/addons/platformer_controller/LICENSE b/addons/platformer_controller/LICENSE
new file mode 100644
index 0000000..09a955f
--- /dev/null
+++ b/addons/platformer_controller/LICENSE
@@ -0,0 +1,21 @@
+MIT License
+
+Copyright (c) 2021 Ev01
+
+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/addons/platformer_controller/README.md b/addons/platformer_controller/README.md
new file mode 100644
index 0000000..cbdfa7e
--- /dev/null
+++ b/addons/platformer_controller/README.md
@@ -0,0 +1,75 @@
+# PlatformerController2D
+
+A 2D platformer class for godot.
+
+## Changelog
+### Version 1.0.1
+- Updated to Godot 3.4
+- Fixed division by zero error when changing min jump height
+- Other minor fixes
+
+## Installation
+
+1. Add platformer_controller.gd to your project
+2. Type `extends PlatformerController2D` to the top of your script
+3. Add these input mappings in your project settings (or you can change the input variables in the inspector)
+ - "move_left"
+ - "move_right"
+ - "jump"
+
+
+## Features
+- Double jump
+- Coyote time
+- Jump buffer
+- Hold jump to go higher
+- Defining jump height and duration (as opposed to setting gravity and jump velocity)
+- Assymetrical jumps (falling faster than rising)
+
+| |
+--|--|--
+
+
+
+## Customization / Export variables
+Here are the values that you can change in the inspector:
+
+### max_jump_height
+The max jump height in pixels. You reach this when you hold down jump.
+
+### Min Jump Height
+The minimum jump height (tapping jump).
+
+### Double Jump Height
+The height of your jump in the air (i.e. double jump, triple jump etc.).
+
+### Jump Duration
+How long it takes to get to the peak of the jump (in seconds).
+
+### Falling Gravity Multiplier
+Multiplies the gravity by this while falling.
+
+### Max Jump Amount
+How many times you can jump before hitting the ground. Set this to 2 for a double jump.
+
+### Max Acceleration
+How much you accelerate when you hold left or right (in pixels/sec^2).
+
+### Friction
+The higher this number, the more friction is on your character.
+
+### Can Hold Jump
+If this is off, you have to press jump down every time you land. If its on you can keep it held.
+
+### Coyote Time
+You can still jump this many seconds after falling off a ledge.
+
+### Jump Buffer
+Pressing jump this many seconds before hitting the ground will still make you jump.\
+Note: This is only needed when can_hold_jump is off.
+
+### Input Variables
+`input_left`\
+`input_right`\
+`input_jump`\
+ Set these to the names of your actions in the Input Map
diff --git a/addons/platformer_controller/platformer_controller.gd b/addons/platformer_controller/platformer_controller.gd
new file mode 100644
index 0000000..1433411
--- /dev/null
+++ b/addons/platformer_controller/platformer_controller.gd
@@ -0,0 +1,213 @@
+extends KinematicBody2D
+
+class_name PlatformerController2D
+
+# Set these to the name of your action (in the Input Map)
+export var input_left : String = "move_left"
+export var input_right : String = "move_right"
+export var input_jump : String = "jump"
+
+# The max jump height in pixels (holding jump)
+export var max_jump_height = 150 setget set_max_jump_height
+# The minimum jump height (tapping jump)
+export var min_jump_height = 40 setget set_min_jump_height
+# The height of your jump in the air
+export var double_jump_height = 100 setget set_double_jump_height
+# How long it takes to get to the peak of the jump in seconds
+export var jump_duration = 0.3 setget set_jump_duration
+# Multiplies the gravity by this while falling
+export var falling_gravity_multiplier = 1.5
+# Set to 2 for double jump
+export var max_jump_amount = 1
+export var max_acceleration = 4000
+export var friction = 8
+export var can_hold_jump : bool = false
+# You can still jump this many seconds after falling off a ledge
+export var coyote_time : float = 0.1
+# Only neccessary when can_hold_jump is off
+# Pressing jump this many seconds before hitting the ground will still make you jump
+export var jump_buffer : float = 0.1
+
+
+# not used
+var max_speed = 100
+var acceleration_time = 10
+
+
+# These will be calcualted automatically
+var default_gravity : float
+var jump_velocity : float
+var double_jump_velocity : float
+# Multiplies the gravity by this when we release jump
+var release_gravity_multiplier : float
+
+
+var jumps_left : int
+var holding_jump := false
+
+var vel = Vector2()
+var acc = Vector2()
+
+onready var coyote_timer = Timer.new()
+onready var jump_buffer_timer = Timer.new()
+
+
+func _init():
+ default_gravity = calculate_gravity(max_jump_height, jump_duration)
+ jump_velocity = calculate_jump_velocity(max_jump_height, jump_duration)
+ double_jump_velocity = calculate_jump_velocity2(double_jump_height, default_gravity)
+ release_gravity_multiplier = calculate_release_gravity_multiplier(
+ jump_velocity, min_jump_height, default_gravity)
+
+
+func _ready():
+ add_child(coyote_timer)
+ coyote_timer.wait_time = coyote_time
+ coyote_timer.one_shot = true
+
+ add_child(jump_buffer_timer)
+ jump_buffer_timer.wait_time = jump_buffer
+ jump_buffer_timer.one_shot = true
+
+
+func _physics_process(delta):
+ acc.x = 0
+
+ if is_on_floor():
+ coyote_timer.start()
+ if not coyote_timer.is_stopped():
+ jumps_left = max_jump_amount
+
+ if Input.is_action_pressed(input_left):
+ acc.x = -max_acceleration
+ if Input.is_action_pressed(input_right):
+ acc.x = max_acceleration
+
+
+ # Check for ground jumps when we can hold jump
+ if can_hold_jump:
+ if Input.is_action_pressed(input_jump):
+ # Dont use double jump when holding down
+ if is_on_floor():
+ jump()
+
+ # Check for ground jumps when we cannot hold jump
+ if not can_hold_jump:
+ if not jump_buffer_timer.is_stopped() and is_on_floor():
+ jump()
+
+ # Check for jumps in the air
+ if Input.is_action_just_pressed(input_jump):
+ holding_jump = true
+ jump_buffer_timer.start()
+
+ # Only jump in the air when press the button down, code above already jumps when we are grounded
+ if not is_on_floor():
+ jump()
+
+
+ if Input.is_action_just_released(input_jump):
+ holding_jump = false
+
+
+ var gravity = default_gravity
+
+ if vel.y > 0: # If we are falling
+ gravity *= falling_gravity_multiplier
+
+ if not holding_jump and vel.y < 0: # if we released jump and are still rising
+ if not jumps_left < max_jump_amount - 1: # Always jump to max height when we are using a double jump
+ gravity *= release_gravity_multiplier # multiply the gravity so we have a lower jump
+
+ acc.y = -gravity
+ vel.x *= 1 / (1 + (delta * friction))
+
+ vel += acc * delta
+ vel = move_and_slide(vel, Vector2.UP)
+
+
+
+func calculate_gravity(p_max_jump_height, p_jump_duration):
+ # Calculates the desired gravity by looking at our jump height and jump duration
+ # Formula is from this video https://www.youtube.com/watch?v=hG9SzQxaCm8
+ return (-2 *p_max_jump_height) / pow(p_jump_duration, 2)
+
+
+func calculate_jump_velocity(p_max_jump_height, p_jump_duration):
+ # Calculates the desired jump velocity by lookihg at our jump height and jump duration
+ return (2 * p_max_jump_height) / (p_jump_duration)
+
+
+func calculate_jump_velocity2(p_max_jump_height, p_gravity):
+ # Calculates jump velocity from jump height and gravity
+ # formula from
+ # https://sciencing.com/acceleration-velocity-distance-7779124.html#:~:text=in%20every%20step.-,Starting%20from%3A,-v%5E2%3Du
+ return sqrt(-2 * p_gravity * p_max_jump_height)
+
+
+func calculate_release_gravity_multiplier(p_jump_velocity, p_min_jump_height, p_gravity):
+ # Calculates the gravity when the key is released based on the minimum jump height and jump velocity
+ # Formula is from this website https://sciencing.com/acceleration-velocity-distance-7779124.html
+ var release_gravity = 0 - pow(p_jump_velocity, 2) / (2 * p_min_jump_height)
+ return release_gravity / p_gravity
+
+
+func calculate_friction(time_to_max):
+ # Formula from https://www.reddit.com/r/gamedev/comments/bdbery/comment/ekxw9g4/?utm_source=share&utm_medium=web2x&context=3
+ # this friction will hit 90% of max speed after the accel time
+ return 1 - (2.30259 / time_to_max)
+
+
+func calculate_speed(p_max_speed, p_friction):
+ # Formula from https://www.reddit.com/r/gamedev/comments/bdbery/comment/ekxw9g4/?utm_source=share&utm_medium=web2x&context=3
+ return (p_max_speed / p_friction) - p_max_speed
+
+
+func jump():
+ if jumps_left == max_jump_amount and coyote_timer.is_stopped():
+ # Your first jump must be used when on the ground
+ # If you fall off the ground and then jump you will be using you second jump
+ jumps_left -= 1
+
+ if jumps_left > 0:
+ if jumps_left < max_jump_amount: # If we are double jumping
+ vel.y = -double_jump_velocity
+ else:
+ vel.y = -jump_velocity
+ jumps_left -= 1
+
+
+ coyote_timer.stop()
+
+
+func set_max_jump_height(value):
+ max_jump_height = value
+
+ default_gravity = calculate_gravity(max_jump_height, jump_duration)
+ jump_velocity = calculate_jump_velocity(max_jump_height, jump_duration)
+ double_jump_velocity = calculate_jump_velocity2(double_jump_height, default_gravity)
+ release_gravity_multiplier = calculate_release_gravity_multiplier(
+ jump_velocity, min_jump_height, default_gravity)
+
+
+func set_jump_duration(value):
+ jump_duration = value
+
+ default_gravity = calculate_gravity(max_jump_height, jump_duration)
+ jump_velocity = calculate_jump_velocity(max_jump_height, jump_duration)
+ double_jump_velocity = calculate_jump_velocity2(double_jump_height, default_gravity)
+ release_gravity_multiplier = calculate_release_gravity_multiplier(
+ jump_velocity, min_jump_height, default_gravity)
+
+
+func set_min_jump_height(value):
+ min_jump_height = value
+ release_gravity_multiplier = calculate_release_gravity_multiplier(
+ jump_velocity, min_jump_height, default_gravity)
+
+
+func set_double_jump_height(value):
+ double_jump_height = value
+ double_jump_velocity = calculate_jump_velocity2(double_jump_height, default_gravity)
+
+
diff --git a/player/Player.tscn b/player/Player.tscn
index 985f5dc..163732c 100644
--- a/player/Player.tscn
+++ b/player/Player.tscn
@@ -1,7 +1,7 @@
[gd_scene load_steps=12 format=2]
[ext_resource path="res://assets/images/Clean-Retro-Lines-Player-Plain.png" type="Texture" id=1]
-[ext_resource path="res://player/Player.gd" type="Script" id=2]
+[ext_resource path="res://addons/platformer_controller/platformer_controller.gd" type="Script" id=2]
[sub_resource type="AtlasTexture" id=5]
atlas = ExtResource( 1 )
@@ -11,10 +11,6 @@ region = Rect2( 0, 16, 16, 16 )
atlas = ExtResource( 1 )
region = Rect2( 16, 16, 16, 16 )
-[sub_resource type="AtlasTexture" id=7]
-atlas = ExtResource( 1 )
-region = Rect2( 16, 32, 16, 16 )
-
[sub_resource type="AtlasTexture" id=1]
atlas = ExtResource( 1 )
region = Rect2( 0, 0, 16, 16 )
@@ -31,6 +27,10 @@ region = Rect2( 32, 0, 16, 16 )
atlas = ExtResource( 1 )
region = Rect2( 48, 0, 16, 16 )
+[sub_resource type="AtlasTexture" id=7]
+atlas = ExtResource( 1 )
+region = Rect2( 16, 32, 16, 16 )
+
[sub_resource type="SpriteFrames" id=8]
animations = [ {
"frames": [ SubResource( 5 ), SubResource( 6 ) ],
@@ -38,15 +38,15 @@ animations = [ {
"name": "idle",
"speed": 2.0
}, {
-"frames": [ SubResource( 7 ) ],
-"loop": true,
-"name": "jump",
-"speed": 5.0
-}, {
"frames": [ SubResource( 1 ), SubResource( 2 ), SubResource( 3 ), SubResource( 4 ) ],
"loop": true,
"name": "walk",
"speed": 5.0
+}, {
+"frames": [ SubResource( 7 ) ],
+"loop": true,
+"name": "jump",
+"speed": 5.0
} ]
[sub_resource type="RectangleShape2D" id=9]
@@ -54,10 +54,16 @@ extents = Vector2( 4, 7 )
[node name="Player" type="KinematicBody2D"]
script = ExtResource( 2 )
+max_jump_height = 50
+min_jump_height = 25
+double_jump_height = 65
+max_jump_amount = 2
+max_acceleration = 1500
[node name="AnimatedSprite" type="AnimatedSprite" parent="."]
frames = SubResource( 8 )
animation = "idle"
+frame = 1
playing = true
[node name="CollisionShape2D" type="CollisionShape2D" parent="."]
diff --git a/project.godot b/project.godot
index b198fd0..44df75b 100644
--- a/project.godot
+++ b/project.godot
@@ -8,6 +8,34 @@
config_version=4
+_global_script_classes=[ {
+"base": "Reference",
+"class": "Mod",
+"language": "GDScript",
+"path": "res://addons/modplayer/Mod.gd"
+}, {
+"base": "Node",
+"class": "ModPlayer",
+"language": "GDScript",
+"path": "res://addons/modplayer/ModPlayer.gd"
+}, {
+"base": "KinematicBody2D",
+"class": "PlatformerController2D",
+"language": "GDScript",
+"path": "res://addons/platformer_controller/platformer_controller.gd"
+}, {
+"base": "Reference",
+"class": "XM",
+"language": "GDScript",
+"path": "res://addons/modplayer/XM.gd"
+} ]
+_global_script_class_icons={
+"Mod": "",
+"ModPlayer": "res://addons/modplayer/icon.png",
+"PlatformerController2D": "",
+"XM": ""
+}
+
[application]
config/name="Retro Platformer"
@@ -15,6 +43,7 @@ run/main_scene="res://levels/Level01.tscn"
boot_splash/image="res://assets/images/WithinLogo.png"
boot_splash/fullsize=false
boot_splash/use_filter=false
+boot_splash/bg_color=Color( 0.141176, 0.141176, 0.141176, 1 )
config/icon="res://icon.png"
[autoload]