Save/load game before after/payments, working payments

main
Cameron Murphy Reikes 2 years ago
parent c3d6a3b0b6
commit 11b1c0964e

@ -10,9 +10,10 @@ call run_codegen.bat || goto :error
set FLAGS=-s TOTAL_STACK=5242880 set FLAGS=-s TOTAL_STACK=5242880
copy marketing_page\favicon.ico build_web\favicon.ico copy marketing_page\favicon.ico build_web\favicon.ico
copy main.c build_web\main.c || goto :error
@echo on @echo on
emcc -sEXPORTED_FUNCTIONS=_main,_end_text_input,_stop_controlling_input,_start_controlling_input -sEXPORTED_RUNTIME_METHODS=ccall,cwrap -O0 -s ALLOW_MEMORY_GROWTH %FLAGS% --source-map-base ../ -gsource-map -DDEVTOOLS -Ithirdparty -Igen main.c -o build_web\index.html --preload-file assets --shell-file web_template.html || goto :error emcc -sEXPORTED_FUNCTIONS=_main,_end_text_input,_stop_controlling_input,_start_controlling_input,_read_from_save_data,_dump_save_data,_in_dialog -sEXPORTED_RUNTIME_METHODS=ccall,cwrap -O0 -s ALLOW_MEMORY_GROWTH %FLAGS% --source-map-base http://localhost:8000/ -gsource-map -g3 -gdwarf -DDEVTOOLS -Ithirdparty -Igen main.c -o build_web\index.html --preload-file assets --shell-file web_template.html || goto :error
@echo off @echo off
goto :EOF goto :EOF

@ -13,7 +13,7 @@ copy marketing_page\eye_closed.svg build_web_release\eye_closed.svg
copy marketing_page\eye_open.svg build_web_release\eye_open.svg copy marketing_page\eye_open.svg build_web_release\eye_open.svg
echo Building release echo Building release
emcc -sEXPORTED_FUNCTIONS=_main,_end_text_input,_stop_controlling_input,_start_controlling_input -sEXPORTED_RUNTIME_METHODS=ccall,cwrap -DNDEBUG -O2 -s ALLOW_MEMORY_GROWTH %FLAGS% -Ithirdparty -Igen main.c -o build_web_release\index.html --preload-file assets --shell-file web_template.html || goto :error emcc -sEXPORTED_FUNCTIONS=_main,_end_text_input,_stop_controlling_input,_start_controlling_input,_read_from_save_data,_dump_save_data,_in_dialog -sEXPORTED_RUNTIME_METHODS=ccall,cwrap -DNDEBUG -O2 -s ALLOW_MEMORY_GROWTH %FLAGS% -Ithirdparty -Igen main.c -o build_web_release\index.html --preload-file assets --shell-file web_template.html || goto :error
goto :EOF goto :EOF

163
main.c

@ -1,3 +1,6 @@
// you will die someday
#define CURRENT_VERSION 3 // wehenver you change Entity increment this boz
#define SOKOL_IMPL #define SOKOL_IMPL
#if defined(WIN32) || defined(_WIN32) #if defined(WIN32) || defined(_WIN32)
#define DESKTOP #define DESKTOP
@ -188,14 +191,14 @@ typedef struct Entity
bool destroy; bool destroy;
int generation; int generation;
// fields for all entities // fields for all gs.entities
Vec2 pos; Vec2 pos;
Vec2 vel; // only used sometimes, like in old man and bullet Vec2 vel; // only used sometimes, like in old man and bullet
float damage; // at 1.0, dead! zero initialized float damage; // at 1.0, dead! zero initialized
bool facing_left; bool facing_left;
double dead_time; double dead_time;
bool dead; bool dead;
// multiple entities have a sword swing // multiple gs.entities have a sword swing
BUFF(EntityRef, 8) done_damage_to_this_swing; // only do damage once, but hitbox stays around BUFF(EntityRef, 8) done_damage_to_this_swing; // only do damage once, but hitbox stays around
bool is_bullet; bool is_bullet;
@ -259,7 +262,7 @@ typedef BUFF(Overlap, 16) Overlapping;
typedef struct Level typedef struct Level
{ {
TileInstance tiles[LAYERS][LEVEL_TILES][LEVEL_TILES]; TileInstance tiles[LAYERS][LEVEL_TILES][LEVEL_TILES];
Entity initial_entities[MAX_ENTITIES]; // shouldn't be directly modified, only used to initialize entities on loading of level Entity initial_entities[MAX_ENTITIES]; // shouldn't be directly modified, only used to initialize gs.entities on loading of level
} Level; } Level;
typedef struct TileCoord typedef struct TileCoord
@ -351,14 +354,15 @@ void play_audio(AudioSample *sample)
// on web it disables event handling so the button up event isn't received // on web it disables event handling so the button up event isn't received
bool keydown[SAPP_KEYCODE_MENU] = {0}; bool keydown[SAPP_KEYCODE_MENU] = {0};
bool in_dialog()
{
return player->state == CHARACTER_TALKING;
}
#ifdef DESKTOP #ifdef DESKTOP
bool receiving_text_input = false; bool receiving_text_input = false;
Sentence text_input_buffer = {0}; Sentence text_input_buffer = {0};
void begin_text_input()
{
receiving_text_input = true;
BUFF_CLEAR(&text_input_buffer);
}
#else #else
#ifdef WEB #ifdef WEB
EMSCRIPTEN_KEEPALIVE EMSCRIPTEN_KEEPALIVE
@ -369,13 +373,9 @@ void stop_controlling_input()
EMSCRIPTEN_KEEPALIVE EMSCRIPTEN_KEEPALIVE
void start_controlling_input() void start_controlling_input()
{
_sapp_emsc_register_eventhandlers();
}
void begin_text_input()
{ {
memset(keydown, 0, ARRLEN(keydown)); memset(keydown, 0, ARRLEN(keydown));
emscripten_run_script("start_dialog();"); _sapp_emsc_register_eventhandlers();
} }
#else #else
#error "No platform defined for text input! #error "No platform defined for text input!
@ -730,23 +730,27 @@ void add_new_npc_sentence(Entity *npc, char *sentence)
} }
AABB level_aabb = { .upper_left = {0.0f, 0.0f}, .lower_right = {TILE_SIZE * LEVEL_TILES, -(TILE_SIZE * LEVEL_TILES)} }; AABB level_aabb = { .upper_left = {0.0f, 0.0f}, .lower_right = {TILE_SIZE * LEVEL_TILES, -(TILE_SIZE * LEVEL_TILES)} };
Entity entities[MAX_ENTITIES] = {0}; typedef struct GameState {
int version;
Entity entities[MAX_ENTITIES];
} GameState;
GameState gs = {0};
EntityRef frome(Entity *e) EntityRef frome(Entity *e)
{ {
EntityRef to_return = { EntityRef to_return = {
.index = (int)(e - entities), .index = (int)(e - gs.entities),
.generation = e->generation, .generation = e->generation,
}; };
assert(to_return.index >= 0); assert(to_return.index >= 0);
assert(to_return.index < ARRLEN(entities)); assert(to_return.index < ARRLEN(gs.entities));
return to_return; return to_return;
} }
Entity *gete(EntityRef ref) Entity *gete(EntityRef ref)
{ {
if(ref.generation == 0) return 0; if(ref.generation == 0) return 0;
Entity *to_return = &entities[ref.index]; Entity *to_return = &gs.entities[ref.index];
if(!to_return->exists || to_return->generation != ref.generation) if(!to_return->exists || to_return->generation != ref.generation)
{ {
return 0; return 0;
@ -764,11 +768,11 @@ bool eq(EntityRef ref1, EntityRef ref2)
Entity *new_entity() Entity *new_entity()
{ {
for(int i = 0; i < ARRLEN(entities); i++) for(int i = 0; i < ARRLEN(gs.entities); i++)
{ {
if(!entities[i].exists) if(!gs.entities[i].exists)
{ {
Entity *to_return = &entities[i]; Entity *to_return = &gs.entities[i];
int gen = to_return->generation; int gen = to_return->generation;
*to_return = (Entity){0}; *to_return = (Entity){0};
to_return->exists = true; to_return->exists = true;
@ -780,7 +784,62 @@ Entity *new_entity()
return NULL; return NULL;
} }
void begin_text_input(); // called when player engages in dialog, must say something and fill text_input_buffer void update_player_from_entities()
{
player = 0;
ENTITIES_ITER(gs.entities)
{
if(it->is_character)
{
assert(player == 0);
player = it;
}
}
assert(player != 0);
}
void reset_level()
{
// load level
Level *to_load = &level_level0;
{
assert(ARRLEN(to_load->initial_entities) == ARRLEN(gs.entities));
memcpy(gs.entities, to_load->initial_entities, sizeof(Entity) * MAX_ENTITIES);
gs.version = CURRENT_VERSION;
ENTITIES_ITER(gs.entities)
{
if(it->generation == 0) it->generation = 1; // zero value generation means doesn't exist
}
}
update_player_from_entities();
}
#ifdef WEB
EMSCRIPTEN_KEEPALIVE
void dump_save_data()
{
EM_ASM({
save_game_data = new Int8Array(Module.HEAP8.buffer, $0, $1);
}, (char*)(&gs), sizeof(gs));
}
EMSCRIPTEN_KEEPALIVE
void read_from_save_data(char *data, size_t length)
{
GameState read_data = {0};
memcpy((char*)(&read_data), data, length);
if(read_data.version != CURRENT_VERSION)
{
Log("Bad gamestate, has version %d expected version %d\n", read_data.version, CURRENT_VERSION);
}
else
{
gs = read_data;
update_player_from_entities();
}
}
#endif
// a callback, when 'text backend' has finished making text. End dialog // a callback, when 'text backend' has finished making text. End dialog
void end_text_input(char *what_player_said) void end_text_input(char *what_player_said)
{ {
@ -1086,27 +1145,7 @@ static struct
sg_bindings bind; sg_bindings bind;
} state; } state;
void reset_level()
{
// load level
Level *to_load = &level_level0;
{
assert(ARRLEN(to_load->initial_entities) == ARRLEN(entities));
memcpy(entities, to_load->initial_entities, sizeof(Entity) * MAX_ENTITIES);
player = NULL;
ENTITIES_ITER(entities)
{
if(it->generation == 0) it->generation = 1; // zero value generation means doesn't exist
if(it->is_character)
{
assert(player == NULL);
player = it;
}
}
assert(player != NULL); // level initial config must have player entity
}
}
void audio_stream_callback(float *buffer, int num_frames, int num_channels) void audio_stream_callback(float *buffer, int num_frames, int num_channels)
{ {
@ -1178,7 +1217,7 @@ void init(void)
}, SERVER_URL); }, SERVER_URL);
#endif #endif
Log("Size of entity struct: %zu\n", sizeof(Entity)); Log("Size of entity struct: %zu\n", sizeof(Entity));
Log("Size of %d entities: %zu kb\n", (int)ARRLEN(entities), sizeof(entities)/1024); Log("Size of %d gs.entities: %zu kb\n", (int)ARRLEN(gs.entities), sizeof(gs.entities)/1024);
sg_setup(&(sg_desc){ sg_setup(&(sg_desc){
.context = sapp_sgcontext(), .context = sapp_sgcontext(),
}); });
@ -1193,6 +1232,12 @@ void init(void)
load_assets(); load_assets();
reset_level(); reset_level();
#ifdef WEB
EM_ASM({
load_all();
});
#endif
// load font // load font
{ {
FILE* fontFile = fopen("assets/orange kid.ttf", "rb"); FILE* fontFile = fopen("assets/orange kid.ttf", "rb");
@ -2054,7 +2099,7 @@ void draw_animated_sprite(AnimatedSprite *s, double elapsed_time, bool flipped,
draw_quad((DrawParams){true, q, spritesheet_img, region, tint, .y_coord_sorting = y_sort_pos, .alpha_clip_threshold = 0.2f}); draw_quad((DrawParams){true, q, spritesheet_img, region, tint, .y_coord_sorting = y_sort_pos, .alpha_clip_threshold = 0.2f});
} }
// gets aabbs overlapping the input aabb, including entities and tiles // gets aabbs overlapping the input aabb, including gs.entities and tiles
Overlapping get_overlapping(Level *l, AABB aabb) Overlapping get_overlapping(Level *l, AABB aabb)
{ {
Overlapping to_return = {0}; Overlapping to_return = {0};
@ -2072,8 +2117,8 @@ Overlapping get_overlapping(Level *l, AABB aabb)
} }
} }
// the entities jessie // the gs.entities jessie
ENTITIES_ITER(entities) ENTITIES_ITER(gs.entities)
{ {
if(!(it->is_character && it->is_rolling) && overlapping(aabb, entity_aabb(it))) if(!(it->is_character && it->is_rolling) && overlapping(aabb, entity_aabb(it)))
{ {
@ -2138,7 +2183,7 @@ Vec2 move_and_slide(MoveSlideParams p)
// add entity boxes // add entity boxes
if(!p.dont_collide_with_entities && !(p.from->is_character && p.from->is_rolling)) if(!p.dont_collide_with_entities && !(p.from->is_character && p.from->is_rolling))
{ {
ENTITIES_ITER(entities) ENTITIES_ITER(gs.entities)
{ {
if(!(it->is_character && it->is_rolling) && it != p.from && !(it->is_npc && it->dead) && !it->is_item) if(!(it->is_character && it->is_rolling) && it != p.from && !(it->is_npc && it->dead) && !it->is_item)
{ {
@ -2385,16 +2430,25 @@ void frame(void)
return; return;
#endif #endif
#ifdef DESKTOP
if(!receiving_text_input && in_dialog())
{
receiving_text_input = true;
BUFF_CLEAR(&text_input_buffer);
}
#endif
// better for vertical aspect ratios // better for vertical aspect ratios
if(screen_size().x < 0.7f*screen_size().y) if(screen_size().x < 0.7f*screen_size().y)
{ {
cam.scale = 3.5f; cam.scale = 2.5f;
} }
else else
{ {
cam.scale = 2.0f; cam.scale = 2.0f;
} }
uint64_t time_start_frame = stm_now(); uint64_t time_start_frame = stm_now();
// elapsed_time // elapsed_time
double dt_double = 0.0; double dt_double = 0.0;
@ -2539,7 +2593,7 @@ void frame(void)
{ {
Vec2 pos = V2(0.0, screen_size().Y); Vec2 pos = V2(0.0, screen_size().Y);
int num_entities = 0; int num_entities = 0;
ENTITIES_ITER(entities) num_entities++; ENTITIES_ITER(gs.entities) num_entities++;
char *stats = tprint("Frametime: %.1f ms\nProcessing: %.1f ms\nEntities: %d\nDraw calls: %d\nProfiling: %s\n", dt*1000.0, last_frame_processing_time*1000.0, num_entities, num_draw_calls, profiling ? "yes" : "no"); char *stats = tprint("Frametime: %.1f ms\nProcessing: %.1f ms\nEntities: %d\nDraw calls: %d\nProfiling: %s\n", dt*1000.0, last_frame_processing_time*1000.0, num_entities, num_draw_calls, profiling ? "yes" : "no");
AABB bounds = draw_text((TextParams){false, true, stats, pos, BLACK, 1.0f}); AABB bounds = draw_text((TextParams){false, true, stats, pos, BLACK, 1.0f});
pos.Y -= bounds.upper_left.Y - screen_size().Y; pos.Y -= bounds.upper_left.Y - screen_size().Y;
@ -2551,9 +2605,9 @@ void frame(void)
} }
#endif // devtools #endif // devtools
// process entities // process gs.entities
PROFILE_SCOPE("entity processing") PROFILE_SCOPE("entity processing")
ENTITIES_ITER(entities) ENTITIES_ITER(gs.entities)
{ {
assert(!(it->exists && it->generation == 0)); assert(!(it->exists && it->generation == 0));
#ifdef WEB #ifdef WEB
@ -2812,9 +2866,9 @@ void frame(void)
} }
} }
PROFILE_SCOPE("Destroy entities") PROFILE_SCOPE("Destroy gs.entities")
{ {
ENTITIES_ITER(entities) ENTITIES_ITER(gs.entities)
{ {
if(it->destroy) if(it->destroy)
{ {
@ -2902,7 +2956,6 @@ void frame(void)
// begin dialog with closest npc // begin dialog with closest npc
player->state = CHARACTER_TALKING; player->state = CHARACTER_TALKING;
player->talking_to = frome(closest_interact_with); player->talking_to = frome(closest_interact_with);
begin_text_input();
} }
else if(closest_interact_with->is_item) else if(closest_interact_with->is_item)
{ {
@ -3056,9 +3109,9 @@ void frame(void)
} }
} }
// render entities // render gs.entities
PROFILE_SCOPE("entity rendering") PROFILE_SCOPE("entity rendering")
ENTITIES_ITER(entities) ENTITIES_ITER(gs.entities)
{ {
#ifdef WEB #ifdef WEB
if(it->gen_request_id != 0) if(it->gen_request_id != 0)

@ -0,0 +1,59 @@
package codes
import (
"fmt"
)
type UserCode int
func intPow(n, m int) int {
if m == 0 {
return 1
}
result := n
for i := 2; i <= m; i++ {
result *= n
}
return result
}
var numberToChar = [...]rune{'A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I', 'J', 'K', 'L', 'M', 'N', 'O', 'P', 'Q', 'R', 'S', 'T', 'U', 'V', 'W', 'X', 'Y', 'Z', '0', '1', '2', '3', '4', '5', '6', '7', '8', '9'}
func CodeToString(code UserCode) (string, error) {
toReturn := [4]rune{'A', 'A', 'A', 'A'}
value := int(code)
for place := 3; place >= 0; place-- {
index := 3 - place
currentPlaceValue := value / intPow(36, place)
value -= currentPlaceValue * intPow(36, place)
if currentPlaceValue >= len(numberToChar) { return "", fmt.Errorf("Failed to generate usercode %d to string, currentPlaceValue %d and length of number to char %d in place %d", code, currentPlaceValue, len(numberToChar),place ) }
toReturn[index] = numberToChar[currentPlaceValue]
}
return string(toReturn[:]), nil
}
func ParseUserCode(s string) (UserCode, error) {
asRune := []rune(s)
if len(asRune) != 4 { return 0, fmt.Errorf("String to deconvert is not of length 4: %s", s) }
var toReturn UserCode = 0
for place := 3; place >= 0; place-- {
index := 3 - place
curDigitNum := 0
found := false
for i, letter := range numberToChar {
if letter == asRune[index] {
curDigitNum = i
found = true
}
}
if !found { return 0, fmt.Errorf("Failed to find place's number %s", s) }
toReturn += UserCode(curDigitNum * intPow(36, place))
}
return toReturn, nil
}

@ -0,0 +1,34 @@
package codes
import (
"testing"
"runtime"
)
func assert(t *testing.T, cond bool) {
if !cond {
_, _, line, _ := runtime.Caller(1)
t.Fatalf("Failed on line %d", line)
}
}
func TestCodes(t *testing.T) {
parsed, err := ParseUserCode("AAAA")
assert(t, err == nil)
assert(t, int(parsed) == 0)
var stringed string
stringed, err = CodeToString(UserCode(1))
assert(t, err == nil)
assert(t, stringed == "AAAB")
parsed, err = ParseUserCode("AAAB")
assert(t, err == nil)
assert(t, int(parsed) == 1)
parsed, err = ParseUserCode("BAAA")
assert(t, err == nil)
assert(t, int(parsed) == 46656)
}

@ -1,4 +1,4 @@
module github.com/creikey/rpgpt module github.com/creikey/rpgpt/server
go 1.19 go 1.19

@ -20,6 +20,7 @@ import (
"gorm.io/gorm" "gorm.io/gorm"
"gorm.io/driver/sqlite" "gorm.io/driver/sqlite"
"github.com/creikey/rpgpt/server/codes"
) )
// BoughtType values. do not reorganize these or you fuck up the database // BoughtType values. do not reorganize these or you fuck up the database
@ -32,14 +33,13 @@ const (
MaxCodes = 36 * 36 * 36 * 36 MaxCodes = 36 * 36 * 36 * 36
) )
type userCode int
type User struct { type User struct {
CreatedAt time.Time CreatedAt time.Time
UpdatedAt time.Time UpdatedAt time.Time
DeletedAt gorm.DeletedAt `gorm:"index"` DeletedAt gorm.DeletedAt `gorm:"index"`
Code userCode `gorm:"primaryKey"` // of maximum value max codes, incremented one by one. These are converted to 4 digit alphanumeric code users can remember/use Code codes.UserCode `gorm:"primaryKey"` // of maximum value max codes, incremented one by one. These are converted to 4 digit alphanumeric code users can remember/use
BoughtTime int64 // unix time. Used to figure out if the pass is still valid BoughtTime int64 // unix time. Used to figure out if the pass is still valid
BoughtType int // enum BoughtType int // enum
@ -54,51 +54,23 @@ var checkoutRedirectTo string
var daypassPriceId string var daypassPriceId string
var webhookSecret string var webhookSecret string
var db *gorm.DB var db *gorm.DB
var daypassTimedOut = make(map[codes.UserCode]int64) // value is time last requested, rate limiting by day pass. If exists is rate limited, should be removed when ok to request again
// for 10 free minutes a day, is when ip address began requesting
var ipAddyTenFree = make(map[string]int64)
func intPow(n, m int) int { func cleanTimedOut() {
if m == 0 { for k, v := range daypassTimedOut {
return 1 if currentTime() - v > 1 {
delete(daypassTimedOut, k)
} }
result := n
for i := 2; i <= m; i++ {
result *= n
} }
return result for k, v := range ipAddyTenFree {
if currentTime() - v > 24*60*60 {
delete(ipAddyTenFree, k)
} }
var numberToChar = [...]rune{'A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I', 'J', 'K', 'L', 'M', 'N', 'O', 'P', 'Q', 'R', 'S', 'T', 'U', 'V', 'W', 'X', 'Y', 'Z', '0', '1', '2', '3', '4', '5', '6', '7', '8', '9'}
func codeToString(code userCode) (string, error) {
toReturn := ""
value := int(code)
// converting to base 36 (A-Z + numbers) then into characters, then appending
for digit := 3; digit >= 0; digit-- {
currentPlaceValue := value / intPow(36, digit)
value -= currentPlaceValue * intPow(36, digit)
if currentPlaceValue >= len(numberToChar) { return "", fmt.Errorf("Failed to generate usercode %d to string, currentPlaceValue %d and length of number to char %d", code, currentPlaceValue, len(numberToChar)) }
toReturn += string(numberToChar[currentPlaceValue])
} }
return toReturn, nil;
} }
func parseUserCode(s string) (userCode, error) {
asRune := []rune(s)
if len(asRune) != 4 { return 0, fmt.Errorf("String to deconvert is not of length 4: %s", s) }
var toReturn userCode = 0
for digit := 3; digit >= 0; digit-- {
curDigitNum := 0
found := false
for i, letter := range numberToChar {
if letter == asRune[digit] {
curDigitNum = i
found = true
}
}
if !found { return 0, fmt.Errorf("Failed to find digit's number %s", s) }
toReturn += userCode(curDigitNum * intPow(36, digit))
}
return toReturn, nil
}
func isUserOld(user User) bool { func isUserOld(user User) bool {
return (currentTime() - user.BoughtTime) > 24*60*60 return (currentTime() - user.BoughtTime) > 24*60*60
@ -110,7 +82,7 @@ func clearOld(db *gorm.DB) {
if result.Error != nil { if result.Error != nil {
log.Fatal(result.Error) log.Fatal(result.Error)
} }
var toDelete []userCode // codes var toDelete []codes.UserCode // codes
for _, user := range users { for _, user := range users {
if user.BoughtType != 0 { if user.BoughtType != 0 {
panic("Don't know how to handle bought type " + string(user.BoughtType) + " yet") panic("Don't know how to handle bought type " + string(user.BoughtType) + " yet")
@ -174,11 +146,11 @@ func webhookResponse(w http.ResponseWriter, req *http.Request) {
if !found { if !found {
log.Println("Error Failed to find user in database to fulfill: very bad! ID: " + session.ID) log.Println("Error Failed to find user in database to fulfill: very bad! ID: " + session.ID)
} else { } else {
userString, err := codeToString(toFulfill.Code) userString, err := codes.CodeToString(toFulfill.Code)
if err != nil { if err != nil {
log.Printf("Error strange thing, saved user's code was unable to be converted to a string %s", err) log.Printf("Error strange thing, saved user's code was unable to be converted to a string %s", err)
} }
log.Printf("Fulfilling user with code %s\n", userString) log.Printf("Fulfilling user with code %s number %d\n", userString, toFulfill.Code)
toFulfill.IsFulfilled = true toFulfill.IsFulfilled = true
db.Save(&toFulfill) db.Save(&toFulfill)
} }
@ -194,16 +166,16 @@ func checkout(w http.ResponseWriter, req *http.Request) {
// generate a code // generate a code
var newCode string var newCode string
var newCodeUser userCode var newCodeUser codes.UserCode
found := false found := false
for i := 0; i < 1000; i++ { for i := 0; i < 1000; i++ {
codeInt := rand.Intn(MaxCodes) codeInt := rand.Intn(MaxCodes)
newCodeUser = userCode(codeInt) newCodeUser = codes.UserCode(codeInt)
var tmp User var tmp User
r := db.Where("Code = ?", newCodeUser).Limit(1).Find(&tmp) r := db.Where("Code = ?", newCodeUser).Limit(1).Find(&tmp)
if r.RowsAffected == 0{ if r.RowsAffected == 0{
var err error var err error
newCode, err = codeToString(newCodeUser) newCode, err = codes.CodeToString(newCodeUser)
if err != nil { if err != nil {
w.WriteHeader(http.StatusInternalServerError) w.WriteHeader(http.StatusInternalServerError)
log.Fatalf("Failed to generate code from random number: %s", err) log.Fatalf("Failed to generate code from random number: %s", err)
@ -219,6 +191,7 @@ func checkout(w http.ResponseWriter, req *http.Request) {
return return
} }
customMessage := fmt.Sprintf("**IMPORTANT** Your Day Pass Code is %s", newCode)
params := &stripe.CheckoutSessionParams { params := &stripe.CheckoutSessionParams {
LineItems: []*stripe.CheckoutSessionLineItemParams { LineItems: []*stripe.CheckoutSessionLineItemParams {
&stripe.CheckoutSessionLineItemParams{ &stripe.CheckoutSessionLineItemParams{
@ -229,6 +202,7 @@ func checkout(w http.ResponseWriter, req *http.Request) {
Mode: stripe.String(string(stripe.CheckoutSessionModePayment)), Mode: stripe.String(string(stripe.CheckoutSessionModePayment)),
SuccessURL: stripe.String(checkoutRedirectTo), SuccessURL: stripe.String(checkoutRedirectTo),
CancelURL: stripe.String(checkoutRedirectTo), CancelURL: stripe.String(checkoutRedirectTo),
CustomText: &stripe.CheckoutSessionCustomTextParams{ Submit: &stripe.CheckoutSessionCustomTextSubmitParams { Message: &customMessage } },
} }
s, err := session.New(params) s, err := session.New(params)
@ -237,7 +211,7 @@ func checkout(w http.ResponseWriter, req *http.Request) {
log.Printf("session.New: %v", err) log.Printf("session.New: %v", err)
} }
log.Printf("Creating user with checkout session ID %s\n", s.ID) log.Printf("Creating user code %s with checkout session ID %s\n", newCode, newCodeUser ,s.ID)
result := db.Create(&User { result := db.Create(&User {
Code: newCodeUser, Code: newCodeUser,
BoughtTime: currentTime(), BoughtTime: currentTime(),
@ -275,27 +249,49 @@ func index(w http.ResponseWriter, req *http.Request) {
// see if need to pay // see if need to pay
rejected := false rejected := false
cleanTimedOut()
{ {
if len(userToken) != 4 { if len(userToken) != 4 {
log.Println("Rejected because not 4: `" + userToken + "`") // where I do the IP rate limiting
rejected = true
userKey := req.RemoteAddr
createdTime, ok := ipAddyTenFree[userKey]
if !ok {
ipAddyTenFree[userKey] = currentTime()
rejected = false
} else {
if currentTime() - createdTime < 10*60 {
rejected = false
} else {
rejected = true // out of free time buddy
}
}
} else { } else {
var thisUser User var thisUser User
thisUserCode, err := parseUserCode(userToken) thisUserCode, err := codes.ParseUserCode(userToken)
if err != nil { if err != nil {
log.Printf("Error: Failed to parse user token %s\n", userToken) log.Printf("Error: Failed to parse user token %s\n", userToken)
rejected = true rejected = true
} else { } else {
if db.First(&thisUser, thisUserCode).Error != nil { err := db.First(&thisUser, thisUserCode).Error
log.Printf("User code %d string %s couldn't be found in the database: %s\n", thisUserCode, userToken, db.Error) if err != nil {
log.Printf("User code %d string %s couldn't be found in the database: %s\n", thisUserCode, userToken, err)
rejected = true rejected = true
} else { } else {
if isUserOld(thisUser) { if isUserOld(thisUser) {
log.Println("User code " + userToken + " is old, not valid") log.Println("User code " + userToken + " is old, not valid")
db.Delete(&thisUser) db.Delete(&thisUser)
rejected = true rejected = true
} else {
// now have valid user, in the database, to be rate limit checked
// rate limiting based on user token
_, exists := daypassTimedOut[thisUserCode]
if exists {
rejected = true
} else { } else {
rejected = false rejected = false
daypassTimedOut[thisUserCode] = currentTime()
}
} }
} }
} }
@ -381,7 +377,7 @@ func main() {
http.HandleFunc("/checkout", checkout) http.HandleFunc("/checkout", checkout)
portString := ":8090" portString := ":8090"
log.Println("Serving on " + portString + "...") log.Println("DO NOT RUN WITH CLOUDFLARE PROXY it rate limits based on IP, if behind proxy every IP will be the same. Would need to fix req.RemoteAddr. Serving on " + portString + "...")
http.ListenAndServe(portString, nil) http.ListenAndServe(portString, nil)
} }

@ -1,5 +1,6 @@
Happening by END OF STREAM: Happening by END OF STREAM:
- Payment working - Payment working
- Respond to cancel with stripe, redirect to ?cancelled=true that clears day pass ticket cookie and ui value
- Fixed timesep the gameplay (which means separate player rendering) - Fixed timesep the gameplay (which means separate player rendering)
- Make action between stars much more technical sounding so AI doesn't hallucinate responses - Make action between stars much more technical sounding so AI doesn't hallucinate responses
- Help you fight and fight you actions - Help you fight and fight you actions

@ -156,7 +156,7 @@ body {
<body style="background:black"> <body style="background:black">
<div id="buy_menu"> <div id="buy_menu">
<h1>AI is expensive so you need to pay for it</h1> <h1>AI is expensive so you need to pay for it</h1>
<p>You can play for free 10 minutes per day</p> <p>You can play free 10 minutes a day. If you're seeing this you must buy a day pass to continue. If you already bought one, but are still seeing this, check that the code matches up. If you're unsatisfied with your service, you can get a refund via the email stripe sent you.</p>
<div id="daypass"> <div id="daypass">
<button id="closebutton" onclick="closepayment()"><i class="fa-regular fa-xmark"></i></button> <button id="closebutton" onclick="closepayment()"><i class="fa-regular fa-xmark"></i></button>
<p>24 Hour Pass</p> <p>24 Hour Pass</p>
@ -202,12 +202,32 @@ body {
}; };
</script> </script>
<script type="text/javascript"> <script type="text/javascript">
let game_doing_input = true; let game_doing_input = true;
let server_url = ""; let server_url = "";
let my_code = ""; let codeinput = document.getElementById("codeinput");
function set_my_code(new_code) {
let final_codeinput_string = "";
for(let i = 0; i < new_code.length; i++) {
let cur = new_code[i];
let cur_charcode = cur.charCodeAt(0);
if(cur_charcode >= "a".charCodeAt(0) && cur_charcode <= "z".charCodeAt(0)) {
final_codeinput_string += String.fromCharCode(cur_charcode + ("A".charCodeAt(0) - "a".charCodeAt(0)));
} else {
if((cur_charcode >= "A".charCodeAt(0) && cur_charcode <= "Z".charCodeAt(0)) || (cur_charcode >= "0".charCodeAt(0) && cur_charcode <= "9".charCodeAt(0))) {
final_codeinput_string += cur;
}
}
}
codeinput.value = final_codeinput_string;
}
function get_my_code() {
return codeinput.value;
}
let code_visible = false; let code_visible = false;
let input_modal = document.getElementById("inputdiv"); let input_modal = document.getElementById("inputdiv");
let pause_modal = document.getElementById("buy_menu"); let pause_modal = document.getElementById("buy_menu");
let save_game_data = null;
function visibility_toggle() { function visibility_toggle() {
if(code_visible) { if(code_visible) {
@ -225,6 +245,14 @@ body {
let input_visible = input_modal.style.display === "flex"; let input_visible = input_modal.style.display === "flex";
let pause_visible = pause_modal.style.display === "flex"; let pause_visible = pause_modal.style.display === "flex";
if( Module.ccall('in_dialog', 'bool', [], [])) {
if(!input_visible) {
document.getElementById("inputtext").value = "";
setTimeout(function(){document.getElementById("inputtext").focus();},50); // for some reason focus doesn't work immediately here
document.getElementById("inputdiv").style.display = "flex";
}
}
if( (input_visible || pause_visible) && game_doing_input) if( (input_visible || pause_visible) && game_doing_input)
{ {
Module.ccall('stop_controlling_input', 'void', [], []); Module.ccall('stop_controlling_input', 'void', [], []);
@ -242,11 +270,50 @@ body {
window.requestAnimationFrame(frame); window.requestAnimationFrame(frame);
} }
function start_dialog() { const chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/';
document.getElementById("inputtext").value = ""; // Use a lookup table to find the index.
setTimeout(function(){document.getElementById("inputtext").focus();},50); // for some reason focus doesn't work immediately here const lookup = typeof Uint8Array === 'undefined' ? [] : new Uint8Array(256);
document.getElementById("inputdiv").style.display = "flex"; for (let i = 0; i < chars.length; i++) {
} lookup[chars.charCodeAt(i)] = i;
}
const encode = (arraybuffer) => {
let bytes = new Uint8Array(arraybuffer), i, len = bytes.length, base64 = '';
for (i = 0; i < len; i += 3) {
base64 += chars[bytes[i] >> 2];
base64 += chars[((bytes[i] & 3) << 4) | (bytes[i + 1] >> 4)];
base64 += chars[((bytes[i + 1] & 15) << 2) | (bytes[i + 2] >> 6)];
base64 += chars[bytes[i + 2] & 63];
}
if (len % 3 === 2) {
base64 = base64.substring(0, base64.length - 1) + '=';
}
else if (len % 3 === 1) {
base64 = base64.substring(0, base64.length - 2) + '==';
}
return base64;
};
const decode = (base64) => {
let bufferLength = base64.length * 0.75, len = base64.length, i, p = 0, encoded1, encoded2, encoded3, encoded4;
if (base64[base64.length - 1] === '=') {
bufferLength--;
if (base64[base64.length - 2] === '=') {
bufferLength--;
}
}
const arraybuffer = new ArrayBuffer(bufferLength), bytes = new Uint8Array(arraybuffer);
for (i = 0; i < len; i += 4) {
encoded1 = lookup[base64.charCodeAt(i)];
encoded2 = lookup[base64.charCodeAt(i + 1)];
encoded3 = lookup[base64.charCodeAt(i + 2)];
encoded4 = lookup[base64.charCodeAt(i + 3)];
bytes[p++] = (encoded1 << 2) | (encoded2 >> 4);
bytes[p++] = ((encoded2 & 15) << 4) | (encoded3 >> 2);
bytes[p++] = ((encoded3 & 3) << 6) | (encoded4 & 63);
}
return arraybuffer;
};
function end_dialog() { function end_dialog() {
document.getElementById("inputdiv").style.display = "none"; document.getElementById("inputdiv").style.display = "none";
Module.ccall('end_text_input', 'void', ['string'], [document.getElementById("inputtext").value]); Module.ccall('end_text_input', 'void', ['string'], [document.getElementById("inputtext").value]);
@ -261,21 +328,29 @@ function hide_paywall() {
} }
function on_codeinput_key(event) { function on_codeinput_key(event) {
let final_codeinput_string = ""; set_my_code(get_my_code());
let codeinput = document.getElementById("codeinput");
for(let i = 0; i < codeinput.value.length; i++) {
let cur = codeinput.value[i];
let cur_charcode = cur.charCodeAt(0);
if(cur_charcode >= "a".charCodeAt(0) && cur_charcode <= "z".charCodeAt(0)) {
final_codeinput_string += String.fromCharCode(cur_charcode + ("A".charCodeAt(0) - "a".charCodeAt(0)));
} else {
if((cur_charcode >= "A".charCodeAt(0) && cur_charcode <= "Z".charCodeAt(0)) || (cur_charcode >= "0".charCodeAt(0) && cur_charcode <= "9".charCodeAt(0))) {
final_codeinput_string += cur;
} }
function save_all() {
document.cookie = get_my_code();
if (typeof(Storage) !== "undefined") {
console.log("Saving full game");
Module.ccall('dump_save_data', 'void', [], []);
localStorage.setItem("game", encode(save_game_data));
}
}
function load_all() {
set_my_code(document.cookie);
if (typeof(Storage) !== "undefined") {
console.log("Attempting read of full game...");
let read_data = localStorage.getItem("game");
if (read_data != null) {
console.log("Reading full game");
let decoded = decode(read_data);
Module.ccall('read_from_save_data', 'void', ['array', 'number'], [new Uint8Array(decoded), decoded.byteLength]);
} }
} }
codeinput.value = final_codeinput_string;
my_code = codeinput.value;
} }
function on_textarea_key(event) { function on_textarea_key(event) {
@ -317,11 +392,12 @@ function do_checkout() {
split_up = text.split("|"); split_up = text.split("|");
if(split_up.length !== 2) { if(split_up.length !== 2) {
console.log("Weird response from server, length isn't 2"); console.log("Weird response from server, length isn't 2");
} } else {
my_code = split_up[0]; set_my_code(split_up[0])
document.getElementById("codeinput").value = my_code; save_all();
let url_to_go_to = split_up[1]; let url_to_go_to = split_up[1];
window.location.href = url_to_go_to; window.location.href = url_to_go_to;
}
}).catch((error) => { }).catch((error) => {
console.log("Error doing checkout: " + error); console.log("Error doing checkout: " + error);
}); });
@ -334,7 +410,7 @@ function resend_request(r) {
r.failed = true; r.failed = true;
}; };
r.request.open("POST", r.request_info.url, true); r.request.open("POST", r.request_info.url, true);
r.request.send(my_code + "|" + r.request_info.body); r.request.send(get_my_code() + "|" + r.request_info.body);
} }
// server URL is decided in C // server URL is decided in C

Loading…
Cancel
Save