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