botany/data_manager.py

303 lines
12 KiB
Python

import errno
import getpass
import json
import os
import pickle
import sqlite3
import threading
import time
from glob import glob
import consts
class DataManager(object):
# handles user data, puts a .botany dir in user's home dir (OSX/Linux)
# handles shared data with sqlite db
# TODO: .dat save should only happen on mutation, water, death, exit,
# harvest, otherwise
# data hasn't changed...
# can write json whenever bc this isn't ever read for data within botany
user_dir = os.path.expanduser("~")
botany_dir = os.path.join(user_dir, '.botany')
game_dir = os.path.dirname(os.path.realpath(__file__))
this_user = getpass.getuser()
savefile_name = this_user + '_plant.dat'
savefile_path = os.path.join(botany_dir, savefile_name)
# set this.savefile_path to guest_garden path
garden_db_path = os.path.join(game_dir, 'sqlite/garden_db.sqlite')
garden_json_path = os.path.join(game_dir, 'garden_file.json')
harvest_file_path = os.path.join(botany_dir, 'harvest_file.dat')
harvest_json_path = os.path.join(botany_dir, 'harvest_file.json')
def __init__(self):
self.conn = sqlite3.connect(self.garden_db_path)
self.conn.row_factory = sqlite3.Row
self.cursor = self.conn.cursor()
self.last_water_gain = 0.0
self.this_user = getpass.getuser()
# check if instance is already running
# check for .botany dir in home
try:
os.makedirs(self.botany_dir)
except OSError as exception:
if exception.errno != errno.EEXIST:
raise
self.savefile_name = self.this_user + '_plant.dat'
def check_plant(self):
# check for existing save file
return os.path.isfile(self.savefile_path)
def start_threads(self, this_plant):
# create threads to save files every minute
death_check_thread = threading.Thread(target=self.death_check_update, args=(this_plant,))
death_check_thread.daemon = True
death_check_thread.start()
def death_check_update(self, this_plant):
# .1 second updates and lock to minimize race condition
while True:
is_dead = this_plant.dead_check()
if is_dead:
self.save_plant(this_plant)
self.data_write_json(this_plant)
self.update_garden_db(this_plant)
self.harvest_plant(this_plant)
this_plant.unlock_new_creation()
time.sleep(.1)
def load_plant(self):
# load savefile
with open(self.savefile_path, 'rb') as f:
this_plant = pickle.load(f)
# migrate data structure to create data for empty/nonexistent plant
# properties
this_plant.migrate_properties()
# get status since last login
is_watered = this_plant.water_check(self)
is_dead = this_plant.dead_check()
if not is_dead:
if is_watered:
time_delta_last = int(time.time()) - this_plant.last_time
ticks_to_add = min(time_delta_last, 24 * 3600)
this_plant.time_delta_watered = 0
self.last_water_gain = time.time()
else:
ticks_to_add = 0
this_plant.ticks += ticks_to_add * (0.2 * (this_plant.generation - 1) + 1)
return this_plant
@staticmethod
def plant_age_convert(this_plant):
# human-readable plant age
age_seconds = int(time.time()) - this_plant.start_time
days, age_seconds = divmod(age_seconds, 24 * 60 * 60)
hours, age_seconds = divmod(age_seconds, 60 * 60)
minutes, age_seconds = divmod(age_seconds, 60)
age_formatted = ("%dd:%dh:%dm:%ds" % (days, hours, minutes, age_seconds))
return age_formatted
def init_database(self):
# check if dir exists, create sqlite directory and set OS permissions to 777
sqlite_dir_path = os.path.join(self.game_dir, 'sqlite')
if not os.path.exists(sqlite_dir_path):
os.makedirs(sqlite_dir_path, mode=0o777)
for schema in glob(os.path.join(sqlite_dir_path, 'main', '*.sql')):
with open(schema) as schema_file:
self.cursor.execute(schema_file.read())
self.conn.commit()
# init only, creates and sets permissions for garden db and json
if os.name == "posix":
if os.stat(self.garden_db_path).st_uid == os.getuid():
os.chmod(self.garden_db_path, 0o666)
open(self.garden_json_path, 'a').close()
os.chmod(self.garden_json_path, 0o666)
def update_garden_db(self, this_plant):
# insert or update this plant id's entry in DB
# TODO: make sure other instances of user are deleted
# Could create a clean db function
age_formatted = self.plant_age_convert(this_plant)
# try to insert or replace
self.cursor.execute(
"""INSERT OR REPLACE INTO garden (
plant_id, owner, description, age, score, is_dead
) VALUES (
:pid, :owner, :description, :page, :score, :dead
)""",
{"pid": this_plant.plant_id,
"owner": this_plant.owner,
"description": this_plant.parse_plant(),
"page": age_formatted,
"score": str(this_plant.ticks),
"dead": int(this_plant.dead)})
self.conn.commit()
def retrieve_garden_from_db(self):
# Builds a dict of dicts from garden sqlite db
garden_dict = {}
# Need to allow write permissions by others
self.cursor.execute('SELECT * FROM garden ORDER BY owner')
tuple_list = self.cursor.fetchall()
# Building dict from table rows
for item in tuple_list:
garden_dict[item[0]] = {
"owner": item[1],
"description": item[2],
"age": item[3],
"score": item[4],
"dead": item[5],
}
return garden_dict
def update_garden_json(self):
with open(self.garden_json_path, 'w') as outfile:
json.dump(self.retrieve_garden_from_db(), outfile)
def save_plant(self, this_plant):
# create savefile
this_plant.last_time = int(time.time())
temp_path = self.savefile_path + ".temp"
with open(temp_path, 'wb') as f:
pickle.dump(this_plant, f, protocol=2)
os.replace(temp_path, self.savefile_path)
def data_write_json(self, this_plant):
# create personal json file for user to use outside the game (website?)
json_file = os.path.join(self.botany_dir, self.this_user + '_plant_data.json')
# also updates age
age_formatted = self.plant_age_convert(this_plant)
plant_info = {
"owner": this_plant.owner,
"description": this_plant.parse_plant(),
"age": age_formatted,
"score": this_plant.ticks,
"is_dead": this_plant.dead,
"last_watered": this_plant.watered_timestamp,
"file_name": this_plant.file_name,
"stage": consts.stages[this_plant.stage],
"generation": this_plant.generation,
}
if this_plant.stage >= 3:
plant_info["rarity"] = consts.rarities[this_plant.rarity]
if this_plant.mutation != 0:
plant_info["mutation"] = consts.mutations[this_plant.mutation]
if this_plant.stage >= 4:
plant_info["color"] = consts.colors[this_plant.color]
if this_plant.stage >= 2:
plant_info["species"] = consts.species[this_plant.species]
with open(json_file, 'w') as outfile:
json.dump(plant_info, outfile)
def harvest_plant(self, this_plant):
# TODO: plant history feature - could just use a sqlite query to retrieve all of user's dead plants
# harvest is a dict of dicts
# harvest contains one entry for each plant id
age_formatted = self.plant_age_convert(this_plant)
this_plant_id = this_plant.plant_id
plant_info = {
"description": this_plant.parse_plant(),
"age": age_formatted,
"score": this_plant.ticks,
}
if os.path.isfile(self.harvest_file_path):
# harvest file exists: load data
with open(self.harvest_file_path, 'rb') as f:
this_harvest = pickle.load(f)
new_file_check = False
else:
this_harvest = {}
new_file_check = True
this_harvest[this_plant_id] = plant_info
# dump harvest file
temp_path = self.harvest_file_path + ".temp"
with open(temp_path, 'wb') as f:
pickle.dump(this_harvest, f, protocol=2)
os.rename(temp_path, self.harvest_file_path)
# dump json file
with open(self.harvest_json_path, 'w') as outfile:
json.dump(this_harvest, outfile)
return new_file_check
def get_weekly_visitors(self, plant, maxx):
self.cursor.execute("SELECT * FROM visitors WHERE garden_name = ? ORDER BY weekly_visits", plant.owner)
visitor_data = self.cursor.fetchall()
visitor_block = ""
visitor_line = ""
if visitor_data:
for visitor in visitor_data:
visitor_name = visitor[2]
weekly_visits = visitor[3]
this_visitor_string = "{}({}) ".format(visitor_name, weekly_visits)
if len(visitor_line + this_visitor_string) > maxx - 3:
visitor_block += '\n'
visitor_line = ""
visitor_block += this_visitor_string
visitor_line += this_visitor_string
else:
visitor_block = 'nobody :('
return visitor_block
def update_visitor_db(self, visitor_names, owner_name):
for name in visitor_names:
self.cursor.execute(
"SELECT * FROM visitors WHERE garden_name = ? AND visitor_name = ?", (owner_name, name))
data = self.cursor.fetchone()
if data is None:
self.cursor.execute(" INSERT INTO visitors (garden_name,visitor_name,weekly_visits) VALUES(?, ?, 1)",
(owner_name, name))
else:
self.cursor.execute(
"UPDATE visitors SET weekly_visits = weekly_visits + 1 WHERE garden_name = ? AND visitor_name = ?",
(owner_name, name))
self.conn.commit()
def clear_weekly_visitors(self):
self.cursor.execute("DELETE FROM visitors")
self.conn.commit()
def load_visitors(self, plant):
visitor_filepath = os.path.join(os.path.join(os.path.expanduser("~"), '.botany'), 'visitors.json')
guest_timestamps = []
visitors_this_check = []
if os.path.isfile(visitor_filepath):
with open(visitor_filepath, 'r') as visit:
data = json.load(visit)
if data:
for element in data:
if element['user'] not in plant.visitors:
plant.visitors.append(element['user'])
if element['user'] not in visitors_this_check:
visitors_this_check.append(element['user'])
# prevent users from manually setting watered_time in the future
if int(time.time()) >= element['timestamp'] >= plant.watered_timestamp:
guest_timestamps.append(element['timestamp'])
try:
self.update_visitor_db(visitors_this_check, plant.owner)
except Exception:
pass
with open(visitor_filepath, 'w') as visitor:
visitor.write('[]')
else:
with open(visitor_filepath, 'w') as f:
json.dump([], f)
os.chmod(visitor_filepath, 0o666)
return guest_timestamps