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
copy marketing_page\favicon.ico build_web\favicon.ico
copy main.c build_web\main.c || goto :error
@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
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
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

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
#if defined(WIN32) || defined(_WIN32)
#define DESKTOP
@ -188,14 +191,14 @@ typedef struct Entity
bool destroy;
int generation;
// fields for all entities
// fields for all gs.entities
Vec2 pos;
Vec2 vel; // only used sometimes, like in old man and bullet
float damage; // at 1.0, dead! zero initialized
bool facing_left;
double dead_time;
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
bool is_bullet;
@ -259,7 +262,7 @@ typedef BUFF(Overlap, 16) Overlapping;
typedef struct Level
{
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;
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
bool keydown[SAPP_KEYCODE_MENU] = {0};
bool in_dialog()
{
return player->state == CHARACTER_TALKING;
}
#ifdef DESKTOP
bool receiving_text_input = false;
Sentence text_input_buffer = {0};
void begin_text_input()
{
receiving_text_input = true;
BUFF_CLEAR(&text_input_buffer);
}
#else
#ifdef WEB
EMSCRIPTEN_KEEPALIVE
@ -369,13 +373,9 @@ void stop_controlling_input()
EMSCRIPTEN_KEEPALIVE
void start_controlling_input()
{
_sapp_emsc_register_eventhandlers();
}
void begin_text_input()
{
memset(keydown, 0, ARRLEN(keydown));
emscripten_run_script("start_dialog();");
_sapp_emsc_register_eventhandlers();
}
#else
#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)} };
Entity entities[MAX_ENTITIES] = {0};
typedef struct GameState {
int version;
Entity entities[MAX_ENTITIES];
} GameState;
GameState gs = {0};
EntityRef frome(Entity *e)
{
EntityRef to_return = {
.index = (int)(e - entities),
.index = (int)(e - gs.entities),
.generation = e->generation,
};
assert(to_return.index >= 0);
assert(to_return.index < ARRLEN(entities));
assert(to_return.index < ARRLEN(gs.entities));
return to_return;
}
Entity *gete(EntityRef ref)
{
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)
{
return 0;
@ -764,11 +768,11 @@ bool eq(EntityRef ref1, EntityRef ref2)
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;
*to_return = (Entity){0};
to_return->exists = true;
@ -780,7 +784,62 @@ Entity *new_entity()
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
void end_text_input(char *what_player_said)
{
@ -1086,27 +1145,7 @@ static struct
sg_bindings bind;
} 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)
{
@ -1178,7 +1217,7 @@ void init(void)
}, SERVER_URL);
#endif
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){
.context = sapp_sgcontext(),
});
@ -1193,6 +1232,12 @@ void init(void)
load_assets();
reset_level();
#ifdef WEB
EM_ASM({
load_all();
});
#endif
// load font
{
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});
}
// 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 to_return = {0};
@ -2072,8 +2117,8 @@ Overlapping get_overlapping(Level *l, AABB aabb)
}
}
// the entities jessie
ENTITIES_ITER(entities)
// the gs.entities jessie
ENTITIES_ITER(gs.entities)
{
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
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)
{
@ -2385,16 +2430,25 @@ void frame(void)
return;
#endif
#ifdef DESKTOP
if(!receiving_text_input && in_dialog())
{
receiving_text_input = true;
BUFF_CLEAR(&text_input_buffer);
}
#endif
// better for vertical aspect ratios
if(screen_size().x < 0.7f*screen_size().y)
{
cam.scale = 3.5f;
cam.scale = 2.5f;
}
else
{
cam.scale = 2.0f;
}
uint64_t time_start_frame = stm_now();
// elapsed_time
double dt_double = 0.0;
@ -2539,7 +2593,7 @@ void frame(void)
{
Vec2 pos = V2(0.0, screen_size().Y);
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");
AABB bounds = draw_text((TextParams){false, true, stats, pos, BLACK, 1.0f});
pos.Y -= bounds.upper_left.Y - screen_size().Y;
@ -2551,9 +2605,9 @@ void frame(void)
}
#endif // devtools
// process entities
// process gs.entities
PROFILE_SCOPE("entity processing")
ENTITIES_ITER(entities)
ENTITIES_ITER(gs.entities)
{
assert(!(it->exists && it->generation == 0));
#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)
{
@ -2902,7 +2956,6 @@ void frame(void)
// begin dialog with closest npc
player->state = CHARACTER_TALKING;
player->talking_to = frome(closest_interact_with);
begin_text_input();
}
else if(closest_interact_with->is_item)
{
@ -3056,9 +3109,9 @@ void frame(void)
}
}
// render entities
// render gs.entities
PROFILE_SCOPE("entity rendering")
ENTITIES_ITER(entities)
ENTITIES_ITER(gs.entities)
{
#ifdef WEB
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

@ -20,6 +20,7 @@ import (
"gorm.io/gorm"
"gorm.io/driver/sqlite"
"github.com/creikey/rpgpt/server/codes"
)
// BoughtType values. do not reorganize these or you fuck up the database
@ -32,14 +33,13 @@ const (
MaxCodes = 36 * 36 * 36 * 36
)
type userCode int
type User struct {
CreatedAt time.Time
UpdatedAt time.Time
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
BoughtType int // enum
@ -54,52 +54,24 @@ var checkoutRedirectTo string
var daypassPriceId string
var webhookSecret string
var db *gorm.DB
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 := ""
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])
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 cleanTimedOut() {
for k, v := range daypassTimedOut {
if currentTime() - v > 1 {
delete(daypassTimedOut, k)
}
}
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
}
for k, v := range ipAddyTenFree {
if currentTime() - v > 24*60*60 {
delete(ipAddyTenFree, k)
}
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 {
return (currentTime() - user.BoughtTime) > 24*60*60
}
@ -110,7 +82,7 @@ func clearOld(db *gorm.DB) {
if result.Error != nil {
log.Fatal(result.Error)
}
var toDelete []userCode // codes
var toDelete []codes.UserCode // codes
for _, user := range users {
if user.BoughtType != 0 {
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 {
log.Println("Error Failed to find user in database to fulfill: very bad! ID: " + session.ID)
} else {
userString, err := codeToString(toFulfill.Code)
userString, err := codes.CodeToString(toFulfill.Code)
if err != nil {
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
db.Save(&toFulfill)
}
@ -194,16 +166,16 @@ func checkout(w http.ResponseWriter, req *http.Request) {
// generate a code
var newCode string
var newCodeUser userCode
var newCodeUser codes.UserCode
found := false
for i := 0; i < 1000; i++ {
codeInt := rand.Intn(MaxCodes)
newCodeUser = userCode(codeInt)
newCodeUser = codes.UserCode(codeInt)
var tmp User
r := db.Where("Code = ?", newCodeUser).Limit(1).Find(&tmp)
if r.RowsAffected == 0{
var err error
newCode, err = codeToString(newCodeUser)
newCode, err = codes.CodeToString(newCodeUser)
if err != nil {
w.WriteHeader(http.StatusInternalServerError)
log.Fatalf("Failed to generate code from random number: %s", err)
@ -219,6 +191,7 @@ func checkout(w http.ResponseWriter, req *http.Request) {
return
}
customMessage := fmt.Sprintf("**IMPORTANT** Your Day Pass Code is %s", newCode)
params := &stripe.CheckoutSessionParams {
LineItems: []*stripe.CheckoutSessionLineItemParams {
&stripe.CheckoutSessionLineItemParams{
@ -229,6 +202,7 @@ func checkout(w http.ResponseWriter, req *http.Request) {
Mode: stripe.String(string(stripe.CheckoutSessionModePayment)),
SuccessURL: stripe.String(checkoutRedirectTo),
CancelURL: stripe.String(checkoutRedirectTo),
CustomText: &stripe.CheckoutSessionCustomTextParams{ Submit: &stripe.CheckoutSessionCustomTextSubmitParams { Message: &customMessage } },
}
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("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 {
Code: newCodeUser,
BoughtTime: currentTime(),
@ -275,19 +249,33 @@ func index(w http.ResponseWriter, req *http.Request) {
// see if need to pay
rejected := false
cleanTimedOut()
{
if len(userToken) != 4 {
log.Println("Rejected because not 4: `" + userToken + "`")
rejected = true
// where I do the IP rate limiting
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 {
var thisUser User
thisUserCode, err := parseUserCode(userToken)
thisUserCode, err := codes.ParseUserCode(userToken)
if err != nil {
log.Printf("Error: Failed to parse user token %s\n", userToken)
rejected = true
} else {
if db.First(&thisUser, thisUserCode).Error != nil {
log.Printf("User code %d string %s couldn't be found in the database: %s\n", thisUserCode, userToken, db.Error)
err := db.First(&thisUser, thisUserCode).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
} else {
if isUserOld(thisUser) {
@ -295,7 +283,15 @@ func index(w http.ResponseWriter, req *http.Request) {
db.Delete(&thisUser)
rejected = true
} else {
rejected = false
// 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 {
rejected = false
daypassTimedOut[thisUserCode] = currentTime()
}
}
}
}
@ -381,7 +377,7 @@ func main() {
http.HandleFunc("/checkout", checkout)
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)
}

@ -1,5 +1,6 @@
Happening by END OF STREAM:
- 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)
- Make action between stars much more technical sounding so AI doesn't hallucinate responses
- Help you fight and fight you actions

@ -156,7 +156,7 @@ body {
<body style="background:black">
<div id="buy_menu">
<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">
<button id="closebutton" onclick="closepayment()"><i class="fa-regular fa-xmark"></i></button>
<p>24 Hour Pass</p>
@ -202,54 +202,121 @@ body {
};
</script>
<script type="text/javascript">
let game_doing_input = true;
let server_url = "";
let my_code = "";
let code_visible = false;
let input_modal = document.getElementById("inputdiv");
let pause_modal = document.getElementById("buy_menu");
function visibility_toggle() {
if(code_visible) {
code_visible = false;
document.getElementById("codeinput").type = "password";
document.getElementById("visibility-toggle-eye").className = "fa fa-eye-slash";
} else {
code_visible = true;
document.getElementById("codeinput").type = "text";
document.getElementById("visibility-toggle-eye").className = "fa fa-eye";
}
let game_doing_input = true;
let server_url = "";
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 input_modal = document.getElementById("inputdiv");
let pause_modal = document.getElementById("buy_menu");
let save_game_data = null;
function visibility_toggle() {
if(code_visible) {
code_visible = false;
document.getElementById("codeinput").type = "password";
document.getElementById("visibility-toggle-eye").className = "fa fa-eye-slash";
} else {
code_visible = true;
document.getElementById("codeinput").type = "text";
document.getElementById("visibility-toggle-eye").className = "fa fa-eye";
}
}
function frame(delta) {
let input_visible = input_modal.style.display === "flex";
let pause_visible = pause_modal.style.display === "flex";
function frame(delta) {
let input_visible = input_modal.style.display === "flex";
let pause_visible = pause_modal.style.display === "flex";
if( (input_visible || pause_visible) && game_doing_input)
{
Module.ccall('stop_controlling_input', 'void', [], []);
game_doing_input = false;
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)
{
Module.ccall('stop_controlling_input', 'void', [], []);
game_doing_input = false;
}
if( !input_visible && !pause_visible && !game_doing_input)
{
Module.ccall('start_controlling_input', 'void', [], []);
game_doing_input = true;
}
window.requestAnimationFrame(frame);
}
Module.onRuntimeInitialized = () => {
window.requestAnimationFrame(frame);
}
const chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/';
// Use a lookup table to find the index.
const lookup = typeof Uint8Array === 'undefined' ? [] : new Uint8Array(256);
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( !input_visible && !pause_visible && !game_doing_input)
{
Module.ccall('start_controlling_input', 'void', [], []);
game_doing_input = true;
if (len % 3 === 2) {
base64 = base64.substring(0, base64.length - 1) + '=';
}
window.requestAnimationFrame(frame);
}
Module.onRuntimeInitialized = () => {
window.requestAnimationFrame(frame);
}
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 start_dialog() {
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";
}
function end_dialog() {
document.getElementById("inputdiv").style.display = "none";
Module.ccall('end_text_input', 'void', ['string'], [document.getElementById("inputtext").value]);
document.getElementById("inputdiv").style.display = "none";
Module.ccall('end_text_input', 'void', ['string'], [document.getElementById("inputtext").value]);
}
function show_paywall() {
@ -261,21 +328,29 @@ function hide_paywall() {
}
function on_codeinput_key(event) {
let final_codeinput_string = "";
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;
}
set_my_code(get_my_code());
}
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) {
@ -317,11 +392,12 @@ function do_checkout() {
split_up = text.split("|");
if(split_up.length !== 2) {
console.log("Weird response from server, length isn't 2");
} else {
set_my_code(split_up[0])
save_all();
let url_to_go_to = split_up[1];
window.location.href = url_to_go_to;
}
my_code = split_up[0];
document.getElementById("codeinput").value = my_code;
let url_to_go_to = split_up[1];
window.location.href = url_to_go_to;
}).catch((error) => {
console.log("Error doing checkout: " + error);
});
@ -334,7 +410,7 @@ function resend_request(r) {
r.failed = 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
@ -362,72 +438,72 @@ function make_generation_request(p, api) {
// returns 0 if not done yet, 1 if succeeded, 2 if failed after too many retries (i.e server down, or no internet)
// -1 if it doesn't exist
function get_generation_request_status(id) {
for(let i = 0; i < generation_requests.length; i++) {
if(generation_requests[i].id == id) {
if(generation_requests[i].failed)
{
}
else
{
let http_status = generation_requests[i].request.status;
if(http_status == 200) {
if(generation_requests[i].request.responseText[0] === "0")
{
show_paywall();
return 2;
}
else if(generation_requests[i].request.responseText[0] === "1")
{
return 1; // done, everything ok
}
else
{
console.log("Unknown body code " + generation_requests[i].request.responseText);
}
}
else if(http_status == 0) { // not done yet
return 0;
}
}
{ // errored
if(generation_requests[i].retries_remaining > 0) {
console.log("Retrying request");
generation_requests[i].retries_remaining -= 1;
resend_request(generation_requests[i]);
return 0;
} else {
return 2; // too many retries, failed
}
}
}
for(let i = 0; i < generation_requests.length; i++) {
if(generation_requests[i].id == id) {
if(generation_requests[i].failed)
{
}
else
{
let http_status = generation_requests[i].request.status;
if(http_status == 200) {
if(generation_requests[i].request.responseText[0] === "0")
{
show_paywall();
return 2;
}
else if(generation_requests[i].request.responseText[0] === "1")
{
return 1; // done, everything ok
}
else
{
console.log("Unknown body code " + generation_requests[i].request.responseText);
}
}
return -1;
else if(http_status == 0) { // not done yet
return 0;
}
}
{ // errored
if(generation_requests[i].retries_remaining > 0) {
console.log("Retrying request");
generation_requests[i].retries_remaining -= 1;
resend_request(generation_requests[i]);
return 0;
} else {
return 2; // too many retries, failed
}
}
}
}
return -1;
}
function done_with_generation_request(id) {
console.log("Removing request with id " + id);
let new_generation_requests = [];
for(let i = 0; i < generation_requests.length; i++) {
if(generation_requests[i].id == id)
{
} else {
new_generation_requests.push(generation_requests[i])
}
}
generation_requests = new_generation_requests;
console.log("Removing request with id " + id);
let new_generation_requests = [];
for(let i = 0; i < generation_requests.length; i++) {
if(generation_requests[i].id == id)
{
} else {
new_generation_requests.push(generation_requests[i])
}
}
generation_requests = new_generation_requests;
}
// doesn't fill string if not done yet, or the id doesn't exist
function get_generation_request_content(id) {
let to_return = "";
for(let i = 0; i < generation_requests.length; i++) {
if(generation_requests[i].id == id) {
to_return = generation_requests[i].request.responseText;
break;
}
}
return to_return.slice(1); // first character is a code that says if the request was successful or not
let to_return = "";
for(let i = 0; i < generation_requests.length; i++) {
if(generation_requests[i].id == id) {
to_return = generation_requests[i].request.responseText;
break;
}
}
return to_return.slice(1); // first character is a code that says if the request was successful or not
}
</script>
{{{ SCRIPT }}}

Loading…
Cancel
Save