]> git.localhorst.tv Git - blank.git/blob - src/ui/ui.cpp
transmit player input from client to server
[blank.git] / src / ui / ui.cpp
1 #include "ClientController.hpp"
2 #include "DirectInput.hpp"
3 #include "HUD.hpp"
4 #include "InteractiveManipulator.hpp"
5 #include "Interface.hpp"
6 #include "Keymap.hpp"
7 #include "PlayerController.hpp"
8
9 #include "../app/Assets.hpp"
10 #include "../app/Config.hpp"
11 #include "../app/Environment.hpp"
12 #include "../app/FrameCounter.hpp"
13 #include "../app/init.hpp"
14 #include "../audio/Audio.hpp"
15 #include "../graphics/Font.hpp"
16 #include "../graphics/Viewport.hpp"
17 #include "../io/TokenStreamReader.hpp"
18 #include "../model/shapes.hpp"
19 #include "../world/BlockLookup.hpp"
20 #include "../world/World.hpp"
21 #include "../world/WorldManipulator.hpp"
22
23 #include <algorithm>
24 #include <cmath>
25 #include <iostream>
26 #include <map>
27 #include <sstream>
28 #include <glm/gtc/matrix_transform.hpp>
29 #include <glm/gtx/rotate_vector.hpp>
30 #include <glm/gtx/io.hpp>
31
32
33 namespace blank {
34
35 PlayerController::PlayerController(World &world, Player &player)
36 : world(world)
37 , player(player)
38 , move_dir(0.0f)
39 , pitch(0.0f)
40 , yaw(0.0f)
41 , dirty(true)
42 , aim_world()
43 , aim_entity() {
44
45 }
46
47 void PlayerController::SetMovement(const glm::vec3 &m) noexcept {
48         if (dot(m, m) > 1.0f) {
49                 move_dir = normalize(m);
50         } else {
51                 move_dir = m;
52         }
53         Invalidate();
54 }
55
56 void PlayerController::TurnHead(float dp, float dy) noexcept {
57         pitch += dp;
58         if (pitch > PI / 2) {
59                 pitch = PI / 2;
60         } else if (pitch < -PI / 2) {
61                 pitch = -PI / 2;
62         }
63         yaw += dy;
64         if (yaw > PI) {
65                 yaw -= PI * 2;
66         } else if (yaw < -PI) {
67                 yaw += PI * 2;
68         }
69         Invalidate();
70 }
71
72 void PlayerController::SelectInventory(int i) noexcept {
73         player.SetInventorySlot(i);
74 }
75
76 int PlayerController::InventorySlot() const noexcept {
77         return player.GetInventorySlot();
78 }
79
80 void PlayerController::Invalidate() noexcept {
81         dirty = true;
82 }
83
84 void PlayerController::UpdatePlayer() noexcept {
85         constexpr float max_vel = 0.005f;
86         if (dirty) {
87                 player.GetEntity().Orientation(glm::quat(glm::vec3(pitch, yaw, 0.0f)));
88                 player.GetEntity().Velocity(glm::rotateY(move_dir * max_vel, yaw));
89
90                 Ray aim = player.Aim();
91                 if (!world.Intersection(aim, glm::mat4(1.0f), player.GetEntity().ChunkCoords(), aim_world)) {
92                         aim_world = WorldCollision();
93                 }
94                 if (!world.Intersection(aim, glm::mat4(1.0f), player.GetEntity(), aim_entity)) {
95                         aim_entity = EntityCollision();
96                 }
97                 if (aim_world && aim_entity) {
98                         // got both, pick the closest one
99                         if (aim_world.depth < aim_entity.depth) {
100                                 aim_entity = EntityCollision();
101                         } else {
102                                 aim_world = WorldCollision();
103                         }
104                 }
105                 dirty = false;
106         }
107 }
108
109
110 DirectInput::DirectInput(World &world, Player &player, WorldManipulator &manip)
111 : PlayerController(world, player)
112 , manip(manip)
113 , place_timer(256)
114 , remove_timer(256) {
115
116 }
117
118 void DirectInput::Update(int dt) {
119         Invalidate(); // world has changed in the meantime
120         UpdatePlayer();
121
122         remove_timer.Update(dt);
123         if (remove_timer.Hit()) {
124                 RemoveBlock();
125         }
126
127         place_timer.Update(dt);
128         if (place_timer.Hit()) {
129                 PlaceBlock();
130         }
131 }
132
133 void DirectInput::StartPrimaryAction() {
134         if (!remove_timer.Running()) {
135                 RemoveBlock();
136                 remove_timer.Start();
137         }
138 }
139
140 void DirectInput::StopPrimaryAction() {
141         remove_timer.Stop();
142 }
143
144 void DirectInput::StartSecondaryAction() {
145         if (!place_timer.Running()) {
146                 PlaceBlock();
147                 place_timer.Start();
148         }
149 }
150
151 void DirectInput::StopSecondaryAction() {
152         place_timer.Stop();
153 }
154
155 void DirectInput::StartTertiaryAction() {
156         PickBlock();
157 }
158
159 void DirectInput::StopTertiaryAction() {
160         // nothing
161 }
162
163 void DirectInput::PickBlock() {
164         UpdatePlayer();
165         if (!BlockFocus()) return;
166         SelectInventory(BlockFocus().GetBlock().type - 1);
167 }
168
169 void DirectInput::PlaceBlock() {
170         UpdatePlayer();
171         if (!BlockFocus()) return;
172
173         BlockLookup next_block(BlockFocus().chunk, BlockFocus().BlockPos(), Block::NormalFace(BlockFocus().normal));
174         if (!next_block) {
175                 return;
176         }
177         manip.SetBlock(next_block.GetChunk(), next_block.GetBlockIndex(), Block(InventorySlot() + 1));
178         Invalidate();
179 }
180
181 void DirectInput::RemoveBlock() {
182         UpdatePlayer();
183         if (!BlockFocus()) return;
184         manip.SetBlock(BlockFocus().GetChunk(), BlockFocus().block, Block(0));
185         Invalidate();
186 }
187
188
189 HUD::HUD(Environment &env, Config &config, const Player &player)
190 : env(env)
191 , config(config)
192 , player(player)
193 // block focus
194 , outline()
195 , outline_transform(1.0f)
196 , outline_visible(false)
197 // "inventory"
198 , block()
199 , block_buf()
200 , block_transform(1.0f)
201 , block_label()
202 , block_visible(false)
203 // debug overlay
204 , counter_text()
205 , position_text()
206 , orientation_text()
207 , block_text()
208 , show_block(false)
209 , show_entity(false)
210 // message box
211 , messages(env.assets.small_ui_font)
212 , msg_timer(5000)
213 // crosshair
214 , crosshair() {
215         // "inventory"
216         block_transform = glm::translate(block_transform, glm::vec3(50.0f, 50.0f, 0.0f));
217         block_transform = glm::scale(block_transform, glm::vec3(50.0f));
218         block_transform = glm::rotate(block_transform, 3.5f, glm::vec3(1.0f, 0.0f, 0.0f));
219         block_transform = glm::rotate(block_transform, 0.35f, glm::vec3(0.0f, 1.0f, 0.0f));
220         block_label.Position(
221                 glm::vec3(50.0f, 85.0f, 0.0f),
222                 Gravity::NORTH_WEST,
223                 Gravity::NORTH
224         );
225         block_label.Foreground(glm::vec4(1.0f));
226         block_label.Background(glm::vec4(0.5f));
227
228         // debug overlay
229         counter_text.Position(glm::vec3(-25.0f, 25.0f, 0.0f), Gravity::NORTH_EAST);
230         counter_text.Foreground(glm::vec4(1.0f));
231         counter_text.Background(glm::vec4(0.5f));
232         position_text.Position(glm::vec3(-25.0f, 25.0f + env.assets.small_ui_font.LineSkip(), 0.0f), Gravity::NORTH_EAST);
233         position_text.Foreground(glm::vec4(1.0f));
234         position_text.Background(glm::vec4(0.5f));
235         orientation_text.Position(glm::vec3(-25.0f, 25.0f + 2 * env.assets.small_ui_font.LineSkip(), 0.0f), Gravity::NORTH_EAST);
236         orientation_text.Foreground(glm::vec4(1.0f));
237         orientation_text.Background(glm::vec4(0.5f));
238         block_text.Position(glm::vec3(-25.0f, 25.0f + 4 * env.assets.small_ui_font.LineSkip(), 0.0f), Gravity::NORTH_EAST);
239         block_text.Foreground(glm::vec4(1.0f));
240         block_text.Background(glm::vec4(0.5f));
241         block_text.Set(env.assets.small_ui_font, "Block: none");
242         entity_text.Position(glm::vec3(-25.0f, 25.0f + 4 * env.assets.small_ui_font.LineSkip(), 0.0f), Gravity::NORTH_EAST);
243         entity_text.Foreground(glm::vec4(1.0f));
244         entity_text.Background(glm::vec4(0.5f));
245         entity_text.Set(env.assets.small_ui_font, "Entity: none");
246
247         // message box
248         messages.Position(glm::vec3(25.0f, -25.0f, 0.0f), Gravity::SOUTH_WEST);
249         messages.Foreground(glm::vec4(1.0f));
250         messages.Background(glm::vec4(0.5f));
251
252         // crosshair
253         OutlineModel::Buffer buf;
254         buf.vertices = std::vector<glm::vec3>({
255                 { -10.0f,   0.0f, 0.0f }, { 10.0f,  0.0f, 0.0f },
256                 {   0.0f, -10.0f, 0.0f }, {  0.0f, 10.0f, 0.0f },
257         });
258         buf.indices = std::vector<OutlineModel::Index>({
259                 0, 1, 2, 3
260         });
261         buf.colors.resize(4, { 10.0f, 10.0f, 10.0f });
262         crosshair.Update(buf);
263 }
264
265 namespace {
266
267 OutlineModel::Buffer outl_buf;
268
269 }
270
271 void HUD::FocusBlock(const Chunk &chunk, int index) {
272         const Block &block = chunk.BlockAt(index);
273         const BlockType &type = chunk.Type(index);
274         outl_buf.Clear();
275         type.FillOutlineModel(outl_buf);
276         outline.Update(outl_buf);
277         outline_transform = chunk.Transform(player.GetEntity().ChunkCoords());
278         outline_transform *= chunk.ToTransform(Chunk::ToPos(index), index);
279         outline_transform *= glm::scale(glm::vec3(1.005f));
280         outline_visible = true;
281         {
282                 std::stringstream s;
283                 s << "Block: "
284                         << type.label
285                         << ", face: " << block.GetFace()
286                         << ", turn: " << block.GetTurn();
287                 block_text.Set(env.assets.small_ui_font, s.str());
288         }
289         show_block = true;
290         show_entity = false;
291 }
292
293 void HUD::FocusEntity(const Entity &entity) {
294         {
295                 std::stringstream s;
296                 s << "Entity: " << entity.Name();
297                 entity_text.Set(env.assets.small_ui_font, s.str());
298         }
299         show_block = false;
300         show_entity = true;
301 }
302
303 void HUD::FocusNone() {
304         outline_visible = false;
305         show_block = false;
306         show_entity = false;
307 }
308
309 void HUD::DisplayNone() {
310         block_visible = false;
311 }
312
313 void HUD::Display(const BlockType &type) {
314         block_buf.Clear();
315         type.FillEntityModel(block_buf);
316         block.Update(block_buf);
317
318         block_label.Set(env.assets.small_ui_font, type.label);
319
320         block_visible = type.visible;
321 }
322
323
324 void HUD::UpdateDebug() {
325         UpdateCounter();
326         UpdatePosition();
327         UpdateOrientation();
328 }
329
330 void HUD::UpdateCounter() {
331         std::stringstream s;
332         s << std::setprecision(3) <<
333                 "avg: " << env.counter.Average().running << "ms, "
334                 "peak: " << env.counter.Peak().running << "ms";
335         std::string text = s.str();
336         counter_text.Set(env.assets.small_ui_font, text);
337 }
338
339 void HUD::UpdatePosition() {
340         std::stringstream s;
341         s << std::setprecision(3) << "pos: " << player.GetEntity().AbsolutePosition();
342         position_text.Set(env.assets.small_ui_font, s.str());
343 }
344
345 void HUD::UpdateOrientation() {
346         //std::stringstream s;
347         //s << std::setprecision(3) << "pitch: " << rad2deg(ctrl.Pitch())
348         //      << ", yaw: " << rad2deg(ctrl.Yaw());
349         //orientation_text.Set(env.assets.small_ui_font, s.str());
350 }
351
352 void HUD::PostMessage(const char *msg) {
353         messages.PushLine(msg);
354         msg_timer.Reset();
355         msg_timer.Start();
356         std::cout << msg << std::endl;
357 }
358
359
360 void HUD::Update(int dt) {
361         msg_timer.Update(dt);
362         if (msg_timer.HitOnce()) {
363                 msg_timer.Stop();
364         }
365
366         if (config.video.debug) {
367                 if (env.counter.Changed()) {
368                         UpdateCounter();
369                 }
370                 UpdatePosition();
371                 UpdateOrientation();
372         }
373 }
374
375 void HUD::Render(Viewport &viewport) noexcept {
376         // block focus
377         if (outline_visible && config.video.world) {
378                 PlainColor &outline_prog = viewport.WorldOutlineProgram();
379                 outline_prog.SetM(outline_transform);
380                 outline.Draw();
381         }
382
383         // clear depth buffer so everything renders above the world
384         viewport.ClearDepth();
385
386         if (config.video.hud) {
387                 // "inventory"
388                 if (block_visible) {
389                         DirectionalLighting &world_prog = viewport.HUDProgram();
390                         world_prog.SetLightDirection({ 1.0f, 3.0f, 5.0f });
391                         // disable distance fog
392                         world_prog.SetFogDensity(0.0f);
393
394                         viewport.DisableBlending();
395                         world_prog.SetM(block_transform);
396                         block.Draw();
397                         block_label.Render(viewport);
398                 }
399
400                 // message box
401                 if (msg_timer.Running()) {
402                         messages.Render(viewport);
403                 }
404
405                 // crosshair
406                 PlainColor &outline_prog = viewport.HUDOutlineProgram();
407                 viewport.EnableInvertBlending();
408                 viewport.SetCursor(glm::vec3(0.0f), Gravity::CENTER);
409                 outline_prog.SetM(viewport.Cursor());
410                 crosshair.Draw();
411         }
412
413         // debug overlay
414         if (config.video.debug) {
415                 counter_text.Render(viewport);
416                 position_text.Render(viewport);
417                 orientation_text.Render(viewport);
418                 if (show_block) {
419                         block_text.Render(viewport);
420                 } else if (show_entity) {
421                         entity_text.Render(viewport);
422                 }
423         }
424 }
425
426
427 InteractiveManipulator::InteractiveManipulator(Environment &env, Entity &player)
428 : player(player)
429 , audio(env.audio)
430 , place_sound(env.loader.LoadSound("thump"))
431 , remove_sound(env.loader.LoadSound("plop")) {
432
433 }
434
435 void InteractiveManipulator::SetBlock(Chunk &chunk, int index, const Block &block) {
436         chunk.SetBlock(index, block);
437         glm::vec3 coords = chunk.ToSceneCoords(player.ChunkCoords(), Chunk::ToCoords(index));
438         // TODO: get sound effect from block type
439         if (block.type == 0) {
440                 audio.Play(remove_sound, coords);
441         } else {
442                 audio.Play(place_sound, coords);
443         }
444 }
445
446
447 Interface::Interface(
448         Config &config,
449         const Keymap &keymap,
450         PlayerController &pc,
451         ClientController &cc)
452 : config(config)
453 , keymap(keymap)
454 , player_ctrl(pc)
455 , client_ctrl(cc)
456 , fwd(0)
457 , rev(0)
458 , slot(0)
459 , num_slots(10) {
460
461 }
462
463
464 void Interface::HandlePress(const SDL_KeyboardEvent &event) {
465         if (!config.input.keyboard) return;
466
467         Keymap::Action action = keymap.Lookup(event);
468         switch (action) {
469                 case Keymap::MOVE_FORWARD:
470                         rev.z = 1;
471                         UpdateMovement();
472                         break;
473                 case Keymap::MOVE_BACKWARD:
474                         fwd.z = 1;
475                         UpdateMovement();
476                         break;
477                 case Keymap::MOVE_LEFT:
478                         rev.x = 1;
479                         UpdateMovement();
480                         break;
481                 case Keymap::MOVE_RIGHT:
482                         fwd.x = 1;
483                         UpdateMovement();
484                         break;
485                 case Keymap::MOVE_UP:
486                         fwd.y = 1;
487                         UpdateMovement();
488                         break;
489                 case Keymap::MOVE_DOWN:
490                         rev.y = 1;
491                         UpdateMovement();
492                         break;
493
494                 case Keymap::PRIMARY:
495                         player_ctrl.StartPrimaryAction();
496                         break;
497                 case Keymap::SECONDARY:
498                         player_ctrl.StartSecondaryAction();
499                         break;
500                 case Keymap::TERTIARY:
501                         player_ctrl.StartTertiaryAction();
502                         break;
503
504                 case Keymap::INV_NEXT:
505                         InvRel(1);
506                         break;
507                 case Keymap::INV_PREVIOUS:
508                         InvRel(-1);
509                         break;
510                 case Keymap::INV_1:
511                 case Keymap::INV_2:
512                 case Keymap::INV_3:
513                 case Keymap::INV_4:
514                 case Keymap::INV_5:
515                 case Keymap::INV_6:
516                 case Keymap::INV_7:
517                 case Keymap::INV_8:
518                 case Keymap::INV_9:
519                 case Keymap::INV_10:
520                         InvAbs(action - Keymap::INV_1);
521                         break;
522
523                 case Keymap::EXIT:
524                         client_ctrl.Exit();
525                         break;
526
527                 case Keymap::TOGGLE_AUDIO:
528                         config.audio.enabled = !config.audio.enabled;
529                         client_ctrl.SetAudio(config.audio.enabled);
530                         break;
531                 case Keymap::TOGGLE_VIDEO:
532                         config.video.world = !config.video.world;
533                         client_ctrl.SetVideo(config.video.world);
534                         break;
535                 case Keymap::TOGGLE_HUD:
536                         config.video.hud = !config.video.hud;
537                         client_ctrl.SetHUD(config.video.hud);
538                         break;
539                 case Keymap::TOGGLE_DEBUG:
540                         config.video.debug = !config.video.debug;
541                         client_ctrl.SetDebug(config.video.debug);
542                         break;
543
544                 default:
545                         break;
546         }
547 }
548
549 void Interface::HandleRelease(const SDL_KeyboardEvent &event) {
550         if (!config.input.keyboard) return;
551
552         switch (keymap.Lookup(event)) {
553                 case Keymap::MOVE_FORWARD:
554                         rev.z = 0;
555                         UpdateMovement();
556                         break;
557                 case Keymap::MOVE_BACKWARD:
558                         fwd.z = 0;
559                         UpdateMovement();
560                         break;
561                 case Keymap::MOVE_LEFT:
562                         rev.x = 0;
563                         UpdateMovement();
564                         break;
565                 case Keymap::MOVE_RIGHT:
566                         fwd.x = 0;
567                         UpdateMovement();
568                         break;
569                 case Keymap::MOVE_UP:
570                         fwd.y = 0;
571                         UpdateMovement();
572                         break;
573                 case Keymap::MOVE_DOWN:
574                         rev.y = 0;
575                         UpdateMovement();
576                         break;
577
578                 case Keymap::PRIMARY:
579                         player_ctrl.StopPrimaryAction();
580                         break;
581                 case Keymap::SECONDARY:
582                         player_ctrl.StopSecondaryAction();
583                         break;
584                 case Keymap::TERTIARY:
585                         player_ctrl.StopTertiaryAction();
586                         break;
587
588                 default:
589                         break;
590         }
591 }
592
593 void Interface::Handle(const SDL_MouseMotionEvent &event) {
594         if (!config.input.mouse) return;
595         player_ctrl.TurnHead(
596                 event.yrel * config.input.pitch_sensitivity,
597                 event.xrel * config.input.yaw_sensitivity);
598 }
599
600 void Interface::HandlePress(const SDL_MouseButtonEvent &event) {
601         if (!config.input.mouse) return;
602
603         switch (event.button) {
604                 case SDL_BUTTON_LEFT:
605                         player_ctrl.StartPrimaryAction();
606                         break;
607                 case SDL_BUTTON_RIGHT:
608                         player_ctrl.StartSecondaryAction();
609                         break;
610                 case SDL_BUTTON_MIDDLE:
611                         player_ctrl.StartTertiaryAction();
612                         break;
613         }
614 }
615
616 void Interface::HandleRelease(const SDL_MouseButtonEvent &event) {
617         if (!config.input.mouse) return;
618
619         switch (event.button) {
620                 case SDL_BUTTON_LEFT:
621                         player_ctrl.StopPrimaryAction();
622                         break;
623                 case SDL_BUTTON_RIGHT:
624                         player_ctrl.StopSecondaryAction();
625                         break;
626                 case SDL_BUTTON_MIDDLE:
627                         player_ctrl.StopTertiaryAction();
628                         break;
629         }
630 }
631
632
633 void Interface::Handle(const SDL_MouseWheelEvent &event) {
634         if (!config.input.mouse) return;
635
636         if (event.y < 0) {
637                 InvRel(1);
638         } else if (event.y > 0) {
639                 InvRel(-1);
640         }
641 }
642
643 void Interface::UpdateMovement() {
644         player_ctrl.SetMovement(glm::vec3(fwd - rev));
645 }
646
647 void Interface::InvAbs(int s) {
648         slot = s % num_slots;
649         while (slot < 0) {
650                 slot += num_slots;
651         }
652         player_ctrl.SelectInventory(slot);
653 }
654
655 void Interface::InvRel(int delta) {
656         InvAbs(slot + delta);
657 }
658
659
660 Keymap::Keymap()
661 : codemap{ NONE } {
662
663 }
664
665 void Keymap::Map(SDL_Scancode scancode, Action action) {
666         if (scancode > MAX_SCANCODE) {
667                 throw std::runtime_error("refusing to map scancode: too damn high");
668         }
669         codemap[scancode] = action;
670 }
671
672 Keymap::Action Keymap::Lookup(SDL_Scancode scancode) const {
673         if (scancode < NUM_SCANCODES) {
674                 return codemap[scancode];
675         } else {
676                 return NONE;
677         }
678 }
679
680
681 void Keymap::LoadDefault() {
682         Map(SDL_SCANCODE_UP, MOVE_FORWARD);
683         Map(SDL_SCANCODE_W, MOVE_FORWARD);
684         Map(SDL_SCANCODE_DOWN, MOVE_BACKWARD);
685         Map(SDL_SCANCODE_S, MOVE_BACKWARD);
686         Map(SDL_SCANCODE_LEFT, MOVE_LEFT);
687         Map(SDL_SCANCODE_A, MOVE_LEFT);
688         Map(SDL_SCANCODE_RIGHT, MOVE_RIGHT);
689         Map(SDL_SCANCODE_D, MOVE_RIGHT);
690         Map(SDL_SCANCODE_SPACE, MOVE_UP);
691         Map(SDL_SCANCODE_RSHIFT, MOVE_UP);
692         Map(SDL_SCANCODE_LSHIFT, MOVE_DOWN);
693         Map(SDL_SCANCODE_LCTRL, MOVE_DOWN);
694         Map(SDL_SCANCODE_RCTRL, MOVE_DOWN);
695
696         Map(SDL_SCANCODE_TAB, INV_NEXT);
697         Map(SDL_SCANCODE_RIGHTBRACKET, INV_NEXT);
698         Map(SDL_SCANCODE_LEFTBRACKET, INV_PREVIOUS);
699         Map(SDL_SCANCODE_1, INV_1);
700         Map(SDL_SCANCODE_2, INV_2);
701         Map(SDL_SCANCODE_3, INV_3);
702         Map(SDL_SCANCODE_4, INV_4);
703         Map(SDL_SCANCODE_5, INV_5);
704         Map(SDL_SCANCODE_6, INV_6);
705         Map(SDL_SCANCODE_7, INV_7);
706         Map(SDL_SCANCODE_8, INV_8);
707         Map(SDL_SCANCODE_9, INV_9);
708         Map(SDL_SCANCODE_0, INV_10);
709
710         Map(SDL_SCANCODE_INSERT, SECONDARY);
711         Map(SDL_SCANCODE_RETURN, SECONDARY);
712         Map(SDL_SCANCODE_MENU, TERTIARY);
713         Map(SDL_SCANCODE_DELETE, PRIMARY);
714         Map(SDL_SCANCODE_BACKSPACE, PRIMARY);
715
716         Map(SDL_SCANCODE_F1, TOGGLE_HUD);
717         Map(SDL_SCANCODE_F2, TOGGLE_VIDEO);
718         Map(SDL_SCANCODE_F3, TOGGLE_DEBUG);
719         Map(SDL_SCANCODE_F4, TOGGLE_AUDIO);
720
721         Map(SDL_SCANCODE_ESCAPE, EXIT);
722 }
723
724
725 void Keymap::Load(std::istream &is) {
726         TokenStreamReader in(is);
727         std::string key_name;
728         std::string action_name;
729         SDL_Scancode key;
730         Action action;
731         while (in.HasMore()) {
732                 if (in.Peek().type == Token::STRING) {
733                         in.ReadString(key_name);
734                         key = SDL_GetScancodeFromName(key_name.c_str());
735                 } else {
736                         key = SDL_Scancode(in.GetInt());
737                 }
738                 in.Skip(Token::EQUALS);
739                 in.ReadIdentifier(action_name);
740                 action = StringToAction(action_name);
741                 if (in.HasMore() && in.Peek().type == Token::SEMICOLON) {
742                         in.Skip(Token::SEMICOLON);
743                 }
744                 Map(key, action);
745         }
746 }
747
748 void Keymap::Save(std::ostream &out) {
749         for (unsigned int i = 0; i < NUM_SCANCODES; ++i) {
750                 if (codemap[i] == NONE) continue;
751
752                 const char *str = SDL_GetScancodeName(SDL_Scancode(i));
753                 if (str && *str) {
754                         out << '"';
755                         while (*str) {
756                                 if (*str == '"') {
757                                         out << "\\\"";
758                                 } else {
759                                         out << *str;
760                                 }
761                                 ++str;
762                         }
763                         out << '"';
764                 } else {
765                         out << i;
766                 }
767
768                 out << " = " << ActionToString(codemap[i]) << std::endl;;
769         }
770 }
771
772
773 namespace {
774
775 std::map<std::string, Keymap::Action> action_map = {
776         { "none", Keymap::NONE },
777         { "move_forward", Keymap::MOVE_FORWARD },
778         { "move_backward", Keymap::MOVE_BACKWARD },
779         { "move_left", Keymap::MOVE_LEFT },
780         { "move_right", Keymap::MOVE_RIGHT },
781         { "move_up", Keymap::MOVE_UP },
782         { "move_down", Keymap::MOVE_DOWN },
783
784         { "primary", Keymap::PRIMARY },
785         { "secondary", Keymap::SECONDARY },
786         { "tertiary", Keymap::TERTIARY },
787
788         { "inventory_next", Keymap::INV_NEXT },
789         { "inventory_prev", Keymap::INV_PREVIOUS },
790         { "inventory_1", Keymap::INV_1 },
791         { "inventory_2", Keymap::INV_2 },
792         { "inventory_3", Keymap::INV_3 },
793         { "inventory_4", Keymap::INV_4 },
794         { "inventory_5", Keymap::INV_5 },
795         { "inventory_6", Keymap::INV_6 },
796         { "inventory_7", Keymap::INV_7 },
797         { "inventory_8", Keymap::INV_8 },
798         { "inventory_9", Keymap::INV_9 },
799         { "inventory_10", Keymap::INV_10 },
800
801         { "toggle_audio", Keymap::TOGGLE_AUDIO },
802         { "toggle_video", Keymap::TOGGLE_VIDEO },
803         { "toggle_hud", Keymap::TOGGLE_HUD },
804         { "toggle_debug", Keymap::TOGGLE_DEBUG },
805
806         { "exit", Keymap::EXIT },
807 };
808
809 }
810
811 const char *Keymap::ActionToString(Action action) {
812         for (const auto &entry : action_map) {
813                 if (action == entry.second) {
814                         return entry.first.c_str();
815                 }
816         }
817         return "none";
818 }
819
820 Keymap::Action Keymap::StringToAction(const std::string &str) {
821         auto entry = action_map.find(str);
822         if (entry != action_map.end()) {
823                 return entry->second;
824         } else {
825                 std::cerr << "unknown action \"" << str << '"' << std::endl;
826                 return NONE;
827         }
828 }
829
830 }