Lines 85.18% 23 / 27
Functions and Methods 66.66% 4 / 6
Classes and Traits 0.00% 0 / 1
Name Lines Functions and Methods CRAP Classes and Traits
PlayerAwayUpdatedEvent 85.18% 23 / 27 66.66% 4 / 6 19.05 0.00% 0 / 1
 __construct 100.00% 1 / 1 100.00% 1 / 1 1
 from 100.00% 5 / 5 100.00% 1 / 1 6
 summary 100.00% 1 / 1 100.00% 1 / 1 2
 delimited 100.00% 7 / 7 100.00% 1 / 1 4
 json 80.00% 8 / 10 0.00% 0 / 1 3.07
 player 33.33% 1 / 3 0.00% 0 / 1 3.19
1<?php
2
3declare(strict_types=1);
4
5namespace Lorekeeper\Server\Event\Player;
6
7use Lorekeeper\Core\Player\PlayerName;
8use Lorekeeper\Server\Exception\MalformedEventException;
9use Lorekeeper\Server\Message\DelimitedMessage;
10use Lorekeeper\Server\Message\JsonMessage;
11use Lorekeeper\Server\Message\Message;
12use Override;
13use Psl\Option;
14use Psl\Type;
15use Psl\Type\Exception\CoercionException;
16
17final readonly class PlayerAwayUpdatedEvent extends PlayerUpdatedEvent
18{
19    public function __construct(
20        PlayerName $name,
21        public bool $away,
22    ) {
23        parent::__construct($name);
24    }
25
26    /** @return Option\Option<static> */
27    #[Override]
28    public static function from(Message $message): Option\Option
29    {
30        return match (true) {
31            $message instanceof DelimitedMessage && ($message->data[1] ?? null) === 'uotls' => self::delimited(
32                $message,
33            ),
34            $message instanceof JsonMessage && ($message->data['cmd'] ?? null) === 'uotls' => self::json($message),
35            default => Option\none(),
36        };
37    }
38
39    #[Override]
40    public function summary(): string
41    {
42        return $this->away ? "{$this->name->value} is away" : "{$this->name->value} is back";
43    }
44
45    /**
46     * @throws MalformedEventException
47     *
48     * @return Option\Option<static>
49     */
50    private static function delimited(DelimitedMessage $message): Option\Option
51    {
52        $matches = [];
53        if (preg_match('/(?:^|,)afk:([^,]*)/', $message->data[4] ?? '', $matches) !== 1) {
54            return Option\none();
55        }
56
57        $away = $matches[1];
58        if ($away !== 'true' && $away !== 'false') {
59            throw new MalformedEventException(static::class, "away is '{$away}', not true or false");
60        }
61
62        return Option\some(new static(self::player($message->data[3] ?? null), $away === 'true'));
63    }
64
65    /**
66     * @throws MalformedEventException
67     *
68     * @return Option\Option<static>
69     */
70    private static function json(JsonMessage $message): Option\Option
71    {
72        try {
73            $update = Type\shape([
74                'unm' => Type\optional(Type\mixed()),
75                'o' => Type\optional(Type\shape(['afk' => Type\optional(Type\bool())], allowUnknownFields: true)),
76            ], allowUnknownFields: true)->coerce($message->data);
77        } catch (CoercionException $exception) {
78            throw new MalformedEventException(static::class, 'away is not true or false', $exception);
79        }
80
81        $away = $update['o']['afk'] ?? null;
82        if ($away === null) {
83            return Option\none();
84        }
85
86        return Option\some(new static(self::player($update['unm'] ?? null), $away));
87    }
88
89    /** @throws MalformedEventException */
90    private static function player(mixed $name): PlayerName
91    {
92        try {
93            return new PlayerName(Type\non_empty_string()->coerce($name));
94        } catch (CoercionException $exception) {
95            throw new MalformedEventException(static::class, 'player name is missing or invalid', $exception);
96        }
97    }
98}