feat: add KineticLyrics 3D lyrics rendering system
|
|
@ -0,0 +1,28 @@
|
||||||
|
{
|
||||||
|
"enabled": true,
|
||||||
|
"layout": {
|
||||||
|
"pos": [
|
||||||
|
16.0,
|
||||||
|
212.0
|
||||||
|
],
|
||||||
|
"space": "logical",
|
||||||
|
"parent": "keybinds",
|
||||||
|
"side": "RIGHT",
|
||||||
|
"pox": 0.0,
|
||||||
|
"poy": 0.0
|
||||||
|
},
|
||||||
|
"admins_scale": 1.0,
|
||||||
|
"admins_layout": "Split Header",
|
||||||
|
"admins_color_mode": "Theme",
|
||||||
|
"admins_bg": "#EB111318",
|
||||||
|
"admins_bg_secondary": "#F7161616",
|
||||||
|
"admins_stroke": "#5A5A5A",
|
||||||
|
"admins_bg_alpha": 131.0,
|
||||||
|
"admins_text": "#FFFFFF",
|
||||||
|
"admins_muted": "#A5A5A5",
|
||||||
|
"admins_bg_effect": "Blur",
|
||||||
|
"admins_blur_alpha": 255.0,
|
||||||
|
"admins_header_icon_pulse": true,
|
||||||
|
"admins_header_icon_pulse_speed": 18.0,
|
||||||
|
"admins_header_icon_pulse_intensity": 100.0
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,17 @@
|
||||||
|
{
|
||||||
|
"enabled": false,
|
||||||
|
"layout": {
|
||||||
|
"pos": [
|
||||||
|
1616.315,
|
||||||
|
1003.6977
|
||||||
|
],
|
||||||
|
"space": "logical"
|
||||||
|
},
|
||||||
|
"armor_scale": 3.68,
|
||||||
|
"armor_layout": "Horizontal",
|
||||||
|
"armor_reverse": false,
|
||||||
|
"armor_state_mode": "Text",
|
||||||
|
"armor_state_bar_threshold": 70.0,
|
||||||
|
"armor_state_threshold": 70.0,
|
||||||
|
"armor_state_color_threshold": 70.0
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,27 @@
|
||||||
|
{
|
||||||
|
"enabled": true,
|
||||||
|
"layout": {
|
||||||
|
"pos": [
|
||||||
|
16.0,
|
||||||
|
485.12
|
||||||
|
],
|
||||||
|
"space": "logical"
|
||||||
|
},
|
||||||
|
"width_ratio": 0.54,
|
||||||
|
"font_scale": 1.36,
|
||||||
|
"lines": 12.0,
|
||||||
|
"fade_seconds": 14.98,
|
||||||
|
"liquid_glass_alpha": 230.0,
|
||||||
|
"hide_vanilla": true,
|
||||||
|
"history_enabled": true,
|
||||||
|
"history_limit": 32000.0,
|
||||||
|
"timestamps_enabled": true,
|
||||||
|
"timestamp_color": "#C6D2CD",
|
||||||
|
"timestamp_toggles": {
|
||||||
|
"with_seconds": false,
|
||||||
|
"with_brackets": true,
|
||||||
|
"active_only": false,
|
||||||
|
"hover_unix": false,
|
||||||
|
"hover_date": true
|
||||||
|
}
|
||||||
|
}
|
||||||
18
combatant-client-26.2/config/lovisual/hud/draggable/fps.json
Normal file
|
|
@ -0,0 +1,18 @@
|
||||||
|
{
|
||||||
|
"enabled": true,
|
||||||
|
"fps_scale": 2.37,
|
||||||
|
"fps_color_mode": "Theme",
|
||||||
|
"fps_icon_color": "#FFFFFF",
|
||||||
|
"fps_value_color": "#FFFFFF",
|
||||||
|
"fps_meta_color": "#9B9B9B",
|
||||||
|
"fps_bg_effect": "Blur",
|
||||||
|
"fps_bg": "#F7343434",
|
||||||
|
"fps_bg_secondary": "#F7161616",
|
||||||
|
"fps_stroke": "#5A5A5A",
|
||||||
|
"fps_bg_alpha": 133.0,
|
||||||
|
"fps_blur_alpha": 255.0,
|
||||||
|
"fps_label_effect": "Flow",
|
||||||
|
"fps_label_effect_speed": 18.0,
|
||||||
|
"fps_digit_animation": false,
|
||||||
|
"fps_digit_poll_ms": 495.0
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,16 @@
|
||||||
|
{
|
||||||
|
"enabled": false,
|
||||||
|
"game_time_weather": true,
|
||||||
|
"game_time_scale": 2.37,
|
||||||
|
"game_time_color_mode": "Theme",
|
||||||
|
"game_time_icon_color": "#FFFFFF",
|
||||||
|
"game_time_value_color": "#FFFFFF",
|
||||||
|
"game_time_bg_effect": "None",
|
||||||
|
"game_time_bg": "#EB111318",
|
||||||
|
"game_time_bg_secondary": "#F7161616",
|
||||||
|
"game_time_stroke": "#5A5A5A",
|
||||||
|
"game_time_bg_alpha": 235.0,
|
||||||
|
"game_time_blur_alpha": 140.0,
|
||||||
|
"game_time_icon_effect": "Mix",
|
||||||
|
"game_time_icon_effect_speed": 18.0
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,9 @@
|
||||||
|
{
|
||||||
|
"enabled": true,
|
||||||
|
"duration_ms": 2000.0,
|
||||||
|
"fade_ms": 250.0,
|
||||||
|
"scale": 1.35,
|
||||||
|
"sound": true,
|
||||||
|
"sound_mode": "2",
|
||||||
|
"sound_volume": 0.47
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,25 @@
|
||||||
|
{
|
||||||
|
"enabled": true,
|
||||||
|
"layout": {
|
||||||
|
"pos": [
|
||||||
|
1255.352,
|
||||||
|
658.36005
|
||||||
|
],
|
||||||
|
"space": "logical"
|
||||||
|
},
|
||||||
|
"toggle_bind": "I",
|
||||||
|
"inventory_scale": 1.32,
|
||||||
|
"inventory_layout": "Unified Divider",
|
||||||
|
"inventory_color_mode": "Theme",
|
||||||
|
"inventory_bg": "#F7343434",
|
||||||
|
"inventory_bg_secondary": "#F7161616",
|
||||||
|
"inventory_stroke": "#5A5A5A",
|
||||||
|
"inventory_bg_alpha": 194.0,
|
||||||
|
"inventory_text": "#FFFFFF",
|
||||||
|
"inventory_muted": "#A5A5A5",
|
||||||
|
"inventory_bg_effect": "Blur",
|
||||||
|
"inventory_blur_alpha": 255.0,
|
||||||
|
"inventory_header_icon_pulse": true,
|
||||||
|
"inventory_header_icon_pulse_speed": 15.0,
|
||||||
|
"inventory_header_icon_pulse_intensity": 100.0
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,22 @@
|
||||||
|
{
|
||||||
|
"enabled": true,
|
||||||
|
"layout": {
|
||||||
|
"pos": [
|
||||||
|
-16.0,
|
||||||
|
48.0
|
||||||
|
],
|
||||||
|
"space": "logical",
|
||||||
|
"ax": "CENTER",
|
||||||
|
"ay": "CENTER"
|
||||||
|
},
|
||||||
|
"itemizer_scale": 1.0,
|
||||||
|
"itemizer_display_mode": "icons",
|
||||||
|
"itemizer_direction": "horizontal",
|
||||||
|
"itemizer_duration": 1.15,
|
||||||
|
"itemizer_max_entries": 3.0,
|
||||||
|
"itemizer_auto_eat": true,
|
||||||
|
"itemizer_auto_totem": true,
|
||||||
|
"itemizer_elytra_swap": true,
|
||||||
|
"itemizer_blur": true,
|
||||||
|
"itemizer_bg_alpha": 168.0
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,33 @@
|
||||||
|
{
|
||||||
|
"enabled": true,
|
||||||
|
"layout": {
|
||||||
|
"pos": [
|
||||||
|
456.6228,
|
||||||
|
248.0
|
||||||
|
],
|
||||||
|
"space": "logical",
|
||||||
|
"parent": "potions",
|
||||||
|
"side": "RIGHT",
|
||||||
|
"pox": 0.0,
|
||||||
|
"poy": 0.0
|
||||||
|
},
|
||||||
|
"keybinds_scale": 1.0,
|
||||||
|
"keybinds_layout": "Unified Divider",
|
||||||
|
"keybinds_color_mode": "Theme",
|
||||||
|
"keybinds_bg": "#F7343434",
|
||||||
|
"keybinds_bg_secondary": "#F7161616",
|
||||||
|
"keybinds_stroke": "#5A5A5A",
|
||||||
|
"keybinds_bg_alpha": 225.0,
|
||||||
|
"keybinds_text": "#FFFFFF",
|
||||||
|
"keybinds_muted": "#A5A5A5",
|
||||||
|
"keybinds_bg_effect": "None",
|
||||||
|
"keybinds_blur_alpha": 140.0,
|
||||||
|
"keybinds_local_actions": true,
|
||||||
|
"keybinds_local_action_modes": {
|
||||||
|
"Press": true,
|
||||||
|
"Hold": true
|
||||||
|
},
|
||||||
|
"keybinds_header_icon_pulse": true,
|
||||||
|
"keybinds_header_icon_pulse_speed": 18.0,
|
||||||
|
"keybinds_header_icon_pulse_intensity": 100.0
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,19 @@
|
||||||
|
{
|
||||||
|
"enabled": false,
|
||||||
|
"layout": {
|
||||||
|
"pos": [
|
||||||
|
12.0,
|
||||||
|
82.30222
|
||||||
|
],
|
||||||
|
"space": "logical"
|
||||||
|
},
|
||||||
|
"media_player_scale": 1.51,
|
||||||
|
"media_player_sync_theme": true,
|
||||||
|
"media_player_bg": "#60000000",
|
||||||
|
"media_player_bg_alpha": 124.0,
|
||||||
|
"media_player_accent": "#FF2F2F",
|
||||||
|
"media_player_text": "#FFFFFF",
|
||||||
|
"media_player_bg_effect": "None",
|
||||||
|
"media_player_blur_alpha": 255.0,
|
||||||
|
"media_player_gradient": true
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,17 @@
|
||||||
|
{
|
||||||
|
"enabled": true,
|
||||||
|
"memory_scale": 2.37,
|
||||||
|
"memory_color_mode": "Theme",
|
||||||
|
"memory_icon_color": "#FFFFFF",
|
||||||
|
"memory_value_color": "#FFFFFF",
|
||||||
|
"memory_meta_color": "#9B9B9B",
|
||||||
|
"memory_extra_color": "#BEBEBE",
|
||||||
|
"memory_bg_effect": "Blur",
|
||||||
|
"memory_bg": "#F7343434",
|
||||||
|
"memory_bg_secondary": "#F7161616",
|
||||||
|
"memory_stroke": "#5A5A5A",
|
||||||
|
"memory_bg_alpha": 135.0,
|
||||||
|
"memory_blur_alpha": 255.0,
|
||||||
|
"memory_label_effect": "None",
|
||||||
|
"memory_label_effect_speed": 18.0
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,16 @@
|
||||||
|
{
|
||||||
|
"enabled": false,
|
||||||
|
"module_list_scale": 3.67,
|
||||||
|
"module_list_color_mode": "Custom",
|
||||||
|
"module_list_bg_effect": "Blur",
|
||||||
|
"module_list_bg_alpha": 226.0,
|
||||||
|
"module_list_blur_alpha": 255.0,
|
||||||
|
"module_list_bg": "#FF444242",
|
||||||
|
"module_list_bg_secondary": "#22000000",
|
||||||
|
"module_list_text": "RNB#E87171",
|
||||||
|
"module_list_text_secondary": "#93118C",
|
||||||
|
"module_list_text_gradient_mode": "rainbow",
|
||||||
|
"module_list_text_gradient_speed": 18.0,
|
||||||
|
"module_list_text_effect": "None",
|
||||||
|
"module_list_text_effect_speed": 30.0
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,16 @@
|
||||||
|
{
|
||||||
|
"enabled": false,
|
||||||
|
"ping_scale": 2.58,
|
||||||
|
"ping_color_mode": "Theme",
|
||||||
|
"ping_icon_color": "#FFFFFF",
|
||||||
|
"ping_value_color": "#FFFFFF",
|
||||||
|
"ping_meta_color": "#9B9B9B",
|
||||||
|
"ping_bg_effect": "None",
|
||||||
|
"ping_bg": "#F7343434",
|
||||||
|
"ping_bg_secondary": "#F7161616",
|
||||||
|
"ping_stroke": "#5A5A5A",
|
||||||
|
"ping_bg_alpha": 225.0,
|
||||||
|
"ping_blur_alpha": 140.0,
|
||||||
|
"ping_label_effect": "None",
|
||||||
|
"ping_label_effect_speed": 18.0
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,24 @@
|
||||||
|
{
|
||||||
|
"enabled": true,
|
||||||
|
"layout": {
|
||||||
|
"pos": [
|
||||||
|
31.729004,
|
||||||
|
182.0
|
||||||
|
],
|
||||||
|
"space": "logical"
|
||||||
|
},
|
||||||
|
"potions_scale": 1.05,
|
||||||
|
"potions_layout": "Unified Divider",
|
||||||
|
"potions_color_mode": "Theme",
|
||||||
|
"potions_bg": "#EB784F4F",
|
||||||
|
"potions_bg_secondary": "#F7161616",
|
||||||
|
"potions_stroke": "#5A5A5A",
|
||||||
|
"potions_bg_alpha": 200.0,
|
||||||
|
"potions_text": "#FFFFFF",
|
||||||
|
"potions_muted": "#A5A5A5",
|
||||||
|
"potions_bg_effect": "Blur",
|
||||||
|
"potions_blur_alpha": 159.0,
|
||||||
|
"potions_header_icon_pulse": true,
|
||||||
|
"potions_header_icon_pulse_speed": 18.0,
|
||||||
|
"potions_header_icon_pulse_intensity": 100.0
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,24 @@
|
||||||
|
{
|
||||||
|
"enabled": false,
|
||||||
|
"layout": {
|
||||||
|
"pos": [
|
||||||
|
1666.0,
|
||||||
|
17.915222
|
||||||
|
],
|
||||||
|
"space": "logical"
|
||||||
|
},
|
||||||
|
"radar_mode": "Rect",
|
||||||
|
"radar_color_mode": "Sync",
|
||||||
|
"radar_size": 217.27,
|
||||||
|
"radar_bg_color": "#CC101010",
|
||||||
|
"radar_bg_alpha": 109.0,
|
||||||
|
"radar_text_color": "#FFFFFF",
|
||||||
|
"radar_field_1": "Name",
|
||||||
|
"radar_field_2": "Hp",
|
||||||
|
"radar_field_3": "Ping",
|
||||||
|
"radar_field_4": "None",
|
||||||
|
"radar_field_5": "None",
|
||||||
|
"radar_blur": true,
|
||||||
|
"radar_blur_alpha": 255.0,
|
||||||
|
"radar_gradient": false
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,31 @@
|
||||||
|
{
|
||||||
|
"enabled": false,
|
||||||
|
"layout": {
|
||||||
|
"pos": [
|
||||||
|
1721.69,
|
||||||
|
158.0
|
||||||
|
],
|
||||||
|
"space": "logical"
|
||||||
|
},
|
||||||
|
"toggle_bind": "Э",
|
||||||
|
"scoreboard_scale": 1.93,
|
||||||
|
"scoreboard_color_mode": "Theme",
|
||||||
|
"scoreboard_bg_effect": "Blur",
|
||||||
|
"scoreboard_bg_alpha": 182.0,
|
||||||
|
"scoreboard_blur_alpha": 255.0,
|
||||||
|
"scoreboard_bg": "#820A0A0A",
|
||||||
|
"scoreboard_bg_secondary": "#B8141414",
|
||||||
|
"scoreboard_stroke": "#4A4A4A",
|
||||||
|
"scoreboard_title_color": "#FFCC00",
|
||||||
|
"scoreboard_label_color": "#FFFFFF",
|
||||||
|
"scoreboard_value_color": "#FF4A4A",
|
||||||
|
"scoreboard_footer_color": "#FFD44A",
|
||||||
|
"scoreboard_text_effect": "Stripe",
|
||||||
|
"scoreboard_text_effect_speed": 18.0,
|
||||||
|
"scoreboard_text_effect_sections": {
|
||||||
|
"header": true,
|
||||||
|
"labels": false,
|
||||||
|
"values": false,
|
||||||
|
"footer": false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,18 @@
|
||||||
|
{
|
||||||
|
"enabled": true,
|
||||||
|
"speed_bps_bps": true,
|
||||||
|
"speed_bps_average": false,
|
||||||
|
"speed_bps_scale": 2.37,
|
||||||
|
"speed_bps_color_mode": "Theme",
|
||||||
|
"speed_bps_icon_color": "#FFFFFF",
|
||||||
|
"speed_bps_value_color": "#FFFFFF",
|
||||||
|
"speed_bps_meta_color": "#9B9B9B",
|
||||||
|
"speed_bps_bg_effect": "Blur",
|
||||||
|
"speed_bps_bg": "#F7343434",
|
||||||
|
"speed_bps_bg_secondary": "#F7161616",
|
||||||
|
"speed_bps_stroke": "#5A5A5A",
|
||||||
|
"speed_bps_bg_alpha": 135.0,
|
||||||
|
"speed_bps_blur_alpha": 255.0,
|
||||||
|
"speed_bps_label_effect": "Mix",
|
||||||
|
"speed_bps_label_effect_speed": 60.0
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,16 @@
|
||||||
|
{
|
||||||
|
"enabled": false,
|
||||||
|
"system_time_seconds": false,
|
||||||
|
"system_time_scale": 2.37,
|
||||||
|
"system_time_color_mode": "Theme",
|
||||||
|
"system_time_icon_color": "#FFFFFF",
|
||||||
|
"system_time_value_color": "#FFFFFF",
|
||||||
|
"system_time_bg_effect": "Blur",
|
||||||
|
"system_time_bg": "#EB111318",
|
||||||
|
"system_time_bg_secondary": "#F7161616",
|
||||||
|
"system_time_stroke": "#5A5A5A",
|
||||||
|
"system_time_bg_alpha": 235.0,
|
||||||
|
"system_time_blur_alpha": 140.0,
|
||||||
|
"system_time_icon_effect": "None",
|
||||||
|
"system_time_icon_effect_speed": 18.0
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,41 @@
|
||||||
|
{
|
||||||
|
"enabled": true,
|
||||||
|
"layout": {
|
||||||
|
"pos": [
|
||||||
|
7.6799927,
|
||||||
|
721.12
|
||||||
|
],
|
||||||
|
"space": "logical",
|
||||||
|
"ax": "CENTER"
|
||||||
|
},
|
||||||
|
"target_scale": 3.28,
|
||||||
|
"target_hold_ms": 1000.0,
|
||||||
|
"totem_pop_counter": true,
|
||||||
|
"totem_pop_reset_seconds": 300.0,
|
||||||
|
"target_totem_pop_text_color": "RNB#FFFFFF",
|
||||||
|
"target_marquee": true,
|
||||||
|
"target_players_only": false,
|
||||||
|
"target_effects_mode": "Off",
|
||||||
|
"target_effects_self": true,
|
||||||
|
"target_equipment_mode": "Off",
|
||||||
|
"target_hp_mode": "Arc",
|
||||||
|
"target_color_mode": "Theme",
|
||||||
|
"target_bg": "#F7343434",
|
||||||
|
"target_bg_secondary": "#F7161616",
|
||||||
|
"target_stroke": "#5A5A5A",
|
||||||
|
"target_bg_alpha": 162.0,
|
||||||
|
"target_text": "#FFFFFF",
|
||||||
|
"target_text_secondary": "#D7D7D7",
|
||||||
|
"target_bar_bg": "#1E1E1E",
|
||||||
|
"target_health_start": "#F3F3F3",
|
||||||
|
"target_health_end": "#9E9E9E",
|
||||||
|
"target_trail": "#373737",
|
||||||
|
"target_absorption_start": "#FFC300",
|
||||||
|
"target_absorption_end": "#FFD96B",
|
||||||
|
"target_hit_outer": "#FF5A5A",
|
||||||
|
"target_hit_inner": "#FF7373",
|
||||||
|
"target_bg_effect": "Glass",
|
||||||
|
"target_blur_alpha": 255.0,
|
||||||
|
"target_hit_anim": true,
|
||||||
|
"target_hit_pulse": 1.0
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,21 @@
|
||||||
|
{
|
||||||
|
"enabled": false,
|
||||||
|
"layout": {
|
||||||
|
"pos": [
|
||||||
|
1242.2065,
|
||||||
|
889.5111
|
||||||
|
],
|
||||||
|
"space": "logical"
|
||||||
|
},
|
||||||
|
"scale": 1.0,
|
||||||
|
"always_show": true,
|
||||||
|
"sync_theme": true,
|
||||||
|
"blur": false,
|
||||||
|
"blur_alpha": 140.0,
|
||||||
|
"blur_bg_alpha": 128.0,
|
||||||
|
"gradient": true,
|
||||||
|
"bg": "#B012151B",
|
||||||
|
"bar_start": "#5CC8E7",
|
||||||
|
"bar_end": "#55FFFF",
|
||||||
|
"text_color": "#FFFFFF"
|
||||||
|
}
|
||||||
18
combatant-client-26.2/config/lovisual/hud/draggable/tps.json
Normal file
|
|
@ -0,0 +1,18 @@
|
||||||
|
{
|
||||||
|
"enabled": false,
|
||||||
|
"tps_extra": false,
|
||||||
|
"tps_scale": 2.37,
|
||||||
|
"tps_color_mode": "Theme",
|
||||||
|
"tps_icon_color": "#FFFFFF",
|
||||||
|
"tps_value_color": "#FFFFFF",
|
||||||
|
"tps_meta_color": "#9B9B9B",
|
||||||
|
"tps_extra_color": "#BEBEBE",
|
||||||
|
"tps_bg_effect": "Blur",
|
||||||
|
"tps_bg": "#F7343434",
|
||||||
|
"tps_bg_secondary": "#F7161616",
|
||||||
|
"tps_stroke": "#5A5A5A",
|
||||||
|
"tps_bg_alpha": 133.0,
|
||||||
|
"tps_blur_alpha": 255.0,
|
||||||
|
"tps_label_effect": "None",
|
||||||
|
"tps_label_effect_speed": 18.0
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,29 @@
|
||||||
|
{
|
||||||
|
"enabled": false,
|
||||||
|
"layout": {
|
||||||
|
"pos": [
|
||||||
|
1302.04,
|
||||||
|
330.69208
|
||||||
|
],
|
||||||
|
"space": "logical"
|
||||||
|
},
|
||||||
|
"triangulator_scale": 1.68,
|
||||||
|
"triangulator_color_mode": "Theme",
|
||||||
|
"triangulator_bg": "#F7343434",
|
||||||
|
"triangulator_bg_secondary": "#F7161616",
|
||||||
|
"triangulator_stroke": "#5A5A5A",
|
||||||
|
"triangulator_bg_alpha": 225.0,
|
||||||
|
"triangulator_text": "#FFFFFF",
|
||||||
|
"triangulator_muted": "#A5A5A5",
|
||||||
|
"triangulator_bg_effect": "Blur",
|
||||||
|
"triangulator_blur_alpha": 140.0,
|
||||||
|
"triangulator_icon_effect": "None",
|
||||||
|
"triangulator_icon_effect_speed": 18.0,
|
||||||
|
"triangulator_min_observation_ticks": 6.0,
|
||||||
|
"triangulator_stability_threshold": 0.99,
|
||||||
|
"triangulator_min_pair_angle": 6.0,
|
||||||
|
"triangulator_auto_reset": true,
|
||||||
|
"triangulator_auto_reset_distance": 96.0,
|
||||||
|
"triangulator_max_cached_throws": 6.0,
|
||||||
|
"triangulator_cache_lifetime": 180.0
|
||||||
|
}
|
||||||
18
combatant-client-26.2/config/lovisual/hud/draggable/xyz.json
Normal file
|
|
@ -0,0 +1,18 @@
|
||||||
|
{
|
||||||
|
"enabled": true,
|
||||||
|
"xyz_nether": "On",
|
||||||
|
"xyz_scale": 2.37,
|
||||||
|
"xyz_color_mode": "Theme",
|
||||||
|
"xyz_icon_color": "#FFFFFF",
|
||||||
|
"xyz_label_color": "#9B9B9B",
|
||||||
|
"xyz_value_color": "#FFFFFF",
|
||||||
|
"xyz_extra_color": "#BEBEBE",
|
||||||
|
"xyz_bg_effect": "Blur",
|
||||||
|
"xyz_bg": "#F7343434",
|
||||||
|
"xyz_bg_secondary": "#F7161616",
|
||||||
|
"xyz_stroke": "#5A5A5A",
|
||||||
|
"xyz_bg_alpha": 184.0,
|
||||||
|
"xyz_blur_alpha": 255.0,
|
||||||
|
"xyz_label_effect": "Flow",
|
||||||
|
"xyz_label_effect_speed": 18.0
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,11 @@
|
||||||
|
{
|
||||||
|
"enabled": true,
|
||||||
|
"dynamic_island_sync_theme": true,
|
||||||
|
"dynamic_island_theme_alpha": 200.0,
|
||||||
|
"dynamic_island_blur": false,
|
||||||
|
"dynamic_island_blur_alpha": 120.0,
|
||||||
|
"dynamic_island_bg": "#E614171D",
|
||||||
|
"dynamic_island_accent": "#6E8DFF",
|
||||||
|
"dynamic_island_text": "#F5F7FA",
|
||||||
|
"dynamic_island_muted": "#A9B1BC"
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,7 @@
|
||||||
|
{
|
||||||
|
"enabled": true,
|
||||||
|
"swap_scale": 0.24,
|
||||||
|
"swap_offset_y": 66.6,
|
||||||
|
"swap_text_effect": "Mix",
|
||||||
|
"swap_text_effect_speed": 18.0
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,10 @@
|
||||||
|
{
|
||||||
|
"enabled": true,
|
||||||
|
"tab_list_max_width": 1680.0,
|
||||||
|
"tab_list_sync_theme": true,
|
||||||
|
"tab_list_theme_alpha": 225.0,
|
||||||
|
"tab_list_bg": "#E50B1018",
|
||||||
|
"tab_list_accent": "#6E8DFF",
|
||||||
|
"tab_list_text": "#F4F7FB",
|
||||||
|
"tab_list_muted": "#9AA5B5"
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,27 @@
|
||||||
|
{
|
||||||
|
"enabled": true,
|
||||||
|
"xp_bar": true,
|
||||||
|
"xp_bar_show_level": true,
|
||||||
|
"bar_mode": "level",
|
||||||
|
"bar_color_mode": "Theme",
|
||||||
|
"custom_bg_color": "#88151515",
|
||||||
|
"custom_fill_color": "#CC4CB2FF",
|
||||||
|
"custom_fill_gradient_color": "#CC76F0FF",
|
||||||
|
"custom_text_color": "#FFA0E5FF",
|
||||||
|
"locator_labels": true,
|
||||||
|
"locator_label_mode": "center",
|
||||||
|
"locator_label_angle": 6.0,
|
||||||
|
"locator_label_distance": true,
|
||||||
|
"locator_search_enabled": true,
|
||||||
|
"locator_search_list": [
|
||||||
|
"imhorny"
|
||||||
|
],
|
||||||
|
"locator_dimmed_color": "#777777",
|
||||||
|
"locator_search_color": "#FFAA00",
|
||||||
|
"locator_dimmed_scale_only": true,
|
||||||
|
"locator_map_filter_enabled": false,
|
||||||
|
"locator_map_server": "",
|
||||||
|
"locator_map_url": "example",
|
||||||
|
"locator_target_toggle": "NONE",
|
||||||
|
"locator_bar_toggle": "U"
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,25 @@
|
||||||
|
{
|
||||||
|
"enabled": true,
|
||||||
|
"style": "Default",
|
||||||
|
"gradient_base_color": "#FF2F2929",
|
||||||
|
"gradient_color": "#FF902F2F",
|
||||||
|
"gradient_angle": 74.25,
|
||||||
|
"gradient_intensity": 0.6,
|
||||||
|
"custom_bg": "#FF1B1B1B",
|
||||||
|
"custom_bg_hover": "#FF2A2A2A",
|
||||||
|
"custom_bg_disabled": "#FF141414",
|
||||||
|
"custom_stroke": "#FF9B5DB8",
|
||||||
|
"custom_stroke_hover": "#FFA710B2",
|
||||||
|
"custom_stroke_disabled": "#FF2A2A2A",
|
||||||
|
"custom_text": "#FFECECEC",
|
||||||
|
"custom_text_disabled": "#FF7A7A7A",
|
||||||
|
"custom_radius": 6.0,
|
||||||
|
"custom_softness": 1.2,
|
||||||
|
"custom_stroke_thickness": 0.7,
|
||||||
|
"custom_gradient": true,
|
||||||
|
"custom_gradient_color": "#73521BA7",
|
||||||
|
"custom_gradient_angle": 90.0,
|
||||||
|
"custom_gradient_intensity": 0.5,
|
||||||
|
"custom_text_scale": 1.0,
|
||||||
|
"custom_text_pad_x": 6.0
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,8 @@
|
||||||
|
{
|
||||||
|
"enabled": true,
|
||||||
|
"health_alpha": 255.0,
|
||||||
|
"health_bar_style": "TargetHud",
|
||||||
|
"custom_bar_radius": 5.0,
|
||||||
|
"custom_text_scale": 0.24,
|
||||||
|
"custom_text_padding": 3.0
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,14 @@
|
||||||
|
{
|
||||||
|
"enabled": true,
|
||||||
|
"offhand_mode": "merged",
|
||||||
|
"hotbar_color_mode": "Custom",
|
||||||
|
"bg_color": "#7A191414",
|
||||||
|
"hotbar_bg_alpha": 167.0,
|
||||||
|
"custom_select_color": false,
|
||||||
|
"select_color": "#2F6277",
|
||||||
|
"custom_divider_color": false,
|
||||||
|
"divider_color": "#C13434",
|
||||||
|
"accent_alpha": 255.0,
|
||||||
|
"bg_effect": "Glass",
|
||||||
|
"blur_alpha": 255.0
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,22 @@
|
||||||
|
{
|
||||||
|
"enabled": true,
|
||||||
|
"item_tooltip": true,
|
||||||
|
"shulker_preview": true,
|
||||||
|
"item_info_colorize": true,
|
||||||
|
"tooltip_font": "Iosevka",
|
||||||
|
"tooltip_text_size": 1.0,
|
||||||
|
"top_enchant_ignore": [
|
||||||
|
"minecraft:binding_curse",
|
||||||
|
"minecraft:vanishing_curse",
|
||||||
|
"minecraft:mending",
|
||||||
|
"minecraft:infinity",
|
||||||
|
"minecraft:silk_touch",
|
||||||
|
"minecraft:aqua_affinity",
|
||||||
|
"minecraft:flame",
|
||||||
|
"minecraft:channeling",
|
||||||
|
"minecraft:multishot",
|
||||||
|
"minecraft:unbreaking"
|
||||||
|
],
|
||||||
|
"shulker_slot_size": 18.0,
|
||||||
|
"shulker_preview_hold": "LEFT_SHIFT"
|
||||||
|
}
|
||||||
30
combatant-client-26.2/config/lovisual/mainconfig.json5
Normal file
|
|
@ -0,0 +1,30 @@
|
||||||
|
{
|
||||||
|
'debug': 'off',
|
||||||
|
'forcePvp': false,
|
||||||
|
'menuBg': 'off',
|
||||||
|
'menuClockShowSeconds': false,
|
||||||
|
'menuBgUseTheme': false,
|
||||||
|
'msaa3d': 'off',
|
||||||
|
'clickGuiModulesHints': true,
|
||||||
|
'clickGuiHudEditorHints': true,
|
||||||
|
'inventorySwapPolicy': 'none',
|
||||||
|
'inventorySwapScope': 'full',
|
||||||
|
'inventorySwapVisibility': 'silent',
|
||||||
|
'inventorySwapRestore': true,
|
||||||
|
'inventorySwapPreferHotbar': true,
|
||||||
|
'inventorySwapLegitWaitTicks': 2.0,
|
||||||
|
'inventorySwapStrictInventoryWaitTicks': 1.0,
|
||||||
|
'inventorySwapStrictMovementLockTicks': 2.0,
|
||||||
|
'disableNarrator': true,
|
||||||
|
'backdoorProtection': true,
|
||||||
|
'backdoorTranslationFilter': true,
|
||||||
|
'backdoorKeybindFilter': true,
|
||||||
|
'backdoorLocalHttpGuard': true,
|
||||||
|
'backdoorPackCacheIsolation': true,
|
||||||
|
'backdoorAllowLocalHttpWhenServerLocal': true,
|
||||||
|
'backdoorAllowedTranslationPacks': [
|
||||||
|
'vanilla',
|
||||||
|
],
|
||||||
|
'backdoorAllowedKeybinds': [
|
||||||
|
],
|
||||||
|
}
|
||||||
|
|
@ -60,7 +60,6 @@ import dev.loki.lovisual.util.click.AttackPressing;
|
||||||
import dev.loki.lovisual.util.combat.AntiBotTracker;
|
import dev.loki.lovisual.util.combat.AntiBotTracker;
|
||||||
import dev.loki.lovisual.util.combat.protocol.CombatProtocolHeuristics;
|
import dev.loki.lovisual.util.combat.protocol.CombatProtocolHeuristics;
|
||||||
import dev.loki.lovisual.util.combat.SprintController;
|
import dev.loki.lovisual.util.combat.SprintController;
|
||||||
import dev.loki.lovisual.util.combat.VulcanReachController;
|
|
||||||
import dev.loki.lovisual.util.entity.simulation.BoatSimulationCache;
|
import dev.loki.lovisual.util.entity.simulation.BoatSimulationCache;
|
||||||
import dev.loki.lovisual.util.media.MediaSessionService;
|
import dev.loki.lovisual.util.media.MediaSessionService;
|
||||||
import dev.loki.lovisual.util.network.BacktrackController;
|
import dev.loki.lovisual.util.network.BacktrackController;
|
||||||
|
|
@ -318,7 +317,6 @@ public class LoVisual implements ClientModInitializer {
|
||||||
Events.BUS.register(InventorySwap.INSTANCE);
|
Events.BUS.register(InventorySwap.INSTANCE);
|
||||||
Events.BUS.register(SprintController.INSTANCE);
|
Events.BUS.register(SprintController.INSTANCE);
|
||||||
Events.BUS.register(AntiBotTracker.INSTANCE);
|
Events.BUS.register(AntiBotTracker.INSTANCE);
|
||||||
Events.BUS.register(VulcanReachController.INSTANCE);
|
|
||||||
Events.BUS.register(CombatProtocolHeuristics.INSTANCE);
|
Events.BUS.register(CombatProtocolHeuristics.INSTANCE);
|
||||||
Events.BUS.register(PvpTracker.INSTANCE);
|
Events.BUS.register(PvpTracker.INSTANCE);
|
||||||
Events.BUS.register(PvpTargetTracker.INSTANCE);
|
Events.BUS.register(PvpTargetTracker.INSTANCE);
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,55 @@
|
||||||
|
package dev.loki.lovisual.features.cosmetic;
|
||||||
|
|
||||||
|
import com.mojang.blaze3d.vertex.PoseStack;
|
||||||
|
import com.mojang.math.Axis;
|
||||||
|
|
||||||
|
public final class CosmeticAttachmentTransforms {
|
||||||
|
private CosmeticAttachmentTransforms() {
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void apply(PoseStack matrices, CosmeticType type, CosmeticModel model) {
|
||||||
|
if (type == CosmeticType.PET) {
|
||||||
|
// TODO: replace with CosmeticAnimator.getPetHoverOffset() once the animator is ported.
|
||||||
|
float bob = 0f;
|
||||||
|
matrices.mulPose(Axis.YP.rotationDegrees(180f));
|
||||||
|
matrices.translate(0.45f, 2.15f + bob, 0.12f);
|
||||||
|
matrices.scale(0.55f, 0.55f, 0.55f);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (type == CosmeticType.ACCESSORY && model != null) {
|
||||||
|
String folder = model.getFolderName().toLowerCase();
|
||||||
|
if (folder.contains("witch") || folder.contains("hat")) {
|
||||||
|
matrices.translate(0f, 1.72f, 0f);
|
||||||
|
matrices.scale(0.52f, 0.52f, 0.52f);
|
||||||
|
} else if (folder.contains("wing")) {
|
||||||
|
matrices.translate(0f, 0.1f, -0.3f);
|
||||||
|
matrices.mulPose(Axis.YP.rotationDegrees(180f));
|
||||||
|
matrices.scale(0.85f, 0.85f, 0.85f);
|
||||||
|
} else if (folder.contains("wheel")) {
|
||||||
|
matrices.translate(0f, 0.55f, 0.25f);
|
||||||
|
matrices.scale(0.55f, 0.55f, 0.55f);
|
||||||
|
} else {
|
||||||
|
matrices.translate(0f, 1.3f, 0f);
|
||||||
|
matrices.scale(0.5f, 0.5f, 0.5f);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void applyPreview(PoseStack matrices, CosmeticModel model) {
|
||||||
|
if (model == null) return;
|
||||||
|
String folder = model.getFolderName().toLowerCase();
|
||||||
|
if (model.getType() == CosmeticType.PET) {
|
||||||
|
matrices.translate(6f, 22f, 1f);
|
||||||
|
matrices.scale(1.8f, 1.8f, 1.8f);
|
||||||
|
} else if (model.getType() == CosmeticType.ACCESSORY) {
|
||||||
|
if (folder.contains("witch") || folder.contains("hat")) {
|
||||||
|
matrices.translate(0f, 20f, 0f);
|
||||||
|
} else if (folder.contains("wing")) {
|
||||||
|
matrices.translate(0f, 2f, -4f);
|
||||||
|
matrices.scale(1.3f, 1.3f, 1.3f);
|
||||||
|
} else if (folder.contains("wheel")) {
|
||||||
|
matrices.translate(0f, 6f, 3f);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,223 @@
|
||||||
|
package dev.loki.lovisual.features.cosmetic;
|
||||||
|
|
||||||
|
import com.google.gson.JsonArray;
|
||||||
|
import com.google.gson.JsonElement;
|
||||||
|
import com.google.gson.JsonObject;
|
||||||
|
import org.joml.Vector3f;
|
||||||
|
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.Comparator;
|
||||||
|
import java.util.LinkedHashMap;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
|
public final class CosmeticBbAnimation {
|
||||||
|
private final String name;
|
||||||
|
private final float length;
|
||||||
|
private final boolean loop;
|
||||||
|
private final Map<String, BoneTrack> tracksByBoneUuid = new java.util.HashMap<>();
|
||||||
|
|
||||||
|
public CosmeticBbAnimation(String name, float length, boolean loop) {
|
||||||
|
this.name = name;
|
||||||
|
this.length = Math.max(length, 1f / 20f);
|
||||||
|
this.loop = loop;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getName() {
|
||||||
|
return name;
|
||||||
|
}
|
||||||
|
|
||||||
|
public float getLength() {
|
||||||
|
return length;
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean isLoop() {
|
||||||
|
return loop;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Map<String, BoneTrack> getTracks() {
|
||||||
|
return tracksByBoneUuid;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static Map<String, CosmeticBbAnimation> parseAll(JsonObject bbmodel) {
|
||||||
|
Map<String, CosmeticBbAnimation> map = new LinkedHashMap<>();
|
||||||
|
if (!bbmodel.has("animations")) return map;
|
||||||
|
|
||||||
|
for (JsonElement el : bbmodel.getAsJsonArray("animations")) {
|
||||||
|
if (!el.isJsonObject()) continue;
|
||||||
|
JsonObject obj = el.getAsJsonObject();
|
||||||
|
String name = obj.has("name") ? obj.get("name").getAsString() : "anim";
|
||||||
|
if (name.startsWith("--") || name.isBlank()) continue;
|
||||||
|
|
||||||
|
float length = obj.has("length") ? obj.get("length").getAsFloat() : 1f;
|
||||||
|
boolean loop = !obj.has("loop") || obj.get("loop").getAsBoolean();
|
||||||
|
CosmeticBbAnimation anim = new CosmeticBbAnimation(name, length, loop);
|
||||||
|
|
||||||
|
if (obj.has("animators")) {
|
||||||
|
JsonObject animators = obj.getAsJsonObject("animators");
|
||||||
|
for (Map.Entry<String, JsonElement> entry : animators.entrySet()) {
|
||||||
|
if (!entry.getValue().isJsonObject()) continue;
|
||||||
|
JsonObject boneAnim = entry.getValue().getAsJsonObject();
|
||||||
|
String boneUuid = entry.getKey();
|
||||||
|
if (boneAnim.has("uuid")) {
|
||||||
|
boneUuid = boneAnim.get("uuid").getAsString();
|
||||||
|
}
|
||||||
|
BoneTrack track = BoneTrack.parse(boneAnim);
|
||||||
|
if (!track.isEmpty()) {
|
||||||
|
anim.tracksByBoneUuid.put(boneUuid, track);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (anim.tracksByBoneUuid.isEmpty()) continue;
|
||||||
|
map.put(name.toLowerCase(), anim);
|
||||||
|
}
|
||||||
|
return map;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void apply(CosmeticModel model, CosmeticBbAnimation animation, float time) {
|
||||||
|
java.util.Set<String> filter = model.getAnimationBoneFilter();
|
||||||
|
for (Map.Entry<String, BoneTrack> entry : animation.tracksByBoneUuid.entrySet()) {
|
||||||
|
if (filter != null && !filter.contains(entry.getKey())) continue;
|
||||||
|
CosmeticBone bone = model.getBoneByUuid(entry.getKey());
|
||||||
|
if (bone == null) continue;
|
||||||
|
|
||||||
|
BoneTrack track = entry.getValue();
|
||||||
|
Vector3f rot = track.sampleRotation(time, animation.length, animation.loop);
|
||||||
|
Vector3f pos = track.samplePosition(time, animation.length, animation.loop);
|
||||||
|
|
||||||
|
if (rot != null) {
|
||||||
|
bone.setAnimRotation(new Vector3f(
|
||||||
|
rot.x - bone.getDefaultRotation().x,
|
||||||
|
rot.y - bone.getDefaultRotation().y,
|
||||||
|
rot.z - bone.getDefaultRotation().z
|
||||||
|
));
|
||||||
|
}
|
||||||
|
if (pos != null) {
|
||||||
|
bone.setAnimPosition(pos);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public static final class BoneTrack {
|
||||||
|
private final List<Keyframe> rotations = new ArrayList<>();
|
||||||
|
private final List<Keyframe> positions = new ArrayList<>();
|
||||||
|
|
||||||
|
static BoneTrack parse(JsonObject boneAnim) {
|
||||||
|
BoneTrack track = new BoneTrack();
|
||||||
|
if (!boneAnim.has("keyframes")) return track;
|
||||||
|
|
||||||
|
for (JsonElement el : boneAnim.getAsJsonArray("keyframes")) {
|
||||||
|
if (!el.isJsonObject()) continue;
|
||||||
|
JsonObject kf = el.getAsJsonObject();
|
||||||
|
String channel = kf.has("channel") ? kf.get("channel").getAsString() : "";
|
||||||
|
float t = kf.has("time") ? kf.get("time").getAsFloat() : 0f;
|
||||||
|
Vector3f value = readPoint(kf);
|
||||||
|
String interp = kf.has("interpolation") ? kf.get("interpolation").getAsString() : "linear";
|
||||||
|
Keyframe keyframe = new Keyframe(t, value, interp);
|
||||||
|
|
||||||
|
switch (channel) {
|
||||||
|
case "rotation" -> track.rotations.add(keyframe);
|
||||||
|
case "position" -> track.positions.add(keyframe);
|
||||||
|
default -> {
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
track.rotations.sort(Comparator.comparing(Keyframe::time));
|
||||||
|
track.positions.sort(Comparator.comparing(Keyframe::time));
|
||||||
|
return track;
|
||||||
|
}
|
||||||
|
|
||||||
|
boolean isEmpty() {
|
||||||
|
return rotations.isEmpty() && positions.isEmpty();
|
||||||
|
}
|
||||||
|
|
||||||
|
Vector3f sampleRotation(float time, float length, boolean loop) {
|
||||||
|
return sample(rotations, time, length, loop);
|
||||||
|
}
|
||||||
|
|
||||||
|
Vector3f samplePosition(float time, float length, boolean loop) {
|
||||||
|
return sample(positions, time, length, loop);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static Vector3f sample(List<Keyframe> frames, float time, float length, boolean loop) {
|
||||||
|
if (frames.isEmpty()) return null;
|
||||||
|
if (frames.size() == 1) return new Vector3f(frames.get(0).value);
|
||||||
|
|
||||||
|
float t = loop ? wrapTime(time, length) : Math.min(time, length);
|
||||||
|
Keyframe prev = frames.get(0);
|
||||||
|
Keyframe next = frames.get(frames.size() - 1);
|
||||||
|
|
||||||
|
for (int i = 0; i < frames.size() - 1; i++) {
|
||||||
|
Keyframe a = frames.get(i);
|
||||||
|
Keyframe b = frames.get(i + 1);
|
||||||
|
if (t >= a.time && t <= b.time) {
|
||||||
|
prev = a;
|
||||||
|
next = b;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (t >= frames.get(frames.size() - 1).time) {
|
||||||
|
prev = frames.get(frames.size() - 1);
|
||||||
|
next = prev;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (prev == next || Math.abs(next.time - prev.time) < 0.0001f) {
|
||||||
|
return new Vector3f(prev.value);
|
||||||
|
}
|
||||||
|
|
||||||
|
float alpha = (t - prev.time) / (next.time - prev.time);
|
||||||
|
if ("step".equals(prev.interpolation)) {
|
||||||
|
return new Vector3f(prev.value);
|
||||||
|
}
|
||||||
|
return lerp(prev.value, next.value, clamp01(alpha));
|
||||||
|
}
|
||||||
|
|
||||||
|
private static float wrapTime(float time, float length) {
|
||||||
|
if (length <= 0f) return time;
|
||||||
|
float wrapped = time % length;
|
||||||
|
return wrapped < 0f ? wrapped + length : wrapped;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static float clamp01(float v) {
|
||||||
|
return Math.max(0f, Math.min(1f, v));
|
||||||
|
}
|
||||||
|
|
||||||
|
private static Vector3f lerp(Vector3f a, Vector3f b, float t) {
|
||||||
|
return new Vector3f(
|
||||||
|
a.x + (b.x - a.x) * t,
|
||||||
|
a.y + (b.y - a.y) * t,
|
||||||
|
a.z + (b.z - a.z) * t
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static Vector3f readPoint(JsonObject kf) {
|
||||||
|
if (!kf.has("data_points")) return new Vector3f();
|
||||||
|
JsonArray points = kf.getAsJsonArray("data_points");
|
||||||
|
if (points.isEmpty()) return new Vector3f();
|
||||||
|
JsonObject point = points.get(0).getAsJsonObject();
|
||||||
|
return new Vector3f(
|
||||||
|
parseNum(point, "x"),
|
||||||
|
parseNum(point, "y"),
|
||||||
|
parseNum(point, "z")
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static float parseNum(JsonObject obj, String key) {
|
||||||
|
if (!obj.has(key)) return 0f;
|
||||||
|
JsonElement el = obj.get(key);
|
||||||
|
if (el.isJsonPrimitive() && el.getAsJsonPrimitive().isNumber()) {
|
||||||
|
return el.getAsFloat();
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
return Float.parseFloat(el.getAsString().trim());
|
||||||
|
} catch (Exception ignored) {
|
||||||
|
return 0f;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private record Keyframe(float time, Vector3f value, String interpolation) {
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,66 @@
|
||||||
|
package dev.loki.lovisual.features.cosmetic;
|
||||||
|
|
||||||
|
import lombok.Getter;
|
||||||
|
import lombok.Setter;
|
||||||
|
import org.joml.Matrix4f;
|
||||||
|
import org.joml.Vector3f;
|
||||||
|
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
@Getter
|
||||||
|
public class CosmeticBone {
|
||||||
|
private final String name;
|
||||||
|
private final String uuid;
|
||||||
|
private final Vector3f pivot;
|
||||||
|
private final Vector3f defaultRotation;
|
||||||
|
private final List<CosmeticCube> cubes = new ArrayList<>();
|
||||||
|
private final List<CosmeticBone> children = new ArrayList<>();
|
||||||
|
|
||||||
|
@Setter private Vector3f animRotation = new Vector3f(0, 0, 0);
|
||||||
|
@Setter private Vector3f animPosition = new Vector3f(0, 0, 0);
|
||||||
|
|
||||||
|
public CosmeticBone(String name, String uuid, Vector3f pivot, Vector3f defaultRotation) {
|
||||||
|
this.name = name;
|
||||||
|
this.uuid = uuid;
|
||||||
|
this.pivot = pivot;
|
||||||
|
this.defaultRotation = defaultRotation;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Matrix4f buildMatrix() {
|
||||||
|
Matrix4f mat = new Matrix4f();
|
||||||
|
|
||||||
|
mat.translate(pivot.x / 16f, pivot.y / 16f, pivot.z / 16f);
|
||||||
|
|
||||||
|
if (defaultRotation.x != 0 || defaultRotation.y != 0 || defaultRotation.z != 0
|
||||||
|
|| animRotation.x != 0 || animRotation.y != 0 || animRotation.z != 0) {
|
||||||
|
float rx = (float) Math.toRadians(defaultRotation.x + animRotation.x);
|
||||||
|
float ry = (float) Math.toRadians(defaultRotation.y + animRotation.y);
|
||||||
|
float rz = (float) Math.toRadians(defaultRotation.z + animRotation.z);
|
||||||
|
mat.rotateZYX(rz, ry, rx);
|
||||||
|
}
|
||||||
|
|
||||||
|
mat.translate(-pivot.x / 16f, -pivot.y / 16f, -pivot.z / 16f);
|
||||||
|
|
||||||
|
if (animPosition.x != 0 || animPosition.y != 0 || animPosition.z != 0) {
|
||||||
|
mat.translate(animPosition.x / 16f, animPosition.y / 16f, animPosition.z / 16f);
|
||||||
|
}
|
||||||
|
|
||||||
|
return mat;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void resetAnim() {
|
||||||
|
animRotation = new Vector3f(0, 0, 0);
|
||||||
|
animPosition = new Vector3f(0, 0, 0);
|
||||||
|
for (CosmeticBone child : children) child.resetAnim();
|
||||||
|
}
|
||||||
|
|
||||||
|
public CosmeticBone deepClone() {
|
||||||
|
CosmeticBone copy = new CosmeticBone(name, uuid, new Vector3f(pivot), new Vector3f(defaultRotation));
|
||||||
|
copy.getCubes().addAll(cubes);
|
||||||
|
for (CosmeticBone child : children) {
|
||||||
|
copy.getChildren().add(child.deepClone());
|
||||||
|
}
|
||||||
|
return copy;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,42 @@
|
||||||
|
package dev.loki.lovisual.features.cosmetic;
|
||||||
|
|
||||||
|
import lombok.Getter;
|
||||||
|
import org.joml.Vector3f;
|
||||||
|
|
||||||
|
@Getter
|
||||||
|
public class CosmeticCube {
|
||||||
|
private final Vector3f from;
|
||||||
|
private final Vector3f to;
|
||||||
|
private final Vector3f rotation;
|
||||||
|
private final Vector3f origin;
|
||||||
|
private final float inflate;
|
||||||
|
private final CubeFace[] faces;
|
||||||
|
private final int textureWidth;
|
||||||
|
private final int textureHeight;
|
||||||
|
|
||||||
|
public CosmeticCube(Vector3f from, Vector3f to, Vector3f rotation, Vector3f origin,
|
||||||
|
float inflate, CubeFace[] faces, int textureWidth, int textureHeight) {
|
||||||
|
this.from = from;
|
||||||
|
this.to = to;
|
||||||
|
this.rotation = rotation;
|
||||||
|
this.origin = origin;
|
||||||
|
this.inflate = inflate;
|
||||||
|
this.faces = faces;
|
||||||
|
this.textureWidth = textureWidth;
|
||||||
|
this.textureHeight = textureHeight;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Getter
|
||||||
|
public static class CubeFace {
|
||||||
|
private final float u1, v1, u2, v2;
|
||||||
|
private final int textureIndex;
|
||||||
|
|
||||||
|
public CubeFace(float u1, float v1, float u2, float v2, int textureIndex) {
|
||||||
|
this.u1 = u1;
|
||||||
|
this.v1 = v1;
|
||||||
|
this.u2 = u2;
|
||||||
|
this.v2 = v2;
|
||||||
|
this.textureIndex = textureIndex;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,74 @@
|
||||||
|
package dev.loki.lovisual.features.cosmetic;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Holds which cosmetic (model/pet/accessory) the local user currently has equipped.
|
||||||
|
* Rendering and animation live in separate classes (not part of this core pass).
|
||||||
|
*/
|
||||||
|
public final class CosmeticManager {
|
||||||
|
private static final CosmeticManager INSTANCE = new CosmeticManager();
|
||||||
|
|
||||||
|
private CosmeticModel activeModel;
|
||||||
|
private CosmeticModel activePet;
|
||||||
|
private CosmeticModel activeAccessory;
|
||||||
|
|
||||||
|
private CosmeticManager() {
|
||||||
|
}
|
||||||
|
|
||||||
|
public static CosmeticManager get() {
|
||||||
|
return INSTANCE;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Reloads the catalog from disk, re-resolving the currently equipped cosmetics
|
||||||
|
* (by id) against the freshly loaded models.
|
||||||
|
*/
|
||||||
|
public void init() {
|
||||||
|
String modelId = activeModel != null ? activeModel.getId() : null;
|
||||||
|
String petId = activePet != null ? activePet.getId() : null;
|
||||||
|
String accessoryId = activeAccessory != null ? activeAccessory.getId() : null;
|
||||||
|
|
||||||
|
for (CosmeticModel model : CosmeticRepository.get().all()) {
|
||||||
|
model.resetTexture();
|
||||||
|
}
|
||||||
|
CosmeticRepository.get().reload();
|
||||||
|
|
||||||
|
CosmeticRepository repo = CosmeticRepository.get();
|
||||||
|
activeModel = modelId != null ? repo.get(modelId) : null;
|
||||||
|
activePet = petId != null ? repo.get(petId) : null;
|
||||||
|
activeAccessory = accessoryId != null ? repo.get(accessoryId) : null;
|
||||||
|
}
|
||||||
|
|
||||||
|
public CosmeticModel getActive(CosmeticType type) {
|
||||||
|
return switch (type) {
|
||||||
|
case PET -> activePet;
|
||||||
|
case ACCESSORY -> activeAccessory;
|
||||||
|
case MODEL -> activeModel;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setActive(CosmeticType type, CosmeticModel model) {
|
||||||
|
if (model != null) {
|
||||||
|
CosmeticModel resolved = CosmeticRepository.get().get(model.getId());
|
||||||
|
if (resolved != null) {
|
||||||
|
model = resolved;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
switch (type) {
|
||||||
|
case PET -> activePet = model;
|
||||||
|
case ACCESSORY -> activeAccessory = model;
|
||||||
|
case MODEL -> activeModel = model;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public void clear(CosmeticType type) {
|
||||||
|
setActive(type, null);
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean hasAnyActive() {
|
||||||
|
return activeModel != null || activePet != null || activeAccessory != null;
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean isActive() {
|
||||||
|
return activeModel != null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,659 @@
|
||||||
|
package dev.loki.lovisual.features.cosmetic;
|
||||||
|
|
||||||
|
import com.google.gson.JsonArray;
|
||||||
|
import com.google.gson.JsonElement;
|
||||||
|
import com.google.gson.JsonObject;
|
||||||
|
import com.google.gson.JsonParser;
|
||||||
|
import com.mojang.blaze3d.platform.NativeImage;
|
||||||
|
import lombok.Getter;
|
||||||
|
import lombok.Setter;
|
||||||
|
import net.minecraft.client.Minecraft;
|
||||||
|
import net.minecraft.client.renderer.texture.DynamicTexture;
|
||||||
|
import net.minecraft.resources.Identifier;
|
||||||
|
import org.joml.Vector3f;
|
||||||
|
|
||||||
|
import java.io.File;
|
||||||
|
import java.io.FileInputStream;
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.io.InputStream;
|
||||||
|
import java.io.InputStreamReader;
|
||||||
|
import java.nio.charset.StandardCharsets;
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.Collection;
|
||||||
|
import java.util.HashMap;
|
||||||
|
import java.util.HashSet;
|
||||||
|
import java.util.LinkedHashMap;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
|
import java.util.Set;
|
||||||
|
import java.util.UUID;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A single cosmetic asset: a Blockbench (.bbmodel) bone/cube tree plus its texture(s),
|
||||||
|
* loaded from a user-provided folder (avatar.json + model.bbmodel + textures).
|
||||||
|
*/
|
||||||
|
@Getter
|
||||||
|
public class CosmeticModel {
|
||||||
|
private final String id;
|
||||||
|
private final String name;
|
||||||
|
private final String description;
|
||||||
|
private final String accentColor;
|
||||||
|
private final String folderName;
|
||||||
|
private final CosmeticType type;
|
||||||
|
private final File externalDirectory;
|
||||||
|
@Setter private String source = "Своя";
|
||||||
|
|
||||||
|
private final List<CosmeticBone> rootBones = new ArrayList<>();
|
||||||
|
private final List<CosmeticBone> renderRoots = new ArrayList<>();
|
||||||
|
private boolean customLegsPresent;
|
||||||
|
private boolean bowReplacementAccessory;
|
||||||
|
private boolean swordReplacementAccessory;
|
||||||
|
private boolean hatOnlyAccessory;
|
||||||
|
private boolean wingsOnlyAccessory;
|
||||||
|
private Set<String> visibleBoneUuids;
|
||||||
|
@Setter private Set<String> animationBoneFilter;
|
||||||
|
private final Map<String, CosmeticBone> boneMap = new HashMap<>();
|
||||||
|
private final Map<String, CosmeticBone> boneMapByUuid = new HashMap<>();
|
||||||
|
private final Map<String, CosmeticBbAnimation> bbAnimations = new LinkedHashMap<>();
|
||||||
|
private final List<CosmeticTextureSlot> textureSlots = new ArrayList<>();
|
||||||
|
private int textureWidth = 64;
|
||||||
|
private int textureHeight = 64;
|
||||||
|
private Identifier textureId;
|
||||||
|
private boolean textureLoaded = false;
|
||||||
|
private CosmeticTextureSlot fallbackSlot;
|
||||||
|
|
||||||
|
private CosmeticModel(String id, String name, String description, String accentColor,
|
||||||
|
String folderName, CosmeticType type, File externalDirectory) {
|
||||||
|
this.id = id;
|
||||||
|
this.name = name;
|
||||||
|
this.description = description;
|
||||||
|
this.accentColor = accentColor;
|
||||||
|
this.folderName = folderName;
|
||||||
|
this.type = type;
|
||||||
|
this.externalDirectory = externalDirectory;
|
||||||
|
}
|
||||||
|
|
||||||
|
public CosmeticModel withType(CosmeticType newType) {
|
||||||
|
CosmeticModel copy = new CosmeticModel(id, name, description, accentColor, folderName, newType, externalDirectory);
|
||||||
|
copy.rootBones.addAll(rootBones);
|
||||||
|
copy.renderRoots.addAll(renderRoots);
|
||||||
|
copy.boneMap.putAll(boneMap);
|
||||||
|
copy.boneMapByUuid.putAll(boneMapByUuid);
|
||||||
|
copy.bbAnimations.putAll(bbAnimations);
|
||||||
|
copy.textureSlots.addAll(textureSlots);
|
||||||
|
copy.textureWidth = textureWidth;
|
||||||
|
copy.textureHeight = textureHeight;
|
||||||
|
copy.textureId = textureId;
|
||||||
|
copy.textureLoaded = textureLoaded;
|
||||||
|
copy.source = source;
|
||||||
|
copy.postProcess();
|
||||||
|
return copy;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void resetTexture() {
|
||||||
|
textureLoaded = false;
|
||||||
|
textureId = null;
|
||||||
|
for (CosmeticTextureSlot slot : textureSlots) {
|
||||||
|
slot.reset();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public CosmeticTextureSlot getTextureSlot(int index) {
|
||||||
|
if (!textureSlots.isEmpty()) {
|
||||||
|
if (index >= 0 && index < textureSlots.size()) {
|
||||||
|
return textureSlots.get(index);
|
||||||
|
}
|
||||||
|
return textureSlots.get(0);
|
||||||
|
}
|
||||||
|
if (textureId == null) return null;
|
||||||
|
if (fallbackSlot == null) {
|
||||||
|
fallbackSlot = new CosmeticTextureSlot(0, textureId, textureWidth, textureHeight, "fallback");
|
||||||
|
if (textureLoaded) fallbackSlot.markLoaded();
|
||||||
|
}
|
||||||
|
return fallbackSlot;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Loads a cosmetic from a user folder: {@code avatar.json} (metadata) plus a
|
||||||
|
* {@code .bbmodel} (geometry/animation) and its texture(s).
|
||||||
|
*/
|
||||||
|
public static CosmeticModel loadExternal(File folder, String sourceTag) {
|
||||||
|
if (folder == null || !folder.isDirectory()) return null;
|
||||||
|
String key = sourceTag + ":" + folder.getName().toLowerCase();
|
||||||
|
return load(folder.getName(), folder, key, null);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static CosmeticModel load(String folderName, File externalDir, String catalogId, CosmeticType forcedType) {
|
||||||
|
try {
|
||||||
|
JsonObject avatar = new JsonObject();
|
||||||
|
File avatarFile = new File(externalDir, "avatar.json");
|
||||||
|
if (avatarFile.exists()) {
|
||||||
|
try (InputStreamReader reader = new InputStreamReader(new FileInputStream(avatarFile), StandardCharsets.UTF_8)) {
|
||||||
|
avatar = JsonParser.parseReader(reader).getAsJsonObject();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
String name = avatar.has("name") ? avatar.get("name").getAsString() : folderName;
|
||||||
|
String desc = avatar.has("description") ? avatar.get("description").getAsString() : "";
|
||||||
|
String color = avatar.has("color") ? avatar.get("color").getAsString() : "#ffffff";
|
||||||
|
CosmeticType type = forcedType != null ? forcedType : CosmeticType.fromJson(
|
||||||
|
avatar.has("category") ? avatar.get("category").getAsString()
|
||||||
|
: avatar.has("type") ? avatar.get("type").getAsString() : null
|
||||||
|
);
|
||||||
|
|
||||||
|
CosmeticModel model = new CosmeticModel(catalogId, name, desc, color, folderName, type, externalDir);
|
||||||
|
|
||||||
|
InputStream modelStream = openModelStream(externalDir, folderName);
|
||||||
|
if (modelStream == null) {
|
||||||
|
return model;
|
||||||
|
}
|
||||||
|
|
||||||
|
JsonObject bbmodel;
|
||||||
|
try (InputStreamReader reader = new InputStreamReader(modelStream, StandardCharsets.UTF_8)) {
|
||||||
|
bbmodel = JsonParser.parseReader(reader).getAsJsonObject();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (bbmodel.has("resolution")) {
|
||||||
|
JsonObject res = bbmodel.getAsJsonObject("resolution");
|
||||||
|
model.textureWidth = res.get("width").getAsInt();
|
||||||
|
model.textureHeight = res.get("height").getAsInt();
|
||||||
|
}
|
||||||
|
|
||||||
|
model.textureId = Identifier.fromNamespaceAndPath(
|
||||||
|
"lovisual",
|
||||||
|
"dynamic/cosmetic/" + catalogId.replace(':', '_').replace(' ', '_')
|
||||||
|
);
|
||||||
|
model.loadTextureSlots(bbmodel);
|
||||||
|
|
||||||
|
Map<String, JsonObject> elementMap = new HashMap<>();
|
||||||
|
if (bbmodel.has("elements")) {
|
||||||
|
for (JsonElement el : bbmodel.getAsJsonArray("elements")) {
|
||||||
|
JsonObject obj = el.getAsJsonObject();
|
||||||
|
if (obj.has("uuid")) {
|
||||||
|
elementMap.put(obj.get("uuid").getAsString(), obj);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Map<String, JsonObject> groupMap = buildGroupMap(bbmodel);
|
||||||
|
|
||||||
|
if (bbmodel.has("outliner")) {
|
||||||
|
for (JsonElement el : bbmodel.getAsJsonArray("outliner")) {
|
||||||
|
CosmeticBone bone = parseOutliner(el, elementMap, groupMap, model.textureWidth, model.textureHeight);
|
||||||
|
if (bone != null) {
|
||||||
|
model.rootBones.add(bone);
|
||||||
|
model.indexBone(bone);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
model.bbAnimations.putAll(CosmeticBbAnimation.parseAll(bbmodel));
|
||||||
|
model.postProcess();
|
||||||
|
|
||||||
|
return model;
|
||||||
|
} catch (Exception e) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static InputStream openModelStream(File externalDir, String folderName) throws IOException {
|
||||||
|
File model = new File(externalDir, "model.bbmodel");
|
||||||
|
if (!model.exists()) {
|
||||||
|
model = new File(externalDir, folderName + ".bbmodel");
|
||||||
|
}
|
||||||
|
if (model.exists()) return new FileInputStream(model);
|
||||||
|
File[] bbmodels = externalDir.listFiles((dir, name) -> name.endsWith(".bbmodel"));
|
||||||
|
if (bbmodels != null && bbmodels.length > 0) {
|
||||||
|
return new FileInputStream(bbmodels[0]);
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void loadTextureSlots(JsonObject bbmodel) {
|
||||||
|
textureSlots.clear();
|
||||||
|
if (!bbmodel.has("textures")) return;
|
||||||
|
|
||||||
|
String modelKey = id.replace(':', '_');
|
||||||
|
JsonArray textures = bbmodel.getAsJsonArray("textures");
|
||||||
|
for (int i = 0; i < textures.size(); i++) {
|
||||||
|
if (!textures.get(i).isJsonObject()) continue;
|
||||||
|
JsonObject tex = textures.get(i).getAsJsonObject();
|
||||||
|
textureSlots.add(CosmeticTextureSlot.fromJson(i, modelKey, tex));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public void ensureTextureSlotLoaded(CosmeticTextureSlot slot) {
|
||||||
|
if (slot == null || slot.isLoaded()) return;
|
||||||
|
Minecraft mc = Minecraft.getInstance();
|
||||||
|
if (mc == null) return;
|
||||||
|
slot.load(mc, externalDirectory, folderName);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Loads whichever textures this model needs (per-slot bbmodel textures, or a single
|
||||||
|
* fallback {@code texture.png}/{@code avatar.png} in the cosmetic's folder).
|
||||||
|
*/
|
||||||
|
public void loadTexture() {
|
||||||
|
if (textureLoaded) return;
|
||||||
|
try {
|
||||||
|
Minecraft mc = Minecraft.getInstance();
|
||||||
|
if (mc == null || mc.getTextureManager() == null) return;
|
||||||
|
|
||||||
|
boolean anyLoaded = false;
|
||||||
|
for (CosmeticTextureSlot slot : textureSlots) {
|
||||||
|
slot.load(mc, externalDirectory, folderName);
|
||||||
|
if (slot.isLoaded()) anyLoaded = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!anyLoaded && externalDirectory != null) {
|
||||||
|
String[] names = {"outfits/texture.png", "texture.png", folderName + ".png", "avatar.png"};
|
||||||
|
for (String rel : names) {
|
||||||
|
File tex = new File(externalDirectory, rel);
|
||||||
|
if (!tex.exists()) continue;
|
||||||
|
try (InputStream is = new FileInputStream(tex)) {
|
||||||
|
registerTexture(mc, textureId, is);
|
||||||
|
anyLoaded = true;
|
||||||
|
break;
|
||||||
|
} catch (Exception ignored) {
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else if (!textureSlots.isEmpty() && textureSlots.get(0).isLoaded()) {
|
||||||
|
textureId = textureSlots.get(0).getId();
|
||||||
|
textureWidth = textureSlots.get(0).getWidth();
|
||||||
|
textureHeight = textureSlots.get(0).getHeight();
|
||||||
|
}
|
||||||
|
|
||||||
|
textureLoaded = anyLoaded;
|
||||||
|
if (!anyLoaded) {
|
||||||
|
textureId = null;
|
||||||
|
}
|
||||||
|
} catch (Exception ignored) {
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void registerTexture(Minecraft mc, Identifier rawId, InputStream is) throws IOException {
|
||||||
|
NativeImage img = NativeImage.read(is);
|
||||||
|
DynamicTexture tex = new DynamicTexture(() -> "lovisual_cosmetic", img);
|
||||||
|
mc.getTextureManager().register(rawId, tex);
|
||||||
|
textureId = rawId;
|
||||||
|
textureLoaded = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void indexBone(CosmeticBone bone) {
|
||||||
|
boneMap.put(bone.getName().toLowerCase(), bone);
|
||||||
|
boneMapByUuid.put(bone.getUuid(), bone);
|
||||||
|
for (CosmeticBone child : bone.getChildren()) indexBone(child);
|
||||||
|
}
|
||||||
|
|
||||||
|
public CosmeticBone getBone(String name) {
|
||||||
|
return boneMap.get(name.toLowerCase());
|
||||||
|
}
|
||||||
|
|
||||||
|
public CosmeticBone getBoneByUuid(String uuid) {
|
||||||
|
return boneMapByUuid.get(uuid);
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean hasBbAnimations() {
|
||||||
|
return !bbAnimations.isEmpty();
|
||||||
|
}
|
||||||
|
|
||||||
|
public Collection<CosmeticBbAnimation> getBbAnimations() {
|
||||||
|
return bbAnimations.values();
|
||||||
|
}
|
||||||
|
|
||||||
|
public CosmeticBbAnimation findBbAnimation(String... candidates) {
|
||||||
|
for (String candidate : candidates) {
|
||||||
|
if (candidate == null) continue;
|
||||||
|
CosmeticBbAnimation anim = bbAnimations.get(candidate.toLowerCase());
|
||||||
|
if (anim != null) return anim;
|
||||||
|
}
|
||||||
|
for (String candidate : candidates) {
|
||||||
|
if (candidate == null) continue;
|
||||||
|
String lower = candidate.toLowerCase();
|
||||||
|
for (Map.Entry<String, CosmeticBbAnimation> entry : bbAnimations.entrySet()) {
|
||||||
|
if (entry.getKey().contains(lower) || lower.contains(entry.getKey())) {
|
||||||
|
return entry.getValue();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
public CosmeticBbAnimation getFirstBbAnimation() {
|
||||||
|
for (String preferred : List.of(
|
||||||
|
"idling", "idle", "idle_arms", "idlemonsbob", "defaultstate",
|
||||||
|
"walking", "yap", "painting", "pose", "pose2"
|
||||||
|
)) {
|
||||||
|
CosmeticBbAnimation anim = findBbAnimation(preferred);
|
||||||
|
if (anim != null) return anim;
|
||||||
|
}
|
||||||
|
if (bbAnimations.isEmpty()) return null;
|
||||||
|
return bbAnimations.values().iterator().next();
|
||||||
|
}
|
||||||
|
|
||||||
|
public List<CosmeticBone> getRenderRoots() {
|
||||||
|
return renderRoots.isEmpty() ? rootBones : renderRoots;
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean hasCustomLegs() {
|
||||||
|
return customLegsPresent;
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean isBowReplacementAccessory() {
|
||||||
|
return bowReplacementAccessory;
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean isSwordReplacementAccessory() {
|
||||||
|
return swordReplacementAccessory;
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean isBodyOnlyAccessory() {
|
||||||
|
return type == CosmeticType.ACCESSORY && !bowReplacementAccessory && !swordReplacementAccessory;
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean hasRestPoseAnimation() {
|
||||||
|
return findBbAnimation(
|
||||||
|
"idling", "idle", "idle_arms", "idlemonsbob", "defaultstate",
|
||||||
|
"walking", "walkingback", "flywalk"
|
||||||
|
) != null;
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean shouldSkipBone(CosmeticBone bone) {
|
||||||
|
if (visibleBoneUuids != null) {
|
||||||
|
return !visibleBoneUuids.contains(bone.getUuid());
|
||||||
|
}
|
||||||
|
String lower = bone.getName().toLowerCase();
|
||||||
|
if (customLegsPresent && lower.contains("vanilla") && (lower.contains("leg") || lower.contains("foot"))) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
if (lower.contains("rightarm_fp") || lower.contains("leftarm_fp")) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
if (hatOnlyAccessory) {
|
||||||
|
return lower.equals("body") || lower.equals("leftarm") || lower.equals("rightarm")
|
||||||
|
|| lower.equals("leftleg") || lower.equals("rightleg") || lower.equals("root");
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Derives a standalone pet cosmetic from a single named bone subtree of a parent model
|
||||||
|
* (e.g. turning a model's "head" bone into its own pet cosmetic).
|
||||||
|
*/
|
||||||
|
public static CosmeticModel derivePet(CosmeticModel parent, String boneName, String petFolder, String petName) {
|
||||||
|
if (parent == null) return null;
|
||||||
|
CosmeticBone source = parent.findBoneDeep(boneName);
|
||||||
|
if (source == null) return null;
|
||||||
|
|
||||||
|
CosmeticModel pet = new CosmeticModel(
|
||||||
|
"derived:" + petFolder.replace('/', '_'),
|
||||||
|
petName,
|
||||||
|
"",
|
||||||
|
parent.getAccentColor(),
|
||||||
|
petFolder,
|
||||||
|
CosmeticType.PET,
|
||||||
|
null
|
||||||
|
);
|
||||||
|
pet.source = parent.source;
|
||||||
|
pet.textureWidth = parent.textureWidth;
|
||||||
|
pet.textureHeight = parent.textureHeight;
|
||||||
|
pet.textureId = parent.textureId;
|
||||||
|
pet.textureSlots.addAll(parent.textureSlots);
|
||||||
|
pet.textureLoaded = parent.textureLoaded;
|
||||||
|
|
||||||
|
CosmeticBone root = source.deepClone();
|
||||||
|
pet.rootBones.add(root);
|
||||||
|
pet.renderRoots.add(root);
|
||||||
|
pet.indexBone(root);
|
||||||
|
|
||||||
|
Set<String> uuids = new HashSet<>();
|
||||||
|
pet.collectUuids(root, uuids);
|
||||||
|
pet.animationBoneFilter = uuids;
|
||||||
|
|
||||||
|
for (Map.Entry<String, CosmeticBbAnimation> entry : parent.bbAnimations.entrySet()) {
|
||||||
|
String key = entry.getKey().toLowerCase();
|
||||||
|
if (key.contains("mons") || key.contains("mini")) {
|
||||||
|
pet.bbAnimations.put(entry.getKey(), entry.getValue());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (pet.bbAnimations.isEmpty()) {
|
||||||
|
pet.bbAnimations.putAll(parent.bbAnimations);
|
||||||
|
}
|
||||||
|
pet.postProcess();
|
||||||
|
return pet;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void collectUuids(CosmeticBone bone, Set<String> uuids) {
|
||||||
|
uuids.add(bone.getUuid());
|
||||||
|
for (CosmeticBone child : bone.getChildren()) collectUuids(child, uuids);
|
||||||
|
}
|
||||||
|
|
||||||
|
private CosmeticBone findBoneDeep(String name) {
|
||||||
|
String lower = name.toLowerCase();
|
||||||
|
for (CosmeticBone root : rootBones) {
|
||||||
|
CosmeticBone found = findBoneDeep(root, lower);
|
||||||
|
if (found != null) return found;
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static CosmeticBone findBoneDeep(CosmeticBone bone, String lower) {
|
||||||
|
if (bone.getName().toLowerCase().equals(lower) || bone.getName().toLowerCase().contains(lower)) {
|
||||||
|
return bone;
|
||||||
|
}
|
||||||
|
for (CosmeticBone child : bone.getChildren()) {
|
||||||
|
CosmeticBone found = findBoneDeep(child, lower);
|
||||||
|
if (found != null) return found;
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void postProcess() {
|
||||||
|
customLegsPresent = boneMap.keySet().stream().anyMatch(n ->
|
||||||
|
n.contains("leftleg") || n.contains("rightleg") || n.contains("leftpaw") || n.contains("rightpaw"))
|
||||||
|
&& boneMap.keySet().stream().noneMatch(n -> n.equals("leftlegvanilla") || n.equals("rightlegvanilla")
|
||||||
|
|| (n.contains("vanilla") && n.contains("leg")));
|
||||||
|
|
||||||
|
String folder = folderName.toLowerCase();
|
||||||
|
bowReplacementAccessory = type == CosmeticType.ACCESSORY && folder.contains("bow");
|
||||||
|
swordReplacementAccessory = false;
|
||||||
|
hatOnlyAccessory = type == CosmeticType.ACCESSORY
|
||||||
|
&& (folder.contains("witch") || folder.contains("hat")) && !bowReplacementAccessory;
|
||||||
|
wingsOnlyAccessory = type == CosmeticType.ACCESSORY && folder.contains("wing");
|
||||||
|
visibleBoneUuids = null;
|
||||||
|
if (wingsOnlyAccessory) {
|
||||||
|
visibleBoneUuids = buildWingsBoneChainUuids();
|
||||||
|
}
|
||||||
|
|
||||||
|
renderRoots.clear();
|
||||||
|
for (CosmeticBone root : rootBones) {
|
||||||
|
String n = root.getName().toLowerCase();
|
||||||
|
if (type == CosmeticType.MODEL) {
|
||||||
|
if (n.equals("monst3r") || n.equals("skull") || n.endsWith("_fp")) continue;
|
||||||
|
}
|
||||||
|
if (type == CosmeticType.PET) {
|
||||||
|
if (n.equals("arrow") || n.equals("skull")) continue;
|
||||||
|
}
|
||||||
|
renderRoots.add(root);
|
||||||
|
}
|
||||||
|
if (renderRoots.isEmpty()) {
|
||||||
|
renderRoots.addAll(rootBones);
|
||||||
|
}
|
||||||
|
|
||||||
|
applyAccessorySubtreeFilter();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void applyAccessorySubtreeFilter() {
|
||||||
|
if (hatOnlyAccessory) {
|
||||||
|
CosmeticBone head = findBoneDeep("head");
|
||||||
|
CosmeticBone hatBone = findBoneDeep("bone");
|
||||||
|
if (head != null && hatBone != null) {
|
||||||
|
CosmeticBone mount = new CosmeticBone(
|
||||||
|
head.getName(), head.getUuid(),
|
||||||
|
new Vector3f(head.getPivot()), new Vector3f(head.getDefaultRotation()));
|
||||||
|
mount.getChildren().add(hatBone.deepClone());
|
||||||
|
renderRoots.clear();
|
||||||
|
renderRoots.add(mount);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private Set<String> buildWingsBoneChainUuids() {
|
||||||
|
Set<String> keep = new HashSet<>();
|
||||||
|
CosmeticBone wings = findBoneDeep("wings");
|
||||||
|
if (wings == null) return keep;
|
||||||
|
collectBoneTreeUuids(wings, keep);
|
||||||
|
for (CosmeticBone root : rootBones) {
|
||||||
|
markBonePathToUuid(root, wings.getUuid(), keep);
|
||||||
|
}
|
||||||
|
return keep;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void collectBoneTreeUuids(CosmeticBone bone, Set<String> uuids) {
|
||||||
|
uuids.add(bone.getUuid());
|
||||||
|
for (CosmeticBone child : bone.getChildren()) {
|
||||||
|
collectBoneTreeUuids(child, uuids);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static boolean markBonePathToUuid(CosmeticBone bone, String targetUuid, Set<String> keep) {
|
||||||
|
keep.add(bone.getUuid());
|
||||||
|
if (bone.getUuid().equals(targetUuid)) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
for (CosmeticBone child : bone.getChildren()) {
|
||||||
|
if (markBonePathToUuid(child, targetUuid, keep)) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
keep.remove(bone.getUuid());
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static Map<String, JsonObject> buildGroupMap(JsonObject bbmodel) {
|
||||||
|
Map<String, JsonObject> map = new HashMap<>();
|
||||||
|
if (!bbmodel.has("groups")) return map;
|
||||||
|
for (JsonElement el : bbmodel.getAsJsonArray("groups")) {
|
||||||
|
if (!el.isJsonObject()) continue;
|
||||||
|
JsonObject group = el.getAsJsonObject();
|
||||||
|
if (group.has("uuid")) {
|
||||||
|
map.put(group.get("uuid").getAsString(), group);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return map;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static Vector3f readVec3(JsonObject obj, String key, Vector3f fallback) {
|
||||||
|
if (obj == null || !obj.has(key)) return new Vector3f(fallback);
|
||||||
|
JsonArray arr = obj.getAsJsonArray(key);
|
||||||
|
return new Vector3f(arr.get(0).getAsFloat(), arr.get(1).getAsFloat(), arr.get(2).getAsFloat());
|
||||||
|
}
|
||||||
|
|
||||||
|
private static CosmeticBone parseOutliner(JsonElement element, Map<String, JsonObject> elementMap,
|
||||||
|
Map<String, JsonObject> groupMap, int texW, int texH) {
|
||||||
|
if (element.isJsonPrimitive() || !element.isJsonObject()) return null;
|
||||||
|
JsonObject obj = element.getAsJsonObject();
|
||||||
|
|
||||||
|
String uuid = obj.has("uuid") ? obj.get("uuid").getAsString() : UUID.randomUUID().toString();
|
||||||
|
JsonObject group = groupMap.get(uuid);
|
||||||
|
|
||||||
|
String name = "bone";
|
||||||
|
if (obj.has("name")) {
|
||||||
|
name = obj.get("name").getAsString();
|
||||||
|
} else if (group != null && group.has("name")) {
|
||||||
|
name = group.get("name").getAsString();
|
||||||
|
}
|
||||||
|
|
||||||
|
Vector3f pivot = obj.has("origin")
|
||||||
|
? readVec3(obj, "origin", new Vector3f())
|
||||||
|
: readVec3(group, "origin", new Vector3f());
|
||||||
|
|
||||||
|
Vector3f rotation = obj.has("rotation")
|
||||||
|
? readVec3(obj, "rotation", new Vector3f())
|
||||||
|
: readVec3(group, "rotation", new Vector3f());
|
||||||
|
|
||||||
|
CosmeticBone bone = new CosmeticBone(name, uuid, pivot, rotation);
|
||||||
|
|
||||||
|
if (obj.has("children")) {
|
||||||
|
for (JsonElement child : obj.getAsJsonArray("children")) {
|
||||||
|
if (child.isJsonPrimitive()) {
|
||||||
|
String childUuid = child.getAsString();
|
||||||
|
JsonObject cubeObj = elementMap.get(childUuid);
|
||||||
|
if (cubeObj != null) {
|
||||||
|
CosmeticCube cube = parseCube(cubeObj, texW, texH);
|
||||||
|
if (cube != null) bone.getCubes().add(cube);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
CosmeticBone childBone = parseOutliner(child, elementMap, groupMap, texW, texH);
|
||||||
|
if (childBone != null) bone.getChildren().add(childBone);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return bone;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static CosmeticCube parseCube(JsonObject obj, int texW, int texH) {
|
||||||
|
try {
|
||||||
|
JsonArray fromArr = obj.getAsJsonArray("from");
|
||||||
|
JsonArray toArr = obj.getAsJsonArray("to");
|
||||||
|
Vector3f from = new Vector3f(fromArr.get(0).getAsFloat(), fromArr.get(1).getAsFloat(), fromArr.get(2).getAsFloat());
|
||||||
|
Vector3f to = new Vector3f(toArr.get(0).getAsFloat(), toArr.get(1).getAsFloat(), toArr.get(2).getAsFloat());
|
||||||
|
|
||||||
|
Vector3f rot = new Vector3f(0, 0, 0);
|
||||||
|
if (obj.has("rotation")) {
|
||||||
|
JsonArray r = obj.getAsJsonArray("rotation");
|
||||||
|
rot.set(r.get(0).getAsFloat(), r.get(1).getAsFloat(), r.get(2).getAsFloat());
|
||||||
|
}
|
||||||
|
|
||||||
|
Vector3f origin = new Vector3f(0, 0, 0);
|
||||||
|
if (obj.has("origin")) {
|
||||||
|
JsonArray o = obj.getAsJsonArray("origin");
|
||||||
|
origin.set(o.get(0).getAsFloat(), o.get(1).getAsFloat(), o.get(2).getAsFloat());
|
||||||
|
}
|
||||||
|
|
||||||
|
float inflate = obj.has("inflate") ? obj.get("inflate").getAsFloat() : 0f;
|
||||||
|
|
||||||
|
CosmeticCube.CubeFace[] faces = new CosmeticCube.CubeFace[6];
|
||||||
|
String[] faceNames = {"north", "east", "south", "west", "up", "down"};
|
||||||
|
|
||||||
|
if (obj.has("faces")) {
|
||||||
|
JsonObject facesObj = obj.getAsJsonObject("faces");
|
||||||
|
for (int i = 0; i < faceNames.length; i++) {
|
||||||
|
String fn = faceNames[i];
|
||||||
|
if (!facesObj.has(fn)) continue;
|
||||||
|
JsonObject face = facesObj.getAsJsonObject(fn);
|
||||||
|
if (!face.has("uv") || !face.get("uv").isJsonArray()) continue;
|
||||||
|
JsonArray uv = face.getAsJsonArray("uv");
|
||||||
|
if (uv.size() < 4) continue;
|
||||||
|
int tex = 0;
|
||||||
|
if (face.has("texture") && !face.get("texture").isJsonNull()) {
|
||||||
|
JsonElement texEl = face.get("texture");
|
||||||
|
if (texEl.isJsonPrimitive() && texEl.getAsJsonPrimitive().isNumber()) {
|
||||||
|
tex = texEl.getAsInt();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
float u1 = uv.get(0).getAsFloat();
|
||||||
|
float v1 = uv.get(1).getAsFloat();
|
||||||
|
float u2 = uv.get(2).getAsFloat();
|
||||||
|
float v2 = uv.get(3).getAsFloat();
|
||||||
|
if (u2 < u1) {
|
||||||
|
float t = u1;
|
||||||
|
u1 = u2;
|
||||||
|
u2 = t;
|
||||||
|
}
|
||||||
|
if (v2 < v1) {
|
||||||
|
float t = v1;
|
||||||
|
v1 = v2;
|
||||||
|
v2 = t;
|
||||||
|
}
|
||||||
|
faces[i] = new CosmeticCube.CubeFace(u1, v1, u2, v2, tex);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return new CosmeticCube(from, to, rot, origin, inflate, faces, texW, texH);
|
||||||
|
} catch (Exception e) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,204 @@
|
||||||
|
package dev.loki.lovisual.features.cosmetic;
|
||||||
|
|
||||||
|
import com.mojang.blaze3d.vertex.PoseStack;
|
||||||
|
import com.mojang.math.Axis;
|
||||||
|
import dev.loki.lovisual.render.engine.RenderState;
|
||||||
|
import dev.loki.lovisual.render.engine.math.RenderMath;
|
||||||
|
import dev.loki.lovisual.render.engine.pipeline.LoVisualRenderPipelines;
|
||||||
|
import dev.loki.lovisual.render.engine.renderer.Renderer3D;
|
||||||
|
import dev.loki.lovisual.render.engine.uniform.MeshBuilder;
|
||||||
|
import net.minecraft.client.Minecraft;
|
||||||
|
import net.minecraft.client.renderer.texture.AbstractTexture;
|
||||||
|
import net.minecraft.util.Mth;
|
||||||
|
import net.minecraft.world.entity.player.Player;
|
||||||
|
import net.minecraft.world.phys.Vec3;
|
||||||
|
import org.joml.Matrix4f;
|
||||||
|
import org.joml.Vector3f;
|
||||||
|
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.HashMap;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Renders a rest-pose (no bb-animation) {@link CosmeticModel} bone/cube tree attached to a
|
||||||
|
* player, as camera-relative textured quads through the engine's {@link Renderer3D}/{@link MeshBuilder}.
|
||||||
|
*/
|
||||||
|
public final class CosmeticRenderer {
|
||||||
|
private static final float UNITS_PER_BLOCK = 16.0f;
|
||||||
|
|
||||||
|
private CosmeticRenderer() {
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void renderOnPlayer(Renderer3D renderer, Player player, CosmeticType type, float tickDelta) {
|
||||||
|
if (renderer == null || player == null) return;
|
||||||
|
|
||||||
|
CosmeticModel model = CosmeticManager.get().getActive(type);
|
||||||
|
if (model == null) return;
|
||||||
|
|
||||||
|
model.loadTexture();
|
||||||
|
List<CosmeticBone> roots = model.getRenderRoots();
|
||||||
|
if (roots.isEmpty()) return;
|
||||||
|
|
||||||
|
Vec3 pos = RenderMath.getLerpedPos(player, tickDelta);
|
||||||
|
float bodyYaw = Mth.rotLerp(tickDelta, player.yBodyRotO, player.yBodyRot);
|
||||||
|
|
||||||
|
PoseStack matrices = new PoseStack();
|
||||||
|
matrices.translate(pos.x, pos.y, pos.z);
|
||||||
|
CosmeticAttachmentTransforms.apply(matrices, type, model);
|
||||||
|
matrices.mulPose(Axis.YP.rotationDegrees(180f - bodyYaw));
|
||||||
|
|
||||||
|
Map<Integer, List<PendingFace>> byTexture = new HashMap<>();
|
||||||
|
for (CosmeticBone bone : roots) {
|
||||||
|
collectBone(matrices, model, bone, byTexture);
|
||||||
|
}
|
||||||
|
if (byTexture.isEmpty()) return;
|
||||||
|
|
||||||
|
Minecraft mc = Minecraft.getInstance();
|
||||||
|
Vec3 cam = RenderState.cameraPos;
|
||||||
|
|
||||||
|
for (Map.Entry<Integer, List<PendingFace>> entry : byTexture.entrySet()) {
|
||||||
|
CosmeticTextureSlot slot = model.getTextureSlot(entry.getKey());
|
||||||
|
if (slot == null) continue;
|
||||||
|
if (!slot.isLoaded()) {
|
||||||
|
model.ensureTextureSlotLoaded(slot);
|
||||||
|
}
|
||||||
|
if (!slot.isLoaded()) continue;
|
||||||
|
|
||||||
|
AbstractTexture texture = mc.getTextureManager().getTexture(slot.getId());
|
||||||
|
if (texture == null || texture.getTextureView() == null || texture.getSampler() == null) continue;
|
||||||
|
|
||||||
|
Renderer3D.BatchBindings bindings = Renderer3D.BatchBindings.none()
|
||||||
|
.withSampler("u_Texture", texture.getTextureView(), texture.getSampler());
|
||||||
|
MeshBuilder mesh = renderer.batch(LoVisualRenderPipelines.WORLD_TEXTURED, Renderer3D.DepthMode.MAIN, bindings);
|
||||||
|
if (mesh == null) continue;
|
||||||
|
|
||||||
|
for (PendingFace face : entry.getValue()) {
|
||||||
|
mesh.ensureQuadCapacity();
|
||||||
|
|
||||||
|
int i1 = mesh.vec3(face.xs[0] - cam.x, face.ys[0] - cam.y, face.zs[0] - cam.z)
|
||||||
|
.raw2(face.us[0], face.vs[0]).color(255, 255, 255, 255).next();
|
||||||
|
int i2 = mesh.vec3(face.xs[1] - cam.x, face.ys[1] - cam.y, face.zs[1] - cam.z)
|
||||||
|
.raw2(face.us[1], face.vs[1]).color(255, 255, 255, 255).next();
|
||||||
|
int i3 = mesh.vec3(face.xs[2] - cam.x, face.ys[2] - cam.y, face.zs[2] - cam.z)
|
||||||
|
.raw2(face.us[2], face.vs[2]).color(255, 255, 255, 255).next();
|
||||||
|
int i4 = mesh.vec3(face.xs[3] - cam.x, face.ys[3] - cam.y, face.zs[3] - cam.z)
|
||||||
|
.raw2(face.us[3], face.vs[3]).color(255, 255, 255, 255).next();
|
||||||
|
|
||||||
|
mesh.quad(i1, i2, i3, i4);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void collectBone(PoseStack matrices, CosmeticModel model, CosmeticBone bone,
|
||||||
|
Map<Integer, List<PendingFace>> out) {
|
||||||
|
if (model.shouldSkipBone(bone)) return;
|
||||||
|
|
||||||
|
matrices.pushPose();
|
||||||
|
matrices.last().pose().mul(bone.buildMatrix());
|
||||||
|
|
||||||
|
for (CosmeticCube cube : bone.getCubes()) {
|
||||||
|
collectCube(matrices, cube, out);
|
||||||
|
}
|
||||||
|
for (CosmeticBone child : bone.getChildren()) {
|
||||||
|
collectBone(matrices, model, child, out);
|
||||||
|
}
|
||||||
|
|
||||||
|
matrices.popPose();
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void collectCube(PoseStack matrices, CosmeticCube cube, Map<Integer, List<PendingFace>> out) {
|
||||||
|
matrices.pushPose();
|
||||||
|
|
||||||
|
Vector3f origin = cube.getOrigin();
|
||||||
|
Vector3f rotation = cube.getRotation();
|
||||||
|
if (rotation.x != 0f || rotation.y != 0f || rotation.z != 0f) {
|
||||||
|
matrices.translate(origin.x / UNITS_PER_BLOCK, origin.y / UNITS_PER_BLOCK, origin.z / UNITS_PER_BLOCK);
|
||||||
|
matrices.mulPose(Axis.XP.rotationDegrees(rotation.x));
|
||||||
|
matrices.mulPose(Axis.YP.rotationDegrees(rotation.y));
|
||||||
|
matrices.mulPose(Axis.ZP.rotationDegrees(rotation.z));
|
||||||
|
matrices.translate(-origin.x / UNITS_PER_BLOCK, -origin.y / UNITS_PER_BLOCK, -origin.z / UNITS_PER_BLOCK);
|
||||||
|
}
|
||||||
|
|
||||||
|
float inflate = cube.getInflate();
|
||||||
|
float x1 = (cube.getFrom().x - inflate) / UNITS_PER_BLOCK;
|
||||||
|
float y1 = (cube.getFrom().y - inflate) / UNITS_PER_BLOCK;
|
||||||
|
float z1 = (cube.getFrom().z - inflate) / UNITS_PER_BLOCK;
|
||||||
|
float x2 = (cube.getTo().x + inflate) / UNITS_PER_BLOCK;
|
||||||
|
float y2 = (cube.getTo().y + inflate) / UNITS_PER_BLOCK;
|
||||||
|
float z2 = (cube.getTo().z + inflate) / UNITS_PER_BLOCK;
|
||||||
|
|
||||||
|
Matrix4f m = new Matrix4f(matrices.last().pose());
|
||||||
|
CosmeticCube.CubeFace[] faces = cube.getFaces();
|
||||||
|
int tw = cube.getTextureWidth();
|
||||||
|
int th = cube.getTextureHeight();
|
||||||
|
|
||||||
|
for (int side = 0; side < 6; side++) {
|
||||||
|
CosmeticCube.CubeFace face = faces[side];
|
||||||
|
if (face == null) continue;
|
||||||
|
addFace(m, face, tw, th, x1, y1, z1, x2, y2, z2, side, out);
|
||||||
|
}
|
||||||
|
|
||||||
|
matrices.popPose();
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void addFace(Matrix4f m, CosmeticCube.CubeFace face, float tw, float th,
|
||||||
|
float x1, float y1, float z1, float x2, float y2, float z2,
|
||||||
|
int side, Map<Integer, List<PendingFace>> out) {
|
||||||
|
PendingFace quad = switch (side) {
|
||||||
|
case 0 -> quad(m,
|
||||||
|
x2, y2, z1, x1, y2, z1, x1, y1, z1, x2, y1, z1);
|
||||||
|
case 1 -> quad(m,
|
||||||
|
x2, y2, z2, x2, y2, z1, x2, y1, z1, x2, y1, z2);
|
||||||
|
case 2 -> quad(m,
|
||||||
|
x1, y2, z2, x2, y2, z2, x2, y1, z2, x1, y1, z2);
|
||||||
|
case 3 -> quad(m,
|
||||||
|
x1, y2, z1, x1, y2, z2, x1, y1, z2, x1, y1, z1);
|
||||||
|
case 4 -> quad(m,
|
||||||
|
x1, y2, z2, x2, y2, z2, x2, y2, z1, x1, y2, z1);
|
||||||
|
case 5 -> quad(m,
|
||||||
|
x1, y1, z1, x2, y1, z1, x2, y1, z2, x1, y1, z2);
|
||||||
|
default -> null;
|
||||||
|
};
|
||||||
|
if (quad == null) return;
|
||||||
|
|
||||||
|
float u1 = face.getU1() / tw;
|
||||||
|
float v1 = face.getV1() / th;
|
||||||
|
float u2 = face.getU2() / tw;
|
||||||
|
float v2 = face.getV2() / th;
|
||||||
|
quad.us[0] = u1;
|
||||||
|
quad.vs[0] = v1;
|
||||||
|
quad.us[1] = u2;
|
||||||
|
quad.vs[1] = v1;
|
||||||
|
quad.us[2] = u2;
|
||||||
|
quad.vs[2] = v2;
|
||||||
|
quad.us[3] = u1;
|
||||||
|
quad.vs[3] = v2;
|
||||||
|
|
||||||
|
out.computeIfAbsent(face.getTextureIndex(), k -> new ArrayList<>()).add(quad);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static PendingFace quad(Matrix4f m,
|
||||||
|
float x1, float y1, float z1,
|
||||||
|
float x2, float y2, float z2,
|
||||||
|
float x3, float y3, float z3,
|
||||||
|
float x4, float y4, float z4) {
|
||||||
|
PendingFace face = new PendingFace();
|
||||||
|
Vector3f p1 = m.transformPosition(new Vector3f(x1, y1, z1));
|
||||||
|
Vector3f p2 = m.transformPosition(new Vector3f(x2, y2, z2));
|
||||||
|
Vector3f p3 = m.transformPosition(new Vector3f(x3, y3, z3));
|
||||||
|
Vector3f p4 = m.transformPosition(new Vector3f(x4, y4, z4));
|
||||||
|
face.xs = new float[]{p1.x, p2.x, p3.x, p4.x};
|
||||||
|
face.ys = new float[]{p1.y, p2.y, p3.y, p4.y};
|
||||||
|
face.zs = new float[]{p1.z, p2.z, p3.z, p4.z};
|
||||||
|
return face;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static final class PendingFace {
|
||||||
|
float[] xs;
|
||||||
|
float[] ys;
|
||||||
|
float[] zs;
|
||||||
|
final float[] us = new float[4];
|
||||||
|
final float[] vs = new float[4];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,97 @@
|
||||||
|
package dev.loki.lovisual.features.cosmetic;
|
||||||
|
|
||||||
|
import dev.loki.lovisual.config.ConfigPaths;
|
||||||
|
|
||||||
|
import java.io.File;
|
||||||
|
import java.nio.file.Files;
|
||||||
|
import java.nio.file.Path;
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.LinkedHashMap;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
|
import java.util.stream.Stream;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Scans {@code config/lovisual/cosmetics/<type>/<name>} (and the flat root as a
|
||||||
|
* fallback) for user-provided cosmetic folders (avatar.json + .bbmodel) and keeps the
|
||||||
|
* loaded catalog in memory.
|
||||||
|
*/
|
||||||
|
public final class CosmeticRepository {
|
||||||
|
private static final CosmeticRepository INSTANCE = new CosmeticRepository();
|
||||||
|
|
||||||
|
private final Map<String, CosmeticModel> catalog = new LinkedHashMap<>();
|
||||||
|
|
||||||
|
public static CosmeticRepository get() {
|
||||||
|
return INSTANCE;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Path root() {
|
||||||
|
return ConfigPaths.root().resolve("cosmetics");
|
||||||
|
}
|
||||||
|
|
||||||
|
public void reload() {
|
||||||
|
catalog.clear();
|
||||||
|
discoverUserFolder(root());
|
||||||
|
}
|
||||||
|
|
||||||
|
public List<CosmeticModel> all() {
|
||||||
|
return new ArrayList<>(catalog.values());
|
||||||
|
}
|
||||||
|
|
||||||
|
public List<CosmeticModel> byType(CosmeticType type) {
|
||||||
|
return catalog.values().stream().filter(m -> m.getType() == type).toList();
|
||||||
|
}
|
||||||
|
|
||||||
|
public CosmeticModel get(String id) {
|
||||||
|
return catalog.get(id);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void register(CosmeticModel model) {
|
||||||
|
if (model != null && !catalog.containsKey(model.getId())) {
|
||||||
|
catalog.put(model.getId(), model);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void discoverUserFolder(Path root) {
|
||||||
|
if (!Files.isDirectory(root)) {
|
||||||
|
try {
|
||||||
|
Files.createDirectories(root);
|
||||||
|
for (CosmeticType type : CosmeticType.values()) {
|
||||||
|
Files.createDirectories(root.resolve(type.getId()));
|
||||||
|
}
|
||||||
|
} catch (Exception ignored) {
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
for (CosmeticType type : CosmeticType.values()) {
|
||||||
|
Path typeDir = root.resolve(type.getId());
|
||||||
|
scanDirectory(typeDir, type);
|
||||||
|
}
|
||||||
|
scanDirectory(root, null);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void scanDirectory(Path dir, CosmeticType forcedType) {
|
||||||
|
if (!Files.isDirectory(dir)) return;
|
||||||
|
try (Stream<Path> stream = Files.list(dir)) {
|
||||||
|
stream.filter(Files::isDirectory).forEach(sub -> {
|
||||||
|
File folder = sub.toFile();
|
||||||
|
if (new File(folder, "avatar.json").exists() || hasBbmodel(folder)) {
|
||||||
|
CosmeticModel model = CosmeticModel.loadExternal(folder, "user");
|
||||||
|
if (model != null) {
|
||||||
|
if (forcedType != null && model.getType() == CosmeticType.MODEL) {
|
||||||
|
model = model.withType(forcedType);
|
||||||
|
}
|
||||||
|
register(model);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
} catch (Exception ignored) {
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static boolean hasBbmodel(File folder) {
|
||||||
|
return new File(folder, "model.bbmodel").exists()
|
||||||
|
|| new File(folder, folder.getName() + ".bbmodel").exists();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,119 @@
|
||||||
|
package dev.loki.lovisual.features.cosmetic;
|
||||||
|
|
||||||
|
import com.google.gson.JsonObject;
|
||||||
|
import com.mojang.blaze3d.platform.NativeImage;
|
||||||
|
import net.minecraft.client.Minecraft;
|
||||||
|
import net.minecraft.client.renderer.texture.DynamicTexture;
|
||||||
|
import net.minecraft.resources.Identifier;
|
||||||
|
|
||||||
|
import java.io.ByteArrayInputStream;
|
||||||
|
import java.io.File;
|
||||||
|
import java.io.FileInputStream;
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.io.InputStream;
|
||||||
|
import java.util.Base64;
|
||||||
|
|
||||||
|
public final class CosmeticTextureSlot {
|
||||||
|
private final int index;
|
||||||
|
private final Identifier id;
|
||||||
|
private int width;
|
||||||
|
private int height;
|
||||||
|
private final String fileName;
|
||||||
|
|
||||||
|
private boolean loaded;
|
||||||
|
private JsonObject textureJson;
|
||||||
|
|
||||||
|
CosmeticTextureSlot(int index, Identifier id, int width, int height, String fileName) {
|
||||||
|
this.index = index;
|
||||||
|
this.id = id;
|
||||||
|
this.width = Math.max(width, 1);
|
||||||
|
this.height = Math.max(height, 1);
|
||||||
|
this.fileName = fileName;
|
||||||
|
}
|
||||||
|
|
||||||
|
public int getIndex() {
|
||||||
|
return index;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Identifier getId() {
|
||||||
|
return id;
|
||||||
|
}
|
||||||
|
|
||||||
|
public int getWidth() {
|
||||||
|
return width;
|
||||||
|
}
|
||||||
|
|
||||||
|
public int getHeight() {
|
||||||
|
return height;
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean isLoaded() {
|
||||||
|
return loaded;
|
||||||
|
}
|
||||||
|
|
||||||
|
void markLoaded() {
|
||||||
|
loaded = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
static CosmeticTextureSlot fromJson(int index, String modelKey, JsonObject texture) {
|
||||||
|
int w = texture.has("width") ? texture.get("width").getAsInt() : 64;
|
||||||
|
int h = texture.has("height") ? texture.get("height").getAsInt() : 64;
|
||||||
|
String name = texture.has("name") ? texture.get("name").getAsString() : "tex" + index;
|
||||||
|
Identifier id = Identifier.fromNamespaceAndPath(
|
||||||
|
"lovisual",
|
||||||
|
"dynamic/cosmetic/" + modelKey + "/tex/" + index
|
||||||
|
);
|
||||||
|
CosmeticTextureSlot slot = new CosmeticTextureSlot(index, id, w, h, name);
|
||||||
|
slot.textureJson = texture;
|
||||||
|
return slot;
|
||||||
|
}
|
||||||
|
|
||||||
|
void reset() {
|
||||||
|
loaded = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Attempts to load this texture slot from, in order: the cosmetic's external folder
|
||||||
|
* (file name, then outfits/<file>, then a generic texture.png fallback), or an
|
||||||
|
* inline base64 "source" embedded in the .bbmodel JSON.
|
||||||
|
*/
|
||||||
|
void load(Minecraft mc, File externalDir, String resourcePath) {
|
||||||
|
if (loaded || mc == null || mc.getTextureManager() == null) return;
|
||||||
|
|
||||||
|
try {
|
||||||
|
if (externalDir != null) {
|
||||||
|
if (tryFile(mc, new File(externalDir, fileName))) return;
|
||||||
|
if (tryFile(mc, new File(externalDir, "outfits/" + fileName))) return;
|
||||||
|
if (tryFile(mc, new File(externalDir, "texture.png"))) return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (textureJson != null && textureJson.has("source")) {
|
||||||
|
String source = textureJson.get("source").getAsString();
|
||||||
|
if (source.startsWith("data:image") && source.contains(",")) {
|
||||||
|
byte[] bytes = Base64.getDecoder().decode(source.substring(source.indexOf(',') + 1));
|
||||||
|
try (InputStream is = new ByteArrayInputStream(bytes)) {
|
||||||
|
register(mc, is);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (Exception ignored) {
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private boolean tryFile(Minecraft mc, File file) throws IOException {
|
||||||
|
if (!file.exists()) return false;
|
||||||
|
try (InputStream is = new FileInputStream(file)) {
|
||||||
|
register(mc, is);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void register(Minecraft mc, InputStream is) throws IOException {
|
||||||
|
NativeImage image = NativeImage.read(is);
|
||||||
|
width = Math.max(image.getWidth(), 1);
|
||||||
|
height = Math.max(image.getHeight(), 1);
|
||||||
|
DynamicTexture texture = new DynamicTexture(() -> "lovisual_cosmetic_" + index, image);
|
||||||
|
mc.getTextureManager().register(id, texture);
|
||||||
|
loaded = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,32 @@
|
||||||
|
package dev.loki.lovisual.features.cosmetic;
|
||||||
|
|
||||||
|
public enum CosmeticType {
|
||||||
|
MODEL("model", "Модель"),
|
||||||
|
PET("pet", "Питомец"),
|
||||||
|
ACCESSORY("accessory", "Аксессуар");
|
||||||
|
|
||||||
|
private final String id;
|
||||||
|
private final String displayName;
|
||||||
|
|
||||||
|
CosmeticType(String id, String displayName) {
|
||||||
|
this.id = id;
|
||||||
|
this.displayName = displayName;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getId() {
|
||||||
|
return id;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getDisplayName() {
|
||||||
|
return displayName;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static CosmeticType fromJson(String raw) {
|
||||||
|
if (raw == null || raw.isBlank()) return MODEL;
|
||||||
|
return switch (raw.toLowerCase()) {
|
||||||
|
case "pet", "pets", "питомец", "питомцы" -> PET;
|
||||||
|
case "accessory", "accessories", "item", "items", "аксессуар", "аксессуары" -> ACCESSORY;
|
||||||
|
default -> MODEL;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -63,7 +63,11 @@ final class ClickGuiInputHandler {
|
||||||
static void onEditorMouseButtonScaled(double x, double y, int button, boolean pressed) {
|
static void onEditorMouseButtonScaled(double x, double y, int button, boolean pressed) {
|
||||||
updateMouse(x, y, true);
|
updateMouse(x, y, true);
|
||||||
if (ClickGuiRenderer.waitingForKey) {
|
if (ClickGuiRenderer.waitingForKey) {
|
||||||
if (pressed) ClickGuiKeyBindManager.addPendingMouseButton(button);
|
if (pressed) {
|
||||||
|
ClickGuiKeyBindManager.addPendingMouseButton(button);
|
||||||
|
} else {
|
||||||
|
ClickGuiKeyBindManager.onMouseReleased(button);
|
||||||
|
}
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (ClickGuiRenderer.textEditor != null) {
|
if (ClickGuiRenderer.textEditor != null) {
|
||||||
|
|
@ -96,7 +100,11 @@ final class ClickGuiInputHandler {
|
||||||
private static void handleMouseButton(int button, boolean pressed) {
|
private static void handleMouseButton(int button, boolean pressed) {
|
||||||
if (!ClickGuiLifecycle.isInputReady()) return;
|
if (!ClickGuiLifecycle.isInputReady()) return;
|
||||||
if (ClickGuiRenderer.waitingForKey) {
|
if (ClickGuiRenderer.waitingForKey) {
|
||||||
if (pressed) ClickGuiKeyBindManager.addPendingMouseButton(button);
|
if (pressed) {
|
||||||
|
ClickGuiKeyBindManager.addPendingMouseButton(button);
|
||||||
|
} else {
|
||||||
|
ClickGuiKeyBindManager.onMouseReleased(button);
|
||||||
|
}
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -6,6 +6,7 @@ import dev.loki.lovisual.util.input.KeyUtil;
|
||||||
import dev.loki.lovisual.util.wav.ClickGuiSounds;
|
import dev.loki.lovisual.util.wav.ClickGuiSounds;
|
||||||
import org.lwjgl.glfw.GLFW;
|
import org.lwjgl.glfw.GLFW;
|
||||||
|
|
||||||
|
import java.util.LinkedHashSet;
|
||||||
import java.util.Set;
|
import java.util.Set;
|
||||||
|
|
||||||
final class ClickGuiKeyBindManager {
|
final class ClickGuiKeyBindManager {
|
||||||
|
|
@ -13,11 +14,18 @@ final class ClickGuiKeyBindManager {
|
||||||
private ClickGuiKeyBindManager() {
|
private ClickGuiKeyBindManager() {
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Keys/buttons currently physically held down while capturing a bind.
|
||||||
|
// A single tap (press+release with nothing else pressed meanwhile) commits
|
||||||
|
// instantly on release; holding it while pressing another key builds a combo,
|
||||||
|
// which commits once every held key in it has been released.
|
||||||
|
private static final Set<Integer> heldKeys = new LinkedHashSet<>();
|
||||||
|
|
||||||
static void beginKeyBind(Object owner) {
|
static void beginKeyBind(Object owner) {
|
||||||
ClickGuiEditorManager.clearInlineText();
|
ClickGuiEditorManager.clearInlineText();
|
||||||
ClickGuiRenderer.waitingForKey = true;
|
ClickGuiRenderer.waitingForKey = true;
|
||||||
ClickGuiRenderer.keyBindOwner = owner;
|
ClickGuiRenderer.keyBindOwner = owner;
|
||||||
ClickGuiRenderer.pendingBindKeys.clear();
|
ClickGuiRenderer.pendingBindKeys.clear();
|
||||||
|
heldKeys.clear();
|
||||||
ClickGuiSearch.deactivate();
|
ClickGuiSearch.deactivate();
|
||||||
ClickGuiSounds.bindingStart();
|
ClickGuiSounds.bindingStart();
|
||||||
}
|
}
|
||||||
|
|
@ -26,17 +34,51 @@ final class ClickGuiKeyBindManager {
|
||||||
ClickGuiRenderer.waitingForKey = false;
|
ClickGuiRenderer.waitingForKey = false;
|
||||||
ClickGuiRenderer.keyBindOwner = null;
|
ClickGuiRenderer.keyBindOwner = null;
|
||||||
ClickGuiRenderer.pendingBindKeys.clear();
|
ClickGuiRenderer.pendingBindKeys.clear();
|
||||||
|
heldKeys.clear();
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Cancels bind capture without touching the previously saved value. */
|
||||||
|
static void cancelKeyBind() {
|
||||||
|
if (!ClickGuiRenderer.waitingForKey) return;
|
||||||
|
ClickGuiRenderer.waitingForKey = false;
|
||||||
|
ClickGuiRenderer.keyBindOwner = null;
|
||||||
|
ClickGuiRenderer.pendingBindKeys.clear();
|
||||||
|
heldKeys.clear();
|
||||||
|
ClickGuiSounds.bindingStart();
|
||||||
}
|
}
|
||||||
|
|
||||||
static void addPendingKey(int keyCode, int scancode) {
|
static void addPendingKey(int keyCode, int scancode) {
|
||||||
int code = KeyUtil.fromKeyInput(keyCode, scancode);
|
int code = KeyUtil.fromKeyInput(keyCode, scancode);
|
||||||
if (code == KeyUtil.NONE) return;
|
if (code == KeyUtil.NONE) return;
|
||||||
ClickGuiRenderer.pendingBindKeys.add(code);
|
ClickGuiRenderer.pendingBindKeys.add(code);
|
||||||
|
heldKeys.add(code);
|
||||||
}
|
}
|
||||||
|
|
||||||
static void addPendingMouseButton(int button) {
|
static void addPendingMouseButton(int button) {
|
||||||
if (button < GLFW.GLFW_MOUSE_BUTTON_1 || button > GLFW.GLFW_MOUSE_BUTTON_LAST) return;
|
if (button < GLFW.GLFW_MOUSE_BUTTON_1 || button > GLFW.GLFW_MOUSE_BUTTON_LAST) return;
|
||||||
ClickGuiRenderer.pendingBindKeys.add(KeyUtil.mouseButtonToCode(button));
|
int code = KeyUtil.mouseButtonToCode(button);
|
||||||
|
ClickGuiRenderer.pendingBindKeys.add(code);
|
||||||
|
heldKeys.add(code);
|
||||||
|
}
|
||||||
|
|
||||||
|
static void onKeyReleased(int keyCode, int scancode) {
|
||||||
|
handleRelease(KeyUtil.fromKeyInput(keyCode, scancode));
|
||||||
|
}
|
||||||
|
|
||||||
|
static void onMouseReleased(int button) {
|
||||||
|
if (button < GLFW.GLFW_MOUSE_BUTTON_1 || button > GLFW.GLFW_MOUSE_BUTTON_LAST) return;
|
||||||
|
handleRelease(KeyUtil.mouseButtonToCode(button));
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void handleRelease(int code) {
|
||||||
|
if (!ClickGuiRenderer.waitingForKey) return;
|
||||||
|
if (code == KeyUtil.NONE) return;
|
||||||
|
if (!heldKeys.remove(code)) return;
|
||||||
|
if (!heldKeys.isEmpty()) return;
|
||||||
|
if (ClickGuiRenderer.pendingBindKeys.isEmpty()) return;
|
||||||
|
|
||||||
|
submitKeySelection(ClickGuiRenderer.pendingBindKeys);
|
||||||
|
finishKeyBind();
|
||||||
}
|
}
|
||||||
|
|
||||||
static void submitKeySelection(Set<Integer> keys) {
|
static void submitKeySelection(Set<Integer> keys) {
|
||||||
|
|
|
||||||
|
|
@ -122,8 +122,9 @@ final class ClickGuiKeyboardHandler {
|
||||||
if (ClickGuiRenderer.waitingForKey) {
|
if (ClickGuiRenderer.waitingForKey) {
|
||||||
|
|
||||||
if (key == GLFW.GLFW_KEY_ESCAPE) {
|
if (key == GLFW.GLFW_KEY_ESCAPE) {
|
||||||
ClickGuiKeyBindManager.submitKeySelection((String) null);
|
// Escape always exits the bind capture immediately without
|
||||||
ClickGuiKeyBindManager.finishKeyBind();
|
// touching the previously saved bind.
|
||||||
|
ClickGuiKeyBindManager.cancelKeyBind();
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -135,6 +136,11 @@ final class ClickGuiKeyboardHandler {
|
||||||
}
|
}
|
||||||
|
|
||||||
ClickGuiKeyBindManager.addPendingKey(key, scancode);
|
ClickGuiKeyBindManager.addPendingKey(key, scancode);
|
||||||
|
} else if (action == GLFW.GLFW_RELEASE) {
|
||||||
|
// A tap (press+release with no other key held) commits instantly;
|
||||||
|
// holding this key while pressing another builds a combo, which
|
||||||
|
// commits once every held key in it has been released.
|
||||||
|
ClickGuiKeyBindManager.onKeyReleased(key, scancode);
|
||||||
}
|
}
|
||||||
|
|
||||||
return true;
|
return true;
|
||||||
|
|
|
||||||
|
|
@ -53,6 +53,9 @@ import java.util.Map;
|
||||||
@HudElementRegister(order = 20)
|
@HudElementRegister(order = 20)
|
||||||
public final class BetterTooltips extends AbstractHudElement {
|
public final class BetterTooltips extends AbstractHudElement {
|
||||||
|
|
||||||
|
private static final String[] TOOLTIP_FONT_OPTIONS = {
|
||||||
|
"Onest", "OnestMedium", "OnestLight", "Inter", "InterMedium", "Comfortaa", "Iosevka", "ProFont", "Monsterrat"
|
||||||
|
};
|
||||||
public static final BetterTooltips INSTANCE = new BetterTooltips();
|
public static final BetterTooltips INSTANCE = new BetterTooltips();
|
||||||
public static final String GROUP_TOOLTIP_GUI = "vanilla_tooltip_gui";
|
public static final String GROUP_TOOLTIP_GUI = "vanilla_tooltip_gui";
|
||||||
public static final String GROUP_TOOLTIP_ITEMS = "vanilla_tooltip_items";
|
public static final String GROUP_TOOLTIP_ITEMS = "vanilla_tooltip_items";
|
||||||
|
|
@ -95,6 +98,9 @@ public final class BetterTooltips extends AbstractHudElement {
|
||||||
private final BooleanValue itemTooltipEnabled = bool("item_tooltip", true);
|
private final BooleanValue itemTooltipEnabled = bool("item_tooltip", true);
|
||||||
private final BooleanValue shulkerPreviewEnabled = bool("shulker_preview", true);
|
private final BooleanValue shulkerPreviewEnabled = bool("shulker_preview", true);
|
||||||
private final BooleanValue itemInfoColorize = bool("item_info_colorize", true);
|
private final BooleanValue itemInfoColorize = bool("item_info_colorize", true);
|
||||||
|
private final ModeValue tooltipFont = mode("tooltip_font", "Iosevka", TOOLTIP_FONT_OPTIONS);
|
||||||
|
private final NumberValue<Float> tooltipTextSize =
|
||||||
|
num("tooltip_text_size", 1.0f, 0.5f, 2.0f);
|
||||||
private final ItemIdSetValue topIgnore = registerTopIgnore();
|
private final ItemIdSetValue topIgnore = registerTopIgnore();
|
||||||
private final NumberValue<Float> shulkerPreviewSlotSize =
|
private final NumberValue<Float> shulkerPreviewSlotSize =
|
||||||
visibleWhen(num("shulker_slot_size", 18.0f, 12.0f, 24.0f), this::isShulkerPreviewEnabled);
|
visibleWhen(num("shulker_slot_size", 18.0f, 12.0f, 24.0f), this::isShulkerPreviewEnabled);
|
||||||
|
|
@ -197,14 +203,14 @@ public final class BetterTooltips extends AbstractHudElement {
|
||||||
Minecraft mc = Minecraft.getInstance();
|
Minecraft mc = Minecraft.getInstance();
|
||||||
if (mc == null) return;
|
if (mc == null) return;
|
||||||
|
|
||||||
TextRenderer tr = Fonts.renderer("Iosevka", FontInfo.Type.Regular, TextRenderer.get());
|
TextRenderer tr = Fonts.renderer(INSTANCE.tooltipFont.get(), FontInfo.Type.Regular, TextRenderer.get());
|
||||||
if (tr == null) return;
|
if (tr == null) return;
|
||||||
|
|
||||||
PreviewLayout preview = buildShulkerPreviewLayout();
|
PreviewLayout preview = buildShulkerPreviewLayout();
|
||||||
boolean previewOpen = preview != null;
|
boolean previewOpen = preview != null;
|
||||||
List<TooltipLine> finalLines = prepareTooltipLines(lines, previewOpen);
|
List<TooltipLine> finalLines = prepareTooltipLines(lines, previewOpen);
|
||||||
|
|
||||||
float textScale = TOOLTIP_TEXT_SCALE * TOOLTIP_SCALE_OVERRIDE;
|
float textScale = TOOLTIP_TEXT_SCALE * TOOLTIP_SCALE_OVERRIDE * INSTANCE.tooltipTextSize.get();
|
||||||
tr.begin(textScale, false, false);
|
tr.begin(textScale, false, false);
|
||||||
int maxWidth = 0;
|
int maxWidth = 0;
|
||||||
for (TooltipLine line : finalLines) {
|
for (TooltipLine line : finalLines) {
|
||||||
|
|
|
||||||
|
|
@ -28,6 +28,8 @@ import static dev.loki.lovisual.features.theme.Theme.theme;
|
||||||
@HudElementRegister(order = 40)
|
@HudElementRegister(order = 40)
|
||||||
public final class CustomBar extends AbstractHudElement {
|
public final class CustomBar extends AbstractHudElement {
|
||||||
|
|
||||||
|
static final String COLOR_THEME = "Theme";
|
||||||
|
static final String COLOR_CUSTOM = "Custom";
|
||||||
public static final CustomBar INSTANCE = new CustomBar();
|
public static final CustomBar INSTANCE = new CustomBar();
|
||||||
static final float XP_BAR_WIDTH = 182f;
|
static final float XP_BAR_WIDTH = 182f;
|
||||||
static final double XP_TEXT_SCALE = 0.59;
|
static final double XP_TEXT_SCALE = 0.59;
|
||||||
|
|
@ -77,8 +79,6 @@ public final class CustomBar extends AbstractHudElement {
|
||||||
static final float LOCATOR_LABEL_CHIP_RADIUS = 3.6f;
|
static final float LOCATOR_LABEL_CHIP_RADIUS = 3.6f;
|
||||||
static final float LOCATOR_LABEL_ACCENT_W = 1.15f;
|
static final float LOCATOR_LABEL_ACCENT_W = 1.15f;
|
||||||
static final int LOCATOR_LABEL_MAX_ROWS = 4;
|
static final int LOCATOR_LABEL_MAX_ROWS = 4;
|
||||||
static final String COLOR_THEME = "Theme";
|
|
||||||
static final String COLOR_CUSTOM = "Custom";
|
|
||||||
|
|
||||||
private final NumberValue<Integer> overlayAlpha =
|
private final NumberValue<Integer> overlayAlpha =
|
||||||
new NumberValue<>("overlay_alpha", 255, 30, 255);
|
new NumberValue<>("overlay_alpha", 255, 30, 255);
|
||||||
|
|
|
||||||
|
|
@ -39,8 +39,10 @@ import static dev.loki.lovisual.features.theme.Theme.theme;
|
||||||
@HudElementRegister(order = 50)
|
@HudElementRegister(order = 50)
|
||||||
public final class CustomHealthBar extends AbstractHudElement {
|
public final class CustomHealthBar extends AbstractHudElement {
|
||||||
|
|
||||||
public static final CustomHealthBar INSTANCE = new CustomHealthBar();
|
|
||||||
private static final String LAYOUT_ID = "lovisual:api/hud/static/custom_health_bar";
|
private static final String LAYOUT_ID = "lovisual:api/hud/static/custom_health_bar";
|
||||||
|
private static final String STYLE_TARGET_HUD = "TargetHud";
|
||||||
|
private static final String STYLE_DYNAMIC = "Dynamic";
|
||||||
|
public static final CustomHealthBar INSTANCE = new CustomHealthBar();
|
||||||
private static final float HEALTH_BAR_WIDTH = 81f;
|
private static final float HEALTH_BAR_WIDTH = 81f;
|
||||||
private static final float HEALTH_BAR_HEIGHT = 9f;
|
private static final float HEALTH_BAR_HEIGHT = 9f;
|
||||||
private static final float HEALTH_BAR_RADIUS = 5.0f;
|
private static final float HEALTH_BAR_RADIUS = 5.0f;
|
||||||
|
|
@ -50,8 +52,6 @@ public final class CustomHealthBar extends AbstractHudElement {
|
||||||
private static final int HEALTH_BAR_X_OFFSET = 0;
|
private static final int HEALTH_BAR_X_OFFSET = 0;
|
||||||
private static final int HEALTH_BAR_Y_OFFSET = 0;
|
private static final int HEALTH_BAR_Y_OFFSET = 0;
|
||||||
private static final float HEALTH_ANIM_SPEED = 10.0f;
|
private static final float HEALTH_ANIM_SPEED = 10.0f;
|
||||||
private static final String STYLE_TARGET_HUD = "TargetHud";
|
|
||||||
private static final String STYLE_DYNAMIC = "Dynamic";
|
|
||||||
private static float animatedHealth = -1.0f;
|
private static float animatedHealth = -1.0f;
|
||||||
private static float animatedAbsorb = -1.0f;
|
private static float animatedAbsorb = -1.0f;
|
||||||
private static long lastHealthAnimMs = -1L;
|
private static long lastHealthAnimMs = -1L;
|
||||||
|
|
|
||||||
|
|
@ -33,6 +33,11 @@ import static dev.loki.lovisual.features.theme.Theme.theme;
|
||||||
@HudElementRegister(order = 30)
|
@HudElementRegister(order = 30)
|
||||||
public final class CustomHotbar extends AbstractHudElement {
|
public final class CustomHotbar extends AbstractHudElement {
|
||||||
|
|
||||||
|
private static final String EFFECT_NONE = "None";
|
||||||
|
private static final String EFFECT_BLUR = "Blur";
|
||||||
|
private static final String EFFECT_GLASS = "Glass";
|
||||||
|
private static final String COLOR_THEME = "Theme";
|
||||||
|
private static final String COLOR_CUSTOM = "Custom";
|
||||||
public static final CustomHotbar INSTANCE = new CustomHotbar();
|
public static final CustomHotbar INSTANCE = new CustomHotbar();
|
||||||
private static final int BG_ALPHA = 0xAA;
|
private static final int BG_ALPHA = 0xAA;
|
||||||
private static final float RADIUS = 6.0f;
|
private static final float RADIUS = 6.0f;
|
||||||
|
|
@ -49,11 +54,6 @@ public final class CustomHotbar extends AbstractHudElement {
|
||||||
private static final float SELECT_SLOT_STEP = SLOT_STEP;
|
private static final float SELECT_SLOT_STEP = SLOT_STEP;
|
||||||
private static final float SELECT_ANIMATION_MS = 145.0f;
|
private static final float SELECT_ANIMATION_MS = 145.0f;
|
||||||
private static final float SELECT_ANIMATION_MIN_PROGRESS = 0.015f;
|
private static final float SELECT_ANIMATION_MIN_PROGRESS = 0.015f;
|
||||||
private static final String EFFECT_NONE = "None";
|
|
||||||
private static final String EFFECT_BLUR = "Blur";
|
|
||||||
private static final String EFFECT_GLASS = "Glass";
|
|
||||||
private static final String COLOR_THEME = "Theme";
|
|
||||||
private static final String COLOR_CUSTOM = "Custom";
|
|
||||||
private static int animatedSelectedSlot = -1;
|
private static int animatedSelectedSlot = -1;
|
||||||
private static float selectionStartSlot = 0.0f;
|
private static float selectionStartSlot = 0.0f;
|
||||||
private static float selectionTargetSlot = 0.0f;
|
private static float selectionTargetSlot = 0.0f;
|
||||||
|
|
|
||||||
|
|
@ -69,9 +69,8 @@ import static dev.loki.lovisual.features.theme.Theme.theme;
|
||||||
|
|
||||||
@HudElementRegister(order = 60)
|
@HudElementRegister(order = 60)
|
||||||
public final class DynamicIsland extends AbstractHudElement {
|
public final class DynamicIsland extends AbstractHudElement {
|
||||||
public static final DynamicIsland INSTANCE = new DynamicIsland();
|
|
||||||
|
|
||||||
private static final String LAYOUT_ID = "lovisual:api/hud/static/dynamic_island";
|
private static final String LAYOUT_ID = "lovisual:api/hud/static/dynamic_island";
|
||||||
|
public static final DynamicIsland INSTANCE = new DynamicIsland();
|
||||||
|
|
||||||
private static final float TOP_Y = 18f;
|
private static final float TOP_Y = 18f;
|
||||||
private static final float CLOSED_HEIGHT = 35f;
|
private static final float CLOSED_HEIGHT = 35f;
|
||||||
|
|
|
||||||
|
|
@ -1,258 +0,0 @@
|
||||||
/*
|
|
||||||
* This file is part of the LoVisual Client distribution.
|
|
||||||
* Copyright (c) 2026 pivosos2007.
|
|
||||||
*
|
|
||||||
* Licensed under the GNU General Public License v3.0.
|
|
||||||
*/
|
|
||||||
|
|
||||||
package dev.loki.lovisual.features.module.modules.combat;
|
|
||||||
|
|
||||||
import net.minecraft.client.Minecraft;
|
|
||||||
import net.minecraft.client.player.LocalPlayer;
|
|
||||||
import net.minecraft.world.entity.Entity;
|
|
||||||
import net.minecraft.world.entity.LivingEntity;
|
|
||||||
import net.minecraft.world.entity.player.Player;
|
|
||||||
import net.minecraft.world.level.ClipContext;
|
|
||||||
import net.minecraft.world.phys.*;
|
|
||||||
import dev.loki.lovisual.config.common.CommonSettingSchemas;
|
|
||||||
import dev.loki.lovisual.config.values.EnumValue;
|
|
||||||
import dev.loki.lovisual.config.values.NumberValue;
|
|
||||||
import dev.loki.lovisual.features.module.Module;
|
|
||||||
import dev.loki.lovisual.features.module.ModuleCategory;
|
|
||||||
import dev.loki.lovisual.features.module.ModuleInfo;
|
|
||||||
import dev.loki.lovisual.util.combat.VulcanReachController;
|
|
||||||
|
|
||||||
import java.util.List;
|
|
||||||
import java.util.Optional;
|
|
||||||
|
|
||||||
//todo Description
|
|
||||||
@ModuleInfo(
|
|
||||||
id = "reach",
|
|
||||||
displayName = "Reach",
|
|
||||||
category = ModuleCategory.COMBAT
|
|
||||||
)
|
|
||||||
public class Reach extends Module {
|
|
||||||
|
|
||||||
public enum RaycastMode {
|
|
||||||
STRICT,
|
|
||||||
THROUGH_WALLS,
|
|
||||||
NONE
|
|
||||||
}
|
|
||||||
|
|
||||||
public final EnumValue<RaycastMode> raycast =
|
|
||||||
enumCommon(
|
|
||||||
"reachContext",
|
|
||||||
"raycast",
|
|
||||||
CommonSettingSchemas.COMBAT_RAYCAST,
|
|
||||||
RaycastMode.STRICT,
|
|
||||||
RaycastMode.values()
|
|
||||||
);
|
|
||||||
private final Minecraft mc = Minecraft.getInstance();
|
|
||||||
|
|
||||||
private final EnumValue<Mode> mode =
|
|
||||||
enumCommon(
|
|
||||||
"reachMode",
|
|
||||||
"mode",
|
|
||||||
CommonSettingSchemas.ANTICHEAT_MODE,
|
|
||||||
Mode.NORMAL,
|
|
||||||
Mode.values()
|
|
||||||
);
|
|
||||||
|
|
||||||
private final NumberValue<Double> distance =
|
|
||||||
numCommon(
|
|
||||||
"reachDistance",
|
|
||||||
"range",
|
|
||||||
CommonSettingSchemas.COMBAT_RANGE,
|
|
||||||
4.5,
|
|
||||||
3.0,
|
|
||||||
6.0
|
|
||||||
);
|
|
||||||
private final NumberValue<Double> wallDistance =
|
|
||||||
visibleWhen(numCommon(
|
|
||||||
"reachWallDistance",
|
|
||||||
"wall_range",
|
|
||||||
CommonSettingSchemas.COMBAT_WALL_RANGE,
|
|
||||||
3.0,
|
|
||||||
0.0,
|
|
||||||
6.0
|
|
||||||
), () -> raycast.get() == RaycastMode.THROUGH_WALLS);
|
|
||||||
|
|
||||||
private static double boxDistanceSq(AABB box, Vec3 p) {
|
|
||||||
double dx = 0.0;
|
|
||||||
if (p.x < box.minX) dx = box.minX - p.x;
|
|
||||||
else if (p.x > box.maxX) dx = p.x - box.maxX;
|
|
||||||
|
|
||||||
double dy = 0.0;
|
|
||||||
if (p.y < box.minY) dy = box.minY - p.y;
|
|
||||||
else if (p.y > box.maxY) dy = p.y - box.maxY;
|
|
||||||
|
|
||||||
double dz = 0.0;
|
|
||||||
if (p.z < box.minZ) dz = box.minZ - p.z;
|
|
||||||
else if (p.z > box.maxZ) dz = p.z - box.maxZ;
|
|
||||||
|
|
||||||
return dx * dx + dy * dy + dz * dz;
|
|
||||||
}
|
|
||||||
|
|
||||||
public EntityHitResult raycastEntities(LocalPlayer player) {
|
|
||||||
if (!isEnabled()) return null;
|
|
||||||
if (player == null || mc.level == null) return null;
|
|
||||||
|
|
||||||
double visibleRange = distance.get();
|
|
||||||
double throughWallsRange = raycast.get() == RaycastMode.THROUGH_WALLS
|
|
||||||
? wallDistance.get()
|
|
||||||
: 0.0;
|
|
||||||
double searchRange = Math.max(visibleRange, throughWallsRange);
|
|
||||||
|
|
||||||
Vec3 start = player.getEyePosition(1.0F);
|
|
||||||
Vec3 direction = player.getViewVector(1.0F).normalize();
|
|
||||||
Vec3 end = start.add(direction.scale(searchRange));
|
|
||||||
|
|
||||||
double blockLimitSq = resolveBlockLimitSq(player, start, end, visibleRange, throughWallsRange);
|
|
||||||
|
|
||||||
AABB search = new AABB(start, end).inflate(1.0);
|
|
||||||
|
|
||||||
List<Entity> candidates = mc.level.getEntities(
|
|
||||||
player,
|
|
||||||
search,
|
|
||||||
e -> e instanceof LivingEntity && e.isAlive()
|
|
||||||
);
|
|
||||||
|
|
||||||
return findBestHit(player, start, end, candidates, blockLimitSq, searchRange);
|
|
||||||
}
|
|
||||||
|
|
||||||
public EntityHitResult raycastEntities(LocalPlayer player, Vec3 start, Vec3 direction) {
|
|
||||||
if (!isEnabled()) return null;
|
|
||||||
if (player == null || mc.level == null) return null;
|
|
||||||
|
|
||||||
double visibleRange = distance.get();
|
|
||||||
double throughWallsRange = raycast.get() == RaycastMode.THROUGH_WALLS
|
|
||||||
? wallDistance.get()
|
|
||||||
: 0.0;
|
|
||||||
double searchRange = Math.max(visibleRange, throughWallsRange);
|
|
||||||
|
|
||||||
Vec3 dir = direction.normalize();
|
|
||||||
Vec3 end = start.add(dir.scale(searchRange));
|
|
||||||
|
|
||||||
double blockLimitSq = resolveBlockLimitSq(player, start, end, visibleRange, throughWallsRange);
|
|
||||||
|
|
||||||
AABB search = new AABB(start, end).inflate(1.0);
|
|
||||||
|
|
||||||
List<Entity> candidates = mc.level.getEntities(
|
|
||||||
player,
|
|
||||||
search,
|
|
||||||
e -> e instanceof LivingEntity && e.isAlive()
|
|
||||||
);
|
|
||||||
|
|
||||||
return findBestHit(player, start, end, candidates, blockLimitSq, searchRange);
|
|
||||||
}
|
|
||||||
|
|
||||||
private EntityHitResult findBestHit(LocalPlayer player,
|
|
||||||
Vec3 start,
|
|
||||||
Vec3 end,
|
|
||||||
List<Entity> candidates,
|
|
||||||
double blockLimitSq,
|
|
||||||
double requestedReach) {
|
|
||||||
EntityHitResult bestHit = null;
|
|
||||||
double bestBoxDistSq = Double.MAX_VALUE;
|
|
||||||
double bestHitDistSq = Double.MAX_VALUE;
|
|
||||||
|
|
||||||
for (Entity e : candidates) {
|
|
||||||
AABB baseBox = e.getBoundingBox();
|
|
||||||
AABB bb = baseBox;
|
|
||||||
|
|
||||||
Optional<Vec3> hit = bb.contains(start)
|
|
||||||
? Optional.of(start)
|
|
||||||
: bb.clip(start, end);
|
|
||||||
|
|
||||||
if (hit.isEmpty()) continue;
|
|
||||||
|
|
||||||
double boxDistSq = boxDistanceSq(baseBox, start);
|
|
||||||
double entityReach = resolveEntityReach(e, requestedReach);
|
|
||||||
double entityReachSq = entityReach * entityReach;
|
|
||||||
if (boxDistSq > entityReachSq) continue;
|
|
||||||
if (boxDistSq > blockLimitSq) continue;
|
|
||||||
|
|
||||||
double hitDistSq = start.distanceToSqr(hit.get());
|
|
||||||
|
|
||||||
if (boxDistSq < bestBoxDistSq
|
|
||||||
|| (boxDistSq == bestBoxDistSq && hitDistSq < bestHitDistSq)) {
|
|
||||||
bestBoxDistSq = boxDistSq;
|
|
||||||
bestHitDistSq = hitDistSq;
|
|
||||||
bestHit = new EntityHitResult(e, hit.get());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return bestHit;
|
|
||||||
}
|
|
||||||
|
|
||||||
private double resolveEntityReach(Entity entity, double requestedReach) {
|
|
||||||
if (mode.get() == Mode.VULCAN_297 && entity instanceof Player) {
|
|
||||||
return VulcanReachController.INSTANCE.clampReach(entity, requestedReach);
|
|
||||||
}
|
|
||||||
return requestedReach;
|
|
||||||
}
|
|
||||||
|
|
||||||
public boolean isVulcan297Mode() {
|
|
||||||
return mode.get() == Mode.VULCAN_297;
|
|
||||||
}
|
|
||||||
|
|
||||||
public double getConfiguredDistance() {
|
|
||||||
return distance.get();
|
|
||||||
}
|
|
||||||
|
|
||||||
private double resolveBlockLimitSq(LocalPlayer player,
|
|
||||||
Vec3 start,
|
|
||||||
Vec3 end,
|
|
||||||
double visibleRange,
|
|
||||||
double throughWallsRange) {
|
|
||||||
RaycastMode mode = raycast.get();
|
|
||||||
|
|
||||||
if (mode == RaycastMode.NONE) {
|
|
||||||
double maxRange = Math.max(visibleRange, throughWallsRange);
|
|
||||||
return maxRange * maxRange;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (mode == RaycastMode.THROUGH_WALLS) {
|
|
||||||
double maxAllowed = Math.max(visibleRange, throughWallsRange);
|
|
||||||
|
|
||||||
HitResult blockHit = mc.level.clip(new ClipContext(
|
|
||||||
start,
|
|
||||||
end,
|
|
||||||
ClipContext.Block.OUTLINE,
|
|
||||||
ClipContext.Fluid.NONE,
|
|
||||||
player
|
|
||||||
));
|
|
||||||
|
|
||||||
if (!(blockHit instanceof BlockHitResult bhr)) {
|
|
||||||
return maxAllowed * maxAllowed;
|
|
||||||
}
|
|
||||||
|
|
||||||
double blockDistSq = bhr.getLocation().distanceToSqr(start);
|
|
||||||
|
|
||||||
if (throughWallsRange <= 0.0) {
|
|
||||||
return blockDistSq;
|
|
||||||
}
|
|
||||||
|
|
||||||
return Math.max(blockDistSq, throughWallsRange * throughWallsRange);
|
|
||||||
}
|
|
||||||
|
|
||||||
HitResult blockHit = mc.level.clip(new ClipContext(
|
|
||||||
start,
|
|
||||||
end,
|
|
||||||
ClipContext.Block.OUTLINE,
|
|
||||||
ClipContext.Fluid.NONE,
|
|
||||||
player
|
|
||||||
));
|
|
||||||
|
|
||||||
if (blockHit instanceof BlockHitResult bhr) {
|
|
||||||
return bhr.getLocation().distanceToSqr(start);
|
|
||||||
}
|
|
||||||
|
|
||||||
return visibleRange * visibleRange;
|
|
||||||
}
|
|
||||||
|
|
||||||
public enum Mode {
|
|
||||||
NORMAL,
|
|
||||||
VULCAN_297
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -68,6 +68,7 @@ public class TargetESP extends Module {
|
||||||
private static final String SETTING_BLACKHOLE_SIZE = "blackhole_size";
|
private static final String SETTING_BLACKHOLE_SIZE = "blackhole_size";
|
||||||
private static final String SETTING_BLACKHOLE_SPIN_SPEED = "blackhole_spin_speed";
|
private static final String SETTING_BLACKHOLE_SPIN_SPEED = "blackhole_spin_speed";
|
||||||
private static final String SETTING_BLACKHOLE_STYLE = "blackhole_style";
|
private static final String SETTING_BLACKHOLE_STYLE = "blackhole_style";
|
||||||
|
private static final String SETTING_VORTEX_SPEED = "vortex_speed";
|
||||||
private static final Identifier GHOST_PARTICLE_TEXTURE = TextureStorage.FIRE_FLY;
|
private static final Identifier GHOST_PARTICLE_TEXTURE = TextureStorage.FIRE_FLY;
|
||||||
private static final Identifier CAPTURE_MARK_TEXTURE = TextureStorage.CAPTURE;
|
private static final Identifier CAPTURE_MARK_TEXTURE = TextureStorage.CAPTURE;
|
||||||
private static final Identifier CRYSTAL_BLOOM = TextureStorage.BLOOM;
|
private static final Identifier CRYSTAL_BLOOM = TextureStorage.BLOOM;
|
||||||
|
|
@ -87,7 +88,7 @@ public class TargetESP extends Module {
|
||||||
"targetEspMode",
|
"targetEspMode",
|
||||||
SETTING_MODE,
|
SETTING_MODE,
|
||||||
"ghosts",
|
"ghosts",
|
||||||
"ghosts", "ring", "capture_mark", "crystals", "blackhole"
|
"ghosts", "ring", "capture_mark", "crystals", "blackhole", "vortex"
|
||||||
);
|
);
|
||||||
private final RGBAColorValue colorValue = color("targetEspColor", SETTING_COLOR, "#FFFF5555");
|
private final RGBAColorValue colorValue = color("targetEspColor", SETTING_COLOR, "#FFFF5555");
|
||||||
private final ModeValue colorMode = visibleWhen(modeSetting(
|
private final ModeValue colorMode = visibleWhen(modeSetting(
|
||||||
|
|
@ -130,6 +131,8 @@ public class TargetESP extends Module {
|
||||||
visibleWhen(num("targetEspBlackholeSpinSpeed", SETTING_BLACKHOLE_SPIN_SPEED, 1.0f, 0.2f, 4.0f), this::isBlackholeMode);
|
visibleWhen(num("targetEspBlackholeSpinSpeed", SETTING_BLACKHOLE_SPIN_SPEED, 1.0f, 0.2f, 4.0f), this::isBlackholeMode);
|
||||||
private final ModeValue blackholeStyle =
|
private final ModeValue blackholeStyle =
|
||||||
visibleWhen(modeSetting("targetEspBlackholeStyle", SETTING_BLACKHOLE_STYLE, "Single", "Single", "Body"), this::isBlackholeMode);
|
visibleWhen(modeSetting("targetEspBlackholeStyle", SETTING_BLACKHOLE_STYLE, "Single", "Single", "Body"), this::isBlackholeMode);
|
||||||
|
private final NumberValue<Float> vortexSpeed =
|
||||||
|
visibleWhen(num("targetEspVortexSpeed", SETTING_VORTEX_SPEED, 1.2f, 0.2f, 4.0f), this::isVortexMode);
|
||||||
private final List<CrystalInstance> crystalList = new ArrayList<>();
|
private final List<CrystalInstance> crystalList = new ArrayList<>();
|
||||||
private LivingEntity target;
|
private LivingEntity target;
|
||||||
private LivingEntity renderTarget;
|
private LivingEntity renderTarget;
|
||||||
|
|
@ -237,6 +240,75 @@ public class TargetESP extends Module {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static final int VORTEX_HELIX_STEPS = 28;
|
||||||
|
private static final int VORTEX_RING_SEGMENTS = 24;
|
||||||
|
|
||||||
|
private static void renderVortex(Renderer3D renderer,
|
||||||
|
LivingEntity target,
|
||||||
|
float tickDelta,
|
||||||
|
IntFunction<Integer> palette,
|
||||||
|
float spinSpeed) {
|
||||||
|
if (renderer == null || target == null) return;
|
||||||
|
|
||||||
|
Vec3 pos = RenderMath.getLerpedPos(target, tickDelta);
|
||||||
|
float radius = target.getBbWidth() * 0.7f;
|
||||||
|
float height = target.getBbHeight();
|
||||||
|
double time = (GameClock.millis() / 1000.0) * spinSpeed;
|
||||||
|
|
||||||
|
float prevWidth = RenderState.lineWidth;
|
||||||
|
RenderState.lineWidth = 1.4f;
|
||||||
|
MeshBuilder mesh;
|
||||||
|
try {
|
||||||
|
mesh = renderer.batch(LoVisualRenderPipelines.WORLD_COLORED_LINES, Renderer3D.DepthMode.NONE);
|
||||||
|
} finally {
|
||||||
|
RenderState.lineWidth = prevWidth;
|
||||||
|
}
|
||||||
|
if (mesh == null) return;
|
||||||
|
|
||||||
|
for (int strand = 0; strand < 2; strand++) {
|
||||||
|
double strandOffset = strand * Math.PI;
|
||||||
|
int prevIndex = -1;
|
||||||
|
for (int i = 0; i <= VORTEX_HELIX_STEPS; i++) {
|
||||||
|
double t = (double) i / VORTEX_HELIX_STEPS;
|
||||||
|
double angle = time * 2.4 + t * Math.PI * 3.0 + strandOffset;
|
||||||
|
double r = radius * (0.85 + 0.15 * Math.sin(t * Math.PI));
|
||||||
|
double x = pos.x + Math.cos(angle) * r;
|
||||||
|
double y = pos.y + t * height;
|
||||||
|
double z = pos.z + Math.sin(angle) * r;
|
||||||
|
int color = applyOpacity(palette.apply((int) (t * 180)), (float) (1.0 - t * 0.35));
|
||||||
|
|
||||||
|
int index = addVertex(mesh, x, y, z, color);
|
||||||
|
if (prevIndex >= 0) {
|
||||||
|
mesh.line(prevIndex, index);
|
||||||
|
}
|
||||||
|
prevIndex = index;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
float[] ringHeights = {0.05f, height * 0.5f, height * 0.95f};
|
||||||
|
float[] ringSpeeds = {0.4f, -0.55f, 0.35f};
|
||||||
|
for (int ring = 0; ring < ringHeights.length; ring++) {
|
||||||
|
double ringY = pos.y + ringHeights[ring];
|
||||||
|
double ringRotation = time * ringSpeeds[ring];
|
||||||
|
int ringColor = applyOpacity(palette.apply(60), 0.4f);
|
||||||
|
int prevIndex = -1;
|
||||||
|
int firstIndex = -1;
|
||||||
|
for (int i = 0; i <= VORTEX_RING_SEGMENTS; i++) {
|
||||||
|
double angle = ringRotation + (i % VORTEX_RING_SEGMENTS) * 2.0 * Math.PI / VORTEX_RING_SEGMENTS;
|
||||||
|
double x = pos.x + Math.cos(angle) * radius;
|
||||||
|
double z = pos.z + Math.sin(angle) * radius;
|
||||||
|
int index = addVertex(mesh, x, ringY, z, ringColor);
|
||||||
|
if (firstIndex < 0) {
|
||||||
|
firstIndex = index;
|
||||||
|
}
|
||||||
|
if (prevIndex >= 0) {
|
||||||
|
mesh.line(prevIndex, index);
|
||||||
|
}
|
||||||
|
prevIndex = index;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private static void renderCaptureMark(Renderer3D renderer,
|
private static void renderCaptureMark(Renderer3D renderer,
|
||||||
Entity target,
|
Entity target,
|
||||||
float tickDelta,
|
float tickDelta,
|
||||||
|
|
@ -730,6 +802,8 @@ public class TargetESP extends Module {
|
||||||
} else if ("blackhole".equals(mode)) {
|
} else if ("blackhole".equals(mode)) {
|
||||||
renderBlackhole(renderer, renderTarget, tickDelta, palette,
|
renderBlackhole(renderer, renderTarget, tickDelta, palette,
|
||||||
blackholeSize.get(), blackholeSpinSpeed.get(), targetAlpha, blackholeStyle.get());
|
blackholeSize.get(), blackholeSpinSpeed.get(), targetAlpha, blackholeStyle.get());
|
||||||
|
} else if ("vortex".equals(mode)) {
|
||||||
|
renderVortex(renderer, renderTarget, tickDelta, palette, vortexSpeed.get());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -900,6 +974,10 @@ public class TargetESP extends Module {
|
||||||
return "blackhole".equals(espMode.get());
|
return "blackhole".equals(espMode.get());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private boolean isVortexMode() {
|
||||||
|
return "vortex".equals(espMode.get());
|
||||||
|
}
|
||||||
|
|
||||||
private boolean usesSecondaryColor() {
|
private boolean usesSecondaryColor() {
|
||||||
return AnimatedRenderColors.usesSecondary(animatedColorMode());
|
return AnimatedRenderColors.usesSecondary(animatedColorMode());
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,276 @@
|
||||||
|
package dev.loki.lovisual.features.module.modules.visuals;
|
||||||
|
|
||||||
|
import net.minecraft.client.Minecraft;
|
||||||
|
import net.minecraft.core.BlockPos;
|
||||||
|
import net.minecraft.core.registries.BuiltInRegistries;
|
||||||
|
import net.minecraft.world.level.block.Block;
|
||||||
|
import net.minecraft.world.level.block.state.BlockState;
|
||||||
|
import net.minecraft.world.level.material.MapColor;
|
||||||
|
import net.minecraft.world.phys.AABB;
|
||||||
|
import dev.loki.lovisual.config.values.EnumValue;
|
||||||
|
import dev.loki.lovisual.config.values.ItemIdSetValue;
|
||||||
|
import dev.loki.lovisual.config.values.NumberValue;
|
||||||
|
import dev.loki.lovisual.config.values.RGBAColorValue;
|
||||||
|
import dev.loki.lovisual.features.gui.clickgui.settings.TextListSetting;
|
||||||
|
import dev.loki.lovisual.features.module.Module;
|
||||||
|
import dev.loki.lovisual.features.module.ModuleCategory;
|
||||||
|
import dev.loki.lovisual.features.module.ModuleInfo;
|
||||||
|
import dev.loki.lovisual.features.module.WorldPhase;
|
||||||
|
import dev.loki.lovisual.render.engine.animation.AnimatedRenderColors;
|
||||||
|
import dev.loki.lovisual.render.engine.renderer.Renderer3D;
|
||||||
|
import dev.loki.lovisual.util.time.Timer;
|
||||||
|
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.concurrent.ExecutorService;
|
||||||
|
import java.util.concurrent.Executors;
|
||||||
|
import java.util.concurrent.atomic.AtomicBoolean;
|
||||||
|
|
||||||
|
//todo Description
|
||||||
|
@ModuleInfo(
|
||||||
|
id = "blockesp",
|
||||||
|
displayName = "BlockESP",
|
||||||
|
category = ModuleCategory.VISUALS,
|
||||||
|
description = "Подсвечивает выбранные блоки сквозь стены"
|
||||||
|
)
|
||||||
|
public final class BlockESP extends Module {
|
||||||
|
|
||||||
|
private final Minecraft mc = Minecraft.getInstance();
|
||||||
|
|
||||||
|
private final ItemIdSetValue blockList =
|
||||||
|
itemList("block_list", TextListSetting.PickerMode.BLOCKS);
|
||||||
|
|
||||||
|
private final NumberValue<Integer> range =
|
||||||
|
num("range", 64, 8, 160);
|
||||||
|
|
||||||
|
private final NumberValue<Float> scanInterval =
|
||||||
|
num("scan_interval", 2.0f, 0.5f, 10.0f);
|
||||||
|
|
||||||
|
private final RGBAColorValue fillColor =
|
||||||
|
color("fill_color", "#4000FFC8");
|
||||||
|
|
||||||
|
private final RGBAColorValue fillColor2 =
|
||||||
|
visibleWhen(color("fill_color2", "#40FFAA00"), this::usesSecondaryColor);
|
||||||
|
|
||||||
|
private final EnumValue<ColorMode> colorMode =
|
||||||
|
enumMode("color_mode", ColorMode.PER_BLOCK, ColorMode.values());
|
||||||
|
|
||||||
|
private final NumberValue<Integer> colorSpeed =
|
||||||
|
visibleWhen(num("color_speed", 18, 2, 54), () -> colorMode.get() != ColorMode.PER_BLOCK && colorMode.get() != ColorMode.STATIC);
|
||||||
|
|
||||||
|
private final Timer scanTimer = new Timer();
|
||||||
|
private final ExecutorService executor = Executors.newSingleThreadExecutor(r -> {
|
||||||
|
Thread thread = new Thread(r, "BlockESP-Scanner");
|
||||||
|
thread.setDaemon(true);
|
||||||
|
return thread;
|
||||||
|
});
|
||||||
|
private final AtomicBoolean scanning = new AtomicBoolean(false);
|
||||||
|
private final AtomicBoolean scanInProgress = new AtomicBoolean(false);
|
||||||
|
private volatile List<BlockPos> foundBlocks = new ArrayList<>();
|
||||||
|
|
||||||
|
public BlockESP() {
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onEnable() {
|
||||||
|
scanTimer.setMs(0);
|
||||||
|
scanning.set(true);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onDisable() {
|
||||||
|
scanning.set(false);
|
||||||
|
foundBlocks = new ArrayList<>();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onTick() {
|
||||||
|
if (mc.level == null || mc.player == null) return;
|
||||||
|
|
||||||
|
if (scanTimer.passedS(scanInterval.get())) {
|
||||||
|
scanTimer.reset();
|
||||||
|
requestScan();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public WorldPhase getWorldPhase() {
|
||||||
|
return WorldPhase.BEFORE_TRANSLUCENT;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onRenderWorldEngine(Renderer3D renderer, Renderer3D depthRenderer, float tickDelta) {
|
||||||
|
if (!isEnabled() || mc.level == null || mc.player == null) return;
|
||||||
|
if (blockList.get().isEmpty()) return;
|
||||||
|
|
||||||
|
List<BlockPos> snapshot = foundBlocks;
|
||||||
|
for (BlockPos pos : snapshot) {
|
||||||
|
BlockState state = mc.level.getBlockState(pos);
|
||||||
|
if (state.isAir()) continue;
|
||||||
|
if (!blockList.get().contains(blockId(state.getBlock()))) continue;
|
||||||
|
|
||||||
|
int argb = resolveColor(state.getBlock(), pos);
|
||||||
|
addFilledBox(renderer, new AABB(pos), argb);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void requestScan() {
|
||||||
|
if (mc.level == null || mc.player == null) return;
|
||||||
|
if (!scanning.get()) return;
|
||||||
|
if (executor.isShutdown()) return;
|
||||||
|
if (!scanInProgress.compareAndSet(false, true)) return;
|
||||||
|
|
||||||
|
executor.submit(() -> {
|
||||||
|
try {
|
||||||
|
scan();
|
||||||
|
} finally {
|
||||||
|
scanInProgress.set(false);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private void scan() {
|
||||||
|
if (mc.level == null || mc.player == null) return;
|
||||||
|
|
||||||
|
List<BlockPos> result = new ArrayList<>();
|
||||||
|
BlockPos center = mc.player.blockPosition();
|
||||||
|
int r = range.get();
|
||||||
|
|
||||||
|
int minX = center.getX() - r;
|
||||||
|
int maxX = center.getX() + r;
|
||||||
|
int minY = Math.max(mc.level.getMinY(), center.getY() - r);
|
||||||
|
int maxY = Math.min(mc.level.getMaxY(), center.getY() + r);
|
||||||
|
int minZ = center.getZ() - r;
|
||||||
|
int maxZ = center.getZ() + r;
|
||||||
|
|
||||||
|
int minChunkX = minX >> 4;
|
||||||
|
int maxChunkX = maxX >> 4;
|
||||||
|
int minChunkZ = minZ >> 4;
|
||||||
|
int maxChunkZ = maxZ >> 4;
|
||||||
|
|
||||||
|
for (int cx = minChunkX; cx <= maxChunkX; cx++) {
|
||||||
|
for (int cz = minChunkZ; cz <= maxChunkZ; cz++) {
|
||||||
|
if (!scanning.get()) return;
|
||||||
|
if (!mc.level.hasChunk(cx, cz)) continue;
|
||||||
|
|
||||||
|
int chunkMinX = Math.max(minX, cx << 4);
|
||||||
|
int chunkMaxX = Math.min(maxX, (cx << 4) + 15);
|
||||||
|
int chunkMinZ = Math.max(minZ, cz << 4);
|
||||||
|
int chunkMaxZ = Math.min(maxZ, (cz << 4) + 15);
|
||||||
|
|
||||||
|
for (int x = chunkMinX; x <= chunkMaxX; x++) {
|
||||||
|
for (int y = minY; y <= maxY; y++) {
|
||||||
|
for (int z = chunkMinZ; z <= chunkMaxZ; z++) {
|
||||||
|
if (!scanning.get()) return;
|
||||||
|
|
||||||
|
BlockPos pos = new BlockPos(x, y, z);
|
||||||
|
BlockState state = mc.level.getBlockState(pos);
|
||||||
|
if (state.isAir()) continue;
|
||||||
|
|
||||||
|
if (blockList.get().contains(blockId(state.getBlock()))) {
|
||||||
|
result.add(pos.immutable());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
foundBlocks = result;
|
||||||
|
}
|
||||||
|
|
||||||
|
private String blockId(Block block) {
|
||||||
|
return BuiltInRegistries.BLOCK.getKey(block).toString();
|
||||||
|
}
|
||||||
|
|
||||||
|
private int resolveColor(Block block, BlockPos pos) {
|
||||||
|
if (colorMode.get() == ColorMode.PER_BLOCK) {
|
||||||
|
MapColor mapColor = block.defaultMapColor();
|
||||||
|
return withAlpha(mapColor.col, alpha(fillColor.getArgb()));
|
||||||
|
}
|
||||||
|
|
||||||
|
return AnimatedRenderColors.resolve(
|
||||||
|
animatedColorMode(),
|
||||||
|
colorSpeed.get(),
|
||||||
|
spatialIndex(pos),
|
||||||
|
fillColor.getArgb(),
|
||||||
|
fillColor2.getArgb(),
|
||||||
|
true
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
private AnimatedRenderColors.Mode animatedColorMode() {
|
||||||
|
return switch (colorMode.get()) {
|
||||||
|
case PER_BLOCK, STATIC -> AnimatedRenderColors.Mode.STATIC;
|
||||||
|
case RAINBOW -> AnimatedRenderColors.Mode.RAINBOW;
|
||||||
|
case FADE -> AnimatedRenderColors.Mode.FADE;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
private boolean usesSecondaryColor() {
|
||||||
|
return AnimatedRenderColors.usesSecondary(animatedColorMode());
|
||||||
|
}
|
||||||
|
|
||||||
|
private int spatialIndex(BlockPos pos) {
|
||||||
|
return Math.floorMod(pos.getX() * 13 + pos.getY() * 7 + pos.getZ() * 17, 3600);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void addFilledBox(Renderer3D renderer, AABB box, int argb) {
|
||||||
|
int a = alpha(argb);
|
||||||
|
int r = red(argb);
|
||||||
|
int g = green(argb);
|
||||||
|
int b = blue(argb);
|
||||||
|
|
||||||
|
double minX = box.minX;
|
||||||
|
double minY = box.minY;
|
||||||
|
double minZ = box.minZ;
|
||||||
|
double maxX = box.maxX;
|
||||||
|
double maxY = box.maxY;
|
||||||
|
double maxZ = box.maxZ;
|
||||||
|
|
||||||
|
renderer.quad(minX, minY, minZ, maxX, minY, minZ, maxX, minY, maxZ, minX, minY, maxZ, r, g, b, a);
|
||||||
|
renderer.quad(minX, maxY, minZ, minX, maxY, maxZ, maxX, maxY, maxZ, maxX, maxY, minZ, r, g, b, a);
|
||||||
|
renderer.quad(minX, minY, maxZ, maxX, minY, maxZ, maxX, maxY, maxZ, minX, maxY, maxZ, r, g, b, a);
|
||||||
|
renderer.quad(minX, minY, minZ, minX, maxY, minZ, maxX, maxY, minZ, maxX, minY, minZ, r, g, b, a);
|
||||||
|
renderer.quad(maxX, minY, minZ, maxX, maxY, minZ, maxX, maxY, maxZ, maxX, minY, maxZ, r, g, b, a);
|
||||||
|
renderer.quad(minX, minY, minZ, minX, minY, maxZ, minX, maxY, maxZ, minX, maxY, minZ, r, g, b, a);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static int withAlpha(int argb, int alpha) {
|
||||||
|
return (alpha << 24) | (argb & 0x00FFFFFF);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static int red(int argb) {
|
||||||
|
return (argb >>> 16) & 255;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static int green(int argb) {
|
||||||
|
return (argb >>> 8) & 255;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static int blue(int argb) {
|
||||||
|
return argb & 255;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static int alpha(int argb) {
|
||||||
|
return (argb >>> 24) & 255;
|
||||||
|
}
|
||||||
|
|
||||||
|
public enum ColorMode implements EnumValue.IdProvider {
|
||||||
|
PER_BLOCK("per_block"),
|
||||||
|
STATIC("static"),
|
||||||
|
RAINBOW("rainbow"),
|
||||||
|
FADE("fade");
|
||||||
|
|
||||||
|
private final String id;
|
||||||
|
|
||||||
|
ColorMode(String id) {
|
||||||
|
this.id = id;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String id() {
|
||||||
|
return id;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,77 @@
|
||||||
|
package dev.loki.lovisual.features.module.modules.visuals;
|
||||||
|
|
||||||
|
import dev.loki.lovisual.features.cosmetic.CosmeticManager;
|
||||||
|
import dev.loki.lovisual.features.cosmetic.CosmeticModel;
|
||||||
|
import dev.loki.lovisual.features.cosmetic.CosmeticRenderer;
|
||||||
|
import dev.loki.lovisual.features.cosmetic.CosmeticRepository;
|
||||||
|
import dev.loki.lovisual.features.cosmetic.CosmeticType;
|
||||||
|
import dev.loki.lovisual.features.module.Module;
|
||||||
|
import dev.loki.lovisual.features.module.ModuleCategory;
|
||||||
|
import dev.loki.lovisual.features.module.ModuleInfo;
|
||||||
|
import dev.loki.lovisual.features.module.WorldPhase;
|
||||||
|
import dev.loki.lovisual.render.engine.renderer.Renderer3D;
|
||||||
|
import net.minecraft.client.Minecraft;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Renders the locally equipped cosmetic (model/pet/accessory) attached to the local player.
|
||||||
|
* Equip picking is a minimal next/prev keybind for now; a proper catalog screen is a follow-up.
|
||||||
|
*/
|
||||||
|
@ModuleInfo(
|
||||||
|
id = "cosmeticdisplay",
|
||||||
|
displayName = "Cosmetics",
|
||||||
|
aliases = {"cosmetics"},
|
||||||
|
category = ModuleCategory.VISUALS,
|
||||||
|
description = "Отображает надетую косметику (модель/питомец/аксессуар)"
|
||||||
|
)
|
||||||
|
public final class CosmeticDisplay extends Module {
|
||||||
|
|
||||||
|
private final Minecraft mc = Minecraft.getInstance();
|
||||||
|
|
||||||
|
{
|
||||||
|
action("Next Cosmetic", "NONE");
|
||||||
|
action("Prev Cosmetic", "NONE");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onEnable() {
|
||||||
|
CosmeticRepository.get().reload();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onTick() {
|
||||||
|
if (!isEnabled()) return;
|
||||||
|
|
||||||
|
if (isActionPressedOnce("Next Cosmetic")) {
|
||||||
|
cycle(1);
|
||||||
|
} else if (isActionPressedOnce("Prev Cosmetic")) {
|
||||||
|
cycle(-1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void cycle(int direction) {
|
||||||
|
List<CosmeticModel> models = CosmeticRepository.get().byType(CosmeticType.MODEL);
|
||||||
|
if (models.isEmpty()) return;
|
||||||
|
|
||||||
|
CosmeticModel current = CosmeticManager.get().getActive(CosmeticType.MODEL);
|
||||||
|
int index = current == null ? -1 : models.indexOf(current);
|
||||||
|
int next = Math.floorMod(index + direction, models.size());
|
||||||
|
|
||||||
|
CosmeticManager.get().setActive(CosmeticType.MODEL, models.get(next));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public WorldPhase getWorldPhase() {
|
||||||
|
return WorldPhase.AFTER_POST_PROCESS;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onRenderWorldEngine(Renderer3D renderer, Renderer3D depthRenderer, float tickDelta) {
|
||||||
|
if (!isEnabled() || mc.level == null || mc.player == null) return;
|
||||||
|
|
||||||
|
CosmeticRenderer.renderOnPlayer(renderer, mc.player, CosmeticType.MODEL, tickDelta);
|
||||||
|
CosmeticRenderer.renderOnPlayer(renderer, mc.player, CosmeticType.PET, tickDelta);
|
||||||
|
CosmeticRenderer.renderOnPlayer(renderer, mc.player, CosmeticType.ACCESSORY, tickDelta);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -7,7 +7,6 @@
|
||||||
|
|
||||||
package dev.loki.lovisual.features.module.modules.visuals;
|
package dev.loki.lovisual.features.module.modules.visuals;
|
||||||
|
|
||||||
import dev.loki.lovisual.util.time.GameClock;
|
|
||||||
|
|
||||||
import dev.loki.lovisual.features.module.*;
|
import dev.loki.lovisual.features.module.*;
|
||||||
import dev.loki.lovisual.features.module.Module;
|
import dev.loki.lovisual.features.module.Module;
|
||||||
|
|
@ -18,7 +17,6 @@ import net.minecraft.client.gui.GuiGraphicsExtractor;
|
||||||
import net.minecraft.core.BlockPos;
|
import net.minecraft.core.BlockPos;
|
||||||
import net.minecraft.core.component.DataComponents;
|
import net.minecraft.core.component.DataComponents;
|
||||||
import net.minecraft.core.registries.BuiltInRegistries;
|
import net.minecraft.core.registries.BuiltInRegistries;
|
||||||
import net.minecraft.network.protocol.game.ServerboundInteractPacket;
|
|
||||||
import net.minecraft.util.Mth;
|
import net.minecraft.util.Mth;
|
||||||
import net.minecraft.world.entity.Entity;
|
import net.minecraft.world.entity.Entity;
|
||||||
import net.minecraft.world.entity.EquipmentSlot;
|
import net.minecraft.world.entity.EquipmentSlot;
|
||||||
|
|
@ -37,14 +35,8 @@ import dev.loki.lovisual.config.values.EnumValue;
|
||||||
import dev.loki.lovisual.config.values.NumberValue;
|
import dev.loki.lovisual.config.values.NumberValue;
|
||||||
import dev.loki.lovisual.config.values.RGBAColorValue;
|
import dev.loki.lovisual.config.values.RGBAColorValue;
|
||||||
import dev.loki.lovisual.config.common.CommonSettingSchemas;
|
import dev.loki.lovisual.config.common.CommonSettingSchemas;
|
||||||
import dev.loki.lovisual.events.EventHandler;
|
|
||||||
import dev.loki.lovisual.events.impl.AttackEntityEvent;
|
|
||||||
import dev.loki.lovisual.events.impl.PacketEvent;
|
|
||||||
import dev.loki.lovisual.features.gui.hud.HudRenderUtil;
|
import dev.loki.lovisual.features.gui.hud.HudRenderUtil;
|
||||||
import dev.loki.lovisual.features.gui.hud.draggable.impl.Itemizer;
|
import dev.loki.lovisual.features.gui.hud.draggable.impl.Itemizer;
|
||||||
import dev.loki.lovisual.features.module.*;
|
|
||||||
import dev.loki.lovisual.features.module.Module;
|
|
||||||
import dev.loki.lovisual.features.module.modules.combat.Reach;
|
|
||||||
import dev.loki.lovisual.features.relations.CategoryService;
|
import dev.loki.lovisual.features.relations.CategoryService;
|
||||||
import dev.loki.lovisual.features.relations.EntityFilters;
|
import dev.loki.lovisual.features.relations.EntityFilters;
|
||||||
import dev.loki.lovisual.features.relations.PlayerRelations;
|
import dev.loki.lovisual.features.relations.PlayerRelations;
|
||||||
|
|
@ -57,10 +49,8 @@ import dev.loki.lovisual.render.engine.renderer.Renderer2D;
|
||||||
import dev.loki.lovisual.render.engine.text.FontInfo;
|
import dev.loki.lovisual.render.engine.text.FontInfo;
|
||||||
import dev.loki.lovisual.render.engine.text.Fonts;
|
import dev.loki.lovisual.render.engine.text.Fonts;
|
||||||
import dev.loki.lovisual.render.engine.text.TextRenderer;
|
import dev.loki.lovisual.render.engine.text.TextRenderer;
|
||||||
import dev.loki.lovisual.util.combat.VulcanReachController;
|
|
||||||
|
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Locale;
|
|
||||||
|
|
||||||
//todo Description
|
//todo Description
|
||||||
@ModuleInfo(
|
@ModuleInfo(
|
||||||
|
|
@ -79,9 +69,6 @@ public class Crosshair extends Module {
|
||||||
private static final float ORBIZ_PROGRESS_SMOOTH_SPEED = 22.0f;
|
private static final float ORBIZ_PROGRESS_SMOOTH_SPEED = 22.0f;
|
||||||
private static final float COOLDOWN_LEAD = 0.06f;
|
private static final float COOLDOWN_LEAD = 0.06f;
|
||||||
private static final float RIPTIDE_CROSS_LENGTH_FACTOR = 0.65f;
|
private static final float RIPTIDE_CROSS_LENGTH_FACTOR = 0.65f;
|
||||||
private static final float VULCAN_REACH_FONT_SIZE = 14.0f;
|
|
||||||
private static final int VULCAN_REACH_PRIMARY = 0xF2FFFFFF;
|
|
||||||
private static final int VULCAN_REACH_SECONDARY = 0xD8D7DEE8;
|
|
||||||
private final Minecraft mc = Minecraft.getInstance();
|
private final Minecraft mc = Minecraft.getInstance();
|
||||||
private final EnumValue<CrosshairType> crosshairType =
|
private final EnumValue<CrosshairType> crosshairType =
|
||||||
enumSetting("crosshairType", "type", CrosshairType.CLASSIC);
|
enumSetting("crosshairType", "type", CrosshairType.CLASSIC);
|
||||||
|
|
@ -199,15 +186,6 @@ public class Crosshair extends Module {
|
||||||
num("crosshairOrbizMotionSmoothing", "orbiz_motion_smoothing", 12.0, 1.0, 30.0),
|
num("crosshairOrbizMotionSmoothing", "orbiz_motion_smoothing", 12.0, 1.0, 30.0),
|
||||||
() -> isOrbiz() && isOrbizMotionDynamic()
|
() -> isOrbiz() && isOrbizMotionDynamic()
|
||||||
);
|
);
|
||||||
private final EnumValue<VulcanReachDisplay> vulcanReachDisplay =
|
|
||||||
enumSetting("crosshairVulcanReachDisplay", "vulcan_reach_display",
|
|
||||||
VulcanReachDisplay.TIMED, VulcanReachDisplay.values());
|
|
||||||
private final NumberValue<Integer> vulcanReachLookMs =
|
|
||||||
visibleWhen(num("crosshairVulcanReachLookMs", "vulcan_reach_look_ms", 1200, 0, 3000),
|
|
||||||
() -> vulcanReachDisplay.get() == VulcanReachDisplay.TIMED);
|
|
||||||
private final NumberValue<Integer> vulcanReachAttackMs =
|
|
||||||
visibleWhen(num("crosshairVulcanReachAttackMs", "vulcan_reach_attack_ms", 2500, 0, 6000),
|
|
||||||
() -> vulcanReachDisplay.get() == VulcanReachDisplay.TIMED);
|
|
||||||
private float animatedGap;
|
private float animatedGap;
|
||||||
private int animatedColor = DEFAULT_COLOR;
|
private int animatedColor = DEFAULT_COLOR;
|
||||||
private float animatedOrbizProgress = 1.0f;
|
private float animatedOrbizProgress = 1.0f;
|
||||||
|
|
@ -221,9 +199,6 @@ public class Crosshair extends Module {
|
||||||
private boolean lastSwapCandidate;
|
private boolean lastSwapCandidate;
|
||||||
private boolean lastChestWasElytra;
|
private boolean lastChestWasElytra;
|
||||||
private ItemStack lastChestStack = ItemStack.EMPTY;
|
private ItemStack lastChestStack = ItemStack.EMPTY;
|
||||||
private long lastVulcanReachLookMs;
|
|
||||||
private long lastVulcanReachAttackMs;
|
|
||||||
private Player lastVulcanReachTarget;
|
|
||||||
|
|
||||||
private static int colorDistance(int a, int b) {
|
private static int colorDistance(int a, int b) {
|
||||||
int dr = Math.abs(((a >>> 16) & 0xFF) - ((b >>> 16) & 0xFF));
|
int dr = Math.abs(((a >>> 16) & 0xFF) - ((b >>> 16) & 0xFF));
|
||||||
|
|
@ -322,7 +297,6 @@ public class Crosshair extends Module {
|
||||||
renderOrbiz(renderer, centerX, centerY, scale);
|
renderOrbiz(renderer, centerX, centerY, scale);
|
||||||
float infoGapPx = (float) (orbizRadius.get() * scale);
|
float infoGapPx = (float) (orbizRadius.get() * scale);
|
||||||
float infoLengthPx = (float) ((orbizGlow.get() + orbizThickness.get()) * scale);
|
float infoLengthPx = (float) ((orbizGlow.get() + orbizThickness.get()) * scale);
|
||||||
renderVulcanReachInfo(textRenderer, centerX, centerY, infoGapPx, infoLengthPx);
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -344,7 +318,6 @@ public class Crosshair extends Module {
|
||||||
double stemH = lengthPx + gapPx;
|
double stemH = lengthPx + gapPx;
|
||||||
drawSegment(renderer, barX, barY, barW, thicknessPx, color, outline, outlinePx);
|
drawSegment(renderer, barX, barY, barW, thicknessPx, color, outline, outlinePx);
|
||||||
drawSegment(renderer, stemX, centerY, thicknessPx, stemH, color, outline, outlinePx);
|
drawSegment(renderer, stemX, centerY, thicknessPx, stemH, color, outline, outlinePx);
|
||||||
renderVulcanReachInfo(textRenderer, centerX, centerY, gapPx, lengthPx);
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -361,35 +334,6 @@ public class Crosshair extends Module {
|
||||||
drawSegment(renderer, bottomX, bottomY, thicknessPx, lengthPx, color, outline, outlinePx);
|
drawSegment(renderer, bottomX, bottomY, thicknessPx, lengthPx, color, outline, outlinePx);
|
||||||
drawSegment(renderer, leftX, leftY, lengthPx, thicknessPx, color, outline, outlinePx);
|
drawSegment(renderer, leftX, leftY, lengthPx, thicknessPx, color, outline, outlinePx);
|
||||||
drawSegment(renderer, rightX, rightY, lengthPx, thicknessPx, color, outline, outlinePx);
|
drawSegment(renderer, rightX, rightY, lengthPx, thicknessPx, color, outline, outlinePx);
|
||||||
renderVulcanReachInfo(textRenderer, centerX, centerY, gapPx, lengthPx);
|
|
||||||
}
|
|
||||||
|
|
||||||
@EventHandler
|
|
||||||
private void onAttackEntity(AttackEntityEvent event) {
|
|
||||||
if (!isVulcanReachActive()) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (event.getPlayer() != mc.player || !(event.getTarget() instanceof Player player)) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
lastVulcanReachAttackMs = GameClock.millis();
|
|
||||||
lastVulcanReachTarget = player;
|
|
||||||
}
|
|
||||||
|
|
||||||
@EventHandler
|
|
||||||
private void onPacketSendPost(PacketEvent.SendPost event) {
|
|
||||||
if (!isVulcanReachActive() || mc.level == null) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (!(event.getPacket() instanceof ServerboundInteractPacket packet) || !isAttackPacket(packet)) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
Entity target = mc.level.getEntity(packet.entityId());
|
|
||||||
if (target instanceof Player player) {
|
|
||||||
lastVulcanReachAttackMs = GameClock.millis();
|
|
||||||
lastVulcanReachTarget = player;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private float getAttackCooldownProgress() {
|
private float getAttackCooldownProgress() {
|
||||||
|
|
@ -555,85 +499,6 @@ public class Crosshair extends Module {
|
||||||
return resolveEntityColor(entity);
|
return resolveEntityColor(entity);
|
||||||
}
|
}
|
||||||
|
|
||||||
private void renderVulcanReachInfo(TextRenderer fallback, float centerX, float centerY, float gapPx, float lengthPx) {
|
|
||||||
if (!isVulcanReachActive()) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
Player target = resolveVulcanReachTarget();
|
|
||||||
double requestedReach = resolveVulcanRequestedReach(target);
|
|
||||||
VulcanReachController.HudSnapshot snapshot = target != null && target.isAlive()
|
|
||||||
? VulcanReachController.INSTANCE.hudSnapshot(target, requestedReach)
|
|
||||||
: vulcanReachDisplay.get() == VulcanReachDisplay.ALWAYS
|
|
||||||
? VulcanReachController.INSTANCE.hudSnapshot(requestedReach)
|
|
||||||
: null;
|
|
||||||
if (snapshot == null) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
String reachText = String.format(Locale.ROOT, "%.1f", snapshot.displayReach());
|
|
||||||
String hitsText = Integer.toString(snapshot.longHitsRemaining());
|
|
||||||
|
|
||||||
TextRenderer font = Fonts.renderer("Montserrat", FontInfo.Type.Regular, fallback);
|
|
||||||
float textScale = VULCAN_REACH_FONT_SIZE / 18.0f;
|
|
||||||
float x = centerX + gapPx + lengthPx + 7.0f;
|
|
||||||
float y = centerY - VULCAN_REACH_FONT_SIZE * 0.95f;
|
|
||||||
float lineHeight = VULCAN_REACH_FONT_SIZE * 0.92f;
|
|
||||||
|
|
||||||
font.begin(textScale, false, false);
|
|
||||||
font.render(reachText, x, y, new RenderColor(VULCAN_REACH_PRIMARY), true);
|
|
||||||
font.render(hitsText, x, y + lineHeight, new RenderColor(VULCAN_REACH_SECONDARY), true);
|
|
||||||
font.end();
|
|
||||||
}
|
|
||||||
|
|
||||||
private boolean isVulcanReachActive() {
|
|
||||||
Reach reach = Modules.get(Reach.class);
|
|
||||||
return reach != null && reach.isEnabled() && reach.isVulcan297Mode();
|
|
||||||
}
|
|
||||||
|
|
||||||
private double resolveVulcanRequestedReach(Player target) {
|
|
||||||
Reach reach = Modules.get(Reach.class);
|
|
||||||
if (reach != null && reach.isEnabled() && reach.isVulcan297Mode()) {
|
|
||||||
return reach.getConfiguredDistance();
|
|
||||||
}
|
|
||||||
|
|
||||||
return 3.0;
|
|
||||||
}
|
|
||||||
|
|
||||||
private Player resolveVulcanReachTarget() {
|
|
||||||
long now = GameClock.millis();
|
|
||||||
HitResult hit = mc.hitResult;
|
|
||||||
if (hit instanceof EntityHitResult entityHit
|
|
||||||
&& entityHit.getEntity() instanceof Player player
|
|
||||||
&& player.isAlive()) {
|
|
||||||
lastVulcanReachLookMs = now;
|
|
||||||
lastVulcanReachTarget = player;
|
|
||||||
return player;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (lastVulcanReachTarget != null && !lastVulcanReachTarget.isAlive()) {
|
|
||||||
lastVulcanReachTarget = null;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (vulcanReachDisplay.get() == VulcanReachDisplay.ALWAYS) {
|
|
||||||
return lastVulcanReachTarget;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (lastVulcanReachTarget == null) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
long lookWindow = Math.max(0, vulcanReachLookMs.get());
|
|
||||||
long attackWindow = Math.max(0, vulcanReachAttackMs.get());
|
|
||||||
boolean recentLook = lookWindow > 0 && now - lastVulcanReachLookMs <= lookWindow;
|
|
||||||
boolean recentAttack = attackWindow > 0 && now - lastVulcanReachAttackMs <= attackWindow;
|
|
||||||
return recentLook || recentAttack ? lastVulcanReachTarget : null;
|
|
||||||
}
|
|
||||||
|
|
||||||
private boolean isAttackPacket(ServerboundInteractPacket packet) {
|
|
||||||
return packet.hand() == null && packet.location() == null;
|
|
||||||
}
|
|
||||||
|
|
||||||
private int smoothCrosshairColor(int targetColor) {
|
private int smoothCrosshairColor(int targetColor) {
|
||||||
if (!colorInitialized) {
|
if (!colorInitialized) {
|
||||||
animatedColor = targetColor;
|
animatedColor = targetColor;
|
||||||
|
|
@ -780,22 +645,6 @@ public class Crosshair extends Module {
|
||||||
renderer.quad(x, y, w, h, color);
|
renderer.quad(x, y, w, h, color);
|
||||||
}
|
}
|
||||||
|
|
||||||
public enum VulcanReachDisplay implements EnumValue.IdProvider {
|
|
||||||
TIMED("timed"),
|
|
||||||
ALWAYS("always");
|
|
||||||
|
|
||||||
private final String id;
|
|
||||||
|
|
||||||
VulcanReachDisplay(String id) {
|
|
||||||
this.id = id;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public String id() {
|
|
||||||
return id;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private enum CrosshairType implements EnumValue.AliasProvider {
|
private enum CrosshairType implements EnumValue.AliasProvider {
|
||||||
CLASSIC,
|
CLASSIC,
|
||||||
T_CLASSIC,
|
T_CLASSIC,
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,61 @@
|
||||||
|
/*
|
||||||
|
* This file is part of the LoVisual Client distribution.
|
||||||
|
* Copyright (c) 2026 pivosos2007.
|
||||||
|
*
|
||||||
|
* Portions of this file are based on, adapted from, or implemented
|
||||||
|
* with reference to a "Hold My Items" mixin distributed for another
|
||||||
|
* Minecraft client. The original attribution was not preserved.
|
||||||
|
* This implementation has since been substantially modified for LoVisual.
|
||||||
|
*
|
||||||
|
* Licensed under the GNU General Public License v3.0.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package dev.loki.lovisual.features.module.modules.visuals;
|
||||||
|
|
||||||
|
import dev.loki.lovisual.config.values.BooleanValue;
|
||||||
|
import dev.loki.lovisual.config.values.NumberValue;
|
||||||
|
import dev.loki.lovisual.features.module.Module;
|
||||||
|
import dev.loki.lovisual.features.module.ModuleCategory;
|
||||||
|
import dev.loki.lovisual.features.module.ModuleInfo;
|
||||||
|
import net.minecraft.client.Minecraft;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Smooths and adds a subtle hand-held sway to first-person item rendering, so weapons and tools
|
||||||
|
* settle into place with an ease-out-back curve instead of vanilla's linear equip motion.
|
||||||
|
*/
|
||||||
|
@ModuleInfo(
|
||||||
|
id = "holdmyitems",
|
||||||
|
displayName = "Hold My Items",
|
||||||
|
aliases = {"hmi"},
|
||||||
|
category = ModuleCategory.VISUALS,
|
||||||
|
description = "Плавная анимация удержания предмета в руке вместо линейного ваниллового equip"
|
||||||
|
)
|
||||||
|
public final class HoldMyItems extends Module {
|
||||||
|
|
||||||
|
public final Minecraft mc = Minecraft.getInstance();
|
||||||
|
|
||||||
|
public final NumberValue<Float> swayIntensity = num("sway_intensity", 1.0f, 0.0f, 2.0f);
|
||||||
|
public final NumberValue<Float> easeStrength = num("ease_strength", 1.6f, 0.5f, 3.5f);
|
||||||
|
public final BooleanValue offhandToo = bool("offhand", true);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Ease-out-back: overshoots slightly past 1.0 before settling, giving the equip motion weight.
|
||||||
|
* Ported from the original "Hold My Items" easing curve, generalized with a configurable strength.
|
||||||
|
*/
|
||||||
|
public float easeOutBack(float x) {
|
||||||
|
float c1 = easeStrength.get();
|
||||||
|
float c3 = c1 + 1.0f;
|
||||||
|
float t = x - 1.0f;
|
||||||
|
return 1.0f + c3 * t * t * t + c1 * t * t;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Reshapes the raw [0,1] equip progress with the ease-out-back curve, then blends by intensity
|
||||||
|
* so a intensity of 0 falls back to vanilla's linear motion.
|
||||||
|
*/
|
||||||
|
public float shapeEquipProgress(float linear) {
|
||||||
|
float eased = easeOutBack(linear);
|
||||||
|
float amount = swayIntensity.get();
|
||||||
|
return linear + (eased - linear) * amount;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -7,12 +7,14 @@
|
||||||
|
|
||||||
package dev.loki.lovisual.features.module.modules.visuals;
|
package dev.loki.lovisual.features.module.modules.visuals;
|
||||||
|
|
||||||
|
import dev.loki.lovisual.config.values.BooleanValue;
|
||||||
import dev.loki.lovisual.config.values.NumberValue;
|
import dev.loki.lovisual.config.values.NumberValue;
|
||||||
import dev.loki.lovisual.config.values.RGBAColorValue;
|
import dev.loki.lovisual.config.values.RGBAColorValue;
|
||||||
import dev.loki.lovisual.features.module.Module;
|
import dev.loki.lovisual.features.module.Module;
|
||||||
import dev.loki.lovisual.features.module.ModuleCategory;
|
import dev.loki.lovisual.features.module.ModuleCategory;
|
||||||
import dev.loki.lovisual.features.module.ModuleInfo;
|
import dev.loki.lovisual.features.module.ModuleInfo;
|
||||||
import dev.loki.lovisual.features.module.WorldPhase;
|
import dev.loki.lovisual.features.module.WorldPhase;
|
||||||
|
import dev.loki.lovisual.features.relations.PlayerRelations;
|
||||||
import dev.loki.lovisual.render.engine.RenderState;
|
import dev.loki.lovisual.render.engine.RenderState;
|
||||||
import dev.loki.lovisual.render.engine.pipeline.LoVisualRenderPipelines;
|
import dev.loki.lovisual.render.engine.pipeline.LoVisualRenderPipelines;
|
||||||
import dev.loki.lovisual.render.engine.renderer.Renderer3D;
|
import dev.loki.lovisual.render.engine.renderer.Renderer3D;
|
||||||
|
|
@ -35,6 +37,10 @@ public final class InvisEsp extends Module {
|
||||||
|
|
||||||
private final RGBAColorValue outlineColor =
|
private final RGBAColorValue outlineColor =
|
||||||
color("invisesp_color", "color", "#FF00E5FF");
|
color("invisesp_color", "color", "#FF00E5FF");
|
||||||
|
private final BooleanValue highlightFriends =
|
||||||
|
bool("invisesp_highlight_friends", "highlight_friends", true);
|
||||||
|
private final RGBAColorValue friendColor =
|
||||||
|
visibleWhen(color("invisesp_friend_color", "friend_color", "#FF00FF00"), highlightFriends::get);
|
||||||
private final NumberValue<Float> lineWidth =
|
private final NumberValue<Float> lineWidth =
|
||||||
num("invisesp_line_width", "line_width", 1.5f, 0.5f, 6.0f);
|
num("invisesp_line_width", "line_width", 1.5f, 0.5f, 6.0f);
|
||||||
|
|
||||||
|
|
@ -60,11 +66,16 @@ public final class InvisEsp extends Module {
|
||||||
RenderState.lineWidth = previousLineWidth;
|
RenderState.lineWidth = previousLineWidth;
|
||||||
}
|
}
|
||||||
|
|
||||||
int color = outlineColor.getArgb();
|
int defaultColor = outlineColor.getArgb();
|
||||||
|
boolean highlightFriendsEnabled = highlightFriends.get();
|
||||||
for (Player player : mc.level.players()) {
|
for (Player player : mc.level.players()) {
|
||||||
if (player == mc.player) continue;
|
if (player == mc.player) continue;
|
||||||
if (!isInvisible(player)) continue;
|
if (!isInvisible(player)) continue;
|
||||||
|
|
||||||
|
int color = highlightFriendsEnabled && PlayerRelations.get().isFriendOrStaff(player.getGameProfile().name())
|
||||||
|
? friendColor.getArgb()
|
||||||
|
: defaultColor;
|
||||||
|
|
||||||
AABB box = Esp2DRenderer.resolveWorldBox(player, tickDelta);
|
AABB box = Esp2DRenderer.resolveWorldBox(player, tickDelta);
|
||||||
EspMeshBuilder.addOutlineBox(lines, box, color, color, color, color, color, color, color, color);
|
EspMeshBuilder.addOutlineBox(lines, box, color, color, color, color, color, color, color, color);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,315 @@
|
||||||
|
/*
|
||||||
|
* This file is part of the LoVisual Client distribution.
|
||||||
|
* Copyright (c) 2026 pivosos2007.
|
||||||
|
*
|
||||||
|
* Licensed under the GNU General Public License v3.0.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package dev.loki.lovisual.features.module.modules.visuals;
|
||||||
|
|
||||||
|
import dev.loki.lovisual.config.values.BooleanValue;
|
||||||
|
import dev.loki.lovisual.config.values.NumberValue;
|
||||||
|
import dev.loki.lovisual.features.module.Module;
|
||||||
|
import dev.loki.lovisual.features.module.ModuleCategory;
|
||||||
|
import dev.loki.lovisual.features.module.ModuleInfo;
|
||||||
|
import dev.loki.lovisual.events.EventHandler;
|
||||||
|
import dev.loki.lovisual.events.impl.AttackEntityEvent;
|
||||||
|
import dev.loki.lovisual.render.engine.renderer.Renderer3D;
|
||||||
|
import dev.loki.lovisual.render.engine.pipeline.LoVisualRenderPipelines;
|
||||||
|
import dev.loki.lovisual.render.engine.uniform.MeshBuilder;
|
||||||
|
import dev.loki.lovisual.util.target.TargetManager;
|
||||||
|
import net.minecraft.client.Minecraft;
|
||||||
|
import net.minecraft.util.Mth;
|
||||||
|
import net.minecraft.world.entity.LivingEntity;
|
||||||
|
import net.minecraft.world.phys.Vec3;
|
||||||
|
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.concurrent.ThreadLocalRandom;
|
||||||
|
|
||||||
|
@ModuleInfo(
|
||||||
|
id = "lightning_target_esp",
|
||||||
|
displayName = "Lightning Target ESP",
|
||||||
|
category = ModuleCategory.VISUALS
|
||||||
|
)
|
||||||
|
public class LightningTargetESP extends Module {
|
||||||
|
|
||||||
|
private final Minecraft mc = Minecraft.getInstance();
|
||||||
|
|
||||||
|
private final NumberValue<Float> lightningCount = num("lightningCount", "count", 4.0f, 1.0f, 10.0f);
|
||||||
|
private final NumberValue<Float> lightningWidth = num("lightningWidth", "width", 1.0f, 0.1f, 3.0f);
|
||||||
|
private final BooleanValue hurtColor = bool("hurtColor", "hurt_color", true);
|
||||||
|
|
||||||
|
private float appearValue = 0f;
|
||||||
|
private Vec3 lastTargetPos = null;
|
||||||
|
private long lastAttackTime = 0L;
|
||||||
|
private LivingEntity lastAttackedEntity = null;
|
||||||
|
|
||||||
|
private final ArrayList<Bolt> bolts = new ArrayList<>();
|
||||||
|
private long lastBoltSpawn = 0L;
|
||||||
|
|
||||||
|
@EventHandler
|
||||||
|
private void onAttack(AttackEntityEvent event) {
|
||||||
|
if (event.getTarget() instanceof LivingEntity entity) {
|
||||||
|
this.lastAttackTime = System.currentTimeMillis();
|
||||||
|
this.lastAttackedEntity = entity;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onRenderWorldEngine(Renderer3D renderer, Renderer3D depthRenderer, float tickDelta) {
|
||||||
|
if (mc.player == null || mc.level == null) return;
|
||||||
|
|
||||||
|
LivingEntity currentTarget = TargetManager.getTarget();
|
||||||
|
boolean hasTarget = currentTarget != null && currentTarget.isAlive();
|
||||||
|
|
||||||
|
LivingEntity activeTarget = hasTarget ? currentTarget : lastAttackedEntity;
|
||||||
|
boolean hasActiveTarget = activeTarget != null && activeTarget.isAlive();
|
||||||
|
|
||||||
|
appearValue = animateTo(appearValue, hasActiveTarget ? 1f : 0f, 0.05f);
|
||||||
|
|
||||||
|
if (hasActiveTarget) {
|
||||||
|
this.lastTargetPos = new Vec3(
|
||||||
|
activeTarget.getX(),
|
||||||
|
activeTarget.getY(),
|
||||||
|
activeTarget.getZ()
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (appearValue <= 0.001f) {
|
||||||
|
if (!hasActiveTarget) {
|
||||||
|
lastTargetPos = null;
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
renderLightning(renderer, tickDelta, hasActiveTarget, activeTarget);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void renderLightning(Renderer3D renderer, float partialTicks, boolean hasActiveTarget, LivingEntity activeTarget) {
|
||||||
|
if (lastTargetPos == null) return;
|
||||||
|
|
||||||
|
long now = System.currentTimeMillis();
|
||||||
|
int maxBolts = Math.max(4, Math.round(lightningCount.get()) * 2);
|
||||||
|
long spawnInterval = (long) Math.max(15, 65.0f / (lightningCount.get() / 3.0f));
|
||||||
|
|
||||||
|
if (hasActiveTarget && activeTarget != null && activeTarget.isAlive()) {
|
||||||
|
if (bolts.size() < maxBolts) {
|
||||||
|
if (now - lastBoltSpawn > spawnInterval) {
|
||||||
|
bolts.add(spawnBolt(activeTarget));
|
||||||
|
lastBoltSpawn = now;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
bolts.removeIf(b -> now - b.spawn > b.life);
|
||||||
|
|
||||||
|
if (bolts.isEmpty()) return;
|
||||||
|
|
||||||
|
Vec3 basePos = lastTargetPos;
|
||||||
|
Vec3 cam = mc.gameRenderer.mainCamera().position();
|
||||||
|
float hurtPC = getHurtPC();
|
||||||
|
int baseColor = 0xFF55FFFF;
|
||||||
|
int redColor = 0xFFFF3C3C;
|
||||||
|
int glowColor = blendColor(baseColor, redColor, hurtPC);
|
||||||
|
int coreColor = blendColor(0xFFFFFFFF, redColor, hurtPC);
|
||||||
|
|
||||||
|
float widthScale = lightningWidth.get();
|
||||||
|
|
||||||
|
renderRibbonLayer(renderer, basePos, cam, bolts,
|
||||||
|
0.045f * widthScale, setAlpha(glowColor, (int) (appearValue * 45)), now, appearValue);
|
||||||
|
renderRibbonLayer(renderer, basePos, cam, bolts,
|
||||||
|
0.018f * widthScale, setAlpha(glowColor, (int) (appearValue * 95)), now, appearValue);
|
||||||
|
renderRibbonLayer(renderer, basePos, cam, bolts,
|
||||||
|
0.006f * widthScale, setAlpha(coreColor, (int) (appearValue * 235)), now, appearValue);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void renderRibbonLayer(Renderer3D renderer, Vec3 base, Vec3 cam,
|
||||||
|
ArrayList<Bolt> bolts, float width, int color, long now, float appearValue) {
|
||||||
|
MeshBuilder mesh = renderer.batch(LoVisualRenderPipelines.WORLD_COLORED, Renderer3D.DepthMode.NONE);
|
||||||
|
if (mesh == null) return;
|
||||||
|
|
||||||
|
for (Bolt bolt : bolts) {
|
||||||
|
float alpha = boltAlpha(bolt, now) * appearValue;
|
||||||
|
if (alpha <= 0.02f) continue;
|
||||||
|
int a = (color >> 24) & 0xFF;
|
||||||
|
int layerA = (int) (a * alpha);
|
||||||
|
if (layerA <= 0 || bolt.points.size() < 2) continue;
|
||||||
|
int r = (color >> 16) & 0xFF;
|
||||||
|
int g = (color >> 8) & 0xFF;
|
||||||
|
int b = color & 0xFF;
|
||||||
|
appendRibbonPoints(mesh, base, cam, bolt.points, width, r, g, b, layerA);
|
||||||
|
for (List<Vec3> branch : bolt.branches) {
|
||||||
|
appendRibbonPoints(mesh, base, cam, branch, width * 0.6f, r, g, b, (int) (layerA * 0.85f));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private Bolt spawnBolt(LivingEntity target) {
|
||||||
|
ThreadLocalRandom random = ThreadLocalRandom.current();
|
||||||
|
float radius = target.getBbWidth() * 0.5f;
|
||||||
|
float height = target.getBbHeight();
|
||||||
|
|
||||||
|
double startAngle = random.nextDouble() * Math.PI * 2.0;
|
||||||
|
double startY = 0.15 + random.nextDouble() * Math.max(0.2, height - 0.4);
|
||||||
|
Vec3 start = new Vec3(
|
||||||
|
Math.cos(startAngle) * radius * 0.7,
|
||||||
|
startY,
|
||||||
|
Math.sin(startAngle) * radius * 0.7
|
||||||
|
);
|
||||||
|
|
||||||
|
double theta = random.nextDouble() * Math.PI * 2.0;
|
||||||
|
double phi = Math.toRadians((random.nextDouble() - 0.5) * 150.0);
|
||||||
|
Vec3 dir = new Vec3(
|
||||||
|
Math.cos(phi) * Math.cos(theta),
|
||||||
|
Math.sin(phi) * 0.8 + 0.25,
|
||||||
|
Math.cos(phi) * Math.sin(theta)
|
||||||
|
).normalize();
|
||||||
|
|
||||||
|
double length = 0.35 + random.nextDouble() * 0.45;
|
||||||
|
Vec3 end = start.add(dir.scale(length));
|
||||||
|
|
||||||
|
ArrayList<Vec3> points = new ArrayList<>(List.of(start, end));
|
||||||
|
points = displacePoints(points, 0.09, 3);
|
||||||
|
|
||||||
|
ArrayList<List<Vec3>> branches = new ArrayList<>();
|
||||||
|
int branchCount = 1 + random.nextInt(3);
|
||||||
|
for (int i = 0; i < branchCount; i++) {
|
||||||
|
int anchorIndex = 1 + random.nextInt(Math.max(1, points.size() - 2));
|
||||||
|
Vec3 anchor = points.get(anchorIndex);
|
||||||
|
Vec3 branchDir = randomPerpendicular(dir);
|
||||||
|
if (random.nextBoolean()) {
|
||||||
|
branchDir = branchDir.scale(-1.0);
|
||||||
|
}
|
||||||
|
double branchLength = 0.12 + random.nextDouble() * 0.18;
|
||||||
|
Vec3 branchEnd = anchor.add(branchDir.scale(branchLength));
|
||||||
|
ArrayList<Vec3> branchPoints = new ArrayList<>(List.of(anchor, branchEnd));
|
||||||
|
branches.add(displacePoints(branchPoints, 0.05, 2));
|
||||||
|
}
|
||||||
|
|
||||||
|
return new Bolt(points, branches, 140L + random.nextLong(160L), random.nextFloat() * 6.28f);
|
||||||
|
}
|
||||||
|
|
||||||
|
private ArrayList<Vec3> displacePoints(ArrayList<Vec3> input, double amount, int passes) {
|
||||||
|
ArrayList<Vec3> points = new ArrayList<>(input);
|
||||||
|
double amt = amount;
|
||||||
|
for (int pass = 0; pass < passes; pass++) {
|
||||||
|
ArrayList<Vec3> next = new ArrayList<>(points.size() * 2);
|
||||||
|
for (int i = 0; i < points.size() - 1; i++) {
|
||||||
|
Vec3 a = points.get(i);
|
||||||
|
Vec3 b = points.get(i + 1);
|
||||||
|
next.add(a);
|
||||||
|
Vec3 dir = b.subtract(a);
|
||||||
|
if (dir.lengthSqr() < 1.0E-7) {
|
||||||
|
next.add(a.add(b.scale(0.5)));
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
Vec3 perp = randomPerpendicular(dir.normalize());
|
||||||
|
double offset = (ThreadLocalRandom.current().nextDouble() - 0.5) * 2.0 * amt;
|
||||||
|
next.add(a.add(b.scale(0.5)).add(perp.scale(offset)));
|
||||||
|
}
|
||||||
|
next.add(points.get(points.size() - 1));
|
||||||
|
points = next;
|
||||||
|
amt *= 0.5;
|
||||||
|
}
|
||||||
|
return points;
|
||||||
|
}
|
||||||
|
|
||||||
|
private Vec3 randomPerpendicular(Vec3 dir) {
|
||||||
|
Vec3 arbitrary = Math.abs(dir.y) < 0.9 ? new Vec3(0, 1, 0) : new Vec3(1, 0, 0);
|
||||||
|
Vec3 perp1 = dir.cross(arbitrary).normalize();
|
||||||
|
Vec3 perp2 = dir.cross(perp1).normalize();
|
||||||
|
double angle = ThreadLocalRandom.current().nextDouble() * Math.PI * 2.0;
|
||||||
|
return perp1.scale(Math.cos(angle)).add(perp2.scale(Math.sin(angle)));
|
||||||
|
}
|
||||||
|
|
||||||
|
private void appendRibbonPoints(MeshBuilder mesh, Vec3 base, Vec3 cam,
|
||||||
|
List<Vec3> points, float width, int r, int g, int b, int a) {
|
||||||
|
if (a <= 0 || points.size() < 2) return;
|
||||||
|
float half = width * 0.5f;
|
||||||
|
|
||||||
|
for (int i = 0; i < points.size() - 1; i++) {
|
||||||
|
Vec3 p1 = base.add(points.get(i));
|
||||||
|
Vec3 p2 = base.add(points.get(i + 1));
|
||||||
|
Vec3 segDir = p2.subtract(p1);
|
||||||
|
if (segDir.lengthSqr() < 1.0E-8) continue;
|
||||||
|
Vec3 viewDir = p1.add(p2).scale(0.5).subtract(cam);
|
||||||
|
if (viewDir.lengthSqr() < 1.0E-8) continue;
|
||||||
|
Vec3 side = segDir.cross(viewDir);
|
||||||
|
double sideLen = side.length();
|
||||||
|
if (sideLen < 1.0E-8 || !Double.isFinite(sideLen)) continue;
|
||||||
|
side = side.scale(1.0 / sideLen);
|
||||||
|
|
||||||
|
float x1 = (float) (p1.x - cam.x);
|
||||||
|
float y1 = (float) (p1.y - cam.y);
|
||||||
|
float z1 = (float) (p1.z - cam.z);
|
||||||
|
float x2 = (float) (p2.x - cam.x);
|
||||||
|
float y2 = (float) (p2.y - cam.y);
|
||||||
|
float z2 = (float) (p2.z - cam.z);
|
||||||
|
float sx = (float) side.x * half;
|
||||||
|
float sy = (float) side.y * half;
|
||||||
|
float sz = (float) side.z * half;
|
||||||
|
|
||||||
|
int i1 = mesh.vec3(x1 - sx, y1 - sy, z1 - sz).color(r, g, b, a).next();
|
||||||
|
int i2 = mesh.vec3(x1 + sx, y1 + sy, z1 + sz).color(r, g, b, a).next();
|
||||||
|
int i3 = mesh.vec3(x2 + sx, y2 + sy, z2 + sz).color(r, g, b, a).next();
|
||||||
|
int i4 = mesh.vec3(x2 - sx, y2 - sy, z2 - sz).color(r, g, b, a).next();
|
||||||
|
mesh.quad(i1, i2, i3, i4);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private float boltAlpha(Bolt bolt, long now) {
|
||||||
|
float life = Math.max(0.0f, Math.min(1.0f, (now - bolt.spawn) / (float) bolt.life));
|
||||||
|
float fade = life < 0.18f ? life / 0.18f : 1.0f - (life - 0.18f) / 0.82f;
|
||||||
|
float flicker = 0.78f + 0.22f * (float) Math.sin(now * 0.045f + bolt.phase);
|
||||||
|
return Math.max(0.0f, Math.min(1.0f, fade)) * flicker;
|
||||||
|
}
|
||||||
|
|
||||||
|
private float getHurtPC() {
|
||||||
|
if (!hurtColor.get()) return 0f;
|
||||||
|
float fromAttack = 0f;
|
||||||
|
long elapsed = System.currentTimeMillis() - lastAttackTime;
|
||||||
|
if (elapsed < 420L && lastAttackedEntity != null && lastAttackedEntity.isAlive()) {
|
||||||
|
fromAttack = 1.0f - (float) elapsed / 420.0f;
|
||||||
|
}
|
||||||
|
float progress = Math.max(0f, fromAttack);
|
||||||
|
return progress * progress * (3.0f - 2.0f * progress);
|
||||||
|
}
|
||||||
|
|
||||||
|
private int blendColor(int c1, int c2, float factor) {
|
||||||
|
factor = Math.max(0f, Math.min(1f, factor));
|
||||||
|
int r1 = (c1 >> 16) & 0xFF, g1 = (c1 >> 8) & 0xFF, b1 = c1 & 0xFF, a1 = (c1 >> 24) & 0xFF;
|
||||||
|
int r2 = (c2 >> 16) & 0xFF, g2 = (c2 >> 8) & 0xFF, b2 = c2 & 0xFF, a2 = (c2 >> 24) & 0xFF;
|
||||||
|
int r = (int) (r1 + (r2 - r1) * factor);
|
||||||
|
int g = (int) (g1 + (g2 - g1) * factor);
|
||||||
|
int b = (int) (b1 + (b2 - b1) * factor);
|
||||||
|
int a = (int) (a1 + (a2 - a1) * factor);
|
||||||
|
return (a << 24) | (r << 16) | (g << 8) | b;
|
||||||
|
}
|
||||||
|
|
||||||
|
private int setAlpha(int color, int alpha) {
|
||||||
|
alpha = Math.max(0, Math.min(255, alpha));
|
||||||
|
return (alpha << 24) | (color & 0x00FFFFFF);
|
||||||
|
}
|
||||||
|
|
||||||
|
private float animateTo(float current, float target, float delta) {
|
||||||
|
if (current < target) return Math.min(current + delta, target);
|
||||||
|
if (current > target) return Math.max(current - delta, target);
|
||||||
|
return current;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static class Bolt {
|
||||||
|
final List<Vec3> points;
|
||||||
|
final List<List<Vec3>> branches;
|
||||||
|
final long spawn;
|
||||||
|
final long life;
|
||||||
|
final float phase;
|
||||||
|
|
||||||
|
Bolt(List<Vec3> points, List<List<Vec3>> branches, long life, float phase) {
|
||||||
|
this.points = points;
|
||||||
|
this.branches = branches;
|
||||||
|
this.spawn = System.currentTimeMillis();
|
||||||
|
this.life = life;
|
||||||
|
this.phase = phase;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,869 @@
|
||||||
|
package dev.loki.lovisual.features.module.modules.visuals;
|
||||||
|
|
||||||
|
import dev.loki.lovisual.config.values.BooleanValue;
|
||||||
|
import dev.loki.lovisual.config.values.EnumValue;
|
||||||
|
import dev.loki.lovisual.config.values.NumberValue;
|
||||||
|
import dev.loki.lovisual.config.values.RGBAColorValue;
|
||||||
|
import dev.loki.lovisual.features.module.Module;
|
||||||
|
import dev.loki.lovisual.features.module.ModuleCategory;
|
||||||
|
import dev.loki.lovisual.features.module.ModuleInfo;
|
||||||
|
import dev.loki.lovisual.features.module.WorldPhase;
|
||||||
|
import dev.loki.lovisual.features.theme.Theme;
|
||||||
|
import dev.loki.lovisual.render.engine.RenderState;
|
||||||
|
import dev.loki.lovisual.render.engine.core.LoVisualWorldMatrices;
|
||||||
|
import dev.loki.lovisual.render.engine.pipeline.LoVisualRenderPipelines;
|
||||||
|
import dev.loki.lovisual.render.engine.postprocess.PostProcessContext;
|
||||||
|
import dev.loki.lovisual.render.engine.postprocess.PostProcessManager;
|
||||||
|
import dev.loki.lovisual.render.engine.postprocess.PostProcessPass;
|
||||||
|
import dev.loki.lovisual.render.engine.renderer.FullScreenRenderer;
|
||||||
|
import dev.loki.lovisual.render.engine.renderer.Renderer3D;
|
||||||
|
import dev.loki.lovisual.render.engine.uniform.MeshBuilder;
|
||||||
|
import dev.loki.lovisual.render.engine.uniform.impl.WetSurfaceUniforms;
|
||||||
|
import com.mojang.blaze3d.textures.GpuTextureView;
|
||||||
|
import net.minecraft.client.Camera;
|
||||||
|
import net.minecraft.client.Minecraft;
|
||||||
|
import net.minecraft.core.BlockPos;
|
||||||
|
import net.minecraft.util.Mth;
|
||||||
|
import net.minecraft.world.level.block.state.BlockState;
|
||||||
|
import net.minecraft.world.level.levelgen.Heightmap;
|
||||||
|
import net.minecraft.world.phys.Vec3;
|
||||||
|
import org.joml.Matrix4f;
|
||||||
|
import org.joml.Quaternionf;
|
||||||
|
import org.joml.Vector3f;
|
||||||
|
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.HashMap;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
|
import java.util.Random;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Volumetric rain: falling drop streaks, ground splashes with debris droplets, ground mist and lightning flashes.
|
||||||
|
* Wet-ground screen-space reflections are a separate follow-up (see {@link #renderWetSurfaceFrame}).
|
||||||
|
*/
|
||||||
|
@ModuleInfo(
|
||||||
|
id = "rain",
|
||||||
|
displayName = "Rain",
|
||||||
|
category = ModuleCategory.VISUALS,
|
||||||
|
description = "Реалистичный объёмный дождь с брызгами, туманом и молниями"
|
||||||
|
)
|
||||||
|
public final class Rain extends Module implements PostProcessPass {
|
||||||
|
|
||||||
|
private final Matrix4f wetSurfaceProjection = new Matrix4f();
|
||||||
|
private final Matrix4f wetSurfaceInvView = new Matrix4f();
|
||||||
|
private boolean wetSurfaceDepthSupported = true;
|
||||||
|
|
||||||
|
{
|
||||||
|
PostProcessManager.register(this);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static final double GRAVITY = 22.0;
|
||||||
|
private static final double TERMINAL_VELOCITY = 34.0;
|
||||||
|
private static final int RIPPLE_SEGMENTS = 14;
|
||||||
|
private static final int MAX_DROPS = 4200;
|
||||||
|
private static final int MAX_MIST = 90;
|
||||||
|
|
||||||
|
private final Minecraft mc = Minecraft.getInstance();
|
||||||
|
|
||||||
|
private final EnumValue<Preset> preset = enumMode("preset", Preset.RAIN, Preset.values());
|
||||||
|
private final NumberValue<Float> density = num("density", 1.0f, 0.1f, 3.0f);
|
||||||
|
private final NumberValue<Float> radius = num("radius", 26.0f, 8.0f, 64.0f);
|
||||||
|
private final NumberValue<Float> altitude = num("altitude", 16.0f, 6.0f, 40.0f);
|
||||||
|
private final NumberValue<Float> dropSize = num("drop_size", 1.0f, 0.3f, 2.5f);
|
||||||
|
private final NumberValue<Float> fallSpeed = num("fall_speed", 1.0f, 0.3f, 2.5f);
|
||||||
|
private final NumberValue<Float> wind = num("wind", 1.0f, 0.0f, 3.0f);
|
||||||
|
private final NumberValue<Float> opacity = num("opacity", 1.0f, 0.1f, 2.0f);
|
||||||
|
|
||||||
|
private final BooleanValue splashes = bool("splashes", true);
|
||||||
|
private final BooleanValue droplets = visibleWhen(bool("splash_droplets", true), splashes::get);
|
||||||
|
private final BooleanValue groundMist = bool("ground_mist", true);
|
||||||
|
private final BooleanValue lightning = bool("lightning", false);
|
||||||
|
private final BooleanValue skyCheck = bool("sky_check", true);
|
||||||
|
private final BooleanValue onlyWhenRaining = bool("only_when_raining", false);
|
||||||
|
|
||||||
|
private final EnumValue<ColorMode> colorMode = enumMode("color_mode", ColorMode.REALISTIC, ColorMode.values());
|
||||||
|
private final RGBAColorValue customColor = visibleWhen(color("color", "#FFBFD8E6"), () -> colorMode.get() == ColorMode.CUSTOM);
|
||||||
|
|
||||||
|
private final BooleanValue wetGround = bool("wet_ground", true);
|
||||||
|
private final NumberValue<Float> wetness = visibleWhen(num("wetness", 1.25f, 0.1f, 2.0f), wetGround::get);
|
||||||
|
private final NumberValue<Float> puddleCoverage = visibleWhen(num("puddle_coverage", 0.90f, 0.05f, 1.0f), wetGround::get);
|
||||||
|
private final NumberValue<Float> reflectionStrength = visibleWhen(num("reflection_strength", 1.55f, 0.1f, 2.0f), wetGround::get);
|
||||||
|
private final NumberValue<Float> reflectionDistance = visibleWhen(num("reflection_distance", 56.0f, 12.0f, 96.0f), wetGround::get);
|
||||||
|
private final EnumValue<ReflectionQuality> reflectionQuality = visibleWhen(enumMode("reflection_quality", ReflectionQuality.BALANCED, ReflectionQuality.values()), wetGround::get);
|
||||||
|
private final BooleanValue puddleRipples = visibleWhen(bool("puddle_ripples", true), wetGround::get);
|
||||||
|
private final NumberValue<Float> rippleStrength = visibleWhen(num("ripple_strength", 0.8f, 0.0f, 1.6f), () -> wetGround.get() && puddleRipples.get());
|
||||||
|
|
||||||
|
private final List<Drop> drops = new ArrayList<>();
|
||||||
|
private final List<Splash> splashList = new ArrayList<>();
|
||||||
|
private final List<Droplet> dropletList = new ArrayList<>();
|
||||||
|
private final List<Mist> mistList = new ArrayList<>();
|
||||||
|
private final Map<Long, Integer> groundCache = new HashMap<>();
|
||||||
|
private final BlockPos.MutableBlockPos scratchPos = new BlockPos.MutableBlockPos();
|
||||||
|
private final Random random = new Random();
|
||||||
|
|
||||||
|
private long lastFrameNanos;
|
||||||
|
private float clock;
|
||||||
|
private float flash;
|
||||||
|
private float nextFlashIn = 9.0f;
|
||||||
|
private int cacheTicks;
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onEnable() {
|
||||||
|
lastFrameNanos = 0L;
|
||||||
|
wetSurfaceDepthSupported = true;
|
||||||
|
clearAll();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onDisable() {
|
||||||
|
clearAll();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void clearAll() {
|
||||||
|
drops.clear();
|
||||||
|
splashList.clear();
|
||||||
|
dropletList.clear();
|
||||||
|
mistList.clear();
|
||||||
|
groundCache.clear();
|
||||||
|
flash = 0.0f;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onTick() {
|
||||||
|
if (mc.player == null || mc.level == null) {
|
||||||
|
clearAll();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (++cacheTicks > 30) {
|
||||||
|
cacheTicks = 0;
|
||||||
|
groundCache.clear();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private float presetDensity() {
|
||||||
|
return switch (preset.get()) {
|
||||||
|
case DRIZZLE -> 0.45f;
|
||||||
|
case DOWNPOUR -> 1.9f;
|
||||||
|
case STORM -> 2.8f;
|
||||||
|
case RAIN -> 1.0f;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
private float presetWind() {
|
||||||
|
return switch (preset.get()) {
|
||||||
|
case DRIZZLE -> 0.5f;
|
||||||
|
case DOWNPOUR -> 1.35f;
|
||||||
|
case STORM -> 2.4f;
|
||||||
|
case RAIN -> 1.0f;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
private float presetMass() {
|
||||||
|
return switch (preset.get()) {
|
||||||
|
case DRIZZLE -> 0.55f;
|
||||||
|
case DOWNPOUR -> 1.25f;
|
||||||
|
case STORM -> 1.5f;
|
||||||
|
case RAIN -> 1.0f;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
private int targetDropCount() {
|
||||||
|
float r = radius.get();
|
||||||
|
float area = r * r * 0.0155f;
|
||||||
|
int count = (int) (area * density.get() * presetDensity() * 26.0f);
|
||||||
|
return Mth.clamp(count, 40, MAX_DROPS);
|
||||||
|
}
|
||||||
|
|
||||||
|
private int resolveColorArgb() {
|
||||||
|
return switch (colorMode.get()) {
|
||||||
|
case CUSTOM -> customColor.getArgb();
|
||||||
|
case THEME -> Theme.theme().accent();
|
||||||
|
case REALISTIC -> 0xFFC8DCE8;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
private float random(float min, float max) {
|
||||||
|
return min + random.nextFloat() * (max - min);
|
||||||
|
}
|
||||||
|
|
||||||
|
private int surfaceY(int x, int z) {
|
||||||
|
long key = (((long) x) << 32) ^ (z & 0xFFFFFFFFL);
|
||||||
|
Integer cached = groundCache.get(key);
|
||||||
|
if (cached != null) return cached;
|
||||||
|
|
||||||
|
int top;
|
||||||
|
try {
|
||||||
|
top = mc.level.getHeightmapPos(Heightmap.Types.MOTION_BLOCKING, new BlockPos(x, 0, z)).getY();
|
||||||
|
} catch (Throwable ignored) {
|
||||||
|
top = mc.level.getMinY();
|
||||||
|
}
|
||||||
|
if (groundCache.size() < 20000) groundCache.put(key, top);
|
||||||
|
return top;
|
||||||
|
}
|
||||||
|
|
||||||
|
private boolean isBlocking(double x, double y, double z) {
|
||||||
|
if (mc.level == null) return false;
|
||||||
|
scratchPos.set(Mth.floor(x), Mth.floor(y), Mth.floor(z));
|
||||||
|
|
||||||
|
BlockState state = mc.level.getBlockState(scratchPos);
|
||||||
|
if (!state.getFluidState().isEmpty()) return true;
|
||||||
|
if (state.isAir()) return false;
|
||||||
|
try {
|
||||||
|
return !state.getCollisionShape(mc.level, scratchPos).isEmpty();
|
||||||
|
} catch (Throwable ignored) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private boolean isWater(double x, double y, double z) {
|
||||||
|
if (mc.level == null) return false;
|
||||||
|
scratchPos.set(Mth.floor(x), Mth.floor(y), Mth.floor(z));
|
||||||
|
try {
|
||||||
|
return !mc.level.getBlockState(scratchPos).getFluidState().isEmpty();
|
||||||
|
} catch (Throwable ignored) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private boolean skyVisible(double x, double y, double z) {
|
||||||
|
if (!skyCheck.get() || mc.level == null) return true;
|
||||||
|
scratchPos.set(Mth.floor(x), Mth.floor(y), Mth.floor(z));
|
||||||
|
try {
|
||||||
|
return mc.level.canSeeSky(scratchPos);
|
||||||
|
} catch (Throwable ignored) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void spawnDrop(Vec3 center, boolean fromTop) {
|
||||||
|
float r = radius.get();
|
||||||
|
double angle = random.nextDouble() * Math.PI * 2.0;
|
||||||
|
double dist = Math.sqrt(random.nextDouble()) * r;
|
||||||
|
|
||||||
|
double x = center.x + Math.cos(angle) * dist;
|
||||||
|
double z = center.z + Math.sin(angle) * dist;
|
||||||
|
double y = fromTop
|
||||||
|
? center.y + altitude.get() * random(0.75f, 1.0f)
|
||||||
|
: center.y + random(-4.0f, altitude.get());
|
||||||
|
|
||||||
|
if (!skyVisible(x, y, z)) return;
|
||||||
|
|
||||||
|
Drop drop = new Drop();
|
||||||
|
drop.x = x;
|
||||||
|
drop.y = y;
|
||||||
|
drop.z = z;
|
||||||
|
drop.mass = random(0.6f, 1.4f) * presetMass();
|
||||||
|
drop.vy = -random(6.0f, 11.0f) * drop.mass;
|
||||||
|
drop.phase = random(0.0f, 62.8f);
|
||||||
|
drop.brightness = random(0.55f, 1.0f);
|
||||||
|
drop.ground = surfaceY(Mth.floor(x), Mth.floor(z));
|
||||||
|
drops.add(drop);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void updateDrops(Vec3 center, float dt) {
|
||||||
|
int target = targetDropCount();
|
||||||
|
|
||||||
|
drops.removeIf(d -> {
|
||||||
|
double dx = d.x - center.x;
|
||||||
|
double dz = d.z - center.z;
|
||||||
|
float r = radius.get() + 6.0f;
|
||||||
|
return d.dead || dx * dx + dz * dz > r * r || d.y < center.y - altitude.get() - 12.0;
|
||||||
|
});
|
||||||
|
|
||||||
|
boolean fill = drops.size() < target / 2;
|
||||||
|
int spawnBudget = fill ? Math.min(target - drops.size(), 900) : Math.min(target - drops.size(), 260);
|
||||||
|
for (int i = 0; i < spawnBudget; i++) {
|
||||||
|
spawnDrop(center, !fill);
|
||||||
|
}
|
||||||
|
|
||||||
|
float speedMul = fallSpeed.get();
|
||||||
|
float windMul = wind.get() * presetWind();
|
||||||
|
|
||||||
|
for (int i = 0, size = drops.size(); i < size; i++) {
|
||||||
|
Drop d = drops.get(i);
|
||||||
|
|
||||||
|
double gust = 0.65 + 0.35 * Math.sin(clock * 0.55 + d.phase * 0.15)
|
||||||
|
+ 0.18 * Math.sin(clock * 1.7 + d.phase);
|
||||||
|
double windX = windMul * 3.1 * gust;
|
||||||
|
double windZ = windMul * 1.7 * Math.sin(clock * 0.31 + d.phase * 0.05) * gust;
|
||||||
|
|
||||||
|
double drag = 1.0 - Math.min(0.92, Math.abs(d.vy) / (TERMINAL_VELOCITY * d.mass));
|
||||||
|
d.vy -= GRAVITY * d.mass * drag * dt;
|
||||||
|
if (d.vy < -TERMINAL_VELOCITY * d.mass) d.vy = -TERMINAL_VELOCITY * d.mass;
|
||||||
|
|
||||||
|
d.vx += (windX - d.vx) * Math.min(1.0, dt * 3.2);
|
||||||
|
d.vz += (windZ - d.vz) * Math.min(1.0, dt * 3.2);
|
||||||
|
|
||||||
|
double nx = d.x + d.vx * dt * speedMul;
|
||||||
|
double ny = d.y + d.vy * dt * speedMul;
|
||||||
|
double nz = d.z + d.vz * dt * speedMul;
|
||||||
|
|
||||||
|
boolean hit = false;
|
||||||
|
if (ny <= d.ground + 2.5) {
|
||||||
|
if (isBlocking(nx, ny, nz)) {
|
||||||
|
hit = true;
|
||||||
|
} else if (ny <= d.ground - 3.0) {
|
||||||
|
d.dead = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (hit) {
|
||||||
|
if (splashes.get()) addSplash(nx, ny, nz, d);
|
||||||
|
d.dead = true;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
d.x = nx;
|
||||||
|
d.y = ny;
|
||||||
|
d.z = nz;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void addSplash(double x, double y, double z, Drop d) {
|
||||||
|
if (splashList.size() > 700) return;
|
||||||
|
|
||||||
|
double impactY = Math.floor(y) + 1.0005;
|
||||||
|
Splash splash = new Splash();
|
||||||
|
splash.x = x;
|
||||||
|
splash.y = impactY;
|
||||||
|
splash.z = z;
|
||||||
|
splash.life = 0.0f;
|
||||||
|
splash.maxLife = random(0.30f, 0.52f);
|
||||||
|
splash.scale = (float) (0.32 + 0.22 * d.mass);
|
||||||
|
splash.water = isWater(x, y - 0.15, z);
|
||||||
|
splashList.add(splash);
|
||||||
|
|
||||||
|
if (!droplets.get() || dropletList.size() > 1400) return;
|
||||||
|
|
||||||
|
int shards = splash.water ? 4 : 3;
|
||||||
|
for (int i = 0; i < shards; i++) {
|
||||||
|
double a = random.nextDouble() * Math.PI * 2.0;
|
||||||
|
double sp = random(1.1f, 2.6f) * d.mass;
|
||||||
|
Droplet drop = new Droplet();
|
||||||
|
drop.x = x;
|
||||||
|
drop.y = impactY + 0.02;
|
||||||
|
drop.z = z;
|
||||||
|
drop.vx = Math.cos(a) * sp * 0.45 + d.vx * 0.08;
|
||||||
|
drop.vz = Math.sin(a) * sp * 0.45 + d.vz * 0.08;
|
||||||
|
drop.vy = random(1.9f, 4.1f);
|
||||||
|
drop.maxLife = random(0.28f, 0.5f);
|
||||||
|
dropletList.add(drop);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void updateDroplets(float dt) {
|
||||||
|
for (int i = dropletList.size() - 1; i >= 0; i--) {
|
||||||
|
Droplet dr = dropletList.get(i);
|
||||||
|
dr.life += dt;
|
||||||
|
if (dr.life >= dr.maxLife) {
|
||||||
|
dropletList.remove(i);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
dr.vy -= GRAVITY * 0.55 * dt;
|
||||||
|
dr.x += dr.vx * dt;
|
||||||
|
dr.y += dr.vy * dt;
|
||||||
|
dr.z += dr.vz * dt;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void updateSplashes(float dt) {
|
||||||
|
for (int i = splashList.size() - 1; i >= 0; i--) {
|
||||||
|
Splash s = splashList.get(i);
|
||||||
|
s.life += dt;
|
||||||
|
if (s.life >= s.maxLife) splashList.remove(i);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void updateMist(Vec3 center, float dt) {
|
||||||
|
if (!groundMist.get()) {
|
||||||
|
mistList.clear();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
int target = (int) Mth.clamp(22 * density.get() * presetDensity(), 6, MAX_MIST);
|
||||||
|
|
||||||
|
mistList.removeIf(m -> {
|
||||||
|
m.life += dt;
|
||||||
|
double dx = m.x - center.x;
|
||||||
|
double dz = m.z - center.z;
|
||||||
|
float r = radius.get();
|
||||||
|
m.x += m.vx * dt;
|
||||||
|
m.z += m.vz * dt;
|
||||||
|
return m.life > m.maxLife || dx * dx + dz * dz > r * r;
|
||||||
|
});
|
||||||
|
|
||||||
|
while (mistList.size() < target) {
|
||||||
|
double angle = random.nextDouble() * Math.PI * 2.0;
|
||||||
|
double dist = Math.sqrt(random.nextDouble()) * radius.get() * 0.85;
|
||||||
|
double x = center.x + Math.cos(angle) * dist;
|
||||||
|
double z = center.z + Math.sin(angle) * dist;
|
||||||
|
int ground = surfaceY(Mth.floor(x), Mth.floor(z));
|
||||||
|
|
||||||
|
Mist m = new Mist();
|
||||||
|
m.x = x;
|
||||||
|
m.z = z;
|
||||||
|
m.y = ground + random(0.05f, 0.9f);
|
||||||
|
m.size = random(2.2f, 5.6f);
|
||||||
|
m.maxLife = random(2.4f, 5.5f);
|
||||||
|
m.life = random(0.0f, 1.0f);
|
||||||
|
m.vx = random(-0.35f, 0.35f) * wind.get();
|
||||||
|
m.vz = random(-0.35f, 0.35f) * wind.get();
|
||||||
|
mistList.add(m);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void updateLightning(float dt) {
|
||||||
|
if (!lightning.get()) {
|
||||||
|
flash = 0.0f;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
flash = Math.max(0.0f, flash - dt * 3.4f);
|
||||||
|
nextFlashIn -= dt;
|
||||||
|
if (nextFlashIn <= 0.0f) {
|
||||||
|
flash = random(0.55f, 1.0f);
|
||||||
|
nextFlashIn = random(6.0f, 16.0f);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private float ambientLight(Camera camera) {
|
||||||
|
try {
|
||||||
|
int light = mc.level.getBrightness(net.minecraft.world.level.LightLayer.BLOCK, camera.blockPosition());
|
||||||
|
return Mth.clamp(light / 15.0f, 0.3f, 1.0f);
|
||||||
|
} catch (Throwable ignored) {
|
||||||
|
return 1.0f;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- Wet-ground screen-space reflection pass (PostProcessPass) ---
|
||||||
|
|
||||||
|
private int reflectionSteps() {
|
||||||
|
return switch (reflectionQuality.get()) {
|
||||||
|
case HIGH -> 16;
|
||||||
|
case BALANCED -> 10;
|
||||||
|
case PERFORMANCE -> 6;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
private float rainAmount() {
|
||||||
|
return Mth.clamp(0.55f + density.get() * presetDensity() * 0.28f, 0.55f, 1.45f);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean isActive() {
|
||||||
|
return isEnabled()
|
||||||
|
&& wetGround.get()
|
||||||
|
&& wetSurfaceDepthSupported
|
||||||
|
&& mc.player != null
|
||||||
|
&& mc.level != null
|
||||||
|
&& (!onlyWhenRaining.get() || mc.level.isRaining());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public int getPriority() {
|
||||||
|
return 2;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Phase getPhase() {
|
||||||
|
return Phase.PRE_HAND;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean render(GpuTextureView src, GpuTextureView dst, float tickDelta) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean render(PostProcessContext context, GpuTextureView src, GpuTextureView dst) {
|
||||||
|
if (!isActive() || context == null || src == null || dst == null) return false;
|
||||||
|
|
||||||
|
GpuTextureView mainDepth = context.mainDepth();
|
||||||
|
boolean hasDepth = mainDepth != null;
|
||||||
|
if (!hasDepth) mainDepth = src;
|
||||||
|
|
||||||
|
Matrix4f projection = LoVisualWorldMatrices.renderProjectionMatrix();
|
||||||
|
if (projection == null) projection = RenderState.worldProjection;
|
||||||
|
wetSurfaceProjection.set(projection);
|
||||||
|
wetSurfaceInvView.set(new Matrix4f().rotation(RenderState.cameraRotation));
|
||||||
|
|
||||||
|
WetSurfaceUniforms.update(
|
||||||
|
wetSurfaceProjection,
|
||||||
|
wetSurfaceInvView,
|
||||||
|
(float) RenderState.cameraPos.x, (float) RenderState.cameraPos.y, (float) RenderState.cameraPos.z,
|
||||||
|
context.width() > 0 ? 1.0f / context.width() : 0.0f,
|
||||||
|
context.height() > 0 ? 1.0f / context.height() : 0.0f,
|
||||||
|
clock,
|
||||||
|
hasDepth,
|
||||||
|
wetness.get(),
|
||||||
|
puddleCoverage.get(),
|
||||||
|
reflectionStrength.get(),
|
||||||
|
puddleRipples.get() ? rippleStrength.get() : 0.0f,
|
||||||
|
reflectionDistance.get(),
|
||||||
|
reflectionSteps(),
|
||||||
|
rainAmount(),
|
||||||
|
puddleRipples.get()
|
||||||
|
);
|
||||||
|
|
||||||
|
try {
|
||||||
|
FullScreenRenderer.ensureInit();
|
||||||
|
FullScreenRenderer.begin("LoVisual WetSurface Pass")
|
||||||
|
.attachment(dst)
|
||||||
|
.pipeline(LoVisualRenderPipelines.WET_SURFACE)
|
||||||
|
.uniform("WetSurface", WetSurfaceUniforms.get())
|
||||||
|
.sampler("u_Texture", src, PostProcessManager.getSampler())
|
||||||
|
.sampler("u_MainDepth", mainDepth, PostProcessManager.getSampler())
|
||||||
|
.end();
|
||||||
|
} catch (Throwable t) {
|
||||||
|
wetSurfaceDepthSupported = false;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public WorldPhase getWorldPhase() {
|
||||||
|
return WorldPhase.AFTER_POST_PROCESS;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onRenderWorldEngine(Renderer3D renderer, Renderer3D depthRenderer, float tickDelta) {
|
||||||
|
if (!isEnabled() || mc.player == null || mc.level == null || mc.gameRenderer == null) return;
|
||||||
|
if (mc.gameRenderer.mainCamera() == null) return;
|
||||||
|
if (onlyWhenRaining.get() && !mc.level.isRaining()) {
|
||||||
|
clearAll();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
Camera camera = mc.gameRenderer.mainCamera();
|
||||||
|
|
||||||
|
long now = System.nanoTime();
|
||||||
|
if (lastFrameNanos == 0L) lastFrameNanos = now;
|
||||||
|
float dt = (float) ((now - lastFrameNanos) / 1_000_000_000.0);
|
||||||
|
lastFrameNanos = now;
|
||||||
|
dt = Mth.clamp(dt, 0.0f, 0.05f);
|
||||||
|
clock += dt;
|
||||||
|
|
||||||
|
Vec3 cam = camera.position();
|
||||||
|
|
||||||
|
updateDrops(cam, dt);
|
||||||
|
updateSplashes(dt);
|
||||||
|
updateDroplets(dt);
|
||||||
|
updateMist(cam, dt);
|
||||||
|
updateLightning(dt);
|
||||||
|
|
||||||
|
if (drops.isEmpty() && splashList.isEmpty() && dropletList.isEmpty() && mistList.isEmpty()) return;
|
||||||
|
|
||||||
|
int colorArgb = resolveColorArgb();
|
||||||
|
float r = ((colorArgb >>> 16) & 0xFF) / 255f;
|
||||||
|
float g = ((colorArgb >>> 8) & 0xFF) / 255f;
|
||||||
|
float b = (colorArgb & 0xFF) / 255f;
|
||||||
|
float[] rgb = {r, g, b};
|
||||||
|
|
||||||
|
float ambient = ambientLight(camera);
|
||||||
|
float brightnessMul = Mth.clamp(ambient + flash * 0.9f, 0.0f, 1.65f);
|
||||||
|
float alphaMul = opacity.get() * brightnessMul;
|
||||||
|
|
||||||
|
Quaternionf camRot = RenderState.cameraRotation;
|
||||||
|
float camYaw = camera.yRot();
|
||||||
|
|
||||||
|
MeshBuilder blendMesh = renderer.batch(LoVisualRenderPipelines.WORLD_COLORED, Renderer3D.DepthMode.MAIN);
|
||||||
|
if (blendMesh != null) {
|
||||||
|
if (groundMist.get()) drawMist(blendMesh, camRot, rgb, alphaMul);
|
||||||
|
drawStreaks(blendMesh, camYaw, rgb, alphaMul, false);
|
||||||
|
if (splashes.get()) drawRipples(blendMesh, rgb, alphaMul);
|
||||||
|
}
|
||||||
|
|
||||||
|
MeshBuilder additiveMesh = renderer.batch(LoVisualRenderPipelines.WORLD_COLORED_ADDITIVE_DEPTH, Renderer3D.DepthMode.PRE_DEPTH);
|
||||||
|
if (additiveMesh != null) {
|
||||||
|
drawStreaks(additiveMesh, camYaw, rgb, alphaMul, true);
|
||||||
|
if (splashes.get()) {
|
||||||
|
drawCrowns(additiveMesh, camYaw, rgb, alphaMul);
|
||||||
|
if (droplets.get()) drawDroplets(additiveMesh, camYaw, rgb, alphaMul);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static int packColor(float r, float g, float b, float a) {
|
||||||
|
int ai = Mth.clamp(Math.round(a * 255.0f), 0, 255);
|
||||||
|
int ri = Mth.clamp(Math.round(r * 255.0f), 0, 255);
|
||||||
|
int gi = Mth.clamp(Math.round(g * 255.0f), 0, 255);
|
||||||
|
int bi = Mth.clamp(Math.round(b * 255.0f), 0, 255);
|
||||||
|
return (ai << 24) | (ri << 16) | (gi << 8) | bi;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static Vector3f rotated(Quaternionf rot, float x, float y, float z) {
|
||||||
|
return new Vector3f(x, y, z).rotate(rot);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void drawStreaks(MeshBuilder mesh, float camYaw, float[] rgb, float alphaMul, boolean highlight) {
|
||||||
|
if (drops.isEmpty()) return;
|
||||||
|
|
||||||
|
float sizeMul = dropSize.get();
|
||||||
|
float r = radius.get();
|
||||||
|
float fadeStart = r * 0.62f;
|
||||||
|
|
||||||
|
for (int i = 0, size = drops.size(); i < size; i++) {
|
||||||
|
Drop d = drops.get(i);
|
||||||
|
|
||||||
|
double speed = Math.sqrt(d.vx * d.vx + d.vy * d.vy + d.vz * d.vz);
|
||||||
|
if (speed < 0.0001) continue;
|
||||||
|
|
||||||
|
float length = (float) Mth.clamp((float) (speed * 0.055 * sizeMul), 0.16f, 2.6f);
|
||||||
|
float width = (float) (0.014 + 0.019 * d.mass) * sizeMul * (highlight ? 0.45f : 1.0f);
|
||||||
|
|
||||||
|
float yawRad = (float) Math.toRadians(camYaw);
|
||||||
|
double rightX = Math.cos(yawRad);
|
||||||
|
double rightZ = Math.sin(yawRad);
|
||||||
|
double horizontal = d.vx * rightX + d.vz * rightZ;
|
||||||
|
float tilt = (float) Math.toDegrees(Math.atan2(horizontal, Math.abs(d.vy) + 0.0001));
|
||||||
|
tilt = Mth.clamp(tilt, -55.0f, 55.0f);
|
||||||
|
|
||||||
|
Quaternionf orient = new Quaternionf()
|
||||||
|
.rotateY((float) Math.toRadians(-camYaw))
|
||||||
|
.rotateZ((float) Math.toRadians(-tilt));
|
||||||
|
|
||||||
|
float half = width * 0.5f;
|
||||||
|
float top = length * 0.5f;
|
||||||
|
float bottom = -length * 0.5f;
|
||||||
|
|
||||||
|
float headAlpha = alphaMul * d.brightness * (highlight ? 0.30f : 0.42f);
|
||||||
|
if (headAlpha <= 0.006f) continue;
|
||||||
|
headAlpha = Math.min(headAlpha, 0.95f);
|
||||||
|
float tailAlpha = 0.0f;
|
||||||
|
float headWidth = half;
|
||||||
|
float tailWidth = half * 0.35f;
|
||||||
|
|
||||||
|
Vector3f p1 = rotated(orient, -tailWidth, top, 0.0f);
|
||||||
|
Vector3f p2 = rotated(orient, tailWidth, top, 0.0f);
|
||||||
|
Vector3f p3 = rotated(orient, headWidth, bottom, 0.0f);
|
||||||
|
Vector3f p4 = rotated(orient, -headWidth, bottom, 0.0f);
|
||||||
|
|
||||||
|
mesh.ensureQuadCapacity();
|
||||||
|
int i1 = mesh.vec3(d.x + p1.x, d.y + p1.y, d.z + p1.z).color(packColorObj(rgb, tailAlpha)).next();
|
||||||
|
int i2 = mesh.vec3(d.x + p2.x, d.y + p2.y, d.z + p2.z).color(packColorObj(rgb, tailAlpha)).next();
|
||||||
|
int i3 = mesh.vec3(d.x + p3.x, d.y + p3.y, d.z + p3.z).color(packColorObj(rgb, headAlpha)).next();
|
||||||
|
int i4 = mesh.vec3(d.x + p4.x, d.y + p4.y, d.z + p4.z).color(packColorObj(rgb, headAlpha)).next();
|
||||||
|
mesh.quad(i1, i2, i3, i4);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void drawRipples(MeshBuilder mesh, float[] rgb, float alphaMul) {
|
||||||
|
if (splashList.isEmpty()) return;
|
||||||
|
|
||||||
|
for (int i = 0, size = splashList.size(); i < size; i++) {
|
||||||
|
Splash s = splashList.get(i);
|
||||||
|
float progress = Mth.clamp(s.life / s.maxLife, 0.0f, 1.0f);
|
||||||
|
float eased = 1.0f - (1.0f - progress) * (1.0f - progress);
|
||||||
|
|
||||||
|
float outer = s.scale * (0.18f + eased * (s.water ? 1.35f : 0.85f));
|
||||||
|
float inner = outer * (0.55f + 0.35f * eased);
|
||||||
|
float alpha = alphaMul * (1.0f - progress) * (s.water ? 0.5f : 0.38f);
|
||||||
|
if (alpha <= 0.01f) continue;
|
||||||
|
|
||||||
|
for (int seg = 0; seg < RIPPLE_SEGMENTS; seg++) {
|
||||||
|
double a0 = (Math.PI * 2.0 / RIPPLE_SEGMENTS) * seg;
|
||||||
|
double a1 = (Math.PI * 2.0 / RIPPLE_SEGMENTS) * (seg + 1);
|
||||||
|
|
||||||
|
float ix0 = (float) (Math.cos(a0) * inner);
|
||||||
|
float iz0 = (float) (Math.sin(a0) * inner);
|
||||||
|
float ix1 = (float) (Math.cos(a1) * inner);
|
||||||
|
float iz1 = (float) (Math.sin(a1) * inner);
|
||||||
|
float ox0 = (float) (Math.cos(a0) * outer);
|
||||||
|
float oz0 = (float) (Math.sin(a0) * outer);
|
||||||
|
float ox1 = (float) (Math.cos(a1) * outer);
|
||||||
|
float oz1 = (float) (Math.sin(a1) * outer);
|
||||||
|
|
||||||
|
mesh.ensureQuadCapacity();
|
||||||
|
int i1 = mesh.vec3(s.x + ix0, s.y, s.z + iz0).color(packColorObj(rgb, alpha)).next();
|
||||||
|
int i2 = mesh.vec3(s.x + ix1, s.y, s.z + iz1).color(packColorObj(rgb, alpha)).next();
|
||||||
|
int i3 = mesh.vec3(s.x + ox1, s.y, s.z + oz1).color(packColorObj(rgb, 0.0f)).next();
|
||||||
|
int i4 = mesh.vec3(s.x + ox0, s.y, s.z + oz0).color(packColorObj(rgb, 0.0f)).next();
|
||||||
|
mesh.quad(i1, i2, i3, i4);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void drawCrowns(MeshBuilder mesh, float camYaw, float[] rgb, float alphaMul) {
|
||||||
|
if (splashList.isEmpty()) return;
|
||||||
|
|
||||||
|
Quaternionf orient = new Quaternionf().rotateY((float) Math.toRadians(-camYaw));
|
||||||
|
|
||||||
|
for (int i = 0, size = splashList.size(); i < size; i++) {
|
||||||
|
Splash s = splashList.get(i);
|
||||||
|
float progress = Mth.clamp(s.life / s.maxLife, 0.0f, 1.0f);
|
||||||
|
if (progress > 0.55f) continue;
|
||||||
|
|
||||||
|
float local = progress / 0.55f;
|
||||||
|
float height = s.scale * (0.55f * (float) Math.sin(local * Math.PI));
|
||||||
|
float alpha = alphaMul * (1.0f - local) * 0.32f;
|
||||||
|
if (alpha <= 0.01f || height <= 0.005f) continue;
|
||||||
|
|
||||||
|
float base = s.scale * 0.16f;
|
||||||
|
Vector3f p1 = rotated(orient, -base * 0.25f, height, 0.0f);
|
||||||
|
Vector3f p2 = rotated(orient, base * 0.25f, height, 0.0f);
|
||||||
|
Vector3f p3 = rotated(orient, base, 0.0f, 0.0f);
|
||||||
|
Vector3f p4 = rotated(orient, -base, 0.0f, 0.0f);
|
||||||
|
|
||||||
|
mesh.ensureQuadCapacity();
|
||||||
|
int i1 = mesh.vec3(s.x + p1.x, s.y + p1.y, s.z + p1.z).color(packColorObj(rgb, 0.0f)).next();
|
||||||
|
int i2 = mesh.vec3(s.x + p2.x, s.y + p2.y, s.z + p2.z).color(packColorObj(rgb, 0.0f)).next();
|
||||||
|
int i3 = mesh.vec3(s.x + p3.x, s.y + p3.y, s.z + p3.z).color(packColorObj(rgb, alpha)).next();
|
||||||
|
int i4 = mesh.vec3(s.x + p4.x, s.y + p4.y, s.z + p4.z).color(packColorObj(rgb, alpha)).next();
|
||||||
|
mesh.quad(i1, i2, i3, i4);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void drawDroplets(MeshBuilder mesh, float camYaw, float[] rgb, float alphaMul) {
|
||||||
|
if (dropletList.isEmpty()) return;
|
||||||
|
|
||||||
|
float sizeMul = dropSize.get();
|
||||||
|
float yawRad = (float) Math.toRadians(camYaw);
|
||||||
|
|
||||||
|
for (int i = 0, size = dropletList.size(); i < size; i++) {
|
||||||
|
Droplet d = dropletList.get(i);
|
||||||
|
float progress = Mth.clamp(d.life / d.maxLife, 0.0f, 1.0f);
|
||||||
|
float alpha = alphaMul * (1.0f - progress) * 0.5f;
|
||||||
|
if (alpha <= 0.01f) continue;
|
||||||
|
|
||||||
|
double speed = Math.sqrt(d.vx * d.vx + d.vy * d.vy + d.vz * d.vz);
|
||||||
|
float length = (float) Mth.clamp((float) (speed * 0.03 * sizeMul), 0.04f, 0.4f);
|
||||||
|
float width = 0.016f * sizeMul;
|
||||||
|
|
||||||
|
double horizontal = d.vx * Math.cos(yawRad) + d.vz * Math.sin(yawRad);
|
||||||
|
float tilt = (float) Math.toDegrees(Math.atan2(horizontal, -d.vy + 0.0001));
|
||||||
|
|
||||||
|
Quaternionf orient = new Quaternionf()
|
||||||
|
.rotateY((float) Math.toRadians(-camYaw))
|
||||||
|
.rotateZ((float) Math.toRadians(-tilt));
|
||||||
|
|
||||||
|
Vector3f p1 = rotated(orient, -width * 0.4f, length * 0.5f, 0.0f);
|
||||||
|
Vector3f p2 = rotated(orient, width * 0.4f, length * 0.5f, 0.0f);
|
||||||
|
Vector3f p3 = rotated(orient, width, -length * 0.5f, 0.0f);
|
||||||
|
Vector3f p4 = rotated(orient, -width, -length * 0.5f, 0.0f);
|
||||||
|
|
||||||
|
mesh.ensureQuadCapacity();
|
||||||
|
int i1 = mesh.vec3(d.x + p1.x, d.y + p1.y, d.z + p1.z).color(packColorObj(rgb, 0.0f)).next();
|
||||||
|
int i2 = mesh.vec3(d.x + p2.x, d.y + p2.y, d.z + p2.z).color(packColorObj(rgb, 0.0f)).next();
|
||||||
|
int i3 = mesh.vec3(d.x + p3.x, d.y + p3.y, d.z + p3.z).color(packColorObj(rgb, alpha)).next();
|
||||||
|
int i4 = mesh.vec3(d.x + p4.x, d.y + p4.y, d.z + p4.z).color(packColorObj(rgb, alpha)).next();
|
||||||
|
mesh.quad(i1, i2, i3, i4);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void drawMist(MeshBuilder mesh, Quaternionf camRot, float[] rgb, float alphaMul) {
|
||||||
|
if (mistList.isEmpty()) return;
|
||||||
|
|
||||||
|
for (int i = 0, size = mistList.size(); i < size; i++) {
|
||||||
|
Mist mist = mistList.get(i);
|
||||||
|
float progress = Mth.clamp(mist.life / mist.maxLife, 0.0f, 1.0f);
|
||||||
|
float fade = (float) Math.sin(progress * Math.PI);
|
||||||
|
float alpha = alphaMul * fade * 0.05f;
|
||||||
|
if (alpha <= 0.004f) continue;
|
||||||
|
|
||||||
|
float half = mist.size * 0.5f;
|
||||||
|
float top = mist.size * 0.42f;
|
||||||
|
|
||||||
|
Vector3f right = new Vector3f(1.0f, 0.0f, 0.0f).rotate(camRot);
|
||||||
|
Vector3f up = new Vector3f(0.0f, 1.0f, 0.0f).rotate(camRot);
|
||||||
|
|
||||||
|
double bx = mist.x, by = mist.y, bz = mist.z;
|
||||||
|
|
||||||
|
mesh.ensureQuadCapacity();
|
||||||
|
int i1 = mesh.vec3(bx - right.x * half, by - right.y * half + up.y * top, bz - right.z * half).color(packColorObj(rgb, 0.0f)).next();
|
||||||
|
int i2 = mesh.vec3(bx + right.x * half, by + right.y * half + up.y * top, bz + right.z * half).color(packColorObj(rgb, 0.0f)).next();
|
||||||
|
int i3 = mesh.vec3(bx + right.x * half, by + right.y * half, bz + right.z * half).color(packColorObj(rgb, alpha)).next();
|
||||||
|
int i4 = mesh.vec3(bx - right.x * half, by - right.y * half, bz - right.z * half).color(packColorObj(rgb, alpha)).next();
|
||||||
|
mesh.quad(i1, i2, i3, i4);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private dev.loki.lovisual.render.engine.color.RenderColor packColorObj(float[] rgb, float a) {
|
||||||
|
return new dev.loki.lovisual.render.engine.color.RenderColor(packColor(rgb[0], rgb[1], rgb[2], a));
|
||||||
|
}
|
||||||
|
|
||||||
|
public enum Preset implements EnumValue.IdProvider {
|
||||||
|
DRIZZLE("drizzle"),
|
||||||
|
RAIN("rain"),
|
||||||
|
DOWNPOUR("downpour"),
|
||||||
|
STORM("storm");
|
||||||
|
|
||||||
|
private final String id;
|
||||||
|
|
||||||
|
Preset(String id) {
|
||||||
|
this.id = id;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String id() {
|
||||||
|
return id;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public enum ColorMode implements EnumValue.IdProvider {
|
||||||
|
REALISTIC("realistic"),
|
||||||
|
THEME("theme"),
|
||||||
|
CUSTOM("custom");
|
||||||
|
|
||||||
|
private final String id;
|
||||||
|
|
||||||
|
ColorMode(String id) {
|
||||||
|
this.id = id;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String id() {
|
||||||
|
return id;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public enum ReflectionQuality implements EnumValue.IdProvider {
|
||||||
|
PERFORMANCE("performance"),
|
||||||
|
BALANCED("balanced"),
|
||||||
|
HIGH("high");
|
||||||
|
|
||||||
|
private final String id;
|
||||||
|
|
||||||
|
ReflectionQuality(String id) {
|
||||||
|
this.id = id;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String id() {
|
||||||
|
return id;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static final class Drop {
|
||||||
|
double x, y, z;
|
||||||
|
double vx, vy, vz;
|
||||||
|
double mass = 1.0;
|
||||||
|
double ground;
|
||||||
|
float phase;
|
||||||
|
float brightness = 1.0f;
|
||||||
|
boolean dead;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static final class Droplet {
|
||||||
|
double x, y, z;
|
||||||
|
double vx, vy, vz;
|
||||||
|
float life;
|
||||||
|
float maxLife = 0.4f;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static final class Splash {
|
||||||
|
double x, y, z;
|
||||||
|
float life;
|
||||||
|
float maxLife = 0.4f;
|
||||||
|
float scale = 0.4f;
|
||||||
|
boolean water;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static final class Mist {
|
||||||
|
double x, y, z;
|
||||||
|
double vx, vz;
|
||||||
|
float size = 3.0f;
|
||||||
|
float life;
|
||||||
|
float maxLife = 4.0f;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -5,11 +5,17 @@ import dev.loki.lovisual.config.values.NumberValue;
|
||||||
import dev.loki.lovisual.features.module.Module;
|
import dev.loki.lovisual.features.module.Module;
|
||||||
import dev.loki.lovisual.features.module.ModuleCategory;
|
import dev.loki.lovisual.features.module.ModuleCategory;
|
||||||
import dev.loki.lovisual.features.module.ModuleInfo;
|
import dev.loki.lovisual.features.module.ModuleInfo;
|
||||||
|
import dev.loki.lovisual.features.module.WorldPhase;
|
||||||
|
import dev.loki.lovisual.render.engine.postprocess.SkyShaderRenderer;
|
||||||
|
import dev.loki.lovisual.render.engine.renderer.Renderer3D;
|
||||||
|
import net.minecraft.client.Minecraft;
|
||||||
|
|
||||||
//todo Description
|
//todo Description
|
||||||
@ModuleInfo(id = "shadersky", displayName = "ShaderSky", category = ModuleCategory.VISUALS)
|
@ModuleInfo(id = "shadersky", displayName = "ShaderSky", category = ModuleCategory.VISUALS)
|
||||||
public class ShaderSky extends Module {
|
public class ShaderSky extends Module {
|
||||||
|
|
||||||
|
private final Minecraft mc = Minecraft.getInstance();
|
||||||
|
|
||||||
private static final String SETTING_MODE = "mode";
|
private static final String SETTING_MODE = "mode";
|
||||||
private static final String SETTING_SPEED = "speed";
|
private static final String SETTING_SPEED = "speed";
|
||||||
private static final String SETTING_SCALE = "scale";
|
private static final String SETTING_SCALE = "scale";
|
||||||
|
|
@ -23,7 +29,7 @@ public class ShaderSky extends Module {
|
||||||
"shaderSkyMode",
|
"shaderSkyMode",
|
||||||
SETTING_MODE,
|
SETTING_MODE,
|
||||||
"Thunder",
|
"Thunder",
|
||||||
"Thunder", "Pulsar"
|
"Thunder", "Pulsar", "Sky", "Water", "Caustic"
|
||||||
);
|
);
|
||||||
private final NumberValue<Float> speed =
|
private final NumberValue<Float> speed =
|
||||||
num("shaderSkySpeed", SETTING_SPEED, 1.0f, 0.1f, 5.0f);
|
num("shaderSkySpeed", SETTING_SPEED, 1.0f, 0.1f, 5.0f);
|
||||||
|
|
@ -75,4 +81,41 @@ public class ShaderSky extends Module {
|
||||||
private boolean isThunderMode() {
|
private boolean isThunderMode() {
|
||||||
return "Thunder".equals(mode.get());
|
return "Thunder".equals(mode.get());
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
private boolean isSkyShaderMode() {
|
||||||
|
String m = mode.get();
|
||||||
|
return "Sky".equals(m) || "Water".equals(m) || "Caustic".equals(m);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public WorldPhase getWorldPhase() {
|
||||||
|
return WorldPhase.AFTER_POST_PROCESS;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onEnable() {
|
||||||
|
SkyShaderRenderer.reset();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onRenderWorldEngine(Renderer3D renderer, Renderer3D depthRenderer, float tickDelta) {
|
||||||
|
if (!isEnabled() || mc.level == null || mc.player == null) return;
|
||||||
|
|
||||||
|
if (isSkyShaderMode()) {
|
||||||
|
SkyShaderRenderer.Mode skyMode = switch (mode.get()) {
|
||||||
|
case "Water" -> SkyShaderRenderer.Mode.WATER;
|
||||||
|
case "Caustic" -> SkyShaderRenderer.Mode.CAUSTIC;
|
||||||
|
default -> SkyShaderRenderer.Mode.SKY;
|
||||||
|
};
|
||||||
|
SkyShaderRenderer.render(
|
||||||
|
mc,
|
||||||
|
skyMode,
|
||||||
|
speed.get(),
|
||||||
|
scale.get(),
|
||||||
|
intensity.get(),
|
||||||
|
alpha.get(),
|
||||||
|
0xFF6FA8DC & 0x00FFFFFF
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,432 @@
|
||||||
|
package dev.loki.lovisual.features.module.modules.visuals.lyrics;
|
||||||
|
|
||||||
|
import dev.loki.lovisual.config.values.BooleanValue;
|
||||||
|
import dev.loki.lovisual.config.values.EnumValue;
|
||||||
|
import dev.loki.lovisual.config.values.NumberValue;
|
||||||
|
import dev.loki.lovisual.config.values.RGBAColorValue;
|
||||||
|
import dev.loki.lovisual.features.module.Module;
|
||||||
|
import dev.loki.lovisual.features.module.ModuleCategory;
|
||||||
|
import dev.loki.lovisual.features.module.ModuleInfo;
|
||||||
|
import dev.loki.lovisual.features.module.WorldPhase;
|
||||||
|
import dev.loki.lovisual.features.theme.Theme;
|
||||||
|
import dev.loki.lovisual.render.engine.color.RenderColor;
|
||||||
|
import dev.loki.lovisual.render.engine.renderer.Renderer2D;
|
||||||
|
import dev.loki.lovisual.render.engine.renderer.Renderer3D;
|
||||||
|
import dev.loki.lovisual.render.engine.text.FontInfo;
|
||||||
|
import dev.loki.lovisual.render.engine.text.Fonts;
|
||||||
|
import dev.loki.lovisual.render.engine.text.TextRenderer;
|
||||||
|
import dev.loki.lovisual.util.media.MediaSessionService;
|
||||||
|
import net.minecraft.client.Camera;
|
||||||
|
import net.minecraft.client.Minecraft;
|
||||||
|
import net.minecraft.client.gui.GuiGraphicsExtractor;
|
||||||
|
import net.minecraft.world.phys.Vec3;
|
||||||
|
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.concurrent.CopyOnWriteArrayList;
|
||||||
|
import java.util.concurrent.ThreadLocalRandom;
|
||||||
|
|
||||||
|
@ModuleInfo(
|
||||||
|
id = "kineticlyrics",
|
||||||
|
displayName = "KineticLyrics",
|
||||||
|
aliases = {"lyrics"},
|
||||||
|
category = ModuleCategory.VISUALS,
|
||||||
|
description = "Рендерит текст текущей песни в 3D-пространстве вокруг игрока"
|
||||||
|
)
|
||||||
|
public final class KineticLyrics extends Module {
|
||||||
|
|
||||||
|
private final Minecraft mc = Minecraft.getInstance();
|
||||||
|
|
||||||
|
private final EnumValue<AnimationMode> animation =
|
||||||
|
enumMode("animation", AnimationMode.LYRIC_FLOW, AnimationMode.values());
|
||||||
|
private final EnumValue<ColorMode> colorMode =
|
||||||
|
enumMode("color_mode", ColorMode.THEME, ColorMode.values());
|
||||||
|
private final RGBAColorValue staticColor =
|
||||||
|
visibleWhen(color("static_color", "#FFFFFFFF"), () -> colorMode.get() == ColorMode.STATIC);
|
||||||
|
private final EnumValue<FontMode> fontMode =
|
||||||
|
enumMode("font", FontMode.ONEST_BOLD, FontMode.values());
|
||||||
|
private final EnumValue<SplitMode> splitMode =
|
||||||
|
enumMode("split_mode", SplitMode.SMART_SPLIT, SplitMode.values());
|
||||||
|
private final NumberValue<Integer> maxLines =
|
||||||
|
num("max_lines", 3, 1, 6);
|
||||||
|
private final NumberValue<Float> minDistance =
|
||||||
|
num("min_distance", 2.0f, 1.2f, 4.0f);
|
||||||
|
private final NumberValue<Float> maxDistance =
|
||||||
|
num("max_distance", 4.5f, 2.0f, 7.0f);
|
||||||
|
private final NumberValue<Float> arcSpread =
|
||||||
|
num("arc_spread", 70.0f, 20.0f, 85.0f);
|
||||||
|
private final NumberValue<Float> floatHeight =
|
||||||
|
num("float_height", 0.5f, 0.1f, 2.0f);
|
||||||
|
private final NumberValue<Integer> timeOffsetMs =
|
||||||
|
num("time_offset_ms", 0, -5000, 5000);
|
||||||
|
private final BooleanValue debugMode =
|
||||||
|
bool("debug", false);
|
||||||
|
|
||||||
|
private final List<LyricLine> rawLyrics = new ArrayList<>();
|
||||||
|
private final List<LyricLine> lyricsQueue = new ArrayList<>();
|
||||||
|
private final List<LyricParticle3D> activeParticles = new CopyOnWriteArrayList<>();
|
||||||
|
private int currentLyricIndex = 0;
|
||||||
|
|
||||||
|
private String lastTrackKey = "";
|
||||||
|
private String currentTrackTitle = "";
|
||||||
|
private String currentTrackArtist = "";
|
||||||
|
private long lastAudioTimeMs = -1L;
|
||||||
|
private boolean isPlaying = false;
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onEnable() {
|
||||||
|
resetPlaybackState();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onDisable() {
|
||||||
|
resetPlaybackState();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void resetPlaybackState() {
|
||||||
|
activeParticles.clear();
|
||||||
|
rawLyrics.clear();
|
||||||
|
lyricsQueue.clear();
|
||||||
|
lastTrackKey = "";
|
||||||
|
currentTrackTitle = "";
|
||||||
|
currentTrackArtist = "";
|
||||||
|
currentLyricIndex = 0;
|
||||||
|
lastAudioTimeMs = -1L;
|
||||||
|
isPlaying = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onTick() {
|
||||||
|
if (!isEnabled() || mc.player == null) return;
|
||||||
|
syncPlaybackTime();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void syncPlaybackTime() {
|
||||||
|
MediaSessionService.Snapshot snapshot = MediaSessionService.get().snapshot();
|
||||||
|
if (!snapshot.hasSession() || snapshot.title().isBlank()) {
|
||||||
|
if (isPlaying) {
|
||||||
|
isPlaying = false;
|
||||||
|
activeParticles.clear();
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
String title = snapshot.title();
|
||||||
|
String artist = snapshot.artist();
|
||||||
|
String key = (title + " - " + artist).toLowerCase();
|
||||||
|
|
||||||
|
if (!key.equalsIgnoreCase(lastTrackKey)) {
|
||||||
|
loadTrack(title, artist, key);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!isPlaying && !lyricsQueue.isEmpty()) {
|
||||||
|
isPlaying = true;
|
||||||
|
}
|
||||||
|
if (lyricsQueue.isEmpty()) return;
|
||||||
|
|
||||||
|
long audioTimeMs = Math.max(0L, snapshot.predictedPositionSeconds() * 1000L + timeOffsetMs.get());
|
||||||
|
|
||||||
|
if (lastAudioTimeMs >= 0 && Math.abs(audioTimeMs - lastAudioTimeMs) > 1500L) {
|
||||||
|
resyncQueue(audioTimeMs);
|
||||||
|
}
|
||||||
|
lastAudioTimeMs = audioTimeMs;
|
||||||
|
|
||||||
|
advanceQueue(audioTimeMs);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void loadTrack(String title, String artist, String key) {
|
||||||
|
this.currentTrackTitle = title;
|
||||||
|
this.currentTrackArtist = artist;
|
||||||
|
this.lastTrackKey = key;
|
||||||
|
|
||||||
|
activeParticles.clear();
|
||||||
|
rawLyrics.clear();
|
||||||
|
lyricsQueue.clear();
|
||||||
|
currentLyricIndex = 0;
|
||||||
|
lastAudioTimeMs = -1L;
|
||||||
|
isPlaying = false;
|
||||||
|
|
||||||
|
LrcLibClient.fetchLyricsAsync(title, artist, debugMode.get()).thenAccept(lines -> {
|
||||||
|
if (!this.lastTrackKey.equalsIgnoreCase(key)) return;
|
||||||
|
|
||||||
|
activeParticles.clear();
|
||||||
|
rawLyrics.clear();
|
||||||
|
lyricsQueue.clear();
|
||||||
|
rawLyrics.addAll(lines);
|
||||||
|
rebuildLyricsQueue();
|
||||||
|
currentLyricIndex = 0;
|
||||||
|
isPlaying = !lyricsQueue.isEmpty();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private void rebuildLyricsQueue() {
|
||||||
|
lyricsQueue.clear();
|
||||||
|
if (splitMode.get() == SplitMode.SMART_SPLIT) {
|
||||||
|
lyricsQueue.addAll(LyricLineSplitter.splitLongLines(rawLyrics));
|
||||||
|
} else {
|
||||||
|
lyricsQueue.addAll(rawLyrics);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void resyncQueue(long audioTimeMs) {
|
||||||
|
activeParticles.clear();
|
||||||
|
currentLyricIndex = 0;
|
||||||
|
long now = System.currentTimeMillis();
|
||||||
|
|
||||||
|
for (int i = 0; i < lyricsQueue.size(); i++) {
|
||||||
|
LyricLine line = lyricsQueue.get(i);
|
||||||
|
long nextStartMs = (i + 1 < lyricsQueue.size()) ? lyricsQueue.get(i + 1).timestampMs() : line.timestampMs() + 3500L;
|
||||||
|
long durationMs = Math.max(1200L, Math.min(5000L, nextStartMs - line.timestampMs()));
|
||||||
|
|
||||||
|
if (audioTimeMs >= line.timestampMs()) {
|
||||||
|
long timeSinceLine = audioTimeMs - line.timestampMs();
|
||||||
|
if (timeSinceLine < durationMs) {
|
||||||
|
spawnLyricParticle(line.text(), now - timeSinceLine, durationMs);
|
||||||
|
}
|
||||||
|
currentLyricIndex = i + 1;
|
||||||
|
} else {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void advanceQueue(long audioTimeMs) {
|
||||||
|
if (!isPlaying || lyricsQueue.isEmpty() || mc.player == null) return;
|
||||||
|
|
||||||
|
long now = System.currentTimeMillis();
|
||||||
|
|
||||||
|
while (currentLyricIndex < lyricsQueue.size()) {
|
||||||
|
LyricLine line = lyricsQueue.get(currentLyricIndex);
|
||||||
|
long nextStartMs = (currentLyricIndex + 1 < lyricsQueue.size()) ? lyricsQueue.get(currentLyricIndex + 1).timestampMs() : line.timestampMs() + 3500L;
|
||||||
|
long durationMs = Math.max(1400L, Math.min(5500L, nextStartMs - line.timestampMs()));
|
||||||
|
|
||||||
|
if (audioTimeMs >= line.timestampMs()) {
|
||||||
|
long timeSinceLine = audioTimeMs - line.timestampMs();
|
||||||
|
if (timeSinceLine < durationMs) {
|
||||||
|
spawnLyricParticle(line.text(), now - timeSinceLine, durationMs);
|
||||||
|
}
|
||||||
|
currentLyricIndex++;
|
||||||
|
} else {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
activeParticles.removeIf(particle -> particle.isDead(now));
|
||||||
|
}
|
||||||
|
|
||||||
|
private Vec3 findNonOverlappingSpawnPos(Vec3 headPos, float cameraYaw) {
|
||||||
|
double spread = arcSpread.get();
|
||||||
|
double minD = minDistance.get();
|
||||||
|
double maxD = Math.max(minD + 0.5, maxDistance.get());
|
||||||
|
|
||||||
|
Vec3 bestPos = null;
|
||||||
|
double maxMinDistance = -1.0;
|
||||||
|
|
||||||
|
for (int attempt = 0; attempt < 16; attempt++) {
|
||||||
|
double randomAngleOffset = ThreadLocalRandom.current().nextDouble(-spread, spread);
|
||||||
|
double targetYawDeg = cameraYaw + randomAngleOffset;
|
||||||
|
double distance = ThreadLocalRandom.current().nextDouble(minD, maxD);
|
||||||
|
double yOffset = ThreadLocalRandom.current().nextDouble(-0.25, 0.55);
|
||||||
|
|
||||||
|
double yawRad = Math.toRadians(targetYawDeg);
|
||||||
|
double dirX = -Math.sin(yawRad);
|
||||||
|
double dirZ = Math.cos(yawRad);
|
||||||
|
|
||||||
|
Vec3 candidate = new Vec3(
|
||||||
|
headPos.x + (dirX * distance),
|
||||||
|
headPos.y + yOffset,
|
||||||
|
headPos.z + (dirZ * distance)
|
||||||
|
);
|
||||||
|
|
||||||
|
if (activeParticles.isEmpty()) {
|
||||||
|
return candidate;
|
||||||
|
}
|
||||||
|
|
||||||
|
double minDistanceToOthers = Double.MAX_VALUE;
|
||||||
|
for (LyricParticle3D active : activeParticles) {
|
||||||
|
double d = candidate.distanceTo(active.getBasePosition());
|
||||||
|
if (d < minDistanceToOthers) {
|
||||||
|
minDistanceToOthers = d;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (minDistanceToOthers >= 1.35) {
|
||||||
|
return candidate;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (minDistanceToOthers > maxMinDistance) {
|
||||||
|
maxMinDistance = minDistanceToOthers;
|
||||||
|
bestPos = candidate;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return bestPos;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void spawnLyricParticle(String text, long spawnTimeMs, long durationMs) {
|
||||||
|
if (mc.player == null || text == null || text.isBlank()) return;
|
||||||
|
if (mc.gameRenderer == null || mc.gameRenderer.mainCamera() == null) return;
|
||||||
|
|
||||||
|
Camera camera = mc.gameRenderer.mainCamera();
|
||||||
|
Vec3 headPos = mc.player.getEyePosition();
|
||||||
|
float cameraYaw = camera.yRot();
|
||||||
|
|
||||||
|
int maxAllowed = maxLines.get();
|
||||||
|
while (activeParticles.size() >= maxAllowed) {
|
||||||
|
activeParticles.remove(0);
|
||||||
|
}
|
||||||
|
|
||||||
|
Vec3 spawnPos = findNonOverlappingSpawnPos(headPos, cameraYaw);
|
||||||
|
if (spawnPos == null) return;
|
||||||
|
|
||||||
|
activeParticles.add(new LyricParticle3D(text, spawnPos, spawnTimeMs, durationMs));
|
||||||
|
}
|
||||||
|
|
||||||
|
private TextRenderer getSelectedFont() {
|
||||||
|
return Fonts.renderer(fontMode.get().family, FontInfo.Type.Regular, TextRenderer.get());
|
||||||
|
}
|
||||||
|
|
||||||
|
private int getActiveColorRgb() {
|
||||||
|
return switch (colorMode.get()) {
|
||||||
|
case THEME -> Theme.theme().accent();
|
||||||
|
case THEME_GRADIENT -> {
|
||||||
|
float ratio = (float) (Math.sin(System.currentTimeMillis() / 450.0) * 0.5 + 0.5);
|
||||||
|
int c1 = Theme.theme().accent();
|
||||||
|
int c2 = Theme.theme().accentSoft();
|
||||||
|
yield mixArgb(c1, c2, ratio);
|
||||||
|
}
|
||||||
|
case WHITE -> 0xFFFFFF;
|
||||||
|
case STATIC -> staticColor.getArgb();
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
private static int mixArgb(int from, int to, float t) {
|
||||||
|
t = Math.max(0.0f, Math.min(1.0f, t));
|
||||||
|
int r = mixChannel((from >>> 16) & 0xFF, (to >>> 16) & 0xFF, t);
|
||||||
|
int g = mixChannel((from >>> 8) & 0xFF, (to >>> 8) & 0xFF, t);
|
||||||
|
int b = mixChannel(from & 0xFF, to & 0xFF, t);
|
||||||
|
return (r << 16) | (g << 8) | b;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static int mixChannel(int from, int to, float t) {
|
||||||
|
return from + Math.round((to - from) * t);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public WorldPhase getWorldPhase() {
|
||||||
|
return WorldPhase.AFTER_POST_PROCESS;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onRenderWorldEngine(Renderer3D renderer, Renderer3D depthRenderer, float tickDelta) {
|
||||||
|
if (!isEnabled() || mc.level == null || mc.player == null) return;
|
||||||
|
if (activeParticles.isEmpty()) return;
|
||||||
|
|
||||||
|
TextRenderer font = getSelectedFont();
|
||||||
|
String animMode = animation.get().name();
|
||||||
|
int colorRgb = getActiveColorRgb();
|
||||||
|
long now = System.currentTimeMillis();
|
||||||
|
|
||||||
|
for (LyricParticle3D particle : activeParticles) {
|
||||||
|
particle.render(renderer, font, colorRgb, animMode, now);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onRenderHudEngineForeground(Renderer2D renderer, TextRenderer textRenderer, GuiGraphicsExtractor ctx, float tickDelta) {
|
||||||
|
if (!isEnabled() || !debugMode.get() || mc.player == null) return;
|
||||||
|
|
||||||
|
TextRenderer tr = Fonts.renderer("OnestMedium", FontInfo.Type.Regular, textRenderer);
|
||||||
|
String trackLine = "Track: " + (currentTrackTitle.isEmpty() ? "None" : currentTrackTitle + " - " + currentTrackArtist);
|
||||||
|
long displayTimeMs = Math.max(0L, lastAudioTimeMs);
|
||||||
|
String timerLine = String.format("Audio time: %02d:%02d.%03d (%d ms)",
|
||||||
|
displayTimeMs / 60_000,
|
||||||
|
(displayTimeMs % 60_000) / 1000,
|
||||||
|
displayTimeMs % 1000,
|
||||||
|
displayTimeMs
|
||||||
|
);
|
||||||
|
String particlesLine = "Active particles: " + activeParticles.size();
|
||||||
|
String queueLine = "Queue: " + currentLyricIndex + " / " + lyricsQueue.size() + " lines";
|
||||||
|
|
||||||
|
tr.begin(0.85);
|
||||||
|
tr.render(trackLine, 10.0, 70.0, new RenderColor(0xFFDCDCDC), false);
|
||||||
|
tr.render(timerLine, 10.0, 82.0, new RenderColor(0xFFFFFFFF), false);
|
||||||
|
tr.render(particlesLine, 10.0, 94.0, new RenderColor(0xFF78FF78), false);
|
||||||
|
tr.render(queueLine, 10.0, 106.0, new RenderColor(0xFFB4B4FF), false);
|
||||||
|
tr.end();
|
||||||
|
}
|
||||||
|
|
||||||
|
public enum AnimationMode implements EnumValue.IdProvider {
|
||||||
|
LYRIC_FLOW("lyric_flow"),
|
||||||
|
TYPEWRITER("typewriter"),
|
||||||
|
POP_SCALE("pop_scale"),
|
||||||
|
KINETIC_SLIDE("kinetic_slide"),
|
||||||
|
FADE("fade");
|
||||||
|
|
||||||
|
private final String id;
|
||||||
|
|
||||||
|
AnimationMode(String id) {
|
||||||
|
this.id = id;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String id() {
|
||||||
|
return id;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public enum ColorMode implements EnumValue.IdProvider {
|
||||||
|
THEME("theme"),
|
||||||
|
THEME_GRADIENT("theme_gradient"),
|
||||||
|
WHITE("white"),
|
||||||
|
STATIC("static");
|
||||||
|
|
||||||
|
private final String id;
|
||||||
|
|
||||||
|
ColorMode(String id) {
|
||||||
|
this.id = id;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String id() {
|
||||||
|
return id;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public enum FontMode implements EnumValue.IdProvider {
|
||||||
|
ONEST_BOLD("onest_bold", "OnestBold"),
|
||||||
|
ONEST_MEDIUM("onest_medium", "OnestMedium"),
|
||||||
|
ONEST("onest", "Onest"),
|
||||||
|
INTER_MEDIUM("inter_medium", "InterMedium");
|
||||||
|
|
||||||
|
private final String id;
|
||||||
|
private final String family;
|
||||||
|
|
||||||
|
FontMode(String id, String family) {
|
||||||
|
this.id = id;
|
||||||
|
this.family = family;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String id() {
|
||||||
|
return id;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public enum SplitMode implements EnumValue.IdProvider {
|
||||||
|
SMART_SPLIT("smart_split"),
|
||||||
|
FULL_LINE("full_line");
|
||||||
|
|
||||||
|
private final String id;
|
||||||
|
|
||||||
|
SplitMode(String id) {
|
||||||
|
this.id = id;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String id() {
|
||||||
|
return id;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,184 @@
|
||||||
|
package dev.loki.lovisual.features.module.modules.visuals.lyrics;
|
||||||
|
|
||||||
|
import com.google.gson.JsonArray;
|
||||||
|
import com.google.gson.JsonElement;
|
||||||
|
import com.google.gson.JsonObject;
|
||||||
|
import com.google.gson.JsonParser;
|
||||||
|
import dev.loki.lovisual.util.logging.DebugLog;
|
||||||
|
|
||||||
|
import java.net.URI;
|
||||||
|
import java.net.URLEncoder;
|
||||||
|
import java.net.http.HttpClient;
|
||||||
|
import java.net.http.HttpRequest;
|
||||||
|
import java.net.http.HttpResponse;
|
||||||
|
import java.nio.charset.StandardCharsets;
|
||||||
|
import java.time.Duration;
|
||||||
|
import java.util.Collections;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
|
import java.util.concurrent.CompletableFuture;
|
||||||
|
import java.util.concurrent.ConcurrentHashMap;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Fetches synced lyrics from lrclib.net (no API key required).
|
||||||
|
*/
|
||||||
|
public final class LrcLibClient {
|
||||||
|
private static final String API_GET = "https://lrclib.net/api/get";
|
||||||
|
private static final String API_SEARCH = "https://lrclib.net/api/search";
|
||||||
|
|
||||||
|
private static final String USER_AGENT = "LoVisual/1.0";
|
||||||
|
|
||||||
|
private static final Map<String, List<LyricLine>> LYRICS_CACHE = new ConcurrentHashMap<>();
|
||||||
|
|
||||||
|
private static final HttpClient HTTP_CLIENT = HttpClient.newBuilder()
|
||||||
|
.connectTimeout(Duration.ofSeconds(12))
|
||||||
|
.followRedirects(HttpClient.Redirect.NORMAL)
|
||||||
|
.build();
|
||||||
|
|
||||||
|
private LrcLibClient() {
|
||||||
|
}
|
||||||
|
|
||||||
|
public static CompletableFuture<List<LyricLine>> fetchLyricsAsync(String trackName, String artistName, boolean debugMode) {
|
||||||
|
if (trackName == null || trackName.isBlank()) {
|
||||||
|
return CompletableFuture.completedFuture(Collections.emptyList());
|
||||||
|
}
|
||||||
|
|
||||||
|
String cacheKey = (trackName.trim() + " - " + (artistName != null ? artistName.trim() : "")).toLowerCase();
|
||||||
|
List<LyricLine> cached = LYRICS_CACHE.get(cacheKey);
|
||||||
|
if (cached != null) {
|
||||||
|
if (debugMode) {
|
||||||
|
DebugLog.info("[KineticLyrics] Cache hit: %s (%d lines)", cacheKey, cached.size());
|
||||||
|
}
|
||||||
|
return CompletableFuture.completedFuture(cached);
|
||||||
|
}
|
||||||
|
|
||||||
|
return CompletableFuture.supplyAsync(() -> {
|
||||||
|
List<LyricLine> lines = tryFetchGet(trackName, artistName, debugMode);
|
||||||
|
if (!lines.isEmpty()) {
|
||||||
|
LYRICS_CACHE.put(cacheKey, lines);
|
||||||
|
return lines;
|
||||||
|
}
|
||||||
|
|
||||||
|
lines = tryFetchSearch(trackName, artistName, debugMode);
|
||||||
|
if (!lines.isEmpty()) {
|
||||||
|
LYRICS_CACHE.put(cacheKey, lines);
|
||||||
|
return lines;
|
||||||
|
}
|
||||||
|
|
||||||
|
return Collections.<LyricLine>emptyList();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private static List<LyricLine> tryFetchGet(String trackName, String artistName, boolean debugMode) {
|
||||||
|
String cleanTrack = cleanTrackName(trackName);
|
||||||
|
String cleanArtist = artistName != null ? cleanTrackName(artistName) : "";
|
||||||
|
|
||||||
|
String query = String.format("?track_name=%s&artist_name=%s",
|
||||||
|
URLEncoder.encode(cleanTrack, StandardCharsets.UTF_8),
|
||||||
|
URLEncoder.encode(cleanArtist, StandardCharsets.UTF_8));
|
||||||
|
|
||||||
|
return executeRequest(API_GET + query, debugMode);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static List<LyricLine> tryFetchSearch(String trackName, String artistName, boolean debugMode) {
|
||||||
|
String cleanTrack = cleanTrackName(trackName);
|
||||||
|
String cleanArtist = artistName != null ? cleanTrackName(artistName) : "";
|
||||||
|
String searchQuery = (cleanTrack + " " + cleanArtist).trim();
|
||||||
|
|
||||||
|
String query = "?q=" + URLEncoder.encode(searchQuery, StandardCharsets.UTF_8);
|
||||||
|
|
||||||
|
try {
|
||||||
|
HttpRequest request = HttpRequest.newBuilder()
|
||||||
|
.uri(URI.create(API_SEARCH + query))
|
||||||
|
.header("User-Agent", USER_AGENT)
|
||||||
|
.timeout(Duration.ofSeconds(14))
|
||||||
|
.GET()
|
||||||
|
.build();
|
||||||
|
|
||||||
|
HttpResponse<String> response = HTTP_CLIENT.send(request, HttpResponse.BodyHandlers.ofString());
|
||||||
|
if (response.statusCode() == 200) {
|
||||||
|
JsonArray array = JsonParser.parseString(response.body()).getAsJsonArray();
|
||||||
|
for (JsonElement elem : array) {
|
||||||
|
if (elem.isJsonObject()) {
|
||||||
|
JsonObject obj = elem.getAsJsonObject();
|
||||||
|
if (obj.has("syncedLyrics") && !obj.get("syncedLyrics").isJsonNull()) {
|
||||||
|
String syncedLrc = obj.get("syncedLyrics").getAsString();
|
||||||
|
List<LyricLine> parsed = LrcParser.parse(syncedLrc);
|
||||||
|
if (!parsed.isEmpty()) {
|
||||||
|
if (debugMode) {
|
||||||
|
DebugLog.info("[KineticLyrics] Found lyrics via search fallback");
|
||||||
|
}
|
||||||
|
return parsed;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else if (response.statusCode() == 404) {
|
||||||
|
if (debugMode) {
|
||||||
|
DebugLog.info("[KineticLyrics] Search: track not found (404)");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (Exception e) {
|
||||||
|
if (debugMode) {
|
||||||
|
DebugLog.error("[KineticLyrics] Search fallback failed", e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return Collections.emptyList();
|
||||||
|
}
|
||||||
|
|
||||||
|
private static List<LyricLine> executeRequest(String url, boolean debugMode) {
|
||||||
|
for (int attempt = 1; attempt <= 2; attempt++) {
|
||||||
|
try {
|
||||||
|
HttpRequest request = HttpRequest.newBuilder()
|
||||||
|
.uri(URI.create(url))
|
||||||
|
.header("User-Agent", USER_AGENT)
|
||||||
|
.timeout(Duration.ofSeconds(14))
|
||||||
|
.GET()
|
||||||
|
.build();
|
||||||
|
|
||||||
|
HttpResponse<String> response = HTTP_CLIENT.send(request, HttpResponse.BodyHandlers.ofString());
|
||||||
|
|
||||||
|
if (response.statusCode() == 200) {
|
||||||
|
JsonObject json = JsonParser.parseString(response.body()).getAsJsonObject();
|
||||||
|
if (json.has("syncedLyrics") && !json.get("syncedLyrics").isJsonNull()) {
|
||||||
|
String syncedLrc = json.get("syncedLyrics").getAsString();
|
||||||
|
return LrcParser.parse(syncedLrc);
|
||||||
|
}
|
||||||
|
if (debugMode) {
|
||||||
|
DebugLog.info("[KineticLyrics] No syncedLyrics in response");
|
||||||
|
}
|
||||||
|
return Collections.emptyList();
|
||||||
|
} else if (response.statusCode() == 404) {
|
||||||
|
if (debugMode) {
|
||||||
|
DebugLog.info("[KineticLyrics] Track not found (404)");
|
||||||
|
}
|
||||||
|
return Collections.emptyList();
|
||||||
|
} else {
|
||||||
|
if (debugMode) {
|
||||||
|
DebugLog.info("[KineticLyrics] Attempt %d returned %d", attempt, response.statusCode());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (Exception e) {
|
||||||
|
if (debugMode) {
|
||||||
|
DebugLog.error("[KineticLyrics] Attempt " + attempt + " failed", e);
|
||||||
|
}
|
||||||
|
if (attempt < 2) {
|
||||||
|
try {
|
||||||
|
Thread.sleep(400);
|
||||||
|
} catch (InterruptedException ignored) {
|
||||||
|
Thread.currentThread().interrupt();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return Collections.emptyList();
|
||||||
|
}
|
||||||
|
|
||||||
|
static String cleanTrackName(String trackName) {
|
||||||
|
if (trackName == null) return "";
|
||||||
|
return trackName
|
||||||
|
.replaceAll("\\(.*?\\)", "")
|
||||||
|
.replaceAll("\\[.*?\\]", "")
|
||||||
|
.trim();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,58 @@
|
||||||
|
package dev.loki.lovisual.features.module.modules.visuals.lyrics;
|
||||||
|
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.Collections;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.regex.Matcher;
|
||||||
|
import java.util.regex.Pattern;
|
||||||
|
|
||||||
|
public final class LrcParser {
|
||||||
|
private static final Pattern LRC_PATTERN = Pattern.compile("\\[(\\d+):(\\d{2})(?:\\.(\\d{1,3}))?\\](.*)");
|
||||||
|
|
||||||
|
private LrcParser() {
|
||||||
|
}
|
||||||
|
|
||||||
|
public static List<LyricLine> parse(String lrcContent) {
|
||||||
|
if (lrcContent == null || lrcContent.isBlank()) {
|
||||||
|
return Collections.emptyList();
|
||||||
|
}
|
||||||
|
|
||||||
|
List<LyricLine> lines = new ArrayList<>();
|
||||||
|
String[] rawLines = lrcContent.split("\\r?\\n");
|
||||||
|
|
||||||
|
for (String line : rawLines) {
|
||||||
|
String trimmed = line.trim();
|
||||||
|
if (trimmed.isEmpty()) continue;
|
||||||
|
|
||||||
|
Matcher matcher = LRC_PATTERN.matcher(trimmed);
|
||||||
|
if (matcher.matches()) {
|
||||||
|
try {
|
||||||
|
long minutes = Long.parseLong(matcher.group(1));
|
||||||
|
long seconds = Long.parseLong(matcher.group(2));
|
||||||
|
String fractionStr = matcher.group(3);
|
||||||
|
|
||||||
|
long millis = 0;
|
||||||
|
if (fractionStr != null && !fractionStr.isEmpty()) {
|
||||||
|
millis = Long.parseLong(fractionStr);
|
||||||
|
if (fractionStr.length() == 1) {
|
||||||
|
millis *= 100;
|
||||||
|
} else if (fractionStr.length() == 2) {
|
||||||
|
millis *= 10;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
long totalTimeMs = (minutes * 60_000L) + (seconds * 1_000L) + millis;
|
||||||
|
String text = matcher.group(4).trim();
|
||||||
|
|
||||||
|
if (!text.isEmpty()) {
|
||||||
|
lines.add(new LyricLine(totalTimeMs, text));
|
||||||
|
}
|
||||||
|
} catch (NumberFormatException ignored) {
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Collections.sort(lines);
|
||||||
|
return lines;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,8 @@
|
||||||
|
package dev.loki.lovisual.features.module.modules.visuals.lyrics;
|
||||||
|
|
||||||
|
public record LyricLine(long timestampMs, String text) implements Comparable<LyricLine> {
|
||||||
|
@Override
|
||||||
|
public int compareTo(LyricLine o) {
|
||||||
|
return Long.compare(this.timestampMs, o.timestampMs);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,68 @@
|
||||||
|
package dev.loki.lovisual.features.module.modules.visuals.lyrics;
|
||||||
|
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.Collections;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
public final class LyricLineSplitter {
|
||||||
|
private LyricLineSplitter() {
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Splits long lyric lines (more than 4 words) into shorter rhythmic chunks with micro-timings.
|
||||||
|
*/
|
||||||
|
public static List<LyricLine> splitLongLines(List<LyricLine> originalLines) {
|
||||||
|
if (originalLines == null || originalLines.isEmpty()) {
|
||||||
|
return Collections.emptyList();
|
||||||
|
}
|
||||||
|
|
||||||
|
List<LyricLine> result = new ArrayList<>();
|
||||||
|
|
||||||
|
for (int i = 0; i < originalLines.size(); i++) {
|
||||||
|
LyricLine current = originalLines.get(i);
|
||||||
|
long nextTimeMs = (i + 1 < originalLines.size()) ? originalLines.get(i + 1).timestampMs() : current.timestampMs() + 4000L;
|
||||||
|
long lineDuration = Math.max(1500L, nextTimeMs - current.timestampMs());
|
||||||
|
|
||||||
|
String text = current.text().trim();
|
||||||
|
String[] words = text.split("\\s+");
|
||||||
|
|
||||||
|
if (words.length <= 4) {
|
||||||
|
result.add(current);
|
||||||
|
} else {
|
||||||
|
int wordsPerChunk = words.length > 7 ? 3 : 2;
|
||||||
|
List<String> chunks = new ArrayList<>();
|
||||||
|
StringBuilder sb = new StringBuilder();
|
||||||
|
int count = 0;
|
||||||
|
|
||||||
|
for (String word : words) {
|
||||||
|
if (!sb.isEmpty()) sb.append(" ");
|
||||||
|
sb.append(word);
|
||||||
|
count++;
|
||||||
|
if (count >= wordsPerChunk) {
|
||||||
|
chunks.add(sb.toString());
|
||||||
|
sb.setLength(0);
|
||||||
|
count = 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (!sb.isEmpty()) {
|
||||||
|
chunks.add(sb.toString());
|
||||||
|
}
|
||||||
|
|
||||||
|
int numChunks = chunks.size();
|
||||||
|
if (numChunks == 0) {
|
||||||
|
result.add(current);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
long chunkDuration = lineDuration / numChunks;
|
||||||
|
|
||||||
|
for (int c = 0; c < numChunks; c++) {
|
||||||
|
long chunkStartMs = current.timestampMs() + (c * chunkDuration);
|
||||||
|
result.add(new LyricLine(chunkStartMs, chunks.get(c)));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Collections.sort(result);
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,147 @@
|
||||||
|
package dev.loki.lovisual.features.module.modules.visuals.lyrics;
|
||||||
|
|
||||||
|
import dev.loki.lovisual.render.engine.color.RenderColor;
|
||||||
|
import dev.loki.lovisual.render.engine.renderer.Renderer3D;
|
||||||
|
import dev.loki.lovisual.render.engine.text.TextRenderer;
|
||||||
|
import dev.loki.lovisual.render.engine.text.WorldTextRenderer;
|
||||||
|
import net.minecraft.util.Mth;
|
||||||
|
import net.minecraft.world.phys.Vec3;
|
||||||
|
|
||||||
|
public class LyricParticle3D {
|
||||||
|
private static final double BASE_WORLD_SCALE = 0.0016;
|
||||||
|
|
||||||
|
private final String text;
|
||||||
|
private final Vec3 basePosition;
|
||||||
|
private final long spawnTimeMs;
|
||||||
|
private final long durationMs;
|
||||||
|
private boolean forceExpire = false;
|
||||||
|
|
||||||
|
public LyricParticle3D(String text, Vec3 basePosition, long spawnTimeMs, long durationMs) {
|
||||||
|
this.text = text;
|
||||||
|
this.basePosition = basePosition;
|
||||||
|
this.spawnTimeMs = spawnTimeMs;
|
||||||
|
this.durationMs = durationMs;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Vec3 getBasePosition() {
|
||||||
|
return basePosition;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void dismiss() {
|
||||||
|
this.forceExpire = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean isDead(long nowMs) {
|
||||||
|
return forceExpire || (nowMs - spawnTimeMs >= durationMs);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Renders this lyric line as a camera-facing 3D billboard with a selectable entrance/exit animation.
|
||||||
|
*/
|
||||||
|
public void render(Renderer3D renderer, TextRenderer textRenderer, int colorRgb, String animMode, long nowMs) {
|
||||||
|
long elapsed = nowMs - spawnTimeMs;
|
||||||
|
if (elapsed < 0 || elapsed > durationMs || forceExpire) return;
|
||||||
|
|
||||||
|
float progress = Mth.clamp((float) elapsed / (float) durationMs, 0.0f, 1.0f);
|
||||||
|
|
||||||
|
float alpha = 1.0f;
|
||||||
|
double offsetY = 0.0;
|
||||||
|
float animScale = 1.0f;
|
||||||
|
String displayText = this.text;
|
||||||
|
|
||||||
|
switch (animMode) {
|
||||||
|
case "TYPEWRITER" -> {
|
||||||
|
if (progress < 0.30f) {
|
||||||
|
float typeProgress = progress / 0.30f;
|
||||||
|
int visibleChars = Math.min(this.text.length(), (int) Math.ceil(this.text.length() * typeProgress));
|
||||||
|
displayText = this.text.substring(0, Math.max(1, visibleChars));
|
||||||
|
alpha = Math.min(1.0f, progress / 0.08f);
|
||||||
|
offsetY = progress * 0.1;
|
||||||
|
} else if (progress < 0.75f) {
|
||||||
|
offsetY = progress * 0.1;
|
||||||
|
} else {
|
||||||
|
float eraseProgress = (progress - 0.75f) / 0.25f;
|
||||||
|
int remainingChars = Math.max(0, this.text.length() - (int) Math.floor(this.text.length() * eraseProgress));
|
||||||
|
displayText = this.text.substring(0, remainingChars);
|
||||||
|
alpha = 1.0f - (eraseProgress * 0.7f);
|
||||||
|
offsetY = progress * 0.1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
case "POP_SCALE" -> {
|
||||||
|
if (progress < 0.18f) {
|
||||||
|
float t = progress / 0.18f;
|
||||||
|
alpha = easeOutCubic(t);
|
||||||
|
animScale = 0.35f + 0.65f * easeOutBack(t);
|
||||||
|
} else if (progress >= 0.80f) {
|
||||||
|
float t = (progress - 0.80f) / 0.20f;
|
||||||
|
alpha = 1.0f - easeInCubic(t);
|
||||||
|
animScale = 1.0f - 0.05f * t;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
case "KINETIC_SLIDE" -> {
|
||||||
|
if (progress < 0.20f) {
|
||||||
|
float t = progress / 0.20f;
|
||||||
|
alpha = easeOutCubic(t);
|
||||||
|
offsetY = -0.15 * (1.0f - easeOutCubic(t));
|
||||||
|
animScale = 0.92f + 0.08f * t;
|
||||||
|
} else if (progress >= 0.80f) {
|
||||||
|
float t = (progress - 0.80f) / 0.20f;
|
||||||
|
alpha = 1.0f - easeInCubic(t);
|
||||||
|
offsetY = 0.05 * easeInCubic(t);
|
||||||
|
animScale = 1.0f - 0.04f * t;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
case "FADE" -> {
|
||||||
|
if (progress < 0.15f) {
|
||||||
|
alpha = progress / 0.15f;
|
||||||
|
} else if (progress >= 0.80f) {
|
||||||
|
alpha = 1.0f - ((progress - 0.80f) / 0.20f);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
default -> { // LYRIC_FLOW
|
||||||
|
if (progress < 0.15f) {
|
||||||
|
float t = progress / 0.15f;
|
||||||
|
alpha = easeOutCubic(t);
|
||||||
|
offsetY = -0.06 * (1.0f - easeOutCubic(t));
|
||||||
|
animScale = 0.93f + 0.07f * easeOutBack(t);
|
||||||
|
} else if (progress >= 0.80f) {
|
||||||
|
float t = (progress - 0.80f) / 0.20f;
|
||||||
|
alpha = 1.0f - easeInCubic(t);
|
||||||
|
offsetY = 0.04 * easeInCubic(t);
|
||||||
|
animScale = 1.0f - 0.03f * t;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
alpha = Mth.clamp(alpha, 0.0f, 1.0f);
|
||||||
|
if (alpha <= 0.001f || displayText == null || displayText.isBlank()) return;
|
||||||
|
|
||||||
|
int alphaInt = (int) (alpha * 255.0f);
|
||||||
|
RenderColor color = new RenderColor((alphaInt << 24) | (colorRgb & 0x00FFFFFF));
|
||||||
|
|
||||||
|
Vec3 anchor = basePosition.add(0.0, offsetY, 0.0);
|
||||||
|
|
||||||
|
WorldTextRenderer.Options options = WorldTextRenderer.Options.defaults()
|
||||||
|
.withColor(color)
|
||||||
|
.withCentered(true)
|
||||||
|
.withShadow(true)
|
||||||
|
.withWorldScale(BASE_WORLD_SCALE * animScale)
|
||||||
|
.withDepthMode(Renderer3D.DepthMode.MAIN);
|
||||||
|
|
||||||
|
WorldTextRenderer.drawBillboard(renderer, textRenderer, displayText, anchor, options);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static float easeOutCubic(float t) {
|
||||||
|
return 1.0f - (float) Math.pow(1.0f - t, 3);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static float easeInCubic(float t) {
|
||||||
|
return (float) Math.pow(t, 3);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static float easeOutBack(float t) {
|
||||||
|
float c1 = 1.70158f;
|
||||||
|
float c3 = c1 + 1.0f;
|
||||||
|
return 1.0f + c3 * (float) Math.pow(t - 1.0f, 3) + c1 * (float) Math.pow(t - 1.0f, 2);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,163 @@
|
||||||
|
/*
|
||||||
|
* This file is part of the LoVisual Client distribution.
|
||||||
|
* Copyright (c) 2026 pivosos2007.
|
||||||
|
*
|
||||||
|
* Licensed under the GNU General Public License v3.0.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package dev.loki.lovisual.features.module.modules.visuals.trollface;
|
||||||
|
|
||||||
|
import dev.loki.lovisual.config.values.ModeValue;
|
||||||
|
import dev.loki.lovisual.config.values.NumberValue;
|
||||||
|
import dev.loki.lovisual.features.module.Module;
|
||||||
|
import dev.loki.lovisual.features.module.ModuleCategory;
|
||||||
|
import dev.loki.lovisual.features.module.ModuleInfo;
|
||||||
|
import dev.loki.lovisual.render.engine.renderer.Renderer3D;
|
||||||
|
import dev.loki.lovisual.render.engine.pipeline.LoVisualRenderPipelines;
|
||||||
|
import dev.loki.lovisual.render.engine.TextureStorage;
|
||||||
|
import dev.loki.lovisual.render.engine.uniform.MeshBuilder;
|
||||||
|
import dev.loki.lovisual.util.target.TargetManager;
|
||||||
|
import net.minecraft.client.Minecraft;
|
||||||
|
import net.minecraft.resources.Identifier;
|
||||||
|
import net.minecraft.world.entity.LivingEntity;
|
||||||
|
import net.minecraft.world.phys.Vec3;
|
||||||
|
|
||||||
|
@ModuleInfo(
|
||||||
|
id = "trollface_mask",
|
||||||
|
displayName = "Trollface Mask",
|
||||||
|
category = ModuleCategory.VISUALS
|
||||||
|
)
|
||||||
|
public class TrollfaceMask extends Module {
|
||||||
|
|
||||||
|
private static TrollfaceMask instance;
|
||||||
|
private final Minecraft mc = Minecraft.getInstance();
|
||||||
|
|
||||||
|
public final ModeValue target = mode(
|
||||||
|
"trollfaceTarget", "Себя",
|
||||||
|
"Себя", "Друзей", "Всех"
|
||||||
|
);
|
||||||
|
|
||||||
|
public final ModeValue maskType = mode(
|
||||||
|
"trollfaceMaskType", "Style 1",
|
||||||
|
"Style 1", "Style 2", "Style 3",
|
||||||
|
"Style 4", "Style 5", "Style 6"
|
||||||
|
);
|
||||||
|
|
||||||
|
public final NumberValue<Float> size = num(
|
||||||
|
"trollfaceSize", "size", 1.0f, 0.3f, 3.0f
|
||||||
|
);
|
||||||
|
|
||||||
|
public final NumberValue<Float> alpha = num(
|
||||||
|
"trollfaceAlpha", "alpha", 1.0f, 0.1f, 1.0f
|
||||||
|
);
|
||||||
|
|
||||||
|
public TrollfaceMask() {
|
||||||
|
instance = this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static TrollfaceMask getInstance() {
|
||||||
|
return instance;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onRenderWorldEngine(Renderer3D renderer, Renderer3D depthRenderer, float tickDelta) {
|
||||||
|
if (mc.player == null || mc.level == null) return;
|
||||||
|
|
||||||
|
String targetMode = target.get();
|
||||||
|
boolean selfMode = "Себя".equals(targetMode);
|
||||||
|
|
||||||
|
LivingEntity currentTarget = TargetManager.getTarget();
|
||||||
|
boolean hasCrosshairTarget = currentTarget != null && currentTarget.isAlive();
|
||||||
|
|
||||||
|
if (!selfMode && !hasCrosshairTarget) return;
|
||||||
|
|
||||||
|
LivingEntity renderTarget;
|
||||||
|
boolean shouldRender;
|
||||||
|
|
||||||
|
switch (targetMode) {
|
||||||
|
case "Себя" -> {
|
||||||
|
renderTarget = mc.player;
|
||||||
|
shouldRender = true;
|
||||||
|
}
|
||||||
|
case "Друзей" -> {
|
||||||
|
if (!hasCrosshairTarget) { shouldRender = false; renderTarget = null; break; }
|
||||||
|
renderTarget = currentTarget;
|
||||||
|
shouldRender = true;
|
||||||
|
}
|
||||||
|
case "Всех" -> {
|
||||||
|
if (!hasCrosshairTarget) { shouldRender = false; renderTarget = null; break; }
|
||||||
|
renderTarget = currentTarget;
|
||||||
|
shouldRender = true;
|
||||||
|
}
|
||||||
|
default -> {
|
||||||
|
renderTarget = mc.player;
|
||||||
|
shouldRender = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (!shouldRender || renderTarget == null) return;
|
||||||
|
|
||||||
|
Vec3 camPos = mc.gameRenderer.mainCamera().position();
|
||||||
|
double headX = renderTarget.getX();
|
||||||
|
double headY = renderTarget.getY() + renderTarget.getBbHeight() * 0.72;
|
||||||
|
double headZ = renderTarget.getZ();
|
||||||
|
|
||||||
|
Vec3 headPos = new Vec3(headX, headY, headZ);
|
||||||
|
Vec3 viewDir = camPos.subtract(headPos).normalize();
|
||||||
|
|
||||||
|
if (viewDir.lengthSqr() < 1.0E-8) return;
|
||||||
|
|
||||||
|
Vec3 upVec = new Vec3(0, 1, 0);
|
||||||
|
Vec3 right = viewDir.cross(upVec).normalize();
|
||||||
|
if (right.lengthSqr() < 1.0E-8) {
|
||||||
|
right = new Vec3(1, 0, 0);
|
||||||
|
}
|
||||||
|
Vec3 actualUp = right.cross(viewDir).normalize();
|
||||||
|
|
||||||
|
float s = size.get();
|
||||||
|
float alpha = this.alpha.get();
|
||||||
|
int color = 0xFFFFFFFF;
|
||||||
|
int r = (color >> 16) & 0xFF;
|
||||||
|
int g = (color >> 8) & 0xFF;
|
||||||
|
int b = color & 0xFF;
|
||||||
|
int a = (int) (alpha * 255);
|
||||||
|
|
||||||
|
Vec3 p1 = headPos.add(right.scale(-s)).add(actualUp.scale(-s * 0.75));
|
||||||
|
Vec3 p2 = headPos.add(right.scale(s)).add(actualUp.scale(-s * 0.75));
|
||||||
|
Vec3 p3 = headPos.add(right.scale(s)).add(actualUp.scale(s * 0.75));
|
||||||
|
Vec3 p4 = headPos.add(right.scale(-s)).add(actualUp.scale(s * 0.75));
|
||||||
|
|
||||||
|
int textureIndex = maskTypeIndex(maskType.get());
|
||||||
|
Identifier texId = switch (textureIndex) {
|
||||||
|
case 0 -> TextureStorage.TROLLFACE_1;
|
||||||
|
case 1 -> TextureStorage.TROLLFACE_2;
|
||||||
|
case 2 -> TextureStorage.TROLLFACE_3;
|
||||||
|
case 3 -> TextureStorage.TROLLFACE_4;
|
||||||
|
case 4 -> TextureStorage.TROLLFACE_5;
|
||||||
|
case 5 -> TextureStorage.TROLLFACE_6;
|
||||||
|
default -> TextureStorage.TROLLFACE_1;
|
||||||
|
};
|
||||||
|
|
||||||
|
MeshBuilder mesh = renderer.batchTextured(LoVisualRenderPipelines.WORLD_TEXTURED, texId, Renderer3D.DepthMode.NONE);
|
||||||
|
if (mesh == null) return;
|
||||||
|
|
||||||
|
mesh.ensureQuadCapacity();
|
||||||
|
int i1 = mesh.vec3(p1.x, p1.y, p1.z).vec2(0, 1).color(r, g, b, a).next();
|
||||||
|
int i2 = mesh.vec3(p2.x, p2.y, p2.z).vec2(1, 1).color(r, g, b, a).next();
|
||||||
|
int i3 = mesh.vec3(p3.x, p3.y, p3.z).vec2(1, 0).color(r, g, b, a).next();
|
||||||
|
int i4 = mesh.vec3(p4.x, p4.y, p4.z).vec2(0, 0).color(r, g, b, a).next();
|
||||||
|
mesh.quad(i1, i2, i3, i4);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static int maskTypeIndex(String maskType) {
|
||||||
|
if (maskType == null) return 0;
|
||||||
|
return switch (maskType) {
|
||||||
|
case "Style 1" -> 0;
|
||||||
|
case "Style 2" -> 1;
|
||||||
|
case "Style 3" -> 2;
|
||||||
|
case "Style 4" -> 3;
|
||||||
|
case "Style 5" -> 4;
|
||||||
|
case "Style 6" -> 5;
|
||||||
|
default -> 0;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -26,6 +26,7 @@ import org.spongepowered.asm.mixin.injection.Inject;
|
||||||
import org.spongepowered.asm.mixin.injection.ModifyVariable;
|
import org.spongepowered.asm.mixin.injection.ModifyVariable;
|
||||||
import org.spongepowered.asm.mixin.injection.callback.CallbackInfo;
|
import org.spongepowered.asm.mixin.injection.callback.CallbackInfo;
|
||||||
import dev.loki.lovisual.features.module.Modules;
|
import dev.loki.lovisual.features.module.Modules;
|
||||||
|
import dev.loki.lovisual.features.module.modules.visuals.HoldMyItems;
|
||||||
import dev.loki.lovisual.features.module.modules.visuals.ViewModel;
|
import dev.loki.lovisual.features.module.modules.visuals.ViewModel;
|
||||||
|
|
||||||
@Environment(EnvType.CLIENT)
|
@Environment(EnvType.CLIENT)
|
||||||
|
|
@ -83,6 +84,20 @@ public abstract class ItemInHandRendererMixin {
|
||||||
return player != null && (player.isInWater() || player.isInLava());
|
return player != null && (player.isInWater() || player.isInLava());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ModifyVariable(
|
||||||
|
method = "submitArmWithItem",
|
||||||
|
at = @At("HEAD"),
|
||||||
|
argsOnly = true,
|
||||||
|
index = 7)
|
||||||
|
private float lovisual$holdMyItemsEquipCurve(float equipProgress, AbstractClientPlayer player, float tickDelta,
|
||||||
|
float pitch, InteractionHand hand) {
|
||||||
|
HoldMyItems hmi = Modules.get(HoldMyItems.class);
|
||||||
|
if (hmi == null || !hmi.isEnabled()) return equipProgress;
|
||||||
|
if (hand == InteractionHand.OFF_HAND && !hmi.offhandToo.get()) return equipProgress;
|
||||||
|
|
||||||
|
return hmi.shapeEquipProgress(equipProgress);
|
||||||
|
}
|
||||||
|
|
||||||
@Inject(
|
@Inject(
|
||||||
method = "renderItem(Lnet/minecraft/world/entity/LivingEntity;Lnet/minecraft/world/item/ItemStack;Lnet/minecraft/world/item/ItemDisplayContext;Lcom/mojang/blaze3d/vertex/PoseStack;Lnet/minecraft/client/renderer/SubmitNodeCollector;I)V",
|
method = "renderItem(Lnet/minecraft/world/entity/LivingEntity;Lnet/minecraft/world/item/ItemStack;Lnet/minecraft/world/item/ItemDisplayContext;Lcom/mojang/blaze3d/vertex/PoseStack;Lnet/minecraft/client/renderer/SubmitNodeCollector;I)V",
|
||||||
at = @At(
|
at = @At(
|
||||||
|
|
|
||||||
|
|
@ -51,7 +51,6 @@ public final class LoVisualMixinPlugin implements IMixinConfigPlugin {
|
||||||
"dev.loki.lovisual.mixins.xaero.minimap.AbstractWaypointRenderProviderMixin"
|
"dev.loki.lovisual.mixins.xaero.minimap.AbstractWaypointRenderProviderMixin"
|
||||||
);
|
);
|
||||||
private static final Set<String> OPTIONAL_XAERO_WORLDMAP_MIXINS = Set.of(
|
private static final Set<String> OPTIONAL_XAERO_WORLDMAP_MIXINS = Set.of(
|
||||||
"dev.loki.lovisual.mixins.xaero.worldmap.WorldMapWaypointAccessor",
|
|
||||||
"dev.loki.lovisual.mixins.xaero.worldmap.WaypointRenderProviderMixin"
|
"dev.loki.lovisual.mixins.xaero.worldmap.WaypointRenderProviderMixin"
|
||||||
);
|
);
|
||||||
private static final Set<String> OPTIONAL_MORECULLING_MIXINS = Set.of(
|
private static final Set<String> OPTIONAL_MORECULLING_MIXINS = Set.of(
|
||||||
|
|
|
||||||
|
|
@ -189,21 +189,6 @@ public class MinecraftMixin implements MinecraftGameConfigHolder {
|
||||||
// FREECAM + INTERACT
|
// FREECAM + INTERACT
|
||||||
// ===============================
|
// ===============================
|
||||||
|
|
||||||
// Reach (FREECAM)
|
|
||||||
Reach reachModule = Modules.get(Reach.class);
|
|
||||||
if (reachModule != null && reachModule.isEnabled()) {
|
|
||||||
EntityHitResult hit = reachModule.raycastEntities(
|
|
||||||
mc.player, start, dir
|
|
||||||
);
|
|
||||||
|
|
||||||
if (hit != null && hit.getEntity() instanceof LivingEntity target && target.isAlive()) {
|
|
||||||
mc.hitResult = hit;
|
|
||||||
mc.crosshairPickEntity = target;
|
|
||||||
ci.cancel();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Blocks (FREECAM)
|
// Blocks (FREECAM)
|
||||||
double reach = mc.player.blockInteractionRange();
|
double reach = mc.player.blockInteractionRange();
|
||||||
Vec3 end = start.add(dir.scale(reach));
|
Vec3 end = start.add(dir.scale(reach));
|
||||||
|
|
@ -220,16 +205,6 @@ public class MinecraftMixin implements MinecraftGameConfigHolder {
|
||||||
ci.cancel();
|
ci.cancel();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
Reach reachModule = Modules.get(Reach.class);
|
|
||||||
if (reachModule != null && reachModule.isEnabled()) {
|
|
||||||
EntityHitResult entityHit = reachModule.raycastEntities(mc.player);
|
|
||||||
if (entityHit != null && entityHit.getEntity() instanceof LivingEntity target && target.isAlive()) {
|
|
||||||
mc.hitResult = entityHit;
|
|
||||||
mc.crosshairPickEntity = target;
|
|
||||||
ci.cancel();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Inject(method = "pick", at = @At("RETURN"))
|
@Inject(method = "pick", at = @At("RETURN"))
|
||||||
|
|
|
||||||
|
|
@ -163,9 +163,5 @@ public class MultiPlayerGameModeMixin {
|
||||||
TargetManager.onAttack(target);
|
TargetManager.onAttack(target);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Inject(method = "attack(Lnet/minecraft/world/entity/player/Player;Lnet/minecraft/world/entity/Entity;)V",
|
|
||||||
at = @At("RETURN"))
|
|
||||||
private void lovisual$attributeSwapAfterClientAttack(Player player, Entity target, CallbackInfo ci) {
|
|
||||||
// AttributeSwap module removed; no post-attack shield break remains.
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,30 +0,0 @@
|
||||||
/*
|
|
||||||
* This file is part of the LoVisual Client distribution.
|
|
||||||
* Copyright (c) 2026 pivosos2007.
|
|
||||||
*
|
|
||||||
* Licensed under the GNU General Public License v3.0.
|
|
||||||
*/
|
|
||||||
|
|
||||||
package dev.loki.lovisual.mixins.xaero.worldmap;
|
|
||||||
|
|
||||||
import org.spongepowered.asm.mixin.Mixin;
|
|
||||||
import org.spongepowered.asm.mixin.Pseudo;
|
|
||||||
import org.spongepowered.asm.mixin.gen.Accessor;
|
|
||||||
import xaero.map.mods.gui.Waypoint;
|
|
||||||
|
|
||||||
@Pseudo
|
|
||||||
@Mixin(Waypoint.class)
|
|
||||||
public interface WorldMapWaypointAccessor {
|
|
||||||
|
|
||||||
@Accessor
|
|
||||||
void setX(int x);
|
|
||||||
|
|
||||||
@Accessor
|
|
||||||
void setY(int y);
|
|
||||||
|
|
||||||
@Accessor
|
|
||||||
void setZ(int z);
|
|
||||||
|
|
||||||
@Accessor
|
|
||||||
void setColor(int color);
|
|
||||||
}
|
|
||||||
|
|
@ -42,6 +42,20 @@ public enum TextureStorage {
|
||||||
Identifier.fromNamespaceAndPath("lovisual", "textures/particles/p11.png"),
|
Identifier.fromNamespaceAndPath("lovisual", "textures/particles/p11.png"),
|
||||||
Identifier.fromNamespaceAndPath("lovisual", "textures/particles/p12.png")
|
Identifier.fromNamespaceAndPath("lovisual", "textures/particles/p12.png")
|
||||||
};
|
};
|
||||||
|
public static final Identifier TROLLFACE_1 =
|
||||||
|
Identifier.fromNamespaceAndPath("lovisual", "textures/trollface/trollface_1.png");
|
||||||
|
public static final Identifier TROLLFACE_2 =
|
||||||
|
Identifier.fromNamespaceAndPath("lovisual", "textures/trollface/trollface_2.png");
|
||||||
|
public static final Identifier TROLLFACE_3 =
|
||||||
|
Identifier.fromNamespaceAndPath("lovisual", "textures/trollface/trollface_3.png");
|
||||||
|
public static final Identifier TROLLFACE_4 =
|
||||||
|
Identifier.fromNamespaceAndPath("lovisual", "textures/trollface/trollface_4.png");
|
||||||
|
public static final Identifier TROLLFACE_5 =
|
||||||
|
Identifier.fromNamespaceAndPath("lovisual", "textures/trollface/trollface_5.png");
|
||||||
|
public static final Identifier TROLLFACE_6 =
|
||||||
|
Identifier.fromNamespaceAndPath("lovisual", "textures/trollface/trollface_6.png");
|
||||||
|
public static final Identifier TROLLFACE_CLASSIC =
|
||||||
|
Identifier.fromNamespaceAndPath("lovisual", "textures/trollface/trollface_classic.png");
|
||||||
public static final Identifier CAPTURE =
|
public static final Identifier CAPTURE =
|
||||||
Identifier.fromNamespaceAndPath("lovisual", "textures/hud/elements/capture.png");
|
Identifier.fromNamespaceAndPath("lovisual", "textures/hud/elements/capture.png");
|
||||||
|
|
||||||
|
|
@ -58,6 +72,13 @@ public enum TextureStorage {
|
||||||
tm.getTexture(FUNNEL_DISTORTION);
|
tm.getTexture(FUNNEL_DISTORTION);
|
||||||
tm.getTexture(PARTICLE_STARS);
|
tm.getTexture(PARTICLE_STARS);
|
||||||
tm.getTexture(CAPTURE);
|
tm.getTexture(CAPTURE);
|
||||||
|
tm.getTexture(TROLLFACE_1);
|
||||||
|
tm.getTexture(TROLLFACE_2);
|
||||||
|
tm.getTexture(TROLLFACE_3);
|
||||||
|
tm.getTexture(TROLLFACE_4);
|
||||||
|
tm.getTexture(TROLLFACE_5);
|
||||||
|
tm.getTexture(TROLLFACE_6);
|
||||||
|
tm.getTexture(TROLLFACE_CLASSIC);
|
||||||
for (Identifier id : RANDOM_PARTICLES) {
|
for (Identifier id : RANDOM_PARTICLES) {
|
||||||
tm.getTexture(id);
|
tm.getTexture(id);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -101,6 +101,7 @@ public enum LoVisualRenderPipelines {
|
||||||
public static final Identifier SHADER_POST_FX_FRAG = Identifier.fromNamespaceAndPath("lovisual", "shaders/post_fx.frag");
|
public static final Identifier SHADER_POST_FX_FRAG = Identifier.fromNamespaceAndPath("lovisual", "shaders/post_fx.frag");
|
||||||
public static final Identifier SHADER_MOTION_BLUR_FRAG = Identifier.fromNamespaceAndPath("lovisual", "shaders/motion_blur.frag");
|
public static final Identifier SHADER_MOTION_BLUR_FRAG = Identifier.fromNamespaceAndPath("lovisual", "shaders/motion_blur.frag");
|
||||||
public static final Identifier SHADER_DEPTH_OF_FIELD_FRAG = Identifier.fromNamespaceAndPath("lovisual", "shaders/depth_of_field.frag");
|
public static final Identifier SHADER_DEPTH_OF_FIELD_FRAG = Identifier.fromNamespaceAndPath("lovisual", "shaders/depth_of_field.frag");
|
||||||
|
public static final Identifier SHADER_WET_SURFACE_FRAG = Identifier.fromNamespaceAndPath("lovisual", "shaders/wet_surface.frag");
|
||||||
public static final Identifier SHADER_HEAT_FX_FRAG = Identifier.fromNamespaceAndPath("lovisual", "shaders/heat_fx.frag");
|
public static final Identifier SHADER_HEAT_FX_FRAG = Identifier.fromNamespaceAndPath("lovisual", "shaders/heat_fx.frag");
|
||||||
public static final Identifier SHADER_ESP_GRADIENT_FRAG = Identifier.fromNamespaceAndPath("lovisual", "shaders/shader_esp_gradient.frag");
|
public static final Identifier SHADER_ESP_GRADIENT_FRAG = Identifier.fromNamespaceAndPath("lovisual", "shaders/shader_esp_gradient.frag");
|
||||||
public static final Identifier SHADER_ESP_SHADOW_FRAG = Identifier.fromNamespaceAndPath("lovisual", "shaders/shader_esp_shadow.frag");
|
public static final Identifier SHADER_ESP_SHADOW_FRAG = Identifier.fromNamespaceAndPath("lovisual", "shaders/shader_esp_shadow.frag");
|
||||||
|
|
@ -121,6 +122,9 @@ public enum LoVisualRenderPipelines {
|
||||||
public static final Identifier SHADER_SKY_SUN_FRAG = Identifier.fromNamespaceAndPath("lovisual", "shaders/sky_sun.frag");
|
public static final Identifier SHADER_SKY_SUN_FRAG = Identifier.fromNamespaceAndPath("lovisual", "shaders/sky_sun.frag");
|
||||||
public static final Identifier SHADER_MENU_BACKGROUND_WAVES_FRAG = Identifier.fromNamespaceAndPath("lovisual", "shaders/menu_background_waves.frag");
|
public static final Identifier SHADER_MENU_BACKGROUND_WAVES_FRAG = Identifier.fromNamespaceAndPath("lovisual", "shaders/menu_background_waves.frag");
|
||||||
public static final Identifier SHADER_MENU_BACKGROUND_AURORA_FRAG = Identifier.fromNamespaceAndPath("lovisual", "shaders/menu_background_aurora.frag");
|
public static final Identifier SHADER_MENU_BACKGROUND_AURORA_FRAG = Identifier.fromNamespaceAndPath("lovisual", "shaders/menu_background_aurora.frag");
|
||||||
|
public static final Identifier SHADER_SKY_SHADER_SKY_FRAG = Identifier.fromNamespaceAndPath("lovisual", "shaders/sky_shader_sky.frag");
|
||||||
|
public static final Identifier SHADER_SKY_SHADER_WATER_FRAG = Identifier.fromNamespaceAndPath("lovisual", "shaders/sky_shader_water.frag");
|
||||||
|
public static final Identifier SHADER_SKY_SHADER_CAUSTIC_FRAG = Identifier.fromNamespaceAndPath("lovisual", "shaders/sky_shader_caustic.frag");
|
||||||
private static final List<RenderPipeline> PIPELINES = new ArrayList<>();
|
private static final List<RenderPipeline> PIPELINES = new ArrayList<>();
|
||||||
public static final RenderPipeline GUI_TEXTURE_LOOKUP = add(new ExtendedRenderPipelineBuilder(net.minecraft.client.renderer.RenderPipelines.GUI_TEXTURED_SNIPPET)
|
public static final RenderPipeline GUI_TEXTURE_LOOKUP = add(new ExtendedRenderPipelineBuilder(net.minecraft.client.renderer.RenderPipelines.GUI_TEXTURED_SNIPPET)
|
||||||
.withLocation(Identifier.fromNamespaceAndPath("lovisual", "pipeline/gui_texture_lookup"))
|
.withLocation(Identifier.fromNamespaceAndPath("lovisual", "pipeline/gui_texture_lookup"))
|
||||||
|
|
@ -735,6 +739,45 @@ public enum LoVisualRenderPipelines {
|
||||||
.withCull(false)
|
.withCull(false)
|
||||||
.build()
|
.build()
|
||||||
);
|
);
|
||||||
|
/**
|
||||||
|
* Fullscreen SkyShader passes (pos2). Sky/water/caustic share the same uniform layout.
|
||||||
|
*/
|
||||||
|
public static final RenderPipeline SKY_SHADER_SKY = add(new ExtendedRenderPipelineBuilder(MESH_UNIFORMS)
|
||||||
|
.withLocation(Identifier.fromNamespaceAndPath("lovisual", "pipeline/sky_shader_sky"))
|
||||||
|
.withVertexFormat(LoVisualVertexFormats.POS2, com.mojang.blaze3d.PrimitiveTopology.TRIANGLES)
|
||||||
|
.withVertexShader(SHADER_DAMAGE_TINT_VERT)
|
||||||
|
.withFragmentShader(SHADER_SKY_SHADER_SKY_FRAG)
|
||||||
|
.withUniform("SkyShader", UniformType.UNIFORM_BUFFER)
|
||||||
|
.withDepthTestFunction(DepthTestFunction.NO_DEPTH_TEST)
|
||||||
|
.withDepthWrite(false)
|
||||||
|
.withBlend(BlendFunction.TRANSLUCENT)
|
||||||
|
.withCull(false)
|
||||||
|
.build()
|
||||||
|
);
|
||||||
|
public static final RenderPipeline SKY_SHADER_WATER = add(new ExtendedRenderPipelineBuilder(MESH_UNIFORMS)
|
||||||
|
.withLocation(Identifier.fromNamespaceAndPath("lovisual", "pipeline/sky_shader_water"))
|
||||||
|
.withVertexFormat(LoVisualVertexFormats.POS2, com.mojang.blaze3d.PrimitiveTopology.TRIANGLES)
|
||||||
|
.withVertexShader(SHADER_DAMAGE_TINT_VERT)
|
||||||
|
.withFragmentShader(SHADER_SKY_SHADER_WATER_FRAG)
|
||||||
|
.withUniform("SkyShader", UniformType.UNIFORM_BUFFER)
|
||||||
|
.withDepthTestFunction(DepthTestFunction.NO_DEPTH_TEST)
|
||||||
|
.withDepthWrite(false)
|
||||||
|
.withBlend(BlendFunction.TRANSLUCENT)
|
||||||
|
.withCull(false)
|
||||||
|
.build()
|
||||||
|
);
|
||||||
|
public static final RenderPipeline SKY_SHADER_CAUSTIC = add(new ExtendedRenderPipelineBuilder(MESH_UNIFORMS)
|
||||||
|
.withLocation(Identifier.fromNamespaceAndPath("lovisual", "pipeline/sky_shader_caustic"))
|
||||||
|
.withVertexFormat(LoVisualVertexFormats.POS2, com.mojang.blaze3d.PrimitiveTopology.TRIANGLES)
|
||||||
|
.withVertexShader(SHADER_DAMAGE_TINT_VERT)
|
||||||
|
.withFragmentShader(SHADER_SKY_SHADER_CAUSTIC_FRAG)
|
||||||
|
.withUniform("SkyShader", UniformType.UNIFORM_BUFFER)
|
||||||
|
.withDepthTestFunction(DepthTestFunction.NO_DEPTH_TEST)
|
||||||
|
.withDepthWrite(false)
|
||||||
|
.withBlend(BlendFunction.TRANSLUCENT)
|
||||||
|
.withCull(false)
|
||||||
|
.build()
|
||||||
|
);
|
||||||
/**
|
/**
|
||||||
* Fullscreen main menu background (pos2).
|
* Fullscreen main menu background (pos2).
|
||||||
*/
|
*/
|
||||||
|
|
@ -822,6 +865,23 @@ public enum LoVisualRenderPipelines {
|
||||||
.withCull(false)
|
.withCull(false)
|
||||||
.build()
|
.build()
|
||||||
);
|
);
|
||||||
|
/**
|
||||||
|
* Fullscreen wet-ground screen-space reflection pass (pos2).
|
||||||
|
*/
|
||||||
|
public static final RenderPipeline WET_SURFACE = add(new ExtendedRenderPipelineBuilder(MESH_UNIFORMS)
|
||||||
|
.withLocation(Identifier.fromNamespaceAndPath("lovisual", "pipeline/wet_surface"))
|
||||||
|
.withVertexFormat(LoVisualVertexFormats.POS2, com.mojang.blaze3d.PrimitiveTopology.TRIANGLES)
|
||||||
|
.withVertexShader(SHADER_DAMAGE_TINT_VERT)
|
||||||
|
.withFragmentShader(SHADER_WET_SURFACE_FRAG)
|
||||||
|
.withSampler("u_Texture")
|
||||||
|
.withSampler("u_MainDepth")
|
||||||
|
.withUniform("WetSurface", UniformType.UNIFORM_BUFFER)
|
||||||
|
.withDepthTestFunction(DepthTestFunction.NO_DEPTH_TEST)
|
||||||
|
.withDepthWrite(false)
|
||||||
|
.withBlend(BlendFunction.TRANSLUCENT)
|
||||||
|
.withCull(false)
|
||||||
|
.build()
|
||||||
|
);
|
||||||
/**
|
/**
|
||||||
* Fullscreen heat distortion + vignette (pos2).
|
* Fullscreen heat distortion + vignette (pos2).
|
||||||
*/
|
*/
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,115 @@
|
||||||
|
/*
|
||||||
|
* This file is part of the LoVisual Client distribution.
|
||||||
|
* Copyright (c) 2026 pivosos2007.
|
||||||
|
*
|
||||||
|
* Licensed under the GNU General Public License v3.0.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package dev.loki.lovisual.render.engine.postprocess;
|
||||||
|
|
||||||
|
import com.mojang.blaze3d.ProjectionType;
|
||||||
|
import com.mojang.blaze3d.buffers.GpuBufferSlice;
|
||||||
|
import com.mojang.blaze3d.pipeline.RenderPipeline;
|
||||||
|
import com.mojang.blaze3d.pipeline.RenderTarget;
|
||||||
|
import com.mojang.blaze3d.systems.RenderSystem;
|
||||||
|
import dev.loki.lovisual.render.engine.RenderState;
|
||||||
|
import dev.loki.lovisual.render.engine.pipeline.LoVisualRenderPipelines;
|
||||||
|
import dev.loki.lovisual.render.engine.renderer.FullScreenRenderer;
|
||||||
|
import dev.loki.lovisual.render.engine.renderer.MeshRenderer;
|
||||||
|
import dev.loki.lovisual.render.engine.uniform.impl.SkyShaderUniforms;
|
||||||
|
import net.minecraft.client.Camera;
|
||||||
|
import net.minecraft.client.Minecraft;
|
||||||
|
import net.minecraft.client.renderer.ProjectionMatrixBuffer;
|
||||||
|
import net.minecraft.util.Util;
|
||||||
|
import org.joml.Matrix4f;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Fullscreen SkyShader pass: sky / water / caustic procedural GLSL effect drawn behind the world,
|
||||||
|
* with a camera-direction-driven ray so it reads correctly regardless of look direction.
|
||||||
|
*/
|
||||||
|
public enum SkyShaderRenderer {
|
||||||
|
;
|
||||||
|
|
||||||
|
public enum Mode {
|
||||||
|
SKY, WATER, CAUSTIC
|
||||||
|
}
|
||||||
|
|
||||||
|
private static final Matrix4f IDENTITY = new Matrix4f();
|
||||||
|
private static ProjectionMatrixBuffer projection;
|
||||||
|
private static long startMillis = -1L;
|
||||||
|
|
||||||
|
public static void reset() {
|
||||||
|
startMillis = -1L;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void render(Minecraft mc, Mode mode, float speed, float scale, float intensity,
|
||||||
|
float alpha, int colorRgb) {
|
||||||
|
if (mc == null || mc.level == null || mc.player == null) return;
|
||||||
|
|
||||||
|
RenderTarget fb = mc.gameRenderer.mainRenderTarget();
|
||||||
|
if (fb == null) return;
|
||||||
|
|
||||||
|
int w = mc.getWindow().getWidth();
|
||||||
|
int h = mc.getWindow().getHeight();
|
||||||
|
if (w <= 0 || h <= 0) return;
|
||||||
|
|
||||||
|
FullScreenRenderer.ensureInit();
|
||||||
|
|
||||||
|
if (startMillis < 0) startMillis = Util.getMillis();
|
||||||
|
float time = (Util.getMillis() - startMillis) / 1000.0f;
|
||||||
|
|
||||||
|
float cr = ((colorRgb >>> 16) & 0xFF) / 255f;
|
||||||
|
float cg = ((colorRgb >>> 8) & 0xFF) / 255f;
|
||||||
|
float cb = (colorRgb & 0xFF) / 255f;
|
||||||
|
|
||||||
|
Camera cam = mc.gameRenderer.mainCamera();
|
||||||
|
float yawRad = (float) Math.toRadians(-cam.yRot());
|
||||||
|
float pitchRad = (float) Math.toRadians(cam.xRot());
|
||||||
|
float fov = (float) mc.options.fov().get().intValue();
|
||||||
|
|
||||||
|
SkyShaderUniforms.update(time, speed, scale, intensity, w, h, alpha, fov, cr, cg, cb, yawRad, pitchRad);
|
||||||
|
|
||||||
|
var mv = RenderSystem.getModelViewStack();
|
||||||
|
boolean pushedModelView = false;
|
||||||
|
|
||||||
|
GpuBufferSlice prevProj = RenderSystem.getProjectionMatrixBuffer();
|
||||||
|
ProjectionType prevProjType = RenderSystem.getProjectionType();
|
||||||
|
Matrix4f prevMeshProjection = MeshRenderer.projection();
|
||||||
|
boolean prevRendering3D = RenderState.rendering3D;
|
||||||
|
|
||||||
|
try {
|
||||||
|
mv.pushMatrix();
|
||||||
|
pushedModelView = true;
|
||||||
|
mv.identity();
|
||||||
|
|
||||||
|
if (projection == null) {
|
||||||
|
projection = new ProjectionMatrixBuffer("lovisual-sky-shader-projection");
|
||||||
|
}
|
||||||
|
Matrix4f proj = IDENTITY.identity();
|
||||||
|
RenderSystem.setProjectionMatrix(projection.getBuffer(proj), ProjectionType.PERSPECTIVE);
|
||||||
|
MeshRenderer.setProjection(proj);
|
||||||
|
RenderState.rendering3D = false;
|
||||||
|
|
||||||
|
RenderPipeline pipeline = switch (mode) {
|
||||||
|
case WATER -> LoVisualRenderPipelines.SKY_SHADER_WATER;
|
||||||
|
case CAUSTIC -> LoVisualRenderPipelines.SKY_SHADER_CAUSTIC;
|
||||||
|
default -> LoVisualRenderPipelines.SKY_SHADER_SKY;
|
||||||
|
};
|
||||||
|
|
||||||
|
FullScreenRenderer.begin("LoVisual Fullscreen Pass")
|
||||||
|
.attachment(fb)
|
||||||
|
.pipeline(pipeline)
|
||||||
|
.uniform("SkyShader", SkyShaderUniforms.get())
|
||||||
|
.end();
|
||||||
|
} finally {
|
||||||
|
if (pushedModelView) {
|
||||||
|
mv.popMatrix();
|
||||||
|
}
|
||||||
|
MeshRenderer.setProjection(prevMeshProjection);
|
||||||
|
RenderState.rendering3D = prevRendering3D;
|
||||||
|
if (prevProj != null && prevProjType != null) {
|
||||||
|
RenderSystem.setProjectionMatrix(prevProj, prevProjType);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,81 @@
|
||||||
|
/*
|
||||||
|
* This file is part of the LoVisual Client distribution.
|
||||||
|
* Copyright (c) 2026 pivosos2007.
|
||||||
|
*
|
||||||
|
* Licensed under the GNU General Public License v3.0.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package dev.loki.lovisual.render.engine.uniform.impl;
|
||||||
|
|
||||||
|
import dev.loki.lovisual.render.engine.core.LoVisualRenderSystem;
|
||||||
|
|
||||||
|
import com.mojang.blaze3d.buffers.GpuBufferSlice;
|
||||||
|
import com.mojang.blaze3d.buffers.Std140Builder;
|
||||||
|
import com.mojang.blaze3d.buffers.Std140SizeCalculator;
|
||||||
|
import dev.loki.lovisual.render.engine.rhi.uniform.LoVisualUniformAllocator;
|
||||||
|
|
||||||
|
public enum SkyShaderUniforms {
|
||||||
|
;
|
||||||
|
public static final int SIZE = new Std140SizeCalculator()
|
||||||
|
.putVec4()
|
||||||
|
.putVec4()
|
||||||
|
.putVec4()
|
||||||
|
.putVec4()
|
||||||
|
.get();
|
||||||
|
|
||||||
|
private static final Data DATA = new Data();
|
||||||
|
private static final String UNIFORM_NAME = "LoVisual - SkyShader UBO";
|
||||||
|
private static final int EXPECTED_WRITES_PER_FRAME = 4;
|
||||||
|
|
||||||
|
public static void update(float time, float speed, float scale, float intensity,
|
||||||
|
float width, float height, float alpha, float fov,
|
||||||
|
float colorR, float colorG, float colorB,
|
||||||
|
float cameraDirX, float cameraDirY) {
|
||||||
|
DATA.params0[0] = time;
|
||||||
|
DATA.params0[1] = speed;
|
||||||
|
DATA.params0[2] = scale;
|
||||||
|
DATA.params0[3] = intensity;
|
||||||
|
|
||||||
|
DATA.params1[0] = width;
|
||||||
|
DATA.params1[1] = height;
|
||||||
|
DATA.params1[2] = alpha;
|
||||||
|
DATA.params1[3] = fov;
|
||||||
|
|
||||||
|
DATA.color[0] = colorR;
|
||||||
|
DATA.color[1] = colorG;
|
||||||
|
DATA.color[2] = colorB;
|
||||||
|
DATA.color[3] = 0.0f;
|
||||||
|
|
||||||
|
DATA.cameraDir[0] = cameraDirX;
|
||||||
|
DATA.cameraDir[1] = cameraDirY;
|
||||||
|
DATA.cameraDir[2] = 0.0f;
|
||||||
|
DATA.cameraDir[3] = 0.0f;
|
||||||
|
|
||||||
|
LoVisualRenderSystem.uniforms().write(UNIFORM_NAME, SIZE, EXPECTED_WRITES_PER_FRAME, DATA);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static GpuBufferSlice get() {
|
||||||
|
return LoVisualRenderSystem.uniforms().current(UNIFORM_NAME);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static final class Data implements LoVisualUniformAllocator.UniformWriter {
|
||||||
|
private final float[] params0 = new float[4];
|
||||||
|
private final float[] params1 = new float[4];
|
||||||
|
private final float[] color = new float[4];
|
||||||
|
private final float[] cameraDir = new float[4];
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void write(java.nio.ByteBuffer buffer) {
|
||||||
|
Std140Builder.intoBuffer(buffer)
|
||||||
|
.putFloat(params0[0]).putFloat(params0[1]).putFloat(params0[2]).putFloat(params0[3])
|
||||||
|
.putFloat(params1[0]).putFloat(params1[1]).putFloat(params1[2]).putFloat(params1[3])
|
||||||
|
.putFloat(color[0]).putFloat(color[1]).putFloat(color[2]).putFloat(color[3])
|
||||||
|
.putFloat(cameraDir[0]).putFloat(cameraDir[1]).putFloat(cameraDir[2]).putFloat(cameraDir[3]);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean equals(Object o) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,86 @@
|
||||||
|
/*
|
||||||
|
* This file is part of the LoVisual Client distribution.
|
||||||
|
* Copyright (c) 2026 pivosos2007.
|
||||||
|
*
|
||||||
|
* Licensed under the GNU General Public License v3.0.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package dev.loki.lovisual.render.engine.uniform.impl;
|
||||||
|
|
||||||
|
import com.mojang.blaze3d.buffers.GpuBufferSlice;
|
||||||
|
import com.mojang.blaze3d.buffers.Std140Builder;
|
||||||
|
import com.mojang.blaze3d.buffers.Std140SizeCalculator;
|
||||||
|
import org.joml.Matrix4f;
|
||||||
|
import dev.loki.lovisual.render.engine.core.LoVisualRenderSystem;
|
||||||
|
import dev.loki.lovisual.render.engine.rhi.uniform.LoVisualUniformAllocator;
|
||||||
|
|
||||||
|
public enum WetSurfaceUniforms {
|
||||||
|
;
|
||||||
|
public static final int SIZE = new Std140SizeCalculator()
|
||||||
|
.putMat4f()
|
||||||
|
.putMat4f()
|
||||||
|
.putFloat().putFloat().putFloat().putFloat()
|
||||||
|
.putFloat().putFloat().putFloat().putFloat()
|
||||||
|
.putFloat().putFloat().putFloat().putFloat()
|
||||||
|
.putFloat().putFloat().putFloat().putFloat()
|
||||||
|
.get();
|
||||||
|
|
||||||
|
private static final Data DATA = new Data();
|
||||||
|
private static final String UNIFORM_NAME = "LoVisual - WetSurface UBO";
|
||||||
|
private static final int EXPECTED_WRITES_PER_FRAME = 2;
|
||||||
|
|
||||||
|
public static void update(Matrix4f projection,
|
||||||
|
Matrix4f invView,
|
||||||
|
float camX, float camY, float camZ,
|
||||||
|
float invW, float invH, float time, boolean mainDepthEnabled,
|
||||||
|
float wetness, float puddleCoverage, float reflectionStrength, float rippleStrength,
|
||||||
|
float maxDistance, int reflectionSteps, float rainAmount, boolean ripplesEnabled) {
|
||||||
|
DATA.projection.set(projection);
|
||||||
|
DATA.invView.set(invView);
|
||||||
|
DATA.camX = camX;
|
||||||
|
DATA.camY = camY;
|
||||||
|
DATA.camZ = camZ;
|
||||||
|
DATA.invW = invW;
|
||||||
|
DATA.invH = invH;
|
||||||
|
DATA.time = time;
|
||||||
|
DATA.mainDepthEnabled = mainDepthEnabled ? 1.0f : 0.0f;
|
||||||
|
DATA.wetness = wetness;
|
||||||
|
DATA.puddleCoverage = puddleCoverage;
|
||||||
|
DATA.reflectionStrength = reflectionStrength;
|
||||||
|
DATA.rippleStrength = rippleStrength;
|
||||||
|
DATA.maxDistance = maxDistance;
|
||||||
|
DATA.reflectionSteps = reflectionSteps;
|
||||||
|
DATA.rainAmount = rainAmount;
|
||||||
|
DATA.ripplesEnabled = ripplesEnabled ? 1.0f : 0.0f;
|
||||||
|
LoVisualRenderSystem.uniforms().write(UNIFORM_NAME, SIZE, EXPECTED_WRITES_PER_FRAME, DATA);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static GpuBufferSlice get() {
|
||||||
|
return LoVisualRenderSystem.uniforms().current(UNIFORM_NAME);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static final class Data implements LoVisualUniformAllocator.UniformWriter {
|
||||||
|
private final Matrix4f projection = new Matrix4f();
|
||||||
|
private final Matrix4f invView = new Matrix4f();
|
||||||
|
private float camX, camY, camZ;
|
||||||
|
private float invW, invH, time, mainDepthEnabled;
|
||||||
|
private float wetness, puddleCoverage, reflectionStrength, rippleStrength;
|
||||||
|
private float maxDistance, reflectionSteps, rainAmount, ripplesEnabled;
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void write(java.nio.ByteBuffer buffer) {
|
||||||
|
Std140Builder b = Std140Builder.intoBuffer(buffer);
|
||||||
|
b.putMat4f(projection);
|
||||||
|
b.putMat4f(invView);
|
||||||
|
b.putFloat(camX).putFloat(camY).putFloat(camZ).putFloat(0.0f);
|
||||||
|
b.putFloat(invW).putFloat(invH).putFloat(time).putFloat(mainDepthEnabled);
|
||||||
|
b.putFloat(wetness).putFloat(puddleCoverage).putFloat(reflectionStrength).putFloat(rippleStrength);
|
||||||
|
b.putFloat(maxDistance).putFloat(reflectionSteps).putFloat(rainAmount).putFloat(ripplesEnabled);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean equals(Object o) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -15,7 +15,7 @@ package dev.loki.lovisual.util.combat;
|
||||||
|
|
||||||
import net.minecraft.client.Minecraft;
|
import net.minecraft.client.Minecraft;
|
||||||
import net.minecraft.client.player.LocalPlayer;
|
import net.minecraft.client.player.LocalPlayer;
|
||||||
import net.minecraft.network.protocol.game.ServerboundInteractPacket;
|
import net.minecraft.network.protocol.game.ServerboundAttackPacket;
|
||||||
import net.minecraft.network.protocol.game.ServerboundMovePlayerPacket;
|
import net.minecraft.network.protocol.game.ServerboundMovePlayerPacket;
|
||||||
import net.minecraft.network.protocol.game.ServerboundPlayerCommandPacket;
|
import net.minecraft.network.protocol.game.ServerboundPlayerCommandPacket;
|
||||||
import net.minecraft.world.InteractionHand;
|
import net.minecraft.world.InteractionHand;
|
||||||
|
|
@ -155,7 +155,9 @@ public enum AttackUtil {
|
||||||
}
|
}
|
||||||
|
|
||||||
notifyLocalAttack(target);
|
notifyLocalAttack(target);
|
||||||
mc.getConnection().send(new ServerboundInteractPacket(target.getId(), null, null, player.isShiftKeyDown()));
|
mc.getConnection().send(new ServerboundAttackPacket(target.getId()));
|
||||||
|
player.attack(target);
|
||||||
|
player.resetAttackStrengthTicker();
|
||||||
ResetAttackCooldown.resetAttackCooldown(player);
|
ResetAttackCooldown.resetAttackCooldown(player);
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,721 +0,0 @@
|
||||||
/*
|
|
||||||
* This file is part of the LoVisual Client distribution.
|
|
||||||
* Copyright (c) 2026 pivosos2007.
|
|
||||||
*
|
|
||||||
* Licensed under the GNU General Public License v3.0.
|
|
||||||
*/
|
|
||||||
|
|
||||||
package dev.loki.lovisual.util.combat;
|
|
||||||
|
|
||||||
import net.minecraft.client.Minecraft;
|
|
||||||
import net.minecraft.client.player.LocalPlayer;
|
|
||||||
import net.minecraft.core.Holder;
|
|
||||||
import net.minecraft.core.Registry;
|
|
||||||
import net.minecraft.core.registries.Registries;
|
|
||||||
import net.minecraft.network.protocol.game.*;
|
|
||||||
import net.minecraft.resources.ResourceKey;
|
|
||||||
import net.minecraft.util.Mth;
|
|
||||||
import net.minecraft.world.entity.Entity;
|
|
||||||
import net.minecraft.world.entity.ai.attributes.Attributes;
|
|
||||||
import net.minecraft.world.entity.player.Player;
|
|
||||||
import net.minecraft.world.item.ItemStack;
|
|
||||||
import net.minecraft.world.item.enchantment.Enchantment;
|
|
||||||
import net.minecraft.world.item.enchantment.EnchantmentHelper;
|
|
||||||
import net.minecraft.world.item.enchantment.Enchantments;
|
|
||||||
import net.minecraft.world.phys.AABB;
|
|
||||||
import net.minecraft.world.phys.Vec3;
|
|
||||||
import dev.loki.lovisual.events.EventHandler;
|
|
||||||
import dev.loki.lovisual.events.impl.GameTickEvent;
|
|
||||||
import dev.loki.lovisual.events.impl.PacketEvent;
|
|
||||||
import dev.loki.lovisual.util.network.BacktrackController;
|
|
||||||
import dev.loki.lovisual.util.player.NetworkStatsUtil;
|
|
||||||
|
|
||||||
import java.util.*;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Local model for Vulcan 2.9.7 Reach A/B.
|
|
||||||
* This does not change combat by itself; Reach/Hitbox should query planReach()
|
|
||||||
* and actual sent attacks are observed through the same attack -> movement split.
|
|
||||||
*/
|
|
||||||
public final class VulcanReachController {
|
|
||||||
|
|
||||||
public static final VulcanReachController INSTANCE = new VulcanReachController();
|
|
||||||
|
|
||||||
private static final double REACH_A_CONFIG_MAX = 3.03;
|
|
||||||
private static final double REACH_A_BODY_SUBTRACT = 0.52;
|
|
||||||
private static final double REACH_A_BUFFER_MAX = 3.0;
|
|
||||||
private static final double REACH_A_LATCH_RELEASE = REACH_A_BUFFER_MAX - 1.0;
|
|
||||||
private static final double REACH_A_DECAY = 0.035;
|
|
||||||
private static final double REACH_A_NO_HISTORY_DECAY = 0.025;
|
|
||||||
private static final double REACH_A_RECENT_SERVER_ALLOWANCE = 1.15;
|
|
||||||
private static final double REACH_A_LIQUID_ALLOWANCE = 0.1;
|
|
||||||
private static final double REACH_A_SANITY_CAP = 8.0;
|
|
||||||
|
|
||||||
private static final double REACH_B_CONFIG_MAX = 3.15;
|
|
||||||
private static final double REACH_B_BODY_SUBTRACT = 0.565;
|
|
||||||
private static final double REACH_B_BUFFER_MAX = 6.0;
|
|
||||||
private static final double REACH_B_LATCH_RELEASE = REACH_B_BUFFER_MAX - 1.0;
|
|
||||||
private static final double REACH_B_DECAY = 0.225;
|
|
||||||
private static final double REACH_B_BASE_EXTRA = 0.05;
|
|
||||||
private static final double REACH_B_HORIZONTAL_ALLOWANCE = 0.9;
|
|
||||||
private static final double REACH_B_VERTICAL_ALLOWANCE = 0.66;
|
|
||||||
private static final double REACH_B_MOVING_STATE_ALLOWANCE = 0.32;
|
|
||||||
private static final double REACH_B_FRESH_STATE_ALLOWANCE = 0.35;
|
|
||||||
private static final double REACH_B_STATE_COUNTER_SCALE = 0.125;
|
|
||||||
private static final double REACH_B_HIGH_LATENCY_ALLOWANCE = 1.25;
|
|
||||||
private static final double REACH_B_SANITY_CAP = 6.0;
|
|
||||||
|
|
||||||
private static final long SERVER_POSITION_WINDOW_MS = 1500L;
|
|
||||||
private static final long JOIN_WORLD_WINDOW_MS = 1500L;
|
|
||||||
private static final long HISTORY_MAX_AGE_MS = 700L;
|
|
||||||
private static final long HISTORY_STRICT_WINDOW_MS = 250L;
|
|
||||||
private static final int HISTORY_MAX_SAMPLES = 20;
|
|
||||||
private static final double BUFFER_EPSILON = 1.0E-6;
|
|
||||||
private static final double FAST_POST_HIT_MOVE_THRESHOLD = 0.18;
|
|
||||||
private static final double FAST_POST_HIT_APPROACH_ALLOWANCE = 0.45;
|
|
||||||
|
|
||||||
private final Minecraft mc = Minecraft.getInstance();
|
|
||||||
private final Map<UUID, ArrayDeque<HistorySample>> targetHistory = new HashMap<>();
|
|
||||||
|
|
||||||
private double reachABuffer;
|
|
||||||
private double reachBBuffer;
|
|
||||||
private int pendingAttackEntityId = -1;
|
|
||||||
private long pendingAttackMs;
|
|
||||||
private Vec3 pendingAttackPosition;
|
|
||||||
private long lastServerPositionMs = Long.MIN_VALUE;
|
|
||||||
private long lastJoinOrWorldMs = Long.MIN_VALUE;
|
|
||||||
private Vec3 lastSentPosition;
|
|
||||||
private double lastSentHorizontalDelta;
|
|
||||||
private double lastSentVerticalDelta;
|
|
||||||
private int movementStateTicks;
|
|
||||||
private boolean reachAClampLatched;
|
|
||||||
private boolean reachBClampLatched;
|
|
||||||
|
|
||||||
private VulcanReachController() {
|
|
||||||
}
|
|
||||||
|
|
||||||
public ReachPlan planReach(Entity target, double requestedReach) {
|
|
||||||
LocalPlayer player = mc.player;
|
|
||||||
if (player == null || target == null) {
|
|
||||||
return ReachPlan.empty(requestedReach);
|
|
||||||
}
|
|
||||||
|
|
||||||
CheckState state = buildState(player, target, false);
|
|
||||||
updateClampLatches();
|
|
||||||
|
|
||||||
boolean forceReachAClamp = reachAClampLatched && !state.reachAExempt;
|
|
||||||
boolean forceReachBClamp = reachBClampLatched && !state.reachBExempt;
|
|
||||||
|
|
||||||
double strictReachALimit = state.reachAExempt
|
|
||||||
? requestedReach
|
|
||||||
: reachAEdgeLimit(player, target, state.reachAAllowed);
|
|
||||||
double reachALimit = state.reachAExempt || (state.noReachAHistory && !reachAClampLatched)
|
|
||||||
? requestedReach
|
|
||||||
: strictReachALimit;
|
|
||||||
if (state.fastPostHitApproach) {
|
|
||||||
reachALimit = Math.min(requestedReach, reachALimit + FAST_POST_HIT_APPROACH_ALLOWANCE);
|
|
||||||
}
|
|
||||||
double reachBLimit = state.reachBExempt
|
|
||||||
? requestedReach
|
|
||||||
: reachBEdgeLimit(player, target, state.reachBAllowed);
|
|
||||||
|
|
||||||
boolean overB = !state.reachBExempt
|
|
||||||
&& requestedReach > reachBLimit;
|
|
||||||
boolean spendsA = !state.reachAExempt
|
|
||||||
&& !state.noReachAHistory
|
|
||||||
&& requestedReach > strictReachALimit
|
|
||||||
&& state.reachAMeasured < REACH_A_SANITY_CAP;
|
|
||||||
boolean spendsB = overB && state.reachBMeasured < REACH_B_SANITY_CAP;
|
|
||||||
|
|
||||||
double clamped = requestedReach;
|
|
||||||
if (forceReachAClamp) {
|
|
||||||
clamped = Math.min(clamped, strictReachALimit);
|
|
||||||
}
|
|
||||||
if (forceReachBClamp) {
|
|
||||||
clamped = Math.min(clamped, reachBLimit);
|
|
||||||
}
|
|
||||||
|
|
||||||
return new ReachPlan(
|
|
||||||
requestedReach,
|
|
||||||
Math.max(0.0, clamped),
|
|
||||||
Math.max(0.0, reachALimit),
|
|
||||||
Math.max(0.0, reachBLimit),
|
|
||||||
reachABuffer,
|
|
||||||
reachBBuffer,
|
|
||||||
state.reachAExempt,
|
|
||||||
state.reachBExempt,
|
|
||||||
state.noReachAHistory,
|
|
||||||
spendsA,
|
|
||||||
spendsB,
|
|
||||||
state.reason
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
public double clampReach(Entity target, double requestedReach) {
|
|
||||||
return planReach(target, requestedReach).allowedReach();
|
|
||||||
}
|
|
||||||
|
|
||||||
public HudSnapshot hudSnapshot(Entity target, double requestedReach) {
|
|
||||||
if (!(target instanceof Player)) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
ReachPlan plan = planReach(target, requestedReach);
|
|
||||||
boolean dirty = reachABuffer > 1.0E-4
|
|
||||||
|| reachBBuffer > 1.0E-4
|
|
||||||
|| reachAClampLatched
|
|
||||||
|| reachBClampLatched;
|
|
||||||
|
|
||||||
double displayReach = plan.allowedReach();
|
|
||||||
boolean clamped = displayReach + BUFFER_EPSILON < requestedReach;
|
|
||||||
|
|
||||||
int rawRemaining = Math.min(
|
|
||||||
longHitsRemaining(reachABuffer, REACH_A_BUFFER_MAX, reachAClampLatched),
|
|
||||||
longHitsRemaining(reachBBuffer, REACH_B_BUFFER_MAX, reachBClampLatched)
|
|
||||||
);
|
|
||||||
int remaining = clamped ? 0 : Math.max(1, rawRemaining);
|
|
||||||
|
|
||||||
return new HudSnapshot(displayReach, Math.max(0, remaining), dirty, plan.reason());
|
|
||||||
}
|
|
||||||
|
|
||||||
public HudSnapshot hudSnapshot(double requestedReach) {
|
|
||||||
boolean dirty = reachABuffer > 1.0E-4
|
|
||||||
|| reachBBuffer > 1.0E-4
|
|
||||||
|| reachAClampLatched
|
|
||||||
|| reachBClampLatched;
|
|
||||||
|
|
||||||
int rawRemaining = Math.min(
|
|
||||||
longHitsRemaining(reachABuffer, REACH_A_BUFFER_MAX, reachAClampLatched),
|
|
||||||
longHitsRemaining(reachBBuffer, REACH_B_BUFFER_MAX, reachBClampLatched)
|
|
||||||
);
|
|
||||||
int remaining = reachAClampLatched || reachBClampLatched ? 0 : Math.max(1, rawRemaining);
|
|
||||||
|
|
||||||
return new HudSnapshot(requestedReach, Math.max(0, remaining), dirty, "no_target");
|
|
||||||
}
|
|
||||||
|
|
||||||
public double getReachABuffer() {
|
|
||||||
return reachABuffer;
|
|
||||||
}
|
|
||||||
|
|
||||||
public double getReachBBuffer() {
|
|
||||||
return reachBBuffer;
|
|
||||||
}
|
|
||||||
|
|
||||||
public void clear() {
|
|
||||||
reachABuffer = 0.0;
|
|
||||||
reachBBuffer = 0.0;
|
|
||||||
pendingAttackEntityId = -1;
|
|
||||||
pendingAttackMs = 0L;
|
|
||||||
pendingAttackPosition = null;
|
|
||||||
lastSentPosition = null;
|
|
||||||
lastSentHorizontalDelta = 0.0;
|
|
||||||
lastSentVerticalDelta = 0.0;
|
|
||||||
movementStateTicks = 0;
|
|
||||||
reachAClampLatched = false;
|
|
||||||
reachBClampLatched = false;
|
|
||||||
targetHistory.clear();
|
|
||||||
}
|
|
||||||
|
|
||||||
@EventHandler
|
|
||||||
private void onGameTick(GameTickEvent event) {
|
|
||||||
LocalPlayer player = mc.player;
|
|
||||||
if (player == null || mc.level == null) {
|
|
||||||
clear();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
long now = System.currentTimeMillis();
|
|
||||||
for (Player other : mc.level.players()) {
|
|
||||||
if (other == player || !other.isAlive()) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
addHistorySample(other, now);
|
|
||||||
}
|
|
||||||
pruneHistory(now);
|
|
||||||
}
|
|
||||||
|
|
||||||
@EventHandler
|
|
||||||
private void onPacketSendPost(PacketEvent.SendPost event) {
|
|
||||||
if (event.getPacket() instanceof ServerboundInteractPacket packet) {
|
|
||||||
onAttackPacket(packet);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (event.getPacket() instanceof ServerboundMovePlayerPacket packet) {
|
|
||||||
updateSentMovement(packet);
|
|
||||||
evaluatePendingAttack();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@EventHandler
|
|
||||||
private void onPacketReceive(PacketEvent.Receive event) {
|
|
||||||
if (event.getPacket() instanceof ClientboundPlayerPositionPacket) {
|
|
||||||
lastServerPositionMs = System.currentTimeMillis();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (event.getPacket() instanceof ClientboundLoginPacket) {
|
|
||||||
clear();
|
|
||||||
lastJoinOrWorldMs = System.currentTimeMillis();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (event.getPacket() instanceof ClientboundRespawnPacket) {
|
|
||||||
pendingAttackEntityId = -1;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private void onAttackPacket(ServerboundInteractPacket packet) {
|
|
||||||
if (!isAttack(packet) || mc.level == null) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
int entityId = packet.entityId();
|
|
||||||
Entity target = mc.level.getEntity(entityId);
|
|
||||||
if (!(target instanceof Player)) {
|
|
||||||
pendingAttackEntityId = -1;
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
pendingAttackEntityId = entityId;
|
|
||||||
pendingAttackMs = System.currentTimeMillis();
|
|
||||||
pendingAttackPosition = mc.player != null ? mc.player.position() : null;
|
|
||||||
}
|
|
||||||
|
|
||||||
private void evaluatePendingAttack() {
|
|
||||||
if (pendingAttackEntityId < 0 || mc.level == null || mc.player == null) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
Entity target = mc.level.getEntity(pendingAttackEntityId);
|
|
||||||
pendingAttackEntityId = -1;
|
|
||||||
|
|
||||||
if (!(target instanceof Player)) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
CheckState state = buildState(mc.player, target, true);
|
|
||||||
applyReachA(state);
|
|
||||||
applyReachB(state);
|
|
||||||
pendingAttackPosition = null;
|
|
||||||
}
|
|
||||||
|
|
||||||
private void applyReachA(CheckState state) {
|
|
||||||
if (state.reachAExempt) {
|
|
||||||
decayABuffer(REACH_A_DECAY);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (state.noReachAHistory) {
|
|
||||||
decayABuffer(REACH_A_NO_HISTORY_DECAY);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (state.reachAMeasured > state.reachAAllowed && state.reachAMeasured < REACH_A_SANITY_CAP) {
|
|
||||||
reachABuffer = increaseLocalBuffer(reachABuffer, REACH_A_BUFFER_MAX);
|
|
||||||
} else {
|
|
||||||
decayABuffer(REACH_A_DECAY);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private void applyReachB(CheckState state) {
|
|
||||||
if (state.reachBExempt) {
|
|
||||||
decayBBuffer(REACH_B_DECAY);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (state.reachBMeasured > state.reachBAllowed && state.reachBMeasured < REACH_B_SANITY_CAP) {
|
|
||||||
reachBBuffer = increaseLocalBuffer(reachBBuffer, REACH_B_BUFFER_MAX);
|
|
||||||
} else {
|
|
||||||
decayBBuffer(REACH_B_DECAY);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private CheckState buildState(LocalPlayer player, Entity target, boolean evaluatingPendingAttack) {
|
|
||||||
if (!(target instanceof Player targetPlayer)) {
|
|
||||||
return CheckState.exempt("non_player");
|
|
||||||
}
|
|
||||||
|
|
||||||
boolean attackerVehicle = player.isPassenger();
|
|
||||||
boolean targetVehicle = targetPlayer.isPassenger();
|
|
||||||
boolean attackerBoat = attackerVehicle && isBoatLike(player.getVehicle());
|
|
||||||
boolean targetBoat = targetVehicle && isBoatLike(targetPlayer.getVehicle());
|
|
||||||
boolean creative = player.isCreative();
|
|
||||||
boolean liquid = player.isInWater() || player.isUnderWater() || player.isInLava();
|
|
||||||
boolean recentServer = isRecent(lastServerPositionMs, SERVER_POSITION_WINDOW_MS);
|
|
||||||
boolean recentJoinWorld = isRecent(lastJoinOrWorldMs, JOIN_WORLD_WINDOW_MS);
|
|
||||||
boolean knockback = hasKnockback(player.getMainHandItem());
|
|
||||||
boolean fastPostHitApproach = evaluatingPendingAttack && isFastPostHitApproach(player, targetPlayer);
|
|
||||||
|
|
||||||
boolean reachAExempt = creative || attackerVehicle || attackerBoat || targetVehicle || targetBoat || knockback;
|
|
||||||
boolean reachBExempt = creative || attackerVehicle || attackerBoat || targetVehicle || targetBoat || recentServer || recentJoinWorld;
|
|
||||||
|
|
||||||
double baseReach = getInteractionRange(player);
|
|
||||||
double reachAAllowed = baseReach + (REACH_A_CONFIG_MAX - 3.0);
|
|
||||||
if (recentServer) {
|
|
||||||
reachAAllowed += REACH_A_RECENT_SERVER_ALLOWANCE;
|
|
||||||
}
|
|
||||||
if (liquid) {
|
|
||||||
reachAAllowed += REACH_A_LIQUID_ALLOWANCE;
|
|
||||||
}
|
|
||||||
|
|
||||||
double reachBAllowed = baseReach + (REACH_B_CONFIG_MAX - 3.0) + REACH_B_BASE_EXTRA;
|
|
||||||
reachBAllowed += lastSentHorizontalDelta * REACH_B_HORIZONTAL_ALLOWANCE;
|
|
||||||
reachBAllowed += lastSentVerticalDelta * REACH_B_VERTICAL_ALLOWANCE;
|
|
||||||
if (lastSentHorizontalDelta > 1.0E-4 || lastSentVerticalDelta > 1.0E-4) {
|
|
||||||
reachBAllowed += REACH_B_MOVING_STATE_ALLOWANCE;
|
|
||||||
}
|
|
||||||
if (movementStateTicks < 3) {
|
|
||||||
reachBAllowed += REACH_B_FRESH_STATE_ALLOWANCE;
|
|
||||||
}
|
|
||||||
reachBAllowed += Math.min(4, movementStateTicks) * REACH_B_STATE_COUNTER_SCALE;
|
|
||||||
if (NetworkStatsUtil.getPing(mc) > 100) {
|
|
||||||
reachBAllowed += REACH_B_HIGH_LATENCY_ALLOWANCE;
|
|
||||||
}
|
|
||||||
|
|
||||||
double reachAMeasured = reachAMeasureDistance(player, targetPlayer);
|
|
||||||
double reachBMeasured = Math.max(0.0, player.position().distanceTo(targetPlayer.position()) - REACH_B_BODY_SUBTRACT);
|
|
||||||
boolean noHistory = reachAMeasured < 0.0;
|
|
||||||
|
|
||||||
String reason = resolveReason(reachAExempt, reachBExempt, noHistory, recentServer, recentJoinWorld, liquid, knockback);
|
|
||||||
return new CheckState(
|
|
||||||
reachAExempt,
|
|
||||||
reachBExempt,
|
|
||||||
noHistory,
|
|
||||||
reachAAllowed,
|
|
||||||
reachBAllowed,
|
|
||||||
reachAMeasured,
|
|
||||||
reachBMeasured,
|
|
||||||
fastPostHitApproach,
|
|
||||||
reason
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
private double reachAMeasureDistance(LocalPlayer player, Player target) {
|
|
||||||
if (shouldUseBacktrackHistory(target)) {
|
|
||||||
return reachAHistoryDistance(player, target);
|
|
||||||
}
|
|
||||||
|
|
||||||
Vec3 playerPos = player.position();
|
|
||||||
Vec3 targetPos = target.position();
|
|
||||||
double dx = playerPos.x - targetPos.x;
|
|
||||||
double dz = playerPos.z - targetPos.z;
|
|
||||||
return Math.max(0.0, Math.sqrt(dx * dx + dz * dz) - REACH_A_BODY_SUBTRACT);
|
|
||||||
}
|
|
||||||
|
|
||||||
private boolean shouldUseBacktrackHistory(Entity target) {
|
|
||||||
BacktrackController backtrack = BacktrackController.INSTANCE;
|
|
||||||
Entity backtrackTarget = backtrack.getTarget();
|
|
||||||
return backtrack.isEnabled()
|
|
||||||
&& backtrack.isLagging()
|
|
||||||
&& backtrackTarget != null
|
|
||||||
&& target != null
|
|
||||||
&& backtrackTarget.getId() == target.getId();
|
|
||||||
}
|
|
||||||
|
|
||||||
private double reachAHistoryDistance(LocalPlayer player, Player target) {
|
|
||||||
ArrayDeque<HistorySample> samples = targetHistory.get(target.getUUID());
|
|
||||||
if (samples == null || samples.isEmpty()) {
|
|
||||||
return -1.0;
|
|
||||||
}
|
|
||||||
|
|
||||||
long now = pendingAttackMs > 0L ? pendingAttackMs : System.currentTimeMillis();
|
|
||||||
Vec3 playerPos = player.position();
|
|
||||||
double best = Double.MAX_VALUE;
|
|
||||||
|
|
||||||
for (HistorySample sample : samples) {
|
|
||||||
long age = Math.abs(now - sample.timeMs());
|
|
||||||
if (age > HISTORY_STRICT_WINDOW_MS) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
double dx = playerPos.x - sample.pos().x;
|
|
||||||
double dz = playerPos.z - sample.pos().z;
|
|
||||||
best = Math.min(best, Math.sqrt(dx * dx + dz * dz) - REACH_A_BODY_SUBTRACT);
|
|
||||||
}
|
|
||||||
|
|
||||||
return best == Double.MAX_VALUE ? -1.0 : Math.max(0.0, best);
|
|
||||||
}
|
|
||||||
|
|
||||||
private double reachAEdgeLimit(LocalPlayer player, Entity target, double allowed) {
|
|
||||||
Vec3 from = player.getEyePosition(1.0F);
|
|
||||||
AABB box = target.getBoundingBox();
|
|
||||||
Vec3 center = box.getCenter();
|
|
||||||
|
|
||||||
double centerDistance = Math.sqrt(squaredHorizontalDistance(from.x, from.z, center.x, center.z));
|
|
||||||
double edgeDistance = horizontalBoxDistance(box, from);
|
|
||||||
double centerToEdgeReduction = Math.max(0.0, centerDistance - edgeDistance);
|
|
||||||
|
|
||||||
return Math.max(0.0, allowed + REACH_A_BODY_SUBTRACT - centerToEdgeReduction);
|
|
||||||
}
|
|
||||||
|
|
||||||
private double reachBEdgeLimit(LocalPlayer player, Entity target, double allowed) {
|
|
||||||
Vec3 from = player.getEyePosition(1.0F);
|
|
||||||
AABB box = target.getBoundingBox();
|
|
||||||
Vec3 center = box.getCenter();
|
|
||||||
|
|
||||||
double centerDistance = from.distanceTo(center);
|
|
||||||
double edgeDistance = Math.sqrt(boxDistanceSq(box, from));
|
|
||||||
double centerToEdgeReduction = Math.max(0.0, centerDistance - edgeDistance);
|
|
||||||
|
|
||||||
return Math.max(0.0, allowed + REACH_B_BODY_SUBTRACT - centerToEdgeReduction);
|
|
||||||
}
|
|
||||||
|
|
||||||
private double horizontalBoxDistance(AABB box, Vec3 point) {
|
|
||||||
double dx = 0.0;
|
|
||||||
if (point.x < box.minX) dx = box.minX - point.x;
|
|
||||||
else if (point.x > box.maxX) dx = point.x - box.maxX;
|
|
||||||
|
|
||||||
double dz = 0.0;
|
|
||||||
if (point.z < box.minZ) dz = box.minZ - point.z;
|
|
||||||
else if (point.z > box.maxZ) dz = point.z - box.maxZ;
|
|
||||||
|
|
||||||
return Math.sqrt(dx * dx + dz * dz);
|
|
||||||
}
|
|
||||||
|
|
||||||
private double boxDistanceSq(AABB box, Vec3 point) {
|
|
||||||
double dx = 0.0;
|
|
||||||
if (point.x < box.minX) dx = box.minX - point.x;
|
|
||||||
else if (point.x > box.maxX) dx = point.x - box.maxX;
|
|
||||||
|
|
||||||
double dy = 0.0;
|
|
||||||
if (point.y < box.minY) dy = box.minY - point.y;
|
|
||||||
else if (point.y > box.maxY) dy = point.y - box.maxY;
|
|
||||||
|
|
||||||
double dz = 0.0;
|
|
||||||
if (point.z < box.minZ) dz = box.minZ - point.z;
|
|
||||||
else if (point.z > box.maxZ) dz = point.z - box.maxZ;
|
|
||||||
|
|
||||||
return dx * dx + dy * dy + dz * dz;
|
|
||||||
}
|
|
||||||
|
|
||||||
private double squaredHorizontalDistance(double x1, double z1, double x2, double z2) {
|
|
||||||
double dx = x1 - x2;
|
|
||||||
double dz = z1 - z2;
|
|
||||||
return dx * dx + dz * dz;
|
|
||||||
}
|
|
||||||
|
|
||||||
private void updateSentMovement(ServerboundMovePlayerPacket packet) {
|
|
||||||
LocalPlayer player = mc.player;
|
|
||||||
if (player == null) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
Vec3 current = new Vec3(
|
|
||||||
packet.getX(player.getX()),
|
|
||||||
packet.getY(player.getY()),
|
|
||||||
packet.getZ(player.getZ())
|
|
||||||
);
|
|
||||||
|
|
||||||
if (lastSentPosition == null) {
|
|
||||||
lastSentPosition = current;
|
|
||||||
lastSentHorizontalDelta = 0.0;
|
|
||||||
lastSentVerticalDelta = 0.0;
|
|
||||||
movementStateTicks = 0;
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
double dx = current.x - lastSentPosition.x;
|
|
||||||
double dy = current.y - lastSentPosition.y;
|
|
||||||
double dz = current.z - lastSentPosition.z;
|
|
||||||
lastSentHorizontalDelta = Math.sqrt(dx * dx + dz * dz);
|
|
||||||
lastSentVerticalDelta = Math.abs(dy);
|
|
||||||
lastSentPosition = current;
|
|
||||||
|
|
||||||
if (lastSentHorizontalDelta > 1.0E-4 || lastSentVerticalDelta > 1.0E-4) {
|
|
||||||
movementStateTicks = Mth.clamp(movementStateTicks + 1, 0, 20);
|
|
||||||
} else {
|
|
||||||
movementStateTicks = 0;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private boolean isFastPostHitApproach(LocalPlayer player, Player target) {
|
|
||||||
if (pendingAttackPosition == null) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
Vec3 current = player.position();
|
|
||||||
double moved = Math.sqrt(squaredHorizontalDistance(
|
|
||||||
pendingAttackPosition.x,
|
|
||||||
pendingAttackPosition.z,
|
|
||||||
current.x,
|
|
||||||
current.z
|
|
||||||
));
|
|
||||||
if (moved < FAST_POST_HIT_MOVE_THRESHOLD) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
Vec3 targetCenter = target.getBoundingBox().getCenter();
|
|
||||||
double then = Math.sqrt(squaredHorizontalDistance(
|
|
||||||
pendingAttackPosition.x,
|
|
||||||
pendingAttackPosition.z,
|
|
||||||
targetCenter.x,
|
|
||||||
targetCenter.z
|
|
||||||
));
|
|
||||||
double now = Math.sqrt(squaredHorizontalDistance(
|
|
||||||
current.x,
|
|
||||||
current.z,
|
|
||||||
targetCenter.x,
|
|
||||||
targetCenter.z
|
|
||||||
));
|
|
||||||
|
|
||||||
return now + 0.03 < then;
|
|
||||||
}
|
|
||||||
|
|
||||||
private void addHistorySample(Player player, long now) {
|
|
||||||
ArrayDeque<HistorySample> samples = targetHistory.computeIfAbsent(player.getUUID(), ignored -> new ArrayDeque<>());
|
|
||||||
samples.addLast(new HistorySample(player.position(), now));
|
|
||||||
while (samples.size() > HISTORY_MAX_SAMPLES) {
|
|
||||||
samples.removeFirst();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private void pruneHistory(long now) {
|
|
||||||
Iterator<Map.Entry<UUID, ArrayDeque<HistorySample>>> iterator = targetHistory.entrySet().iterator();
|
|
||||||
while (iterator.hasNext()) {
|
|
||||||
ArrayDeque<HistorySample> samples = iterator.next().getValue();
|
|
||||||
while (!samples.isEmpty() && now - samples.peekFirst().timeMs() > HISTORY_MAX_AGE_MS) {
|
|
||||||
samples.removeFirst();
|
|
||||||
}
|
|
||||||
if (samples.isEmpty()) {
|
|
||||||
iterator.remove();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private double getInteractionRange(LocalPlayer player) {
|
|
||||||
try {
|
|
||||||
return player.getAttributeValue(Attributes.ENTITY_INTERACTION_RANGE);
|
|
||||||
} catch (Throwable ignored) {
|
|
||||||
return 3.0;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private boolean hasKnockback(ItemStack stack) {
|
|
||||||
if (stack == null || stack.isEmpty()) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
Holder<Enchantment> entry = getEnchantmentEntry(Enchantments.KNOCKBACK);
|
|
||||||
return entry != null && EnchantmentHelper.getItemEnchantmentLevel(entry, stack) > 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
private Holder<Enchantment> getEnchantmentEntry(ResourceKey<Enchantment> key) {
|
|
||||||
if (mc.level == null) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
Registry<Enchantment> registry = mc.level.registryAccess().lookupOrThrow(Registries.ENCHANTMENT);
|
|
||||||
Enchantment enchantment = registry.getValue(key);
|
|
||||||
return enchantment != null ? registry.wrapAsHolder(enchantment) : null;
|
|
||||||
}
|
|
||||||
|
|
||||||
private boolean isBoatLike(Entity entity) {
|
|
||||||
return entity != null && entity.getType().toString().toLowerCase().contains("boat");
|
|
||||||
}
|
|
||||||
|
|
||||||
private boolean isAttack(ServerboundInteractPacket packet) {
|
|
||||||
return packet.hand() == null && packet.location() == null;
|
|
||||||
}
|
|
||||||
|
|
||||||
private boolean isAtPreAlert(double buffer, double max) {
|
|
||||||
return buffer + 1.0 > max + BUFFER_EPSILON;
|
|
||||||
}
|
|
||||||
|
|
||||||
private void updateClampLatches() {
|
|
||||||
if (isAtPreAlert(reachABuffer, REACH_A_BUFFER_MAX)) {
|
|
||||||
reachAClampLatched = true;
|
|
||||||
} else if (reachABuffer <= REACH_A_LATCH_RELEASE) {
|
|
||||||
reachAClampLatched = false;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (isAtPreAlert(reachBBuffer, REACH_B_BUFFER_MAX)) {
|
|
||||||
reachBClampLatched = true;
|
|
||||||
} else if (reachBBuffer <= REACH_B_LATCH_RELEASE) {
|
|
||||||
reachBClampLatched = false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private boolean isRecent(long timestamp, long windowMs) {
|
|
||||||
return timestamp != Long.MIN_VALUE && System.currentTimeMillis() - timestamp <= windowMs;
|
|
||||||
}
|
|
||||||
|
|
||||||
private void decayABuffer(double amount) {
|
|
||||||
reachABuffer = Math.max(0.0, reachABuffer - amount);
|
|
||||||
}
|
|
||||||
|
|
||||||
private void decayBBuffer(double amount) {
|
|
||||||
reachBBuffer = Math.max(0.0, reachBBuffer - amount);
|
|
||||||
}
|
|
||||||
|
|
||||||
private double increaseLocalBuffer(double buffer, double max) {
|
|
||||||
return Math.min(max, buffer + 1.0);
|
|
||||||
}
|
|
||||||
|
|
||||||
private int longHitsRemaining(double buffer, double max, boolean latched) {
|
|
||||||
if (latched || isAtPreAlert(buffer, max)) {
|
|
||||||
return 0;
|
|
||||||
}
|
|
||||||
return Math.max(0, (int) Math.floor(max - buffer + BUFFER_EPSILON));
|
|
||||||
}
|
|
||||||
|
|
||||||
private String resolveReason(boolean reachAExempt,
|
|
||||||
boolean reachBExempt,
|
|
||||||
boolean noHistory,
|
|
||||||
boolean recentServer,
|
|
||||||
boolean recentJoinWorld,
|
|
||||||
boolean liquid,
|
|
||||||
boolean knockback) {
|
|
||||||
if (recentServer) return "server_position";
|
|
||||||
if (recentJoinWorld) return "join_world";
|
|
||||||
if (noHistory) return "no_history";
|
|
||||||
if (liquid) return "liquid";
|
|
||||||
if (knockback) return "knockback";
|
|
||||||
if (reachAExempt || reachBExempt) return "exempt";
|
|
||||||
return "normal";
|
|
||||||
}
|
|
||||||
|
|
||||||
public record ReachPlan(
|
|
||||||
double requestedReach,
|
|
||||||
double allowedReach,
|
|
||||||
double reachALimit,
|
|
||||||
double reachBLimit,
|
|
||||||
double reachABuffer,
|
|
||||||
double reachBBuffer,
|
|
||||||
boolean reachAExempt,
|
|
||||||
boolean reachBExempt,
|
|
||||||
boolean noReachAHistory,
|
|
||||||
boolean usesReachABuffer,
|
|
||||||
boolean usesReachBBuffer,
|
|
||||||
String reason
|
|
||||||
) {
|
|
||||||
private static ReachPlan empty(double requestedReach) {
|
|
||||||
return new ReachPlan(requestedReach, requestedReach, requestedReach, requestedReach,
|
|
||||||
0.0, 0.0, true, true, true, false, false, "empty");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public record HudSnapshot(
|
|
||||||
double displayReach,
|
|
||||||
int longHitsRemaining,
|
|
||||||
boolean dirty,
|
|
||||||
String reason
|
|
||||||
) {
|
|
||||||
}
|
|
||||||
|
|
||||||
private record HistorySample(Vec3 pos, long timeMs) {
|
|
||||||
}
|
|
||||||
|
|
||||||
private record CheckState(
|
|
||||||
boolean reachAExempt,
|
|
||||||
boolean reachBExempt,
|
|
||||||
boolean noReachAHistory,
|
|
||||||
double reachAAllowed,
|
|
||||||
double reachBAllowed,
|
|
||||||
double reachAMeasured,
|
|
||||||
double reachBMeasured,
|
|
||||||
boolean fastPostHitApproach,
|
|
||||||
String reason
|
|
||||||
) {
|
|
||||||
private static CheckState exempt(String reason) {
|
|
||||||
return new CheckState(true, true, true, 0.0, 0.0, -1.0, -1.0, false, reason);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -588,6 +588,9 @@
|
||||||
"setting.targetesp.mode.option.ring": "Ring",
|
"setting.targetesp.mode.option.ring": "Ring",
|
||||||
"setting.targetesp.mode.option.capture_mark": "Capture mark",
|
"setting.targetesp.mode.option.capture_mark": "Capture mark",
|
||||||
"setting.targetesp.mode.option.crystals": "Crystals",
|
"setting.targetesp.mode.option.crystals": "Crystals",
|
||||||
|
"setting.targetesp.mode.option.blackhole": "Blackhole",
|
||||||
|
"setting.targetesp.mode.option.vortex": "Vortex",
|
||||||
|
"setting.targetesp.vortex_speed": "Vortex speed",
|
||||||
"setting.targetesp.color": "Color",
|
"setting.targetesp.color": "Color",
|
||||||
"setting.targetesp.color_mode": "Color mode",
|
"setting.targetesp.color_mode": "Color mode",
|
||||||
"setting.targetesp.color_mode.option.Static": "Static",
|
"setting.targetesp.color_mode.option.Static": "Static",
|
||||||
|
|
@ -2820,6 +2823,8 @@
|
||||||
"setting.common.shulker_bg_radius": "Background radius",
|
"setting.common.shulker_bg_radius": "Background radius",
|
||||||
"setting.common.shulker_bg_softness": "Background softness",
|
"setting.common.shulker_bg_softness": "Background softness",
|
||||||
"setting.common.shulker_padding": "Padding",
|
"setting.common.shulker_padding": "Padding",
|
||||||
|
"setting.common.tooltip_font": "Tooltip font",
|
||||||
|
"setting.common.tooltip_text_size": "Tooltip text size",
|
||||||
"setting.common.shulker_preview": "Shulker Preview",
|
"setting.common.shulker_preview": "Shulker Preview",
|
||||||
"setting.common.shulker_preview_hold": "Preview hold",
|
"setting.common.shulker_preview_hold": "Preview hold",
|
||||||
"setting.common.shulker_slot_bg_color": "Slot background color",
|
"setting.common.shulker_slot_bg_color": "Slot background color",
|
||||||
|
|
|
||||||
|
|
@ -523,6 +523,9 @@
|
||||||
"setting.targetesp.mode.option.ring": "Кольцо",
|
"setting.targetesp.mode.option.ring": "Кольцо",
|
||||||
"setting.targetesp.mode.option.capture_mark": "Метка захвата",
|
"setting.targetesp.mode.option.capture_mark": "Метка захвата",
|
||||||
"setting.targetesp.mode.option.crystals": "Кристаллы",
|
"setting.targetesp.mode.option.crystals": "Кристаллы",
|
||||||
|
"setting.targetesp.mode.option.blackhole": "Чёрная дыра",
|
||||||
|
"setting.targetesp.mode.option.vortex": "Вихрь",
|
||||||
|
"setting.targetesp.vortex_speed": "Скорость вихря",
|
||||||
"setting.targetesp.color": "Цвет",
|
"setting.targetesp.color": "Цвет",
|
||||||
"setting.targetesp.color_mode": "Режим цвета",
|
"setting.targetesp.color_mode": "Режим цвета",
|
||||||
"setting.targetesp.color_mode.option.Static": "Статичный",
|
"setting.targetesp.color_mode.option.Static": "Статичный",
|
||||||
|
|
@ -2619,6 +2622,8 @@
|
||||||
"setting.common.shulker_bg_radius": "Радиус фона",
|
"setting.common.shulker_bg_radius": "Радиус фона",
|
||||||
"setting.common.shulker_bg_softness": "Мягкость фона",
|
"setting.common.shulker_bg_softness": "Мягкость фона",
|
||||||
"setting.common.shulker_padding": "Отступ",
|
"setting.common.shulker_padding": "Отступ",
|
||||||
|
"setting.common.tooltip_font": "Шрифт тултипа",
|
||||||
|
"setting.common.tooltip_text_size": "Размер текста тултипа",
|
||||||
"setting.common.shulker_preview": "Превью шалкера",
|
"setting.common.shulker_preview": "Превью шалкера",
|
||||||
"setting.common.shulker_preview_hold": "Удержание превью",
|
"setting.common.shulker_preview_hold": "Удержание превью",
|
||||||
"setting.common.shulker_slot_bg_color": "Цвет фона слота",
|
"setting.common.shulker_slot_bg_color": "Цвет фона слота",
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,91 @@
|
||||||
|
#version 330 core
|
||||||
|
|
||||||
|
/*
|
||||||
|
* This file is part of the LoVisual Client distribution.
|
||||||
|
* Copyright (c) 2026 pivosos2007.
|
||||||
|
*
|
||||||
|
* Portions of this file are based on, adapted from, or implemented
|
||||||
|
* with reference to a "SkyShader" module distributed for another
|
||||||
|
* Minecraft client. The original attribution was not preserved.
|
||||||
|
* This implementation has since been substantially modified for LoVisual.
|
||||||
|
*
|
||||||
|
* Licensed under the GNU General Public License v3.0.
|
||||||
|
*/
|
||||||
|
|
||||||
|
out vec4 fragColor;
|
||||||
|
|
||||||
|
layout (std140) uniform SkyShader {
|
||||||
|
vec4 u_Params0; // x = time, y = speed, z = scale, w = intensity
|
||||||
|
vec4 u_Params1; // x = resWidth, y = resHeight, z = alpha, w = fov
|
||||||
|
vec4 u_Color; // rgb = base color
|
||||||
|
vec4 u_CameraDir; // x = -yawRad, y = pitchRad
|
||||||
|
};
|
||||||
|
|
||||||
|
in vec2 v_TexCoord;
|
||||||
|
|
||||||
|
#define MAX_ITER 4
|
||||||
|
|
||||||
|
mat3 rotX(float a) {
|
||||||
|
float c = cos(a), s = sin(a);
|
||||||
|
return mat3(1.0, 0.0, 0.0,
|
||||||
|
0.0, c, s,
|
||||||
|
0.0, -s, c);
|
||||||
|
}
|
||||||
|
mat3 rotY(float a) {
|
||||||
|
float c = cos(a), s = sin(a);
|
||||||
|
return mat3( c, 0.0, s,
|
||||||
|
0.0, 1.0, 0.0,
|
||||||
|
-s, 0.0, c);
|
||||||
|
}
|
||||||
|
|
||||||
|
void main() {
|
||||||
|
float time = u_Params0.x;
|
||||||
|
float speed = u_Params0.y;
|
||||||
|
float scale = u_Params0.z;
|
||||||
|
float intensity = u_Params0.w;
|
||||||
|
|
||||||
|
vec2 res = u_Params1.xy;
|
||||||
|
float alpha = u_Params1.z;
|
||||||
|
float fov = u_Params1.w;
|
||||||
|
|
||||||
|
vec3 baseColor = u_Color.rgb;
|
||||||
|
|
||||||
|
vec2 sp = v_TexCoord * 2.0 - 1.0;
|
||||||
|
float aspect = res.x / max(res.y, 1.0);
|
||||||
|
|
||||||
|
float tanV = tan(radians(fov) * 0.5);
|
||||||
|
vec3 rayV = normalize(vec3(sp.x * tanV * aspect, sp.y * tanV, 1.0));
|
||||||
|
vec3 rayW = rotY(u_CameraDir.x) * rotX(u_CameraDir.y) * rayV;
|
||||||
|
|
||||||
|
vec3 p = rayW * scale;
|
||||||
|
vec3 i = p;
|
||||||
|
float c = 1.0;
|
||||||
|
float inten = intensity;
|
||||||
|
|
||||||
|
for (int n = 0; n < MAX_ITER; n++) {
|
||||||
|
float t = time * speed * (1.0 - (3.0 / float(n + 1)));
|
||||||
|
i = p + vec3(
|
||||||
|
cos(t - i.x) + sin(t + i.y),
|
||||||
|
sin(t - i.y) + cos(t + i.z),
|
||||||
|
cos(t - i.z) + sin(t + i.x)
|
||||||
|
);
|
||||||
|
c += 1.0 / length(vec3(
|
||||||
|
p.x / (sin(i.x + t) / inten),
|
||||||
|
p.y / (cos(i.y + t) / inten),
|
||||||
|
p.z / (sin(i.z + t) / inten)
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
c /= float(MAX_ITER);
|
||||||
|
c = 1.5 - sqrt(c);
|
||||||
|
float brightness = c * c * c * c;
|
||||||
|
|
||||||
|
// Sharper, more crystalline highlights than the original for a proper caustic look.
|
||||||
|
float sharp = pow(brightness, 1.4);
|
||||||
|
vec3 color = baseColor * sharp * 1.8 + baseColor * 0.15;
|
||||||
|
|
||||||
|
float vignette = smoothstep(1.4, 0.15, length(sp));
|
||||||
|
color *= mix(0.7, 1.0, vignette);
|
||||||
|
|
||||||
|
fragColor = vec4(clamp(color, 0.0, 1.0), alpha);
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,94 @@
|
||||||
|
#version 330 core
|
||||||
|
|
||||||
|
/*
|
||||||
|
* This file is part of the LoVisual Client distribution.
|
||||||
|
* Copyright (c) 2026 pivosos2007.
|
||||||
|
*
|
||||||
|
* Portions of this file are based on, adapted from, or implemented
|
||||||
|
* with reference to a "SkyShader" module distributed for another
|
||||||
|
* Minecraft client. The original attribution was not preserved.
|
||||||
|
* This implementation has since been substantially modified for LoVisual.
|
||||||
|
*
|
||||||
|
* Licensed under the GNU General Public License v3.0.
|
||||||
|
*/
|
||||||
|
|
||||||
|
out vec4 fragColor;
|
||||||
|
|
||||||
|
layout (std140) uniform SkyShader {
|
||||||
|
vec4 u_Params0; // x = time, y = speed, z = scale, w = intensity
|
||||||
|
vec4 u_Params1; // x = resWidth, y = resHeight, z = alpha, w = fov
|
||||||
|
vec4 u_Color; // rgb = base color
|
||||||
|
vec4 u_CameraDir; // x = -yawRad, y = pitchRad
|
||||||
|
};
|
||||||
|
|
||||||
|
in vec2 v_TexCoord;
|
||||||
|
|
||||||
|
mat3 rotX(float a) {
|
||||||
|
float c = cos(a), s = sin(a);
|
||||||
|
return mat3(1.0, 0.0, 0.0,
|
||||||
|
0.0, c, s,
|
||||||
|
0.0, -s, c);
|
||||||
|
}
|
||||||
|
mat3 rotY(float a) {
|
||||||
|
float c = cos(a), s = sin(a);
|
||||||
|
return mat3( c, 0.0, s,
|
||||||
|
0.0, 1.0, 0.0,
|
||||||
|
-s, 0.0, c);
|
||||||
|
}
|
||||||
|
|
||||||
|
float wave(vec3 dir, float phase, float scale, float speed, float time) {
|
||||||
|
return sin(dir.x * scale + time * speed + phase)
|
||||||
|
* sin(dir.z * scale * 0.7 - time * speed * 0.8 + phase * 1.7)
|
||||||
|
* sin(dir.y * scale * 1.3 + time * speed * 0.5 + phase * 2.3);
|
||||||
|
}
|
||||||
|
|
||||||
|
void main() {
|
||||||
|
float time = u_Params0.x;
|
||||||
|
float speed = u_Params0.y;
|
||||||
|
float scale = u_Params0.z;
|
||||||
|
float intensity = u_Params0.w;
|
||||||
|
|
||||||
|
vec2 res = u_Params1.xy;
|
||||||
|
float alpha = u_Params1.z;
|
||||||
|
float fov = u_Params1.w;
|
||||||
|
|
||||||
|
vec3 baseColor = u_Color.rgb;
|
||||||
|
|
||||||
|
vec2 sp = v_TexCoord * 2.0 - 1.0;
|
||||||
|
float aspect = res.x / max(res.y, 1.0);
|
||||||
|
|
||||||
|
float tanV = tan(radians(fov) * 0.5);
|
||||||
|
vec3 rayV = normalize(vec3(sp.x * tanV * aspect, sp.y * tanV, 1.0));
|
||||||
|
vec3 rayW = rotY(u_CameraDir.x) * rotX(u_CameraDir.y) * rayV;
|
||||||
|
|
||||||
|
float h = clamp(rayW.y, -1.0, 1.0);
|
||||||
|
|
||||||
|
// Vertical atmosphere gradient with a warmer, higher-contrast horizon glow.
|
||||||
|
vec3 zenith = baseColor * 0.12;
|
||||||
|
vec3 horizon = baseColor * 0.95 + vec3(0.05, 0.03, 0.01);
|
||||||
|
vec3 base = mix(horizon, zenith, pow(clamp(h, 0.0, 1.0), 0.55));
|
||||||
|
base += baseColor * 0.4 * exp(-abs(h) * 4.5);
|
||||||
|
|
||||||
|
// Flowing aurora bands, seamless on the full sphere (world-space direction).
|
||||||
|
float bands = 0.0;
|
||||||
|
bands += wave(rayW, 0.0, scale, speed, time);
|
||||||
|
bands += wave(rayW * 1.9 + 13.7, 2.1, scale, speed, time) * 0.55;
|
||||||
|
bands += wave(rayW * 3.7 + 41.3, 4.7, scale, speed, time) * 0.3;
|
||||||
|
bands *= intensity * 40.0;
|
||||||
|
|
||||||
|
// Fade aurora towards the horizon so it hugs the upper sky, softened edge.
|
||||||
|
float auroraMask = smoothstep(-0.1, 0.5, h);
|
||||||
|
vec3 aurora = baseColor * bands * auroraMask;
|
||||||
|
|
||||||
|
// Subtle star shimmer near the zenith, gently pulsing.
|
||||||
|
float star = pow(max(sin(rayW.x * 220.0) * sin(rayW.y * 260.0) * sin(rayW.z * 240.0), 0.0), 26.0);
|
||||||
|
star *= smoothstep(0.2, 0.85, h) * (0.55 + 0.45 * sin(time * speed * 2.4));
|
||||||
|
|
||||||
|
vec3 color = base + aurora + vec3(star) * (0.6 + 0.4 * baseColor);
|
||||||
|
|
||||||
|
// Gentle vignette so the effect reads as sky rather than a flat overlay.
|
||||||
|
float vignette = smoothstep(1.35, 0.2, length(sp));
|
||||||
|
color *= mix(0.82, 1.0, vignette);
|
||||||
|
|
||||||
|
fragColor = vec4(clamp(color, 0.0, 1.0), alpha);
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,91 @@
|
||||||
|
#version 330 core
|
||||||
|
|
||||||
|
/*
|
||||||
|
* This file is part of the LoVisual Client distribution.
|
||||||
|
* Copyright (c) 2026 pivosos2007.
|
||||||
|
*
|
||||||
|
* Portions of this file are based on, adapted from, or implemented
|
||||||
|
* with reference to a "SkyShader" module distributed for another
|
||||||
|
* Minecraft client. The original attribution was not preserved.
|
||||||
|
* This implementation has since been substantially modified for LoVisual.
|
||||||
|
*
|
||||||
|
* Licensed under the GNU General Public License v3.0.
|
||||||
|
*/
|
||||||
|
|
||||||
|
out vec4 fragColor;
|
||||||
|
|
||||||
|
layout (std140) uniform SkyShader {
|
||||||
|
vec4 u_Params0; // x = time, y = speed, z = scale, w = intensity
|
||||||
|
vec4 u_Params1; // x = resWidth, y = resHeight, z = alpha, w = fov
|
||||||
|
vec4 u_Color; // rgb = base color
|
||||||
|
vec4 u_CameraDir; // x = -yawRad, y = pitchRad
|
||||||
|
};
|
||||||
|
|
||||||
|
in vec2 v_TexCoord;
|
||||||
|
|
||||||
|
#define MAX_ITER 4
|
||||||
|
|
||||||
|
mat3 rotX(float a) {
|
||||||
|
float c = cos(a), s = sin(a);
|
||||||
|
return mat3(1.0, 0.0, 0.0,
|
||||||
|
0.0, c, s,
|
||||||
|
0.0, -s, c);
|
||||||
|
}
|
||||||
|
mat3 rotY(float a) {
|
||||||
|
float c = cos(a), s = sin(a);
|
||||||
|
return mat3( c, 0.0, s,
|
||||||
|
0.0, 1.0, 0.0,
|
||||||
|
-s, 0.0, c);
|
||||||
|
}
|
||||||
|
|
||||||
|
void main() {
|
||||||
|
float time = u_Params0.x;
|
||||||
|
float speed = u_Params0.y;
|
||||||
|
float scale = u_Params0.z;
|
||||||
|
float intensity = u_Params0.w;
|
||||||
|
|
||||||
|
vec2 res = u_Params1.xy;
|
||||||
|
float alpha = u_Params1.z;
|
||||||
|
float fov = u_Params1.w;
|
||||||
|
|
||||||
|
vec3 baseColor = u_Color.rgb;
|
||||||
|
|
||||||
|
vec2 sp = v_TexCoord * 2.0 - 1.0;
|
||||||
|
float aspect = res.x / max(res.y, 1.0);
|
||||||
|
|
||||||
|
float tanV = tan(radians(fov) * 0.5);
|
||||||
|
vec3 rayV = normalize(vec3(sp.x * tanV * aspect, sp.y * tanV, 1.0));
|
||||||
|
vec3 rayW = rotY(u_CameraDir.x) * rotX(u_CameraDir.y) * rayV;
|
||||||
|
|
||||||
|
// 3D noise from the world ray direction - seamless on the full sphere.
|
||||||
|
vec3 p = rayW * scale;
|
||||||
|
vec3 i = p;
|
||||||
|
float c = 1.0;
|
||||||
|
float inten = intensity;
|
||||||
|
|
||||||
|
for (int n = 0; n < MAX_ITER; n++) {
|
||||||
|
float t = time * speed * (11.0 - (3.0 / float(n + 1)));
|
||||||
|
i = p + vec3(
|
||||||
|
cos(t - i.x) + sin(t + i.y),
|
||||||
|
sin(t - i.y) + cos(t + i.z),
|
||||||
|
cos(t - i.z) + sin(t + i.x)
|
||||||
|
);
|
||||||
|
c += 1.0 / length(vec3(
|
||||||
|
p.x / (sin(i.x + t) / inten),
|
||||||
|
p.y / (cos(i.y + t) / inten),
|
||||||
|
p.z / (sin(i.z + t) / inten)
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
c /= float(MAX_ITER);
|
||||||
|
c = 1.5 - sqrt(c);
|
||||||
|
float brightness = c * c * c * c;
|
||||||
|
|
||||||
|
// Softer highlight rolloff and a deeper base tone for a more "liquid" feel.
|
||||||
|
vec3 color = baseColor * pow(brightness, 0.85) * 1.15 + baseColor * 0.18;
|
||||||
|
|
||||||
|
float vignette = smoothstep(1.4, 0.15, length(sp));
|
||||||
|
color *= mix(0.75, 1.0, vignette);
|
||||||
|
|
||||||
|
fragColor = vec4(clamp(color, 0.0, 1.0), alpha);
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,174 @@
|
||||||
|
#version 330 core
|
||||||
|
|
||||||
|
/*
|
||||||
|
* This file is part of the LoVisual Client distribution.
|
||||||
|
* Copyright (c) 2026 pivosos2007.
|
||||||
|
*
|
||||||
|
* Licensed under the GNU General Public License v3.0.
|
||||||
|
*/
|
||||||
|
|
||||||
|
out vec4 color;
|
||||||
|
|
||||||
|
uniform sampler2D u_Texture;
|
||||||
|
uniform sampler2D u_MainDepth;
|
||||||
|
|
||||||
|
layout (std140) uniform WetSurface {
|
||||||
|
mat4 u_Projection;
|
||||||
|
mat4 u_InvView; // camera-space -> world-space rotation (camera-to-world)
|
||||||
|
vec4 u_CameraPos; // world-space camera position, w unused
|
||||||
|
vec4 u_Screen; // invW, invH, time, mainDepthEnabled
|
||||||
|
vec4 u_Params; // wetness, puddleCoverage, reflectionStrength, rippleStrength
|
||||||
|
vec4 u_Params2; // maxDistance, reflectionSteps, rainAmount, ripplesEnabled
|
||||||
|
};
|
||||||
|
|
||||||
|
in vec2 v_TexCoord;
|
||||||
|
|
||||||
|
const float DEPTH_NEAR_EPS = 0.000001;
|
||||||
|
const float DEPTH_FAR_EPS = 0.999999;
|
||||||
|
|
||||||
|
bool validRawDepth(float d) {
|
||||||
|
return d == d && d > DEPTH_NEAR_EPS && d < DEPTH_FAR_EPS;
|
||||||
|
}
|
||||||
|
|
||||||
|
vec3 viewPositionFromDepth(vec2 uv, float rawDepth) {
|
||||||
|
vec4 clip = vec4(uv * 2.0 - 1.0, rawDepth, 1.0);
|
||||||
|
vec4 view = inverse(u_Projection) * clip;
|
||||||
|
return view.xyz / view.w;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool projectToScreen(vec3 viewPos, out vec2 uv, out float rawDepth) {
|
||||||
|
vec4 clip = u_Projection * vec4(viewPos, 1.0);
|
||||||
|
if (clip.w <= 0.0001) return false;
|
||||||
|
vec3 ndc = clip.xyz / clip.w;
|
||||||
|
uv = ndc.xy * 0.5 + 0.5;
|
||||||
|
rawDepth = ndc.z;
|
||||||
|
return uv.x >= 0.0 && uv.x <= 1.0 && uv.y >= 0.0 && uv.y <= 1.0;
|
||||||
|
}
|
||||||
|
|
||||||
|
float hash(vec2 p) {
|
||||||
|
return fract(sin(dot(p, vec2(127.1, 311.7))) * 43758.5453123);
|
||||||
|
}
|
||||||
|
|
||||||
|
float valueNoise(vec2 p) {
|
||||||
|
vec2 i = floor(p);
|
||||||
|
vec2 f = fract(p);
|
||||||
|
float a = hash(i);
|
||||||
|
float b = hash(i + vec2(1.0, 0.0));
|
||||||
|
float c = hash(i + vec2(0.0, 1.0));
|
||||||
|
float d = hash(i + vec2(1.0, 1.0));
|
||||||
|
vec2 u = f * f * (3.0 - 2.0 * f);
|
||||||
|
return mix(mix(a, b, u.x), mix(c, d, u.x), u.y);
|
||||||
|
}
|
||||||
|
|
||||||
|
float puddleNoise(vec2 worldXZ) {
|
||||||
|
float n = valueNoise(worldXZ * 0.18) * 0.65
|
||||||
|
+ valueNoise(worldXZ * 0.45 + 17.0) * 0.35;
|
||||||
|
return n;
|
||||||
|
}
|
||||||
|
|
||||||
|
void main() {
|
||||||
|
vec2 uv = v_TexCoord;
|
||||||
|
vec4 base = texture(u_Texture, uv);
|
||||||
|
|
||||||
|
if (u_Screen.w < 0.5) {
|
||||||
|
color = base;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
float rawDepth = texture(u_MainDepth, uv).r;
|
||||||
|
if (!validRawDepth(rawDepth)) {
|
||||||
|
color = base;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
vec3 viewPos = viewPositionFromDepth(uv, rawDepth);
|
||||||
|
|
||||||
|
vec3 ddxPos = dFdx(viewPos);
|
||||||
|
vec3 ddyPos = dFdy(viewPos);
|
||||||
|
vec3 viewNormal = normalize(cross(ddxPos, ddyPos));
|
||||||
|
vec3 viewDir = normalize(viewPos);
|
||||||
|
if (dot(viewNormal, -viewDir) < 0.0) {
|
||||||
|
viewNormal = -viewNormal;
|
||||||
|
}
|
||||||
|
|
||||||
|
vec3 worldNormal = normalize(mat3(u_InvView) * viewNormal);
|
||||||
|
float groundFactor = clamp(dot(worldNormal, vec3(0.0, 1.0, 0.0)), 0.0, 1.0);
|
||||||
|
groundFactor = smoothstep(0.82, 0.98, groundFactor);
|
||||||
|
if (groundFactor <= 0.001) {
|
||||||
|
color = base;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
vec3 worldPos = u_CameraPos.xyz + (mat3(u_InvView) * viewPos);
|
||||||
|
|
||||||
|
float wetness = clamp(u_Params.x, 0.0, 2.0);
|
||||||
|
float puddleCoverage = clamp(u_Params.y, 0.0, 1.0);
|
||||||
|
float reflectionStrength = clamp(u_Params.z, 0.0, 2.0);
|
||||||
|
float rippleStrength = clamp(u_Params.w, 0.0, 2.0);
|
||||||
|
float maxDistance = max(u_Params2.x, 1.0);
|
||||||
|
int steps = int(clamp(floor(u_Params2.y + 0.5), 1.0, 32.0));
|
||||||
|
float rainAmount = clamp(u_Params2.z, 0.0, 2.0);
|
||||||
|
bool ripplesEnabled = u_Params2.w > 0.5;
|
||||||
|
|
||||||
|
float noise = puddleNoise(worldPos.xz);
|
||||||
|
float wetMask = smoothstep(0.5 - puddleCoverage * 0.55, 0.5 + puddleCoverage * 0.35, noise);
|
||||||
|
wetMask *= groundFactor;
|
||||||
|
if (wetMask <= 0.003) {
|
||||||
|
color = base;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
vec3 reflNormal = viewNormal;
|
||||||
|
if (ripplesEnabled && rippleStrength > 0.001) {
|
||||||
|
float t = u_Screen.z;
|
||||||
|
float ripple = sin((worldPos.x + worldPos.z) * 3.1 + t * 2.4) * 0.5
|
||||||
|
+ sin((worldPos.x - worldPos.z * 1.7) * 4.3 - t * 3.1) * 0.5;
|
||||||
|
vec2 perturb = vec2(dFdx(ripple), dFdy(ripple)) * 0.0;
|
||||||
|
reflNormal = normalize(viewNormal + vec3(ripple, 0.0, ripple * 0.6) * rippleStrength * 0.05);
|
||||||
|
}
|
||||||
|
|
||||||
|
vec3 reflDir = reflect(viewDir, reflNormal);
|
||||||
|
|
||||||
|
vec3 reflectionColor = mix(vec3(0.45, 0.52, 0.62), vec3(0.75, 0.82, 0.92), clamp(reflDir.y * 0.5 + 0.5, 0.0, 1.0));
|
||||||
|
bool hit = false;
|
||||||
|
|
||||||
|
float stepLen = maxDistance / float(steps);
|
||||||
|
vec3 marchPos = viewPos + reflNormal * 0.02;
|
||||||
|
for (int i = 0; i < 32; i++) {
|
||||||
|
if (i >= steps) break;
|
||||||
|
|
||||||
|
marchPos += reflDir * stepLen;
|
||||||
|
|
||||||
|
vec2 sampleUv;
|
||||||
|
float sampleClipDepth;
|
||||||
|
if (!projectToScreen(marchPos, sampleUv, sampleClipDepth)) break;
|
||||||
|
|
||||||
|
float sceneRawDepth = texture(u_MainDepth, sampleUv).r;
|
||||||
|
if (!validRawDepth(sceneRawDepth)) continue;
|
||||||
|
|
||||||
|
vec3 scenePos = viewPositionFromDepth(sampleUv, sceneRawDepth);
|
||||||
|
|
||||||
|
float marchDist = length(marchPos);
|
||||||
|
float sceneDist = length(scenePos);
|
||||||
|
float thickness = max(0.15, marchDist * 0.03);
|
||||||
|
|
||||||
|
if (marchDist > sceneDist && marchDist - sceneDist < thickness) {
|
||||||
|
reflectionColor = texture(u_Texture, sampleUv).rgb;
|
||||||
|
hit = true;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
float fresnel = pow(1.0 - clamp(dot(-viewDir, viewNormal), 0.0, 1.0), 3.0);
|
||||||
|
fresnel = mix(0.25, 1.0, fresnel);
|
||||||
|
|
||||||
|
float hitBoost = hit ? 1.0 : 0.55;
|
||||||
|
float reflectionAmount = wetMask * reflectionStrength * fresnel * hitBoost
|
||||||
|
* mix(0.6, 1.0, clamp(rainAmount, 0.0, 1.0)) * wetness;
|
||||||
|
reflectionAmount = clamp(reflectionAmount, 0.0, 1.0);
|
||||||
|
|
||||||
|
vec3 darkened = base.rgb * mix(1.0, 0.82, wetMask * clamp(wetness, 0.0, 1.0));
|
||||||
|
vec3 result = mix(darkened, reflectionColor, reflectionAmount);
|
||||||
|
|
||||||
|
color = vec4(clamp(result, 0.0, 1.0), 1.0);
|
||||||
|
}
|
||||||
|
After Width: | Height: | Size: 55 KiB |
|
After Width: | Height: | Size: 70 KiB |
|
After Width: | Height: | Size: 45 KiB |
|
After Width: | Height: | Size: 43 KiB |
|
After Width: | Height: | Size: 45 KiB |
|
After Width: | Height: | Size: 45 KiB |
|
After Width: | Height: | Size: 45 KiB |
|
|
@ -177,8 +177,7 @@
|
||||||
"sodium.SodiumVertexConsumerTrackerMixin",
|
"sodium.SodiumVertexConsumerTrackerMixin",
|
||||||
"sodium.SodiumWorldRendererFreecamMixin",
|
"sodium.SodiumWorldRendererFreecamMixin",
|
||||||
"xaero.minimap.AbstractWaypointRenderProviderMixin",
|
"xaero.minimap.AbstractWaypointRenderProviderMixin",
|
||||||
"xaero.worldmap.WaypointRenderProviderMixin",
|
"xaero.worldmap.WaypointRenderProviderMixin"
|
||||||
"xaero.worldmap.WorldMapWaypointAccessor"
|
|
||||||
],
|
],
|
||||||
"injectors": {
|
"injectors": {
|
||||||
"defaultRequire": 1
|
"defaultRequire": 1
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,57 @@
|
||||||
|
package dev.loki.lovisual.features.module.modules.visuals.lyrics;
|
||||||
|
|
||||||
|
import org.junit.jupiter.api.Test;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
import static org.junit.jupiter.api.Assertions.*;
|
||||||
|
|
||||||
|
final class LrcLibClientTest {
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void cleanTrackNameRemovesParenthetical() {
|
||||||
|
assertEquals("Song Title", LrcLibClient.cleanTrackName("Song Title (feat. Guest)"));
|
||||||
|
assertEquals("Song Title", LrcLibClient.cleanTrackName("Song Title (Remix)"));
|
||||||
|
assertEquals("", LrcLibClient.cleanTrackName("(Song)"));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void cleanTrackNameRemovesBrackets() {
|
||||||
|
assertEquals("Song Title", LrcLibClient.cleanTrackName("Song Title [Deluxe]"));
|
||||||
|
assertEquals("", LrcLibClient.cleanTrackName("[Song]"));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void cleanTrackNameHandlesNull() {
|
||||||
|
assertEquals("", LrcLibClient.cleanTrackName(null));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void cleanTrackNameHandlesEmpty() {
|
||||||
|
assertEquals("", LrcLibClient.cleanTrackName(""));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void cleanTrackNameHandlesWhitespaceOnly() {
|
||||||
|
assertEquals("", LrcLibClient.cleanTrackName(" "));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void cleanTrackNameTrims() {
|
||||||
|
assertEquals("Song Title", LrcLibClient.cleanTrackName(" Song Title "));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void fetchLyricsAsyncReturnsEmptyForBlankTrack() {
|
||||||
|
var future = LrcLibClient.fetchLyricsAsync("", "Artist", false);
|
||||||
|
List<LyricLine> result = future.join();
|
||||||
|
assertTrue(result.isEmpty());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void fetchLyricsAsyncReturnsEmptyForNullTrack() {
|
||||||
|
var future = LrcLibClient.fetchLyricsAsync(null, "Artist", false);
|
||||||
|
List<LyricLine> result = future.join();
|
||||||
|
assertTrue(result.isEmpty());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,136 @@
|
||||||
|
package dev.loki.lovisual.features.module.modules.visuals.lyrics;
|
||||||
|
|
||||||
|
import org.junit.jupiter.api.Test;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
import static org.junit.jupiter.api.Assertions.*;
|
||||||
|
|
||||||
|
final class LrcParserTest {
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void parsesStandardLrcFormat() {
|
||||||
|
String lrc = "[00:12.00]Hello world\n[00:15.00]Second line";
|
||||||
|
List<LyricLine> lines = LrcParser.parse(lrc);
|
||||||
|
|
||||||
|
assertEquals(2, lines.size());
|
||||||
|
assertEquals(12000L, lines.get(0).timestampMs());
|
||||||
|
assertEquals("Hello world", lines.get(0).text());
|
||||||
|
assertEquals(15000L, lines.get(1).timestampMs());
|
||||||
|
assertEquals("Second line", lines.get(1).text());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void parsesSingleDigitMinutes() {
|
||||||
|
String lrc = "[1:05.20]Test";
|
||||||
|
List<LyricLine> lines = LrcParser.parse(lrc);
|
||||||
|
|
||||||
|
assertEquals(1, lines.size());
|
||||||
|
assertEquals(65200L, lines.get(0).timestampMs());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void parsesWithoutFractionalSeconds() {
|
||||||
|
String lrc = "[01:30]No fraction";
|
||||||
|
List<LyricLine> lines = LrcParser.parse(lrc);
|
||||||
|
|
||||||
|
assertEquals(1, lines.size());
|
||||||
|
assertEquals(90000L, lines.get(0).timestampMs());
|
||||||
|
assertEquals("No fraction", lines.get(0).text());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void parsesOneDigitCentisecond() {
|
||||||
|
String lrc = "[00:05.1]Test";
|
||||||
|
List<LyricLine> lines = LrcParser.parse(lrc);
|
||||||
|
|
||||||
|
assertEquals(1, lines.size());
|
||||||
|
assertEquals(5100L, lines.get(0).timestampMs());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void parsesTwoDigitCentisecond() {
|
||||||
|
String lrc = "[00:05.12]Test";
|
||||||
|
List<LyricLine> lines = LrcParser.parse(lrc);
|
||||||
|
|
||||||
|
assertEquals(1, lines.size());
|
||||||
|
assertEquals(5120L, lines.get(0).timestampMs());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void parsesThreeDigitCentisecond() {
|
||||||
|
String lrc = "[00:05.123]Test";
|
||||||
|
List<LyricLine> lines = LrcParser.parse(lrc);
|
||||||
|
|
||||||
|
assertEquals(1, lines.size());
|
||||||
|
assertEquals(5123L, lines.get(0).timestampMs());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void ignoresLinesWithoutTimestamp() {
|
||||||
|
String lrc = "Just a plain line\n[00:01.00]Has timestamp";
|
||||||
|
List<LyricLine> lines = LrcParser.parse(lrc);
|
||||||
|
|
||||||
|
assertEquals(1, lines.size());
|
||||||
|
assertEquals("Has timestamp", lines.get(0).text());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void ignoresEmptyLines() {
|
||||||
|
String lrc = "\n\n[00:01.00]Line\n\n";
|
||||||
|
List<LyricLine> lines = LrcParser.parse(lrc);
|
||||||
|
|
||||||
|
assertEquals(1, lines.size());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void returnsEmptyForNullInput() {
|
||||||
|
assertTrue(LrcParser.parse(null).isEmpty());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void returnsEmptyForBlankInput() {
|
||||||
|
assertTrue(LrcParser.parse("").isEmpty());
|
||||||
|
assertTrue(LrcParser.parse(" ").isEmpty());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void returnsEmptyWhenNoSyncedLyrics() {
|
||||||
|
String lrc = "Some text without timestamp format";
|
||||||
|
List<LyricLine> lines = LrcParser.parse(lrc);
|
||||||
|
|
||||||
|
assertTrue(lines.isEmpty());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void sortsLinesByTimestamp() {
|
||||||
|
String lrc = "[00:30.00]Third\n[00:05.00]First\n[00:15.00]Second";
|
||||||
|
List<LyricLine> lines = LrcParser.parse(lrc);
|
||||||
|
|
||||||
|
assertEquals(3, lines.size());
|
||||||
|
assertEquals("First", lines.get(0).text());
|
||||||
|
assertEquals("Second", lines.get(1).text());
|
||||||
|
assertEquals("Third", lines.get(2).text());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void stripsWhitespaceFromText() {
|
||||||
|
String lrc = "[00:01.00] Trim me ";
|
||||||
|
List<LyricLine> lines = LrcParser.parse(lrc);
|
||||||
|
|
||||||
|
assertEquals(1, lines.size());
|
||||||
|
assertEquals("Trim me", lines.get(0).text());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void handlesMultipleLinesWithMixedFormats() {
|
||||||
|
String lrc = "[0:15.5]Short\n[01:30]No fraction\n[00:05.12]Two digits\n[00:05.123]Three digits";
|
||||||
|
List<LyricLine> lines = LrcParser.parse(lrc);
|
||||||
|
|
||||||
|
assertEquals(4, lines.size());
|
||||||
|
assertEquals(5120L, lines.get(0).timestampMs());
|
||||||
|
assertEquals(5123L, lines.get(1).timestampMs());
|
||||||
|
assertEquals(15500L, lines.get(2).timestampMs());
|
||||||
|
assertEquals(90000L, lines.get(3).timestampMs());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,99 @@
|
||||||
|
package dev.loki.lovisual.features.module.modules.visuals.lyrics;
|
||||||
|
|
||||||
|
import org.junit.jupiter.api.Test;
|
||||||
|
|
||||||
|
import java.util.Arrays;
|
||||||
|
import java.util.Collections;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
import static org.junit.jupiter.api.Assertions.*;
|
||||||
|
|
||||||
|
final class LyricLineSplitterTest {
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void returnsEmptyForNullInput() {
|
||||||
|
assertTrue(LyricLineSplitter.splitLongLines(null).isEmpty());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void returnsEmptyForEmptyList() {
|
||||||
|
assertTrue(LyricLineSplitter.splitLongLines(Collections.emptyList()).isEmpty());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void keepsShortLinesUnsplit() {
|
||||||
|
LyricLine line = new LyricLine(1000L, "Short text");
|
||||||
|
List<LyricLine> input = Collections.singletonList(line);
|
||||||
|
List<LyricLine> result = LyricLineSplitter.splitLongLines(input);
|
||||||
|
|
||||||
|
assertEquals(1, result.size());
|
||||||
|
assertEquals(1000L, result.get(0).timestampMs());
|
||||||
|
assertEquals("Short text", result.get(0).text());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void splitsLongLineIntoChunks() {
|
||||||
|
LyricLine line = new LyricLine(2000L, "This is a long line with many words in it");
|
||||||
|
List<LyricLine> input = Collections.singletonList(line);
|
||||||
|
List<LyricLine> result = LyricLineSplitter.splitLongLines(input);
|
||||||
|
|
||||||
|
assertTrue(result.size() > 1, "Long line should be split into multiple chunks");
|
||||||
|
assertEquals("This is a", result.get(0).text());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void maintainsTimestampOrder() {
|
||||||
|
LyricLine line = new LyricLine(3000L, "One two three four five six seven eight nine ten");
|
||||||
|
List<LyricLine> input = Collections.singletonList(line);
|
||||||
|
List<LyricLine> result = LyricLineSplitter.splitLongLines(input);
|
||||||
|
|
||||||
|
for (int i = 1; i < result.size(); i++) {
|
||||||
|
assertTrue(result.get(i).timestampMs() >= result.get(i - 1).timestampMs(),
|
||||||
|
"Timestamps should be in order");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void sortsResultByTimestamp() {
|
||||||
|
LyricLine line1 = new LyricLine(3000L, "Word word word word word word");
|
||||||
|
LyricLine line2 = new LyricLine(1000L, "Another line with several words here");
|
||||||
|
List<LyricLine> input = Arrays.asList(line1, line2);
|
||||||
|
List<LyricLine> result = LyricLineSplitter.splitLongLines(input);
|
||||||
|
|
||||||
|
for (int i = 1; i < result.size(); i++) {
|
||||||
|
assertTrue(result.get(i).timestampMs() >= result.get(i - 1).timestampMs(),
|
||||||
|
"Result should be sorted by timestamp");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void splitsLineIntoMultipleChunksWithDecreasingDurations() {
|
||||||
|
LyricLine line = new LyricLine(5000L, "This line has too many words in it to be short");
|
||||||
|
List<LyricLine> input = Collections.singletonList(line);
|
||||||
|
List<LyricLine> result = LyricLineSplitter.splitLongLines(input);
|
||||||
|
|
||||||
|
assertTrue(result.size() > 1, "Long line should be split");
|
||||||
|
for (int i = 1; i < result.size(); i++) {
|
||||||
|
assertTrue(result.get(i).timestampMs() > result.get(i - 1).timestampMs(),
|
||||||
|
"Timestamps should be strictly increasing");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void handlesExactFourWordLine() {
|
||||||
|
LyricLine line = new LyricLine(1000L, "One two three four");
|
||||||
|
List<LyricLine> input = Collections.singletonList(line);
|
||||||
|
List<LyricLine> result = LyricLineSplitter.splitLongLines(input);
|
||||||
|
|
||||||
|
assertEquals(1, result.size(), "Exactly 4 words should not be split");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void splitsFiveWordLine() {
|
||||||
|
LyricLine line = new LyricLine(1000L, "One two three four five");
|
||||||
|
List<LyricLine> input = Collections.singletonList(line);
|
||||||
|
List<LyricLine> result = LyricLineSplitter.splitLongLines(input);
|
||||||
|
|
||||||
|
assertTrue(result.size() > 1, "5 words should be split");
|
||||||
|
}
|
||||||
|
}
|
||||||