2024-07-09 16:10:00 +02:00
|
|
|
# Amethyst - An experimental Minecraft server written in Elixir.
|
|
|
|
# Copyright (C) 2024 KodiCraft
|
|
|
|
#
|
|
|
|
# This program is free software: you can redistribute it and/or modify
|
|
|
|
# it under the terms of the GNU Affero General Public License as
|
|
|
|
# published by the Free Software Foundation, either version 3 of the
|
|
|
|
# License, or (at your option) any later version.
|
|
|
|
#
|
|
|
|
# This program is distributed in the hope that it will be useful,
|
|
|
|
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
|
|
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
|
|
# GNU Affero General Public License for more details.
|
|
|
|
#
|
|
|
|
# You should have received a copy of the GNU Affero General Public License
|
|
|
|
# along with this program. If not, see <https://www.gnu.org/licenses/>.
|
|
|
|
|
|
|
|
defmodule Amethyst.Server.Configuration do
|
|
|
|
@moduledoc """
|
|
|
|
This module contains the logic for the Configuration stage of the server.
|
|
|
|
"""
|
|
|
|
require Logger
|
|
|
|
use Amethyst.Server
|
|
|
|
|
|
|
|
alias Amethyst.Minecraft.Read
|
|
|
|
alias Amethyst.Minecraft.Write
|
|
|
|
|
2024-07-10 09:11:45 +02:00
|
|
|
@impl true
|
|
|
|
def init(state) do
|
|
|
|
state
|
|
|
|
end
|
|
|
|
|
2024-07-09 16:10:00 +02:00
|
|
|
## DESERIALIZATION
|
2024-07-10 09:11:45 +02:00
|
|
|
@impl true
|
2024-07-09 16:10:00 +02:00
|
|
|
# Client Information https://wiki.vg/Protocol#Client_Information
|
|
|
|
def deserialize(0x00, data) do
|
|
|
|
{[locale, view_dist, chat_mode, chat_colors, displayed_skin_parts, main_hand, text_filtering, allow_listing], ""} =
|
|
|
|
Read.start(data) |> Read.string |> Read.byte |> Read.varint |> Read.bool |> Read.ubyte |> Read.varint |> Read.bool |> Read.bool |> Read.stop
|
|
|
|
chat_mode = case chat_mode do
|
|
|
|
0 -> :enabled
|
|
|
|
1 -> :commands_only
|
|
|
|
2 -> :hidden
|
|
|
|
_ -> raise RuntimeError, "Unknown chat mode #{chat_mode}"
|
|
|
|
end
|
|
|
|
main_hand = case main_hand do
|
|
|
|
0 -> :left
|
|
|
|
1 -> :right
|
|
|
|
_ -> raise RuntimeError, "Unknown main hand #{main_hand}"
|
|
|
|
end
|
|
|
|
{:client_information, locale, view_dist, chat_mode, chat_colors, displayed_skin_parts, main_hand, text_filtering, allow_listing}
|
|
|
|
end
|
|
|
|
# Cookie Response https://wiki.vg/Protocol#Cookie_Response_(configuration)
|
|
|
|
def deserialize(0x01, data) do
|
|
|
|
{[key, exists], rest} = Read.start(data) |> Read.string |> Read.bool |> Read.stop
|
|
|
|
if exists do
|
|
|
|
{[length], rest} = Read.start(rest) |> Read.varint |> Read.stop
|
|
|
|
{[data], _} = Read.start(rest) |> Read.raw(length) |> Read.stop
|
|
|
|
{:cookie_response, key, data}
|
|
|
|
else
|
|
|
|
{:cookie_response, key, nil}
|
|
|
|
end
|
|
|
|
end
|
|
|
|
# Serverbound Plugin Message https://wiki.vg/Protocol#Serverbound_Plugin_Message_(configuration)
|
|
|
|
def deserialize(0x02, data) do
|
|
|
|
{[channel], rest} = Read.start(data) |> Read.string |> Read.stop
|
|
|
|
{:serverbound_plugin_message, channel, rest}
|
|
|
|
end
|
|
|
|
# Acknowledge Finish Configuration https://wiki.vg/Protocol#Acknowledge_Finish_Configuration
|
|
|
|
def deserialize(0x03, "") do
|
|
|
|
{:acknowledge_finish_configuration}
|
|
|
|
end
|
|
|
|
# Serverbound Keep Alive https://wiki.vg/Protocol#Serverbound_Keep_Alive_(configuration)
|
|
|
|
def deserialize(0x04, data) do
|
|
|
|
{[id], ""} = Read.start(data) |> Read.long |> Read.stop
|
|
|
|
{:serverbound_keep_alive, id}
|
|
|
|
end
|
|
|
|
# Pong https://wiki.vg/Protocol#Pong_(configuration)
|
|
|
|
def deserialize(0x05, data) do
|
|
|
|
{[id], ""} = Read.start(data) |> Read.int |> Read.stop
|
|
|
|
{:pong, id}
|
|
|
|
end
|
|
|
|
# Resource Pack Response https://wiki.vg/Protocol#Resource_Pack_Response_(configuration)
|
|
|
|
def deserialize(0x06, data) do
|
|
|
|
{[uuid, result], ""} = Read.start(data) |> Read.uuid |> Read.varint |> Read.stop
|
|
|
|
result = case result do
|
|
|
|
0 -> :successfully_downloaded
|
|
|
|
1 -> :declined
|
|
|
|
2 -> :failed_to_download
|
|
|
|
3 -> :accepted
|
|
|
|
4 -> :downloaded
|
|
|
|
5 -> :invalid_url
|
|
|
|
6 -> :failed_to_reload
|
|
|
|
7 -> :discarded
|
|
|
|
_ -> raise RuntimeError, "Unknown resource pack response #{result}"
|
|
|
|
end
|
|
|
|
{:resource_pack_response, uuid, result}
|
|
|
|
end
|
|
|
|
# Serverbound Known Packs https://wiki.vg/Protocol#Serverbound_Known_Packs
|
|
|
|
def deserialize(0x07, data) do
|
|
|
|
{[count], rest} = Read.start(data) |> Read.varint |> Read.stop
|
2024-07-21 01:59:57 +02:00
|
|
|
{packs, _} = Enum.reduce(1..count, {[], rest}, fn _, {acc, rest} ->
|
2024-07-09 16:10:00 +02:00
|
|
|
{[namespace, id, version], rest} = Read.start(rest) |> Read.string |> Read.string |> Read.string |> Read.stop
|
|
|
|
{[{namespace, id, version} | acc], rest}
|
|
|
|
end)
|
2024-07-21 01:59:57 +02:00
|
|
|
{:serverbound_known_packs, packs}
|
2024-07-09 16:10:00 +02:00
|
|
|
end
|
|
|
|
def deserialize(type, _) do
|
|
|
|
raise RuntimeError, "Got unknown packet type #{type}!"
|
|
|
|
end
|
|
|
|
|
|
|
|
## SERIALIZATION
|
2024-07-10 09:11:45 +02:00
|
|
|
@impl true
|
2024-07-09 16:10:00 +02:00
|
|
|
# Cookie Request https://wiki.vg/Protocol#Cookie_Request_(configuration)
|
|
|
|
def serialize({:cookie_request, id}) do
|
|
|
|
Write.varint(0x00) <> Write.string(id)
|
|
|
|
end
|
|
|
|
# Clientbound Plugin Message https://wiki.vg/Protocol#Clientbound_Plugin_Message_(configuration)
|
|
|
|
def serialize({:clientbound_plugin_message, channel, data}) do
|
|
|
|
Write.varint(0x01) <> Write.string(channel) <> data
|
|
|
|
end
|
|
|
|
# Disconnect https://wiki.vg/Protocol#Disconnect_(configuration)
|
|
|
|
def serialize({:disconnect, reason}) do
|
|
|
|
Write.varint(0x02) <> Write.string(reason)
|
|
|
|
end
|
|
|
|
# Finish Configuration https://wiki.vg/Protocol#Finish_Configuration
|
|
|
|
def serialize({:finish_configuration}) do
|
|
|
|
Write.varint(0x03)
|
|
|
|
end
|
|
|
|
# Clientbound Keep Alive https://wiki.vg/Protocol#Clientbound_Keep_Alive_(configuration)
|
|
|
|
def serialize({:clientbound_keep_alive, id}) do
|
|
|
|
Write.varint(0x04) <> <<id::64-big-signed>>
|
|
|
|
end
|
|
|
|
# Ping https://wiki.vg/Protocol#Ping_(configuration)
|
|
|
|
def serialize({:ping, id}) do
|
|
|
|
Write.varint(0x05) <> <<id::32-big-signed>>
|
|
|
|
end
|
|
|
|
# Reset Chat https://wiki.vg/Protocol#Reset_Chat
|
|
|
|
def serialize({:reset_chat}) do
|
|
|
|
Write.varint(0x06)
|
|
|
|
end
|
|
|
|
# Registry Data https://wiki.vg/Protocol#Registry_Data
|
2024-08-12 12:14:12 +02:00
|
|
|
def serialize({:registry_data, id, entries}) do
|
|
|
|
Write.varint(0x07) <> Write.string(id) <> Write.varint(length(entries)) <> Enum.map_join(entries, "", fn {name, nbt} ->
|
|
|
|
Write.string(name) <>
|
|
|
|
if nbt == nil do
|
|
|
|
Write.bool(false)
|
|
|
|
else
|
|
|
|
Write.bool(true) <> Amethyst.NBT.Write.write_net(nbt)
|
|
|
|
end
|
|
|
|
end)
|
2024-07-09 16:10:00 +02:00
|
|
|
end
|
|
|
|
# Remove Resource Pack https://wiki.vg/Protocol#Remove_Resource_Pack_(configuration)
|
|
|
|
def serialize({:remove_resource_pack, id}) do
|
2024-08-01 06:05:04 +02:00
|
|
|
Write.option(id, &Write.string/1)
|
2024-07-09 16:10:00 +02:00
|
|
|
end
|
|
|
|
# Add Resource Pack https://wiki.vg/Protocol#Add_Resource_Pack_(configuration)
|
|
|
|
def serialize({:add_resource_pack, id, url, hash, forced, msg}) do
|
|
|
|
Write.varint(0x09) <> Write.string(id) <> Write.string(url) <> Write.string(hash) <>
|
2024-08-01 06:05:04 +02:00
|
|
|
Write.bool(forced) <> Write.option(msg, &Write.string/1)
|
2024-07-09 16:10:00 +02:00
|
|
|
end
|
|
|
|
# Store Cookie https://wiki.vg/Protocol#Store_Cookie_(configuration)
|
|
|
|
def serialize({:store_cookie, id, data}) do
|
|
|
|
Write.varint(0x0A) <> Write.string(id) <> Write.string(data)
|
|
|
|
end
|
|
|
|
# Transfer https://wiki.vg/Protocol#Transfer_(configuration)
|
|
|
|
def serialize({:transfer, addr, port}) do
|
|
|
|
Write.varint(0x0B) <> Write.string(addr) <> Write.varint(port)
|
|
|
|
end
|
|
|
|
# Feature Flags https://wiki.vg/Protocol#Feature_Flags
|
|
|
|
def serialize({:feature_flags, flags}) do
|
2024-08-01 06:05:04 +02:00
|
|
|
Write.varint(0x0C) <> Write.varint(length(flags)) <> Write.list(flags, &Write.string/1)
|
2024-07-09 16:10:00 +02:00
|
|
|
end
|
|
|
|
# Update Tags https://wiki.vg/Protocol#Update_Tags
|
|
|
|
def serialize({:update_tags, tags}) do
|
|
|
|
Write.varint(0x0D) <> Write.varint(length(tags)) <>
|
|
|
|
Enum.reduce(tags, "", &serialize_tag/2)
|
|
|
|
end
|
|
|
|
# Clientbound Known Packs https://wiki.vg/Protocol#Clientbound_Known_Packs
|
|
|
|
def serialize({:clientbound_known_packs, packs}) do
|
|
|
|
Write.varint(0x0E) <> Write.varint(length(packs)) <>
|
2024-08-01 06:05:04 +02:00
|
|
|
Write.list(packs, fn {namespace, id, version} -> Write.string(namespace) <> Write.string(id) <> Write.string(version) end)
|
2024-07-09 16:10:00 +02:00
|
|
|
end
|
|
|
|
# Custom Report Details https://wiki.vg/Protocol#Custom_Report_Details
|
|
|
|
def serialize({:custom_report_details, details}) do
|
|
|
|
Write.varint(0x0F) <> Write.varint(length(details)) <>
|
2024-08-01 06:05:04 +02:00
|
|
|
Write.list(details, fn {id, data} -> Write.string(id) <> Write.string(data) end)
|
2024-07-09 16:10:00 +02:00
|
|
|
end
|
|
|
|
# Server Links https://wiki.vg/Protocol#Server_Links_(configuration)
|
|
|
|
def serialize({:server_links, links}) do
|
|
|
|
Write.varint(0x10) <> Write.varint(length(links)) <>
|
2024-08-01 06:05:04 +02:00
|
|
|
Write.list(links, fn {label, url} -> serialize_link_label(label) <> Write.string(url) end)
|
2024-07-09 16:10:00 +02:00
|
|
|
end
|
|
|
|
def serialize(packet) do
|
|
|
|
raise ArgumentError, "Tried serializing unknown packet #{inspect(packet)}"
|
|
|
|
end
|
|
|
|
|
|
|
|
defp serialize_tag({id, elements}, acc) do
|
|
|
|
acc <> Write.string(id) <> Write.varint(length(elements)) <> serialize_elements(elements)
|
|
|
|
end
|
|
|
|
defp serialize_elements(elements) do
|
2024-08-01 06:05:04 +02:00
|
|
|
Write.list(elements, fn {id, ids} ->
|
|
|
|
Write.string(id) <> Write.varint(length(ids)) <>
|
|
|
|
Write.list(ids, &Write.varint/1)
|
|
|
|
end)
|
2024-07-09 16:10:00 +02:00
|
|
|
end
|
|
|
|
defp serialize_link_label(:bug_report) do
|
|
|
|
<<0x01>> <> Write.varint(0x00)
|
|
|
|
end
|
|
|
|
defp serialize_link_label(:community_guidelines) do
|
|
|
|
<<0x01>> <> Write.varint(0x01)
|
|
|
|
end
|
|
|
|
defp serialize_link_label(:support) do
|
|
|
|
<<0x01>> <> Write.varint(0x02)
|
|
|
|
end
|
|
|
|
defp serialize_link_label(:status) do
|
|
|
|
<<0x01>> <> Write.varint(0x03)
|
|
|
|
end
|
|
|
|
defp serialize_link_label(:feedback) do
|
|
|
|
<<0x01>> <> Write.varint(0x04)
|
|
|
|
end
|
|
|
|
defp serialize_link_label(:community) do
|
|
|
|
<<0x01>> <> Write.varint(0x05)
|
|
|
|
end
|
|
|
|
defp serialize_link_label(:website) do
|
|
|
|
<<0x01>> <> Write.varint(0x06)
|
|
|
|
end
|
|
|
|
defp serialize_link_label(:forums) do
|
|
|
|
<<0x01>> <> Write.varint(0x07)
|
|
|
|
end
|
|
|
|
defp serialize_link_label(:news) do
|
|
|
|
<<0x01>> <> Write.varint(0x08)
|
|
|
|
end
|
|
|
|
defp serialize_link_label(:announcements) do
|
|
|
|
<<0x01>> <> Write.varint(0x09)
|
|
|
|
end
|
|
|
|
defp serialize_link_label(other) do
|
|
|
|
<<0x00>> <> Write.string(other)
|
|
|
|
end
|
|
|
|
|
|
|
|
## HANDLING
|
2024-07-10 09:11:45 +02:00
|
|
|
@impl true
|
2024-07-21 01:59:57 +02:00
|
|
|
# Client Information https://wiki.vg/Protocol#Client_Information
|
|
|
|
def handle({:client_information, locale, v_dist, chat_mode, chat_colors, displayed_skin_parts, main_hand, text_filtering, allow_listing}, client, state) do
|
|
|
|
state = state |> Keyword.put(:locale, locale) |> Keyword.put(:view_dist, v_dist) |> Keyword.put(:chat_mode, chat_mode) |>
|
|
|
|
Keyword.put(:chat_colors, chat_colors) |> Keyword.put(:displayed_skin_parts, displayed_skin_parts) |> Keyword.put(:main_hand, main_hand) |>
|
|
|
|
Keyword.put(:text_filtering, text_filtering) |> Keyword.put(:allow_listing, allow_listing)
|
|
|
|
# TODO: Here we should create the game handling task for this player and give it
|
|
|
|
# this data.
|
|
|
|
transmit({:clientbound_plugin_message, "minecraft:brand", Write.string("amethyst")}, client)
|
2024-08-12 12:14:12 +02:00
|
|
|
transmit({:clientbound_known_packs, [{"minecraft", "core", "1.21"}, {"minecraft", "dimension_type", "1.21"}]}, client)
|
2024-07-21 01:59:57 +02:00
|
|
|
{:ok, state}
|
|
|
|
end
|
|
|
|
# Serverbound Known Packs https://wiki.vg/Protocol#Serverbound_Known_Packs
|
|
|
|
def handle({:serverbound_known_packs, _packs}, client, state) do
|
|
|
|
# L + ratio + don't care + didn't ask + finish configuration
|
2024-08-12 12:14:12 +02:00
|
|
|
import Amethyst.NBT.Write
|
|
|
|
# TODO: This shouldn't be hard-coded but we obviously don't know what we will need until we have game handling
|
|
|
|
# This can at least be followed as a "minimum" of what we need to send for the client not to complain
|
|
|
|
transmit({:registry_data, "minecraft:dimension_type", [{"amethyst:basic", compound(%{
|
|
|
|
"has_skylight" => byte(1),
|
|
|
|
"has_ceiling" => byte(0),
|
|
|
|
"ultrawarm" => byte(0),
|
|
|
|
"natural" => byte(1),
|
|
|
|
"coordinate_scale" => float(1.0),
|
|
|
|
"bed_works" => byte(1),
|
|
|
|
"respawn_anchor_works" => byte(1),
|
|
|
|
"min_y" => int(0),
|
|
|
|
"height" => int(256),
|
|
|
|
"logical_height" => int(256),
|
|
|
|
"infiniburn" => string("#"),
|
|
|
|
"effects" => string("minecraft:overworld"),
|
|
|
|
"ambient_light" => float(0.0),
|
|
|
|
"piglin_safe" => byte(0),
|
|
|
|
"has_raids" => byte(1),
|
|
|
|
"monster_spawn_light_level" => int(0),
|
|
|
|
"monster_spawn_block_light_limit" => int(0)
|
|
|
|
})}]}, client)
|
|
|
|
transmit({:registry_data, "minecraft:painting_variant", [{"minecraft:kebab", compound(%{
|
|
|
|
"asset_id" => string("minecraft:kebab"),
|
|
|
|
"height" => int(1),
|
|
|
|
"width" => int(1),
|
|
|
|
})}]}, client)
|
|
|
|
transmit({:registry_data, "minecraft:wolf_variant", [{"minecraft:wolf_ashen", compound(%{
|
|
|
|
"wild_texture" => string("minecraft:entity/wolf/wolf_ashen"),
|
|
|
|
"tame_texture" => string("minecraft:entity/wolf/wolf_ashen_tame"),
|
|
|
|
"angry_texture" => string("minecraft:entity/wolf/wolf_ashen_angry"),
|
|
|
|
"biomes" => string("amethyst:basic"),
|
|
|
|
})}]}, client)
|
|
|
|
# https://gist.github.com/WinX64/ab8c7a8df797c273b32d3a3b66522906 minecraft:plains
|
|
|
|
basic_biome = compound(%{
|
|
|
|
"effects" => compound(%{
|
|
|
|
"sky_color" => int(7907327),
|
|
|
|
"water_fog_color" => int(329011),
|
|
|
|
"fog_color" => int(12638463),
|
|
|
|
"water_color" => int(4159204),
|
|
|
|
"mood_sound" => compound(%{
|
|
|
|
"tick_delay" => int(6000),
|
|
|
|
"offset" => float(2.0),
|
|
|
|
"sound" => string("minecraft:ambient.cave"),
|
|
|
|
"block_search_extent" => int(8)
|
|
|
|
}),
|
|
|
|
}),
|
|
|
|
"has_precipitation" => byte(1),
|
|
|
|
"temperature" => float(0.8),
|
|
|
|
"downfall" => float(0.4),
|
|
|
|
})
|
|
|
|
transmit({:registry_data, "minecraft:worldgen/biome", [
|
|
|
|
{"amethyst:basic", basic_biome}, {"minecraft:plains", basic_biome}
|
|
|
|
]}, client)
|
|
|
|
# holy fucking shit
|
|
|
|
generic_damage = compound(%{
|
|
|
|
"scaling" => string("when_caused_by_living_non_player"),
|
|
|
|
"exhaustion" => float(0.0),
|
|
|
|
"message_id" => string("generic")
|
|
|
|
})
|
|
|
|
transmit({:registry_data, "minecraft:damage_type", [
|
|
|
|
{"minecraft:in_fire", generic_damage}, {"minecraft:campfire", generic_damage}, {"minecraft:lightning_bolt", generic_damage},
|
|
|
|
{"minecraft:on_fire", generic_damage}, {"minecraft:lava", generic_damage}, {"minecraft:hot_floor", generic_damage},
|
|
|
|
{"minecraft:in_wall", generic_damage}, {"minecraft:cramming", generic_damage}, {"minecraft:drown", generic_damage},
|
|
|
|
{"minecraft:starve", generic_damage}, {"minecraft:cactus", generic_damage}, {"minecraft:fall", generic_damage},
|
|
|
|
{"minecraft:fly_into_wall", generic_damage}, {"minecraft:out_of_world", generic_damage}, {"minecraft:generic", generic_damage},
|
|
|
|
{"minecraft:magic", generic_damage}, {"minecraft:wither", generic_damage}, {"minecraft:dragon_breath", generic_damage},
|
|
|
|
{"minecraft:dry_out", generic_damage}, {"minecraft:sweet_berry_bush", generic_damage}, {"minecraft:freeze", generic_damage},
|
|
|
|
{"minecraft:stalagmite", generic_damage}, {"minecraft:outside_border", generic_damage}, {"minecraft:generic_kill", generic_damage},
|
|
|
|
]}, client)
|
2024-07-21 01:59:57 +02:00
|
|
|
transmit({:finish_configuration}, client)
|
|
|
|
{:ok, state}
|
|
|
|
end
|
|
|
|
# Acknowledge Finish Configuration https://wiki.vg/Protocol#Acknowledge_Finish_Configuration
|
|
|
|
def handle({:acknowledge_finish_configuration}, client, state) do
|
2024-07-21 03:26:10 +02:00
|
|
|
# TODO: All of this stuff should obviously not be hardcoded here
|
|
|
|
Amethyst.Server.Play.transmit({:login,
|
|
|
|
0, false, ["minecraft:overworld"], 0, 16, 16, false, true, true, 0,
|
|
|
|
"minecraft:overworld", <<0::64>>, :spectator, nil, false, true, nil, 0, false
|
|
|
|
}, client)
|
2024-07-21 01:59:57 +02:00
|
|
|
Amethyst.Server.Play.serve(client, state)
|
|
|
|
end
|
2024-07-10 09:11:45 +02:00
|
|
|
# Serverbound Plugin Message https://wiki.vg/Protocol#Serverbound_Plugin_Message_(configuration)
|
2024-07-09 18:57:09 +02:00
|
|
|
def handle({:serverbound_plugin_message, channel, data}, client, state) do
|
|
|
|
handle_plugin_message(channel, data, client, state)
|
2024-07-09 16:18:04 +02:00
|
|
|
end
|
2024-07-21 01:59:57 +02:00
|
|
|
def handle(tuple, state) do
|
2024-07-09 16:10:00 +02:00
|
|
|
Logger.error("Unhandled but known packet #{elem(tuple, 0)}")
|
2024-07-21 01:59:57 +02:00
|
|
|
{:unhandled, state}
|
2024-07-09 16:10:00 +02:00
|
|
|
end
|
2024-08-04 07:47:58 +02:00
|
|
|
defp handle_plugin_message("minecraft:brand", data, _client, state) do
|
2024-07-09 16:18:04 +02:00
|
|
|
{[brand], ""} = Read.start(data) |> Read.string |> Read.stop
|
|
|
|
Logger.info("Client using brand: #{brand}")
|
2024-07-09 19:00:03 +02:00
|
|
|
{:ok, Keyword.put(state, :brand, brand)}
|
2024-07-09 16:18:04 +02:00
|
|
|
end
|
2024-08-04 07:44:03 +02:00
|
|
|
defp handle_plugin_message("amethyst:hello", _data, client, state) do
|
2024-07-09 16:18:04 +02:00
|
|
|
Logger.info("Client is Amethyst aware! Hello!")
|
2024-08-04 07:44:03 +02:00
|
|
|
transmit({:clientbound_plugin_message, "amethyst:hello", ""}, client)
|
2024-07-09 19:00:03 +02:00
|
|
|
{:ok, Keyword.put(state, :knows_amethyst, true)}
|
2024-07-09 16:18:04 +02:00
|
|
|
end
|
2024-07-09 16:10:00 +02:00
|
|
|
end
|