ref: 3a71a567e26efd18a100f0c000c5173f38c99df2
dir: /plan9.c/
#include <npe.h>
#include <thread.h>
#include <draw.h>
#include <mouse.h>
#include <keyboard.h>
#include <control.h>
/********************
usage and controls:
n - new game
u - undo
r - redo
s - solve
Everything else is forwarded to the game.
If the game doesn't redraw properly, try pressing the "game"
button.
*********************/
#undef PI
#include "puzzles.h"
#ifdef COMBINED
#error Plan 9 should not be COMBINED
#endif
//#define PROFILE
int dolog = 0;
int logfd = -1;
int logpipe[2];
#define LOG(c) { if (dolog) fprint(logpipe[1], "%s\n", c); }
void
Log(char *fmt, ...)
{
va_list arg;
if (!dolog)
return;
va_start(arg, fmt);
vfprint(logfd, fmt, arg);
va_end(arg);
}
struct frontend {
midend *me;
Image *background;
Image **colors;
int ncolors;
Point ZP;
Rectangle rect;
Controlset *cs;
Channel *c;
Channel *settingschan;
int showframe;
int timeractive;
};
struct blitter {
Image *blimg;
};
frontend *fe = nil;
struct proftimes {
long draw;
} ptimes;
enum {
GAME = 0,
SETTINGS = 1,
};
void
frontend_default_colour(frontend *, float *output)
{
output[0] = .9;
output[1] = .9;
output[2] = .9;
}
void
get_random_seed(void **randseed, int *randseedsize)
{
long *t = malloc(sizeof(long));
assert(t);
time(t);
*randseed = (void*)t;
*randseedsize = sizeof(long);
}
void
deactivate_timer(frontend *fe)
{
fe->timeractive = 0;
}
void activate_timer(frontend *fe)
{
fe->timeractive = 1;
}
void fatal(const char *fmt, ...)
{
va_list ap;
fprint(2, "fatal error: ");
va_start(ap, fmt);
vfprint(2, fmt, ap);
va_end(ap);
fprint(2, "\n");
sysfatal("error");
}
#ifdef DEBUGGING
void debug_printf(const char *fmt, ...)
{
va_list ap;
va_start(ap, fmt);
vfprint(1, fmt, ap);
va_end(ap);
}
#endif
void
usage(void)
{
fprint(2, "usage: %s [-ho] [--options]\n", argv0);
exits(nil);
}
char *showcmd = "c_game show";
static void p9_draw_text(void *handle, int x, int y, int fonttype, int fontsize, int align, int color, const char *text)
{
// todo: align, fontsize, fonttype
frontend *fe = (frontend*)handle;
string(screen, addpt(Pt(x, y), fe->ZP), fe->colors[color], ZP, font, text);
}
static void
p9_draw_rect(void *handle, int x, int y, int w, int h, int color)
{
frontend *fe = (frontend*)handle;
draw(screen, rectaddpt(Rect(x, y, x+w, y+h), fe->ZP), fe->colors[color], nil, ZP);
}
static void
p9_draw_line(void *handle, int x1, int y1, int x2, int y2, int color)
{
frontend *fe = (frontend*)handle;
line(screen, addpt(Pt(x1, y1), fe->ZP), addpt(Pt(x2, y2), fe->ZP), Endsquare, Endsquare, 0, fe->colors[color], ZP);
}
static void
p9_draw_thick_line(void *handle, float thickness, float x1, float y1, float x2, float y2, int color)
{
frontend *fe = (frontend*)handle;
line(screen, addpt(Pt(x1, y1), fe->ZP), addpt(Pt(x2, y2), fe->ZP), Endsquare, Endsquare, thickness-1, fe->colors[color], ZP);
}
static void
p9_draw_poly(void *handle, const int *coords, int npoints, int fillcolor, int outlinecolor)
{
Point *points;
frontend *fe = (frontend*)handle;
points = malloc(npoints * sizeof(Point));
for (int i = 0; i < npoints; i++) {
points[i].x = coords[i*2+0] + fe->ZP.x;
points[i].y = coords[i*2+1] + fe->ZP.y;
}
if (fillcolor > 0)
fillpoly(screen, points, npoints, 0, fe->colors[fillcolor], ZP);
if (outlinecolor > 0)
poly(screen, points, npoints, Endsquare, Endsquare, 1, fe->colors[outlinecolor], ZP);
free(points);
}
static void
p9_draw_circle(void *handle, int cx, int cy, int radius, int fillcolor, int outlinecolor)
{
frontend *fe = (frontend*)handle;
Point c = addpt(Pt(cx, cy), fe->ZP);
fillellipse(screen, c, radius, radius, fe->colors[fillcolor], ZP);
ellipse(screen, c, radius, radius, 0, fe->colors[outlinecolor], ZP);
}
static void
p9_draw_update(void *handle, int x, int y, int w, int h)
{
USED(handle, x, y, w, h);
//frontend *fe = (frontend*)handle;
}
static void
p9_clip(void *handle, int x, int y, int w, int h)
{
frontend *fe = (frontend*)handle;
screen->clipr = rectaddpt(Rect(x, y, x + w, y + h), fe->ZP);
}
static void
p9_unclip(void *handle)
{
frontend *fe = (frontend*)handle;
screen->clipr = screen->r;
}
long drawtime;
static void
p9_start_draw(void *handle)
{
USED(handle);
LOG("start_draw");
#ifdef PROFILE
drawtime = times(nil);
#endif
}
static void
p9_end_draw(void *handle)
{
frontend *fe = (frontend*)handle;
flushimage(display, 1);
LOG("end_draw");
#ifdef PROFILE
ptimes.draw = times(nil) - drawtime;
#endif
}
static void
p9_status_bar(void *handle, const char *text)
{
frontend *fe = (frontend*)handle;
chanprint(fe->cs->ctl, "l_status value %q", text);
}
static blitter*
p9_blitter_new(void *handle, int w, int h)
{
blitter *bl;
USED(handle);
bl = malloc(sizeof(blitter));
bl->blimg = allocimage(display, Rect(0, 0, w, h), screen->chan, 0, 0);
return bl;
}
static void
p9_blitter_free(void *handle, blitter *bl)
{
USED(handle);
freeimage(bl->blimg);
free(bl);
}
static void
p9_blitter_save(void *handle, blitter *bl, int x, int y)
{
frontend *fe = (frontend*)handle;
draw(bl->blimg, Rect(x, y, x + bl->blimg->r.max.x, y + bl->blimg->r.max.y), screen, nil, Pt(x, y)); // fix position
}
static void
p9_blitter_load(void *handle, blitter *bl, int x, int y)
{
frontend *fe = (frontend*)handle;
draw(screen, Rect(x, y, x + bl->blimg->r.max.x, y + bl->blimg->r.max.y), bl->blimg, nil, Pt(x, y)); // fix position
}
static const drawing_api p9_drawing = {
p9_draw_text,
p9_draw_rect,
p9_draw_line,
p9_draw_poly,
p9_draw_circle,
p9_draw_update,
p9_clip,
p9_unclip,
p9_start_draw,
p9_end_draw,
p9_status_bar,
p9_blitter_new,
p9_blitter_free,
p9_blitter_save,
p9_blitter_load,
nil, nil, nil, nil, nil, nil, nil, nil, /* {begin,end}_{doc,page,puzzle}, line_width, line_dotted */
nil, /* text_fallback */
#ifdef NO_THICK_LINE
nil,
#else
p9_draw_thick_line,
#endif
};
static int rgb2col(int r, int g, int b)
{
return (r<<24) | (g<<16) | (b<<8) | 0xFF;
}
static frontend*
new_window(void)
{
frontend *fe;
fe = mallocz(sizeof(frontend), 1);
if (!fe)
sysfatal("error: out of memory!");
fe->me = midend_new(fe, &thegame, &p9_drawing, fe);
return fe;
}
void
initui(Controlset *cs, Channel *c)
{
Control *b_game, *b_settings, *c_settings;
Point p;
createrow(cs, "rowmain");
b_game = createtextbutton(cs, "b_game");
p = stringsize(font, "game");
chanprint(cs->ctl, "b_game border 1");
chanprint(cs->ctl, "b_game align center");
chanprint(cs->ctl, "b_game text game");
chanprint(cs->ctl, "b_game size %d %d 500 %d", p.x, p.y, p.y);
controlwire(b_game, "event", c);
b_settings = createtextbutton(cs, "b_settings");
p = stringsize(font, "settings");
chanprint(cs->ctl, "b_settings border 1");
chanprint(cs->ctl, "b_settings align center");
chanprint(cs->ctl, "b_settings text settings");
chanprint(cs->ctl, "b_settings size %d %d 500 %d", p.x, p.y, p.y);
c_settings = createcolumn(cs, "c_settings");
chanprint(cs->ctl, "c_settings hide");
controlwire(b_settings, "event", c);
createlabel(cs, "l_status");
chanprint(cs->ctl, "rowmain add b_game\nrowmain add b_settings");
activate(b_game);
activate(b_settings);
}
void
initfe(frontend *fe, Mousectl *mousectl)
{
float *colors;
int ncolors;
int r, g, b;
float bgcol[3];
Channel *c, *d;
frontend_default_colour(fe, bgcol);
fe->background = allocimage(display, Rect(0, 0, 1, 1), screen->chan, 1, rgb2col(bgcol[0]*255., bgcol[1]*255., bgcol[2]*255.));
colors = midend_colours(fe->me, &ncolors);
fe->colors = mallocz(ncolors * sizeof(Image*), 1);
for (int i = 0; i < ncolors; i++) {
r = colors[i*3+0] * 255.;
g = colors[i*3+1] * 255.;
b = colors[i*3+2] * 255.;
fe->colors[i] = allocimage(display, Rect(0, 0, 1, 1), screen->chan, 1, rgb2col(r, g, b));
}
free(colors);
fe->settingschan = chancreate(sizeof(char*), 0);
c = chancreate(sizeof(Mouse), 0);
d = chancreate(sizeof(Rune), 0);
fe->cs = newcontrolset(screen, d, c, mousectl->resizec);
fe->c = chancreate(sizeof(char*), 0);
ctldeletequits = 1;
namectlimage(display->black, "i_black");
namectlimage(display->white, "i_white");
namectlimage(fe->background, "background");
initui(fe->cs, fe->c);
}
int windowset = 0;
Point
resize(int *resizenop)
{
int x, y, fd;
x = Dx(screen->r);
y = Dy(screen->r) - 2*font->height;
midend_size(fe->me, &x, &y, 1, 1.);
// to test
if (0 && !windowset) {
fd = open("/dev/wctl", OWRITE);
if (fd >= 0) {
fprint(fd, "resize -dx %d -dy %d\n", x+2, y + 2*font->height);
close(fd);
windowset = 1;
}
} else
windowset = 0;
/* do not resize if we're waiting for the window resize */
*resizenop = windowset;
return Pt(x, y);
}
void
resizecontrolset(Controlset *cs)
{
Rectangle rmenu, rarea, sarea;
int resizenop;
Control *ctl;
Point newsize;
if (getwindow(display, Refnone) < 0) {
sysfatal("resize failed: %r");
}
rmenu = screen->r;
rmenu.max.y = rmenu.min.y + font->height;
rarea = screen->r;
rarea.min.y = rmenu.min.y + font->height;
rarea.max.y = rarea.max.y - font->height;
sarea = screen->r;
sarea.min.y = sarea.max.y - font->height;
newsize = resize(&resizenop);
if (0 && resizenop)
return;
draw(screen, screen->r, fe->background, nil, ZP);
fe->rect = rarea;
fe->ZP = rarea.min;
chanprint(cs->ctl, "rowmain rect %R\nrowmain show", rmenu);
chanprint(cs->ctl, "l_status rect %R\nl_status show", sarea);
switch (fe->showframe) {
case GAME:
chanprint(cs->ctl, "c_settings hide");
midend_force_redraw(fe->me);
flushimage(display, 1);
break;
case SETTINGS:
chanprint(cs->ctl, "c_settings rect %R\nc_settings reveal\nc_settings show", rarea);
break;
}
LOG("resizecontrolset");
}
void
printoptions(config_item *c)
{
char *t;
char *s = nil, *cnames[16], *ckws[16];
int n = 0, m;
config_item *cfg = midend_get_config(fe->me, CFG_PREFS, &t);
print("Options:\n");
while (cfg->type != C_END) {
switch (cfg->type) {
case C_STRING:
s = cfg->u.string.sval;
break;
case C_BOOLEAN:
s = cfg->u.boolean.bval ? "1" : "0";
break;
case C_CHOICES:
print(" Choices:\n");
n = getfields(cfg->u.choices.choicenames, cnames, 16, 1, ":");
m = getfields(cfg->u.choices.choicekws, ckws, 16, 1, ":");
assert(n == m && cfg->u.choices.selected < n);
s = ckws[cfg->u.choices.selected];
break;
}
print("--%s=%s\n %s\n", cfg->kw, s, cfg->name);
if (cfg->type == C_CHOICES) {
print(" Choices:\n");
for (int i = 0; i < n; i++) {
print(" %s: %s\n", ckws[i], cnames[i]);
}
}
cfg++;
}
}
void
parseoption(config_item *cfg, char *keyval)
{
char *arg[2];
int n;
n = getfields(keyval, arg, 2, 1, "=");
if (n != 2)
usage(); // exits
while (cfg && cfg->type != C_END)
if (strcmp(cfg->kw, arg[0]) != 0)
cfg++;
if (!cfg || cfg->type == C_END) {
fprint(2, "no valid option\n");
return;
}
print("%s : %s\n", cfg->kw, cfg->name);
return;
switch (cfg->type) {
case C_STRING:
cfg->u.string.sval = arg[1];
print("is string");
break;
case C_BOOLEAN:
n = atoi(arg[1]);
print("is boolean");
cfg->u.boolean.bval = n ? 1 : 0;
break;
case C_CHOICES:
// TODO
print("is choices");
fprint(2, "not implemented yet!\n");
break;
case C_END:
default:
print("not found");
break;
}
}
void
addoption(config_item *cfg)
{
char buf[128];
int isnew;
Control *label, *entry;
snprint(buf, 128, "l_%s", cfg->name);
label = controlcalled(buf);
isnew = 0;
if (!label) {
label = createlabel(fe->cs, buf);
isnew = 1;
}
chanprint(fe->cs->ctl, "%q value %q", buf, cfg->name);
chanprint(fe->cs->ctl, "%q align center", buf);
chanprint(fe->cs->ctl, "%q size 100 %d 500 %d", buf, font->height*2, font->height*2);
if (isnew)
chanprint(fe->cs->ctl, "c_settings add %q", buf);
snprint(buf, 128, "e_%s", cfg->name);
isnew = 0;
entry = controlcalled(buf);
if (!entry)
isnew = 1;
switch (cfg->type) {
case C_STRING:
if (!entry)
entry = createentry(fe->cs, buf);
chanprint(fe->cs->ctl, "%q border 1", buf);
chanprint(fe->cs->ctl, "%q size 100 %d 500 %d", buf, font->height, font->height);
chanprint(fe->cs->ctl, "%q value %q", buf, cfg->u.string.sval);
break;
case C_BOOLEAN:
if (!entry)
entry = createbutton(fe->cs, buf);
chanprint(fe->cs->ctl, "%q border 1", buf);
chanprint(fe->cs->ctl, "%q size 100 %d 500 %d", buf, font->height, font->height);
chanprint(fe->cs->ctl, "%q value %d", buf, cfg->u.boolean.bval);
break;
case C_CHOICES:
// todo
break;
}
if (entry && isnew) {
controlwire(entry, "event", fe->settingschan);
activate(entry);
chanprint(fe->cs->ctl, "c_settings add %q", buf);
}
}
int configcats[] = { CFG_SETTINGS, /* CFG_SEED, */ CFG_PREFS };
config_item *configs[] = { nil, /* nil, */ nil };
void
loadoptions(void)
{
char *t;
config_item *cfg;
Control *c, *info;
for (int i = 0; i < nelem(configcats); i++) {
cfg = midend_get_config(fe->me, configcats[i], &t);
if (configs[i]) {
//free_cfg(configs[i]);
configs[i] = nil;
}
configs[i] = cfg;
while (cfg && cfg->type != C_END) {
addoption(cfg);
cfg++;
}
}
c = controlcalled("b_savecfg");
/* if already set up, early return */
if (c)
return;
info = createlabel(fe->cs, "l_cfginfo");
chanprint(fe->cs->ctl, "l_cfginfo align centerleft");
chanprint(fe->cs->ctl, "l_cfginfo size 50 %d 500 %d", font->height, font->height);
chanprint(fe->cs->ctl, "c_settings add l_cfginfo");
c = createtextbutton(fe->cs, "b_savecfg");
chanprint(fe->cs->ctl, "b_savecfg text Save");
chanprint(fe->cs->ctl, "b_savecfg border 1");
chanprint(fe->cs->ctl, "b_savecfg align center");
chanprint(fe->cs->ctl, "b_savecfg size 50 %d 100 %d", font->height, font->height);
chanprint(fe->cs->ctl, "c_settings add b_savecfg");
controlwire(c, "event", fe->c);
activate(c);
}
int
saveoptions(void)
{
char *s;
int r = 1;
for (int i = 0; i < nelem(configcats); i++) {
s = midend_set_config(fe->me, configcats[i], configs[i]);
if (s) {
chanprint(fe->cs->ctl, "l_cfginfo value %q", s);
r = 0;
}
}
loadoptions();
LOG("saved options");
return r;
}
void
setoption(char *name, char *value)
{
int n;
config_item *cfg;
for (int i = 0; i < nelem(configs); i++) {
cfg = configs[i];
while (cfg && cfg->type != C_END) {
if (strcmp(cfg->name, name) == 0)
goto Found;
cfg++;
}
}
return;
Found:
switch (cfg->type) {
case C_STRING:
cfg->u.string.sval = value;
break;
case C_BOOLEAN:
n = atoi(value);
cfg->u.boolean.bval = n ? 1 : 0;
break;
case C_CHOICES:
// todo
break;
}
}
void
showframe(int frame)
{
fe->showframe = frame;
resizecontrolset(fe->cs);
LOG("showframe");
}
int
keyev(Rune k)
{
if (fe->showframe != GAME) {
send(fe->cs->kbdc, &k);
return 0;
}
switch (k) {
case 'q':
case 127:
return 1; /* return 1 to quit */
case 'n':
midend_process_key(fe->me, 0, 0, UI_NEWGAME);
break;
case 'u':
midend_process_key(fe->me, 0, 0, UI_UNDO);
break;
case 'r':
midend_process_key(fe->me, 0, 0, UI_REDO);
break;
case 's':
midend_process_key(fe->me, 0, 0, UI_SOLVE);
break;
default:
if (midend_process_key(fe->me, 0, 0, k) == PKR_QUIT)
return 1;
}
return 0;
}
void
tick(float delta)
{
#ifdef PROFILE
char msg[128];
#endif
if (fe->timeractive) {
midend_timer(fe->me, delta);
}
#ifdef PROFILE
snprint(msg, 128, "draw: %ld", ptimes.draw);
chanprint(fe->cs->ctl, "l_status value %q", msg);
#endif
}
typedef struct Tickinfo Tickinfo;
struct Tickinfo {
long totaltime;
long lasttick;
float delta;
};
void
timerproc(void *v)
{
Channel *c;
Tickinfo ti;
long newtime;
c = v;
ti.totaltime = times(nil);
for (;;) {
sleep(20);
newtime = times(nil);
ti.delta = (newtime - ti.totaltime) / 1000.;
ti.totaltime = newtime;
send(c, &ti.delta);
}
}
void
processmouse(Mouse *m, int *lm)
{
int x, y, r;
if (fe->showframe != GAME)
goto Ctrl;
if (!ptinrect(m->xy, fe->rect))
goto Ctrl;
x = m->xy.x - fe->rect.min.x;
y = m->xy.y - fe->rect.min.y;
r = -1;
if ( ((*lm)&1) && !(m->buttons&1))
r = midend_process_key(fe->me, x, y, LEFT_RELEASE);
if ( ((*lm)&1) && (m->buttons&1))
r = midend_process_key(fe->me, x, y, LEFT_DRAG);
if (!((*lm)&1) && (m->buttons&1))
r = midend_process_key(fe->me, x, y, LEFT_BUTTON);
if ( ((*lm)&2) && !(m->buttons&2))
r = midend_process_key(fe->me, x, y, MIDDLE_RELEASE);
if ( ((*lm)&2) && (m->buttons&2))
r = midend_process_key(fe->me, x, y, MIDDLE_DRAG);
if (!((*lm)&2) && (m->buttons&2))
r = midend_process_key(fe->me, x, y, MIDDLE_BUTTON);
if ( ((*lm)&4) && !(m->buttons&4))
r = midend_process_key(fe->me, x, y, RIGHT_RELEASE);
if ( ((*lm)&4) && (m->buttons&4))
r = midend_process_key(fe->me, x, y, RIGHT_DRAG);
if (!((*lm)&4) && (m->buttons&4))
r = midend_process_key(fe->me, x, y, RIGHT_BUTTON);
if (r >= 0) {
}
*lm = m->buttons;
return;
Ctrl:
send(fe->cs->mousec, m);
}
void
doexit(void)
{
if (dolog) {
close(logfd);
}
}
void
threadmain(int argc, char **argv)
{
int lastmouse;
char *s, *args[6];
int doprintoptions = 0;
char *wintitle;
config_item *cfg;
int changedprefs = 0;
float delta;
Mousectl *mousectl;
Mouse m;
Keyboardctl *keyboardctl;
Rune rune;
Alt a[] = {
{ nil, &s, CHANRCV },
{ nil, &delta, CHANRCV },
{ nil, &m, CHANRCV },
{ nil, &rune, CHANRCV },
{ nil, &s, CHANRCV },
{ nil, nil, CHANEND },
};
fe = new_window();
wintitle = nil;
cfg = midend_get_config(fe->me, CFG_SETTINGS, &wintitle);
ARGBEGIN{
case 'h':
usage();
break;
case 'o':
doprintoptions++;
break;
case 'l':
dolog++;
break;
case '-':
parseoption(cfg, ARGF());
changedprefs++;
break;
}ARGEND;
if (changedprefs) {
s = midend_set_config(fe->me, CFG_SETTINGS, cfg);
if (s) {
fprint(2, "error: %s\n", s);
exits("error");
}
}
if (doprintoptions) {
printoptions(cfg);
exits(nil);
}
atexit(doexit);
if (dolog) {
pipe(logpipe);
logfd = create("/srv/puzzles", OWRITE|ORCLOSE, 0666);
if (logfd < 0)
sysfatal("error opening log file /srv/puzzles: %r");
fprint(logfd, "%d", logpipe[0]);
close(logpipe[0]);
LOG(thegame.name);
}
if (initdraw(nil, nil, thegame.name) < 0) {
sysfatal("initdraw failed: %r");
}
mousectl = initmouse(nil, screen);
keyboardctl = initkeyboard(nil);
initcontrols();
initfe(fe, mousectl);
loadoptions();
midend_new_game(fe->me);
resizecontrolset(fe->cs);
a[0].c = fe->c;
a[1].c = chancreate(sizeof(float), 0);
a[2].c = mousectl->c;
a[3].c = keyboardctl->c;
a[4].c = fe->settingschan;
proccreate(timerproc, a[1].c, 4096);
for (;;) {
switch (alt(a)) {
case 0: /* libcontrol event channel */
tokenize(s, args, nelem(args));
if (strcmp(args[0], "b_game:") == 0) {
showframe(GAME);
chanprint(fe->cs->ctl, "b_game value 0");
} else
if (strcmp(args[0], "b_settings:") == 0) {
showframe(SETTINGS);
chanprint(fe->cs->ctl, "b_settings value 0");
} else
if (strcmp(args[0], "b_savecfg:") == 0) {
chanprint(fe->cs->ctl, "b_savecfg value 0");
if (saveoptions()) {
midend_new_game(fe->me);
showframe(GAME);
}
} else {
Log("unknown command: %s\n", args[0]);
}
break;
case 1: /* timer */
tick(delta);
break;
case 2: /* mouse */
processmouse(&m, &lastmouse);
break;
case 3: /* keyboard */
if (keyev(rune))
goto Out;
break;
case 4: /* settings */
tokenize(s, args, nelem(args));
s = args[0] + 2; /* get rid of "e_" */
s[strlen(s)-1] = 0; /* get rid of sender ":" */
setoption(s, args[2]);
break;
}
}
Out:
threadexitsall(nil);
}