Passed
Push — master ( dce7e6...e2e6ab )
by Vincent
06:59 queued 12s
created

name()   A

Complexity

Conditions 1

Size

Total Lines 3
Code Lines 3

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 1
CRAP Score 1

Importance

Changes 0
Metric Value
cc 1
c 0
b 0
f 0
dl 0
loc 3
ccs 1
cts 1
cp 1
crap 1
rs 10
eloc 3
1
/*
2
 * This file is part of Araknemu.
3
 *
4
 * Araknemu is free software: you can redistribute it and/or modify
5
 * it under the terms of the GNU Lesser General Public License as published by
6
 * the Free Software Foundation, either version 3 of the License, or
7
 * (at your option) any later version.
8
 *
9
 * Araknemu is distributed in the hope that it will be useful,
10
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
11
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
12
 * GNU Lesser General Public License for more details.
13
 *
14
 * You should have received a copy of the GNU Lesser General Public License
15
 * along with Araknemu.  If not, see <https://www.gnu.org/licenses/>.
16
 *
17
 * Copyright (c) 2017-2020 Vincent Quatrevieux
18
 */
19
20
package fr.quatrevieux.araknemu.game.exploration.npc.dialog;
21
22
import fr.quatrevieux.araknemu.data.world.entity.environment.npc.Npc;
23
import fr.quatrevieux.araknemu.data.world.entity.environment.npc.Question;
24
import fr.quatrevieux.araknemu.data.world.entity.environment.npc.ResponseAction;
25
import fr.quatrevieux.araknemu.data.world.repository.environment.npc.QuestionRepository;
26
import fr.quatrevieux.araknemu.data.world.repository.environment.npc.ResponseActionRepository;
27
import fr.quatrevieux.araknemu.game.PreloadableService;
28
import fr.quatrevieux.araknemu.game.exploration.npc.dialog.action.Action;
29
import fr.quatrevieux.araknemu.game.exploration.npc.dialog.action.ActionFactory;
30
import fr.quatrevieux.araknemu.game.exploration.npc.dialog.action.dialog.LeaveDialog;
31
import fr.quatrevieux.araknemu.game.exploration.npc.dialog.action.dialog.NextQuestion;
32
import fr.quatrevieux.araknemu.game.exploration.npc.dialog.parameter.ParametersResolver;
33
import org.apache.commons.lang3.ArrayUtils;
34
import org.apache.logging.log4j.Logger;
35
import org.checkerframework.checker.nullness.qual.Nullable;
36
37
import java.util.ArrayList;
38
import java.util.Arrays;
39
import java.util.Collection;
40
import java.util.HashMap;
41
import java.util.List;
42
import java.util.Map;
43
import java.util.Optional;
44
import java.util.concurrent.ConcurrentHashMap;
45
import java.util.stream.Collectors;
46
47
/**
48
 * Manage dialogs, questions, responses and actions
49
 */
50
public final class DialogService implements PreloadableService {
51
    private final QuestionRepository questionRepository;
52
    private final ResponseActionRepository responseActionRepository;
53
    private final ParametersResolver parametersResolver;
54
    private final Logger logger;
55
56 1
    private final Map<String, ActionFactory> actionFactories = new HashMap<>();
57 1
    private final Map<Integer, NpcQuestion> questions = new ConcurrentHashMap<>();
58 1
    private final Map<Integer, Response> responses = new ConcurrentHashMap<>();
59
60 1
    public DialogService(QuestionRepository questionRepository, ResponseActionRepository responseActionRepository, ActionFactory[] actionFactories, ParametersResolver parametersResolver, Logger logger) {
61 1
        this.questionRepository = questionRepository;
62 1
        this.responseActionRepository = responseActionRepository;
63 1
        this.parametersResolver = parametersResolver;
64 1
        this.logger = logger;
65
66 1
        initActionFactories(actionFactories);
67 1
    }
68
69
    @Override
70
    public void preload(Logger logger) {
71 1
        logger.info("Loading dialogs responses...");
72 1
        createResponses(responseActionRepository.all());
73 1
        logger.info("{} responses loaded", responses.size());
74
75 1
        logger.info("Loading dialogs questions...");
76 1
        questionRepository.all().forEach(question -> createQuestion(question, false));
77 1
        logger.info("{} questions loaded", questions.size());
78 1
    }
79
80
    @Override
81
    public String name() {
82 1
        return "npc.dialog";
83
    }
84
85
    /**
86
     * Get list of questions for a given NPC
87
     */
88
    public Collection<NpcQuestion> forNpc(Npc npc) {
89 1
        return byIds(npc.questions());
90
    }
91
92
    /**
93
     * Get list of questions
94
     *
95
     * Note: If the returned questions size if different from requested questions, a warning will be logged without fail
96
     */
97
    public Collection<NpcQuestion> byIds(int[] ids) {
98 1
        return loadQuestionFromCache(ids).orElseGet(() -> loadQuestionFromDatabase(ids));
99
    }
100
101
    /**
102
     * Register a new response action factory
103
     */
104
    public void registerActionFactory(ActionFactory factory) {
105 1
        actionFactories.put(factory.type(), factory);
106 1
    }
107
108
    /**
109
     * Create or retrieve a NpcQuestion from an entity
110
     *
111
     * @param fromDatabase Allows loading responses from database ? false during preloading
112
     */
113
    private NpcQuestion createQuestion(Question entity, boolean fromDatabase) {
114 1
        return questions.computeIfAbsent(
115 1
            entity.id(),
116 1
            id -> new NpcQuestion(entity, responsesByQuestion(entity, fromDatabase), parametersResolver)
117
        );
118
    }
119
120
    /**
121
     * Try to load all questions from cache
122
     * If at least one question is not into the cache, will returns nothing (and let loading from database)
123
     */
124
    private Optional<Collection<NpcQuestion>> loadQuestionFromCache(int[] ids) {
125 1
        final Collection<NpcQuestion> questions = new ArrayList<>(ids.length);
0 ignored issues
show
Comprehensibility introduced by
The variable questionsshadows a field with the same name declared in line 57. Consider renaming it.
Loading history...
126
127 1
        for (int id : ids) {
128 1
            final NpcQuestion cachedQuestion = this.questions.get(id);
129
130 1
            if (cachedQuestion == null) {
131 1
                return Optional.empty();
132
            }
133
134 1
            questions.add(cachedQuestion);
135
        }
136
137
        // @todo refactor : do not use optional but a callback as parameter
0 ignored issues
show
introduced by
Comment matches to-do format '(TODO:)|(@todo )'.
Loading history...
138 1
        return Optional.of(questions);
139
    }
140
141
    /**
142
     * Load all questions from database
143
     */
144
    private Collection<NpcQuestion> loadQuestionFromDatabase(int[] ids) {
145 1
        final Collection<NpcQuestion> questions = questionRepository.byIds(ids)
0 ignored issues
show
Comprehensibility introduced by
The variable questionsshadows a field with the same name declared in line 57. Consider renaming it.
Loading history...
146 1
            .stream()
147 1
            .map(question -> createQuestion(question, true))
148 1
            .collect(Collectors.toList())
149
        ;
150
151 1
        if (questions.size() != ids.length) {
152 1
            logger.warn(
153
                "NPC question not found : requested {}, actual {}",
154 1
                Arrays.toString(ids),
155 1
                questions.stream()
156 1
                    .map(question -> Integer.toString(question.id()))
157 1
                    .collect(Collectors.joining(", ", "[", "]"))
158
            );
159
        }
160
161 1
        return questions;
162
    }
163
164
    /**
165
     * Load responses for a question
166
     *
167
     * @param question The question entity
168
     * @param fromDatabase Allows loading responses from database ? false during preloading
169
     *
170
     * @return The list of responses
171
     */
172
    private Collection<Response> responsesByQuestion(Question question, boolean fromDatabase) {
173
        // Disallow loading from database : only retrieve loaded questions
174 1
        if (!fromDatabase) {
175 1
            return responsesFromIds(question.responseIds());
176
        }
177
178
        // Check if all responses are already loaded
179 1
        if (responses.keySet().containsAll(Arrays.asList(ArrayUtils.toObject(question.responseIds())))) {
180 1
            return responsesFromIds(question.responseIds());
181
        }
182
183
        // Load an creates responses
184 1
        return createResponses(responseActionRepository.byQuestion(question));
185
    }
186
187
    /**
188
     * Get already loaded responses from ids
189
     *
190
     * @param responseIds List of response ids to get
191
     *
192
     * @return The list of responses
193
     */
194
    private Collection<Response> responsesFromIds(int[] responseIds) {
195 1
        final Collection<Response> responses = new ArrayList<>(responseIds.length);
0 ignored issues
show
Comprehensibility introduced by
The variable responsesshadows a field with the same name declared in line 58. Consider renaming it.
Loading history...
196
197 1
        for (int id : responseIds) {
198 1
            final Response response = this.responses.get(id);
199
200 1
            if (response != null) {
201 1
                responses.add(response);
202
            }
203
        }
204
205 1
        return responses;
206
    }
207
208
    /**
209
     * Create responses from actions
210
     *
211
     * @param responsesActions Actions, grouping by the response id
212
     */
213
    private Collection<Response> createResponses(Map<Integer, List<ResponseAction>> responsesActions) {
214 1
        final Collection<Response> responses = new ArrayList<>();
0 ignored issues
show
Comprehensibility introduced by
The variable responsesshadows a field with the same name declared in line 58. Consider renaming it.
Loading history...
215
216 1
        for (Map.Entry<Integer, List<ResponseAction>> entry : responsesActions.entrySet()) {
217 1
            responses.add(
218 1
                this.responses.computeIfAbsent(
219 1
                    entry.getKey(),
220 1
                    id -> createResponse(id, entry.getValue())
221
                )
222
            );
223 1
        }
224
225 1
        return responses;
226
    }
227
228
    /**
229
     * Create a dialog response
230
     */
231
    private Response createResponse(int id, List<ResponseAction> actionEntities) {
232 1
        final List<Action> actions = new ArrayList<>(actionEntities.size());
233
234 1
        for (ResponseAction actionEntity : actionEntities) {
235 1
            final Action action = createAction(actionEntity);
236
237 1
            if (action != null) {
238 1
                actions.add(action);
239
            }
240 1
        }
241
242 1
        return new Response(id, actions);
243
    }
244
245
    /**
246
     * Create a single response action
247
     * Note: If the action is not supported, the method will log a warning, and return null
248
     *
249
     * @return The created action or null if no factory is found (a warning will be logged)
250
     */
251
    private @Nullable Action createAction(ResponseAction action) {
252 1
        final ActionFactory factory = actionFactories.get(action.action());
253
254 1
        if (factory == null) {
255 1
            logger.warn("Response action {} is not supported for response {}", action.action(), action.responseId());
256
257 1
            return null;
258
        }
259
260 1
        return factory.create(action);
261
    }
262
263
    /**
264
     * Initialize factories
265
     * Note: Because of circular references, all dialog actions are registered here instead of the module
266
     */
267
    private void initActionFactories(ActionFactory[] actionFactories) {
268 1
        for (ActionFactory actionFactory : actionFactories) {
269 1
            registerActionFactory(actionFactory);
270
        }
271
272
        // Register dialog actions
273 1
        registerActionFactory(new NextQuestion.Factory(this));
274 1
        registerActionFactory(new LeaveDialog.Factory());
275 1
    }
276
}
277